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 {
|
pub struct Pipeline {
|
||||||
wire_pipeline: wgpu::RenderPipeline,
|
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.
|
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||||
|
|
@ -283,6 +286,48 @@ impl Pipeline {
|
||||||
cache: None,
|
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
|
// Selection overlay variant: renders selected wires on top of everything
|
||||||
// (depth_compare=Always), without writing depth. Ensures selected entities
|
// (depth_compare=Always), without writing depth. Ensures selected entities
|
||||||
// are always fully visible regardless of occluding geometry.
|
// are always fully visible regardless of occluding geometry.
|
||||||
|
|
@ -1053,6 +1098,7 @@ impl Pipeline {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
wire_pipeline,
|
wire_pipeline,
|
||||||
|
wire_black_pipeline,
|
||||||
wire_xray_pipeline,
|
wire_xray_pipeline,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
wire_const_bgl: Some(wire_const_bgl),
|
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
|
// and alpha blending both depend on it. Scissor and mesh-edge stay
|
||||||
// grouping keys because the draw loop sets one scissor per batch and
|
// grouping keys because the draw loop sets one scissor per batch and
|
||||||
// skips whole mesh-edge batches in shaded modes.
|
// 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 batches: Vec<WireGpu> = Vec::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < wires.len() {
|
while i < wires.len() {
|
||||||
let scissor = wires[i].vp_scissor;
|
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;
|
let mut j = i + 1;
|
||||||
while j < wires.len()
|
while j < wires.len()
|
||||||
&& wires[j].vp_scissor == scissor
|
&& wires[j].vp_scissor == scissor
|
||||||
&& (!wires[j].fill_tris.is_empty()) == mesh_edge
|
&& is_mesh_edge(&wires[j]) == mesh_edge
|
||||||
{
|
{
|
||||||
j += 1;
|
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_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_pipeline(&self.wire_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
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 scissor_active = false;
|
||||||
|
let mut black_active = false;
|
||||||
for (i, wire) in self.gpu_wires.iter().enumerate() {
|
for (i, wire) in self.gpu_wires.iter().enumerate() {
|
||||||
if wire.instance_count == 0 {
|
if wire.instance_count == 0 {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1875,6 +1940,15 @@ impl Pipeline {
|
||||||
if !show_3d_edges && wire.is_3d_mesh_edge {
|
if !show_3d_edges && wire.is_3d_mesh_edge {
|
||||||
continue;
|
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) {
|
match self.wire_pixel_scissors.get(i) {
|
||||||
Some(Some([x, y, w, h])) => {
|
Some(Some([x, y, w, h])) => {
|
||||||
pass.set_scissor_rect(*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);
|
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||||
return vec4<f32>(in.color.rgb, alpha);
|
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);
|
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
|
||||||
return vec4<f32>(in.color.rgb, alpha);
|
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