feat(render): draw 3D mesh outline edges black in filled modes
Extends the solid feature-edge behaviour to mesh entities (PolyfaceMesh / PolygonMesh): their triangulation outline edges now render black when a filled-with-edges mode is active, and keep the entity colour in the lines-only modes (wireframe / hidden-line). A mesh entity emits its face fill and its outline edges as separate WireModels sharing the entity handle: the fill carries fill_tris plus a non-empty fill_tris_low (real 3D depth), the edge carries points. The old is_3d_mesh_edge flag keyed on `!fill_tris.is_empty()`, which tagged the points-less fill wire rather than the edge wire, so the edges were never recognised. upload_wires now collects the handles of real 3D fill wires and flags any points-wire sharing one as the mesh edge (text greek, whose fill has an empty fill_tris_low, is excluded — matching the face3d test). The wire draw loop then routes flagged edges through a new black wire pipeline (fs_black) in filled-with-edges modes. fs_black lives in wire_indexed.wgsl (the native shader) and wire.wgsl (the wasm shader). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1065bc12b2
commit
e6a961be6c
3 changed files with 106 additions and 2 deletions
|
|
@ -29,6 +29,9 @@ const MSAA_SAMPLES: u32 = 4;
|
|||
|
||||
pub struct Pipeline {
|
||||
wire_pipeline: wgpu::RenderPipeline,
|
||||
/// Black-fragment variant of `wire_pipeline` for 3D mesh outline edges in
|
||||
/// filled render modes.
|
||||
wire_black_pipeline: wgpu::RenderPipeline,
|
||||
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
|
|
@ -283,6 +286,48 @@ impl Pipeline {
|
|||
cache: None,
|
||||
});
|
||||
|
||||
// Black variant of `wire_pipeline` — same geometry/depth, black fragment
|
||||
// — for 3D mesh outline edges in filled modes (see the wire draw loop).
|
||||
let wire_black_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("wire.black.pipeline"),
|
||||
layout: Some(&wire_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &wire_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[wire_gpu::WireInstance::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: true,
|
||||
depth_compare: wgpu::CompareFunction::LessEqual,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: MSAA_SAMPLES,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &wire_shader,
|
||||
entry_point: Some("fs_black"),
|
||||
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,
|
||||
});
|
||||
|
||||
// Selection overlay variant: renders selected wires on top of everything
|
||||
// (depth_compare=Always), without writing depth. Ensures selected entities
|
||||
// are always fully visible regardless of occluding geometry.
|
||||
|
|
@ -1053,6 +1098,7 @@ impl Pipeline {
|
|||
|
||||
Self {
|
||||
wire_pipeline,
|
||||
wire_black_pipeline,
|
||||
wire_xray_pipeline,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_const_bgl: Some(wire_const_bgl),
|
||||
|
|
@ -1128,15 +1174,28 @@ impl Pipeline {
|
|||
// and alpha blending both depend on it. Scissor and mesh-edge stay
|
||||
// grouping keys because the draw loop sets one scissor per batch and
|
||||
// skips whole mesh-edge batches in shaded modes.
|
||||
// A 3D mesh entity (PolyfaceMesh / PolygonMesh) emits its face fill and
|
||||
// its outline edges as *separate* WireModels sharing the entity handle
|
||||
// (`name`): the fill carries `fill_tris` + a non-empty `fill_tris_low`
|
||||
// (real 3D depth, same test face3d uses); the edge carries `points`.
|
||||
// Flag the edge wire as `is_3d_mesh_edge` so the draw loop can hide it in
|
||||
// clean-shaded modes and draw it black in filled-with-edges modes.
|
||||
let mesh_names: rustc_hash::FxHashSet<&str> = wires
|
||||
.iter()
|
||||
.filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
|
||||
.map(|w| w.name.as_str())
|
||||
.collect();
|
||||
let is_mesh_edge =
|
||||
|w: &WireModel| !w.points.is_empty() && mesh_names.contains(w.name.as_str());
|
||||
let mut batches: Vec<WireGpu> = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < wires.len() {
|
||||
let scissor = wires[i].vp_scissor;
|
||||
let mesh_edge = !wires[i].fill_tris.is_empty();
|
||||
let mesh_edge = is_mesh_edge(&wires[i]);
|
||||
let mut j = i + 1;
|
||||
while j < wires.len()
|
||||
&& wires[j].vp_scissor == scissor
|
||||
&& (!wires[j].fill_tris.is_empty()) == mesh_edge
|
||||
&& is_mesh_edge(&wires[j]) == mesh_edge
|
||||
{
|
||||
j += 1;
|
||||
}
|
||||
|
|
@ -1861,7 +1920,13 @@ impl Pipeline {
|
|||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_pipeline(&self.wire_pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
// In a filled-with-edges mode the mesh outline edges frame the shaded
|
||||
// fill and should read black; wireframe / hidden-line keep the entity
|
||||
// colour. `wire_black_pipeline` shares the wire layout, so the switch
|
||||
// needs no bind-group rebind.
|
||||
let want_solid_with_edges = !hidden_line && !mesh_wireframe && show_3d_edges;
|
||||
let mut scissor_active = false;
|
||||
let mut black_active = false;
|
||||
for (i, wire) in self.gpu_wires.iter().enumerate() {
|
||||
if wire.instance_count == 0 {
|
||||
continue;
|
||||
|
|
@ -1875,6 +1940,15 @@ impl Pipeline {
|
|||
if !show_3d_edges && wire.is_3d_mesh_edge {
|
||||
continue;
|
||||
}
|
||||
let use_black = want_solid_with_edges && wire.is_3d_mesh_edge;
|
||||
if use_black != black_active {
|
||||
pass.set_pipeline(if use_black {
|
||||
&self.wire_black_pipeline
|
||||
} else {
|
||||
&self.wire_pipeline
|
||||
});
|
||||
black_active = use_black;
|
||||
}
|
||||
match self.wire_pixel_scissors.get(i) {
|
||||
Some(Some([x, y, w, h])) => {
|
||||
pass.set_scissor_rect(*x, *y, *w, *h);
|
||||
|
|
|
|||
|
|
@ -206,3 +206,18 @@ fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
|||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(in.color.rgb, alpha);
|
||||
}
|
||||
|
||||
// Black variant: used for 3D mesh outline edges in filled render modes so the
|
||||
// mesh reads as a shaded surface framed by black edges. Keeps the dash/LOD
|
||||
// logic identical to `fs_main`; only the RGB is forced to black.
|
||||
@fragment fn fs_black(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
if in.pattern_length > 0.0 {
|
||||
if in.min_elem >= u.world_per_pixel {
|
||||
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
}
|
||||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(0.0, 0.0, 0.0, alpha);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,3 +149,18 @@ fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
|||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(in.color.rgb, alpha);
|
||||
}
|
||||
|
||||
// Black variant: used for 3D mesh outline edges in filled render modes so the
|
||||
// mesh reads as a shaded surface framed by black edges. Keeps the dash/LOD
|
||||
// logic identical to `fs_main`; only the RGB is forced to black.
|
||||
@fragment fn fs_black(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
if in.pattern_length > 0.0 {
|
||||
if in.min_elem >= u.world_per_pixel {
|
||||
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
}
|
||||
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(0.0, 0.0, 0.0, alpha);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue