feat(render): wireframe modes hide mesh & 3D fills

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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-27 05:28:20 +03:00
1 parent 2145ab8a66
commit 8dbdebeedc
4 files changed
+131 -12

No files matched your search

+11
View File
@@ -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<Face3DVertex> = 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]
+24
View File
@@ -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<u32> = 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,
}
}
}
+91 -11
View File
@@ -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<u32>,
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);
}
}
+5 -1
View File
@@ -247,7 +247,11 @@ impl shader::Primitive for Primitive {
target: &iced::wgpu::TextureView,
clip: &Rectangle<u32>,
) {
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);
}