fix(render): draw transparent solids without depth writes
Once solids carried a real sub-1 alpha (after the ENC transparency fix), the single depth-writing mesh pass made a transparent solid occlude the geometry behind it: it wrote depth at its near surface, so anything farther failed the LessEqual test and was never drawn — the transparent fill looked like it erased what was behind it. The batched mesh builder now routes each solid's triangles into an opaque or a transparent index stream by its baked colour alpha (< 0.999). The opaque fills draw first and write depth; the transparent fills draw last through a new depth-write-disabled pipeline, so they blend over the geometry behind them instead of culling it. The hidden-line depth prepass still rasterises both streams so solid surfaces keep occluding hidden edges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9f7fdc5f8f
commit
a08cf477c1
2 changed files with 145 additions and 35 deletions
|
|
@ -112,8 +112,14 @@ impl Highlight {
|
||||||
|
|
||||||
pub struct MeshBatchChunk {
|
pub struct MeshBatchChunk {
|
||||||
pub vertex_buffer: wgpu::Buffer,
|
pub vertex_buffer: wgpu::Buffer,
|
||||||
|
/// Opaque triangle indices (mesh colour alpha ≈ 1). Drawn with depth write.
|
||||||
pub index_buffer: wgpu::Buffer,
|
pub index_buffer: wgpu::Buffer,
|
||||||
pub index_count: u32,
|
pub index_count: u32,
|
||||||
|
/// Transparent triangle indices (mesh colour alpha < 1). Drawn after the
|
||||||
|
/// opaque fills with depth write disabled so they blend over — rather than
|
||||||
|
/// erase — the geometry behind them.
|
||||||
|
pub transp_index_buffer: wgpu::Buffer,
|
||||||
|
pub transp_index_count: u32,
|
||||||
pub wire_index_buffer: wgpu::Buffer,
|
pub wire_index_buffer: wgpu::Buffer,
|
||||||
pub wire_index_count: u32,
|
pub wire_index_count: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -122,28 +128,34 @@ fn make_chunk(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
verts: &[MeshVertex],
|
verts: &[MeshVertex],
|
||||||
indices: &[u32],
|
indices: &[u32],
|
||||||
|
transp_indices: &[u32],
|
||||||
wire_indices: &[u32],
|
wire_indices: &[u32],
|
||||||
) -> MeshBatchChunk {
|
) -> MeshBatchChunk {
|
||||||
|
// `create_buffer_init` with an empty slice yields a zero-sized buffer that
|
||||||
|
// some backends reject for INDEX usage; a chunk can legitimately hold only
|
||||||
|
// opaque or only transparent tris, so fall back to a 1-index stub (count
|
||||||
|
// stays 0, so the draw loop skips it).
|
||||||
|
let mk_index = |data: &[u32], label: &'static str| {
|
||||||
|
let stub = [0u32];
|
||||||
|
let src = if data.is_empty() { &stub[..] } else { data };
|
||||||
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
contents: bytemuck::cast_slice(src),
|
||||||
|
usage: wgpu::BufferUsages::INDEX,
|
||||||
|
})
|
||||||
|
};
|
||||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("mesh.batch.vbuf"),
|
label: Some("mesh.batch.vbuf"),
|
||||||
contents: bytemuck::cast_slice(verts),
|
contents: bytemuck::cast_slice(verts),
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
});
|
});
|
||||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
||||||
label: Some("mesh.batch.ibuf"),
|
|
||||||
contents: bytemuck::cast_slice(indices),
|
|
||||||
usage: wgpu::BufferUsages::INDEX,
|
|
||||||
});
|
|
||||||
let wire_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
||||||
label: Some("mesh.batch.wire_ibuf"),
|
|
||||||
contents: bytemuck::cast_slice(wire_indices),
|
|
||||||
usage: wgpu::BufferUsages::INDEX,
|
|
||||||
});
|
|
||||||
MeshBatchChunk {
|
MeshBatchChunk {
|
||||||
vertex_buffer,
|
vertex_buffer,
|
||||||
index_buffer,
|
index_buffer: mk_index(indices, "mesh.batch.ibuf"),
|
||||||
index_count: indices.len() as u32,
|
index_count: indices.len() as u32,
|
||||||
wire_index_buffer,
|
transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"),
|
||||||
|
transp_index_count: transp_indices.len() as u32,
|
||||||
|
wire_index_buffer: mk_index(wire_indices, "mesh.batch.wire_ibuf"),
|
||||||
wire_index_count: wire_indices.len() as u32,
|
wire_index_count: wire_indices.len() as u32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +180,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||||
let mut chunks = Vec::new();
|
let mut chunks = Vec::new();
|
||||||
let mut verts: Vec<MeshVertex> = Vec::new();
|
let mut verts: Vec<MeshVertex> = Vec::new();
|
||||||
let mut indices: Vec<u32> = Vec::new();
|
let mut indices: Vec<u32> = Vec::new();
|
||||||
|
let mut transp_indices: Vec<u32> = Vec::new();
|
||||||
let mut wire_indices: Vec<u32> = Vec::new();
|
let mut wire_indices: Vec<u32> = Vec::new();
|
||||||
let mut total_tris = 0u64;
|
let mut total_tris = 0u64;
|
||||||
for set in sets {
|
for set in sets {
|
||||||
|
|
@ -181,6 +194,9 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||||
color: mesh.color,
|
color: mesh.color,
|
||||||
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
|
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
|
||||||
};
|
};
|
||||||
|
// A solid whose baked colour is not fully opaque routes into the
|
||||||
|
// transparent index stream so it is drawn last, without depth writes.
|
||||||
|
let is_transp = mesh.color[3] < 0.999;
|
||||||
let mesh_tris = mesh.indices.len() / 3;
|
let mesh_tris = mesh.indices.len() / 3;
|
||||||
total_tris += mesh_tris as u64;
|
total_tris += mesh_tris as u64;
|
||||||
|
|
||||||
|
|
@ -188,9 +204,10 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||||
// sub-chunks (corners expanded, no vertex sharing) so each buffer fits.
|
// sub-chunks (corners expanded, no vertex sharing) so each buffer fits.
|
||||||
if mesh.verts.len() > max_verts || mesh_tris > max_tris {
|
if mesh.verts.len() > max_verts || mesh_tris > max_tris {
|
||||||
if !verts.is_empty() {
|
if !verts.is_empty() {
|
||||||
chunks.push(make_chunk(device, &verts, &indices, &wire_indices));
|
chunks.push(make_chunk(device, &verts, &indices, &transp_indices, &wire_indices));
|
||||||
verts.clear();
|
verts.clear();
|
||||||
indices.clear();
|
indices.clear();
|
||||||
|
transp_indices.clear();
|
||||||
wire_indices.clear();
|
wire_indices.clear();
|
||||||
}
|
}
|
||||||
let tris_per = (max_verts / 3).min(max_tris).max(1);
|
let tris_per = (max_verts / 3).min(max_tris).max(1);
|
||||||
|
|
@ -207,7 +224,13 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||||
si.extend_from_slice(&[b, b + 1, b + 2]);
|
si.extend_from_slice(&[b, b + 1, b + 2]);
|
||||||
swi.extend_from_slice(&[b, b + 1, b + 1, b + 2, b + 2, b]);
|
swi.extend_from_slice(&[b, b + 1, b + 1, b + 2, b + 2, b]);
|
||||||
}
|
}
|
||||||
chunks.push(make_chunk(device, &sv, &si, &swi));
|
// The whole mesh shares one colour, so a sub-chunk is entirely
|
||||||
|
// opaque or entirely transparent.
|
||||||
|
if is_transp {
|
||||||
|
chunks.push(make_chunk(device, &sv, &[], &si, &swi));
|
||||||
|
} else {
|
||||||
|
chunks.push(make_chunk(device, &sv, &si, &[], &swi));
|
||||||
|
}
|
||||||
t = end;
|
t = end;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -219,25 +242,27 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
|
||||||
&& (verts.len() + mesh.verts.len() > max_verts
|
&& (verts.len() + mesh.verts.len() > max_verts
|
||||||
|| wire_indices.len() / 6 + mesh_tris > max_tris)
|
|| wire_indices.len() / 6 + mesh_tris > max_tris)
|
||||||
{
|
{
|
||||||
chunks.push(make_chunk(device, &verts, &indices, &wire_indices));
|
chunks.push(make_chunk(device, &verts, &indices, &transp_indices, &wire_indices));
|
||||||
verts.clear();
|
verts.clear();
|
||||||
indices.clear();
|
indices.clear();
|
||||||
|
transp_indices.clear();
|
||||||
wire_indices.clear();
|
wire_indices.clear();
|
||||||
}
|
}
|
||||||
let base = verts.len() as u32;
|
let base = verts.len() as u32;
|
||||||
for i in 0..mesh.verts.len() {
|
for i in 0..mesh.verts.len() {
|
||||||
verts.push(vtx(i));
|
verts.push(vtx(i));
|
||||||
}
|
}
|
||||||
|
let fill = if is_transp { &mut transp_indices } else { &mut indices };
|
||||||
for &idx in &mesh.indices {
|
for &idx in &mesh.indices {
|
||||||
indices.push(base + idx);
|
fill.push(base + idx);
|
||||||
}
|
}
|
||||||
for tri in mesh.indices.chunks_exact(3) {
|
for tri in mesh.indices.chunks_exact(3) {
|
||||||
let (a, b, c) = (base + tri[0], base + tri[1], base + tri[2]);
|
let (a, b, c) = (base + tri[0], base + tri[1], base + tri[2]);
|
||||||
wire_indices.extend_from_slice(&[a, b, b, c, c, a]);
|
wire_indices.extend_from_slice(&[a, b, b, c, c, a]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !indices.is_empty() {
|
if !indices.is_empty() || !transp_indices.is_empty() {
|
||||||
chunks.push(make_chunk(device, &verts, &indices, &wire_indices));
|
chunks.push(make_chunk(device, &verts, &indices, &transp_indices, &wire_indices));
|
||||||
}
|
}
|
||||||
(chunks, total_tris)
|
(chunks, total_tris)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ pub struct Pipeline {
|
||||||
hatch_batched_pipeline: Option<wgpu::RenderPipeline>,
|
hatch_batched_pipeline: Option<wgpu::RenderPipeline>,
|
||||||
image_pipeline: wgpu::RenderPipeline,
|
image_pipeline: wgpu::RenderPipeline,
|
||||||
mesh_pipeline: wgpu::RenderPipeline,
|
mesh_pipeline: wgpu::RenderPipeline,
|
||||||
|
/// Depth-write-disabled variant of `mesh_pipeline` for non-opaque solids.
|
||||||
|
mesh_transparent_pipeline: wgpu::RenderPipeline,
|
||||||
mesh_highlight_pipeline: wgpu::RenderPipeline,
|
mesh_highlight_pipeline: wgpu::RenderPipeline,
|
||||||
/// Wireframe variant of the mesh pipeline (LineList topology, same
|
/// Wireframe variant of the mesh pipeline (LineList topology, same
|
||||||
/// vertex layout / shader). Used when the active render mode is
|
/// vertex layout / shader). Used when the active render mode is
|
||||||
|
|
@ -532,6 +534,55 @@ impl Pipeline {
|
||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Transparent variant — identical to `mesh_pipeline` but with depth
|
||||||
|
// writes disabled. Non-opaque solids are drawn after the opaque fills
|
||||||
|
// with this pipeline so they blend over the geometry behind them (which
|
||||||
|
// has already written depth) instead of erasing it.
|
||||||
|
let mesh_transparent_pipeline =
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("mesh.transparent.pipeline"),
|
||||||
|
layout: Some(&mesh_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &mesh_shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[mesh_gpu::MeshVertex::layout()],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
cull_mode: None,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: false,
|
||||||
|
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: 1,
|
||||||
|
slope_scale: 1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: MSAA_SAMPLES,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &mesh_shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
// Highlight variant — same shader / vertex layout, but `depth_compare =
|
// Highlight variant — same shader / vertex layout, but `depth_compare =
|
||||||
// Always` and no depth write, so a hovered / selected solid's tint is
|
// Always` and no depth write, so a hovered / selected solid's tint is
|
||||||
// drawn on top of everything (it shows through occluding geometry) and
|
// drawn on top of everything (it shows through occluding geometry) and
|
||||||
|
|
@ -1003,6 +1054,7 @@ impl Pipeline {
|
||||||
hatch_batched_pipeline,
|
hatch_batched_pipeline,
|
||||||
image_pipeline,
|
image_pipeline,
|
||||||
mesh_pipeline,
|
mesh_pipeline,
|
||||||
|
mesh_transparent_pipeline,
|
||||||
mesh_highlight_pipeline,
|
mesh_highlight_pipeline,
|
||||||
mesh_wireframe_pipeline,
|
mesh_wireframe_pipeline,
|
||||||
mesh_depth_pipeline,
|
mesh_depth_pipeline,
|
||||||
|
|
@ -1568,14 +1620,25 @@ impl Pipeline {
|
||||||
// per-solid LOD / frustum cull in the batched path (the batch is
|
// per-solid LOD / frustum cull in the batched path (the batch is
|
||||||
// resident in full); that is reintroduced separately if needed.
|
// resident in full); that is reintroduced separately if needed.
|
||||||
if hidden_line {
|
if hidden_line {
|
||||||
|
// Depth-only prepass: every solid surface occludes hidden edges,
|
||||||
|
// so both the opaque and the transparent tris write depth here.
|
||||||
pass.set_pipeline(&self.mesh_depth_pipeline);
|
pass.set_pipeline(&self.mesh_depth_pipeline);
|
||||||
for c in &self.gpu_mesh_batch {
|
for c in &self.gpu_mesh_batch {
|
||||||
if c.index_count == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
|
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
|
||||||
pass.set_index_buffer(c.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
if c.index_count != 0 {
|
||||||
pass.draw_indexed(0..c.index_count, 0, 0..1);
|
pass.set_index_buffer(
|
||||||
|
c.index_buffer.slice(..),
|
||||||
|
wgpu::IndexFormat::Uint32,
|
||||||
|
);
|
||||||
|
pass.draw_indexed(0..c.index_count, 0, 0..1);
|
||||||
|
}
|
||||||
|
if c.transp_index_count != 0 {
|
||||||
|
pass.set_index_buffer(
|
||||||
|
c.transp_index_buffer.slice(..),
|
||||||
|
wgpu::IndexFormat::Uint32,
|
||||||
|
);
|
||||||
|
pass.draw_indexed(0..c.transp_index_count, 0, 0..1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pass.set_pipeline(&self.mesh_wireframe_pipeline);
|
pass.set_pipeline(&self.mesh_wireframe_pipeline);
|
||||||
for c in &self.gpu_mesh_batch {
|
for c in &self.gpu_mesh_batch {
|
||||||
|
|
@ -1592,21 +1655,43 @@ impl Pipeline {
|
||||||
} else {
|
} else {
|
||||||
if mesh_wireframe {
|
if mesh_wireframe {
|
||||||
pass.set_pipeline(&self.mesh_wireframe_pipeline);
|
pass.set_pipeline(&self.mesh_wireframe_pipeline);
|
||||||
} else {
|
for c in &self.gpu_mesh_batch {
|
||||||
pass.set_pipeline(&self.mesh_pipeline);
|
if c.wire_index_count == 0 {
|
||||||
}
|
continue;
|
||||||
for c in &self.gpu_mesh_batch {
|
}
|
||||||
let (ibuf, icount) = if mesh_wireframe {
|
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
|
||||||
(&c.wire_index_buffer, c.wire_index_count)
|
pass.set_index_buffer(
|
||||||
} else {
|
c.wire_index_buffer.slice(..),
|
||||||
(&c.index_buffer, c.index_count)
|
wgpu::IndexFormat::Uint32,
|
||||||
};
|
);
|
||||||
if icount == 0 {
|
pass.draw_indexed(0..c.wire_index_count, 0, 0..1);
|
||||||
continue;
|
}
|
||||||
|
} else {
|
||||||
|
// Opaque fills first (they write depth).
|
||||||
|
pass.set_pipeline(&self.mesh_pipeline);
|
||||||
|
for c in &self.gpu_mesh_batch {
|
||||||
|
if c.index_count == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
|
||||||
|
pass.set_index_buffer(c.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
pass.draw_indexed(0..c.index_count, 0, 0..1);
|
||||||
|
}
|
||||||
|
// Transparent fills last, with depth writes disabled, so they
|
||||||
|
// blend over the opaque geometry behind them instead of
|
||||||
|
// culling it via the depth buffer.
|
||||||
|
pass.set_pipeline(&self.mesh_transparent_pipeline);
|
||||||
|
for c in &self.gpu_mesh_batch {
|
||||||
|
if c.transp_index_count == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
|
||||||
|
pass.set_index_buffer(
|
||||||
|
c.transp_index_buffer.slice(..),
|
||||||
|
wgpu::IndexFormat::Uint32,
|
||||||
|
);
|
||||||
|
pass.draw_indexed(0..c.transp_index_count, 0, 0..1);
|
||||||
}
|
}
|
||||||
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
|
|
||||||
pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
|
|
||||||
pass.draw_indexed(0..icount, 0, 0..1);
|
|
||||||
}
|
}
|
||||||
// Selection / hover highlight: tinted copies of the picked
|
// Selection / hover highlight: tinted copies of the picked
|
||||||
// solids, drawn last with the Always-depth highlight pipeline so
|
// solids, drawn last with the Always-depth highlight pipeline so
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue