From 8dbdebeedc0ac1315139cccc5034798c4336dc0c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 27 May 2026 05:28:20 +0300 Subject: [PATCH] feat(render): wireframe modes hide mesh & 3D fills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wireframe 2D/3D modlarında Solid3D meshlerini edge-only çizmek için LineList topolojili bir mesh wireframe pipeline ve her üçgenin (a,b),(b,c),(c,a) kenarlarına genişletilmiş ayrı bir index buffer eklendi. PolyfaceMesh / PolygonMesh face fill'leri (fill_tris + dolu points) wireframe'de gizleniyor; text greek / MultiLeader gibi 2D fill'ler (points boş) etkilenmiyor. Co-Authored-By: Claude Opus 4.7 --- src/scene/pipeline/face3d_gpu.rs | 11 ++++ src/scene/pipeline/mesh_gpu.rs | 24 ++++++++ src/scene/pipeline/mod.rs | 102 +++++++++++++++++++++++++++---- src/scene/render.rs | 6 +- 4 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index 8be56c95b..f91cdcd00 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -62,6 +62,7 @@ impl Face3DGpu { device: &wgpu::Device, face3d_wires: &[WireModel], all_wires: &[WireModel], + keep_3d_mesh_fills: bool, ) -> Self { let mut vertices: Vec = Vec::with_capacity(face3d_wires.len() * 6); @@ -91,10 +92,20 @@ impl Face3DGpu { // AO-style dim to them would wash out user-picked colors. Wires // with both fill_tris and points (mesh edges + faces) keep the dim // so PolyfaceMesh / PolygonMesh still look 3-D-shaded. + // + // When `keep_3d_mesh_fills` is false (wireframe render modes), + // skip wires that carry 3-D mesh face data (`points` + `fill_tris`) + // while leaving 2-D fills (text greek, MultiLeader background) + // alone — they're not "3D objects" the wireframe toggle should + // hide. for wire in all_wires { if wire.fill_tris.is_empty() { continue; } + let is_3d_mesh_face = !wire.points.is_empty(); + if is_3d_mesh_face && !keep_3d_mesh_fills { + continue; + } let [r, g, b, a] = wire.color; let fill_color = if wire.points.is_empty() { [r, g, b, a] diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index 6676e2244..5d83a52f5 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -53,6 +53,13 @@ pub struct MeshGpu { pub vertex_buffer: wgpu::Buffer, pub index_buffer: wgpu::Buffer, pub index_count: u32, + /// Line-list index buffer: every triangle `(a, b, c)` from the + /// solid index buffer is expanded into three segments + /// `(a,b)(b,c)(c,a)`. Used by the wireframe-mode render path so 3D + /// solids draw as their triangle edges without needing the + /// `POLYGON_MODE_LINE` device feature. + pub wire_index_buffer: wgpu::Buffer, + pub wire_index_count: u32, } /// GPU-side bundle of MeshLodSet — one MeshGpu per available LOD plus @@ -109,10 +116,27 @@ impl MeshGpu { usage: wgpu::BufferUsages::INDEX, }); + // Wireframe-mode index buffer: expand each triangle into its + // three edge segments. Allocates ~2× the solid index count but + // is cheap compared to mesh tessellation and only happens when + // a new mesh is uploaded. + let mut wire_indices: Vec = Vec::with_capacity(mesh.indices.len() * 2); + for tri in mesh.indices.chunks_exact(3) { + let (a, b, c) = (tri[0], tri[1], tri[2]); + wire_indices.extend_from_slice(&[a, b, b, c, c, a]); + } + let wire_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("mesh.wire_ibuf.{}", mesh.name)), + contents: bytemuck::cast_slice(&wire_indices), + usage: wgpu::BufferUsages::INDEX, + }); + Self { vertex_buffer, index_buffer, index_count: mesh.indices.len() as u32, + wire_index_buffer, + wire_index_count: wire_indices.len() as u32, } } } diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 4e57d1a8a..4afefa4d1 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -38,6 +38,11 @@ pub struct Pipeline { hatch_batched_pipeline: wgpu::RenderPipeline, image_pipeline: wgpu::RenderPipeline, mesh_pipeline: wgpu::RenderPipeline, + /// Wireframe variant of the mesh pipeline (LineList topology, same + /// vertex layout / shader). Used when the active render mode is + /// Wireframe 2D or Wireframe 3D so 3D solids draw as their + /// triangle edges instead of filled faces. + mesh_wireframe_pipeline: wgpu::RenderPipeline, face3d_pipeline: wgpu::RenderPipeline, uniform_buffer: wgpu::Buffer, uniform_bind_group: wgpu::BindGroup, @@ -421,6 +426,51 @@ impl Pipeline { cache: None, }); + // Wireframe variant — same shader / vertex layout / depth state, + // only the input topology changes (LineList) and back-face + // culling drops out (each triangle edge is shared between two + // faces, one of which would otherwise hide the edge). + let mesh_wireframe_pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("mesh.wireframe.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::LineList, + 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: &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, + }); + // ── Face3D pipeline ──────────────────────────────────────────────── let face3d_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("face3d.shader"), @@ -666,6 +716,7 @@ impl Pipeline { hatch_batched_pipeline, image_pipeline, mesh_pipeline, + mesh_wireframe_pipeline, face3d_pipeline, uniform_buffer, uniform_bind_group, @@ -790,18 +841,32 @@ impl Pipeline { // outlines stay on the screen regardless of mode. self.gpu_face3d_edges = WireGpu::from_batch(device, face3d_wires); // Fill buffer: in wireframe mode we drop the 3DFACE quads - // (those come from `face3d_wires.key_vertices`) but keep - // `all_wires.fill_tris` — that channel carries 2D content like - // text-LOD greek rectangles and PolyfaceMesh face triangles - // which shouldn't disappear with the toggle. + // (those come from `face3d_wires.key_vertices`) *and* the + // PolyfaceMesh / PolygonMesh face triangles routed through + // `all_wires.fill_tris` (Face3DGpu::from_wires recognises them + // by the wire having both `points` and `fill_tris`). 2-D + // fill_tris (text-LOD greek, MultiLeader background — `points` + // empty) survive in either mode. + let keep_3d_mesh_fills = !wireframe_only; let face3d_for_fill: &[WireModel] = if wireframe_only { &[] } else { face3d_wires }; + let has_any_2d_fill = all_wires + .iter() + .any(|w| !w.fill_tris.is_empty() && w.points.is_empty()); + let has_any_3d_fill = all_wires + .iter() + .any(|w| !w.fill_tris.is_empty() && !w.points.is_empty()); let has_fills = !face3d_for_fill.is_empty() - || all_wires.iter().any(|w| !w.fill_tris.is_empty()); + || has_any_2d_fill + || (keep_3d_mesh_fills && has_any_3d_fill); if !has_fills { self.gpu_face3d_fill = None; } else { - self.gpu_face3d_fill = - Some(Face3DGpu::from_wires(device, face3d_for_fill, all_wires)); + self.gpu_face3d_fill = Some(Face3DGpu::from_wires( + device, + face3d_for_fill, + all_wires, + keep_3d_mesh_fills, + )); } } @@ -870,6 +935,7 @@ impl Pipeline { target: &wgpu::TextureView, clip_bounds: Rectangle, bg_color: [f32; 4], + mesh_wireframe: bool, ) { let vp = clip_bounds; let msaa = &self.msaa_view; @@ -997,7 +1063,16 @@ impl Pipeline { occlusion_query_set: None, }); pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); - pass.set_pipeline(&self.mesh_pipeline); + // Wireframe mode swaps in the LineList pipeline + the + // per-mesh `wire_index_buffer` (triangle indices expanded + // into edge segments at upload time); everything else stays + // identical so the same uniforms / vertex buffers feed both + // paths. + if mesh_wireframe { + pass.set_pipeline(&self.mesh_wireframe_pipeline); + } else { + pass.set_pipeline(&self.mesh_pipeline); + } pass.set_bind_group(0, &self.uniform_bind_group, &[]); for (i, set) in self.gpu_meshes.iter().enumerate() { if !self.mesh_visible.get(i).copied().unwrap_or(true) { @@ -1012,12 +1087,17 @@ impl Pipeline { let Some(mesh) = set.lods.get(level) else { continue; }; - if mesh.index_count == 0 { + let (ibuf, icount) = if mesh_wireframe { + (&mesh.wire_index_buffer, mesh.wire_index_count) + } else { + (&mesh.index_buffer, mesh.index_count) + }; + if icount == 0 { continue; } pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - pass.draw_indexed(0..mesh.index_count, 0, 0..1); + pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32); + pass.draw_indexed(0..icount, 0, 0..1); } } diff --git a/src/scene/render.rs b/src/scene/render.rs index 6d594c334..d2c2290f4 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -247,7 +247,11 @@ impl shader::Primitive for Primitive { target: &iced::wgpu::TextureView, clip: &Rectangle, ) { - pipeline.render(encoder, target, *clip, self.bg_color); + // `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip + // the draw path so meshes use the wireframe pipeline + the + // pre-built triangle-edge index buffer. + let mesh_wireframe = !self.mesh_fill; + pipeline.render(encoder, target, *clip, self.bg_color, mesh_wireframe); if self.show_viewcube { pipeline.viewcube.render(encoder, target, *clip); }