forked from 1b-agent/opencadstudio-upstream
feat(render): Flat/Gouraud shading, WithEdges overlay, paper-viewport mode
Faz 3 — FlatShaded vs GouraudShaded
- Uniforms.flat_shade (f32, 0/1) + mesh shader fragment derivative
bazlı per-triangle face normal hesabı; GouraudShaded ise interpolate
edilen vertex normalini kullanıyor.
Faz 4 — *WithEdges varyantları
- Pipeline::render.show_3d_edges parametresi:
- FlatShaded / GouraudShaded → 3DFACE edge pass'i atlanıyor
- *WithEdges varyantları → shaded fill'in üstüne mesh_wireframe
pipeline ile triangle-edge overlay (LessEqual depth test ile)
- face3d_edges pass'i show_3d_edges ile gate'lendi
Faz 5 — Paper-space viewport entity round-trip
- build_viewport_primitive / build_active_viewport_primitive artık
render_mode parametresi almıyor; viewport entity'sinin kendi
render_mode alanından okuyor. PaperViewportPane'dan render_mode
field'i kaldırıldı. Model layout'taki tab.render_mode hala
pick_list ile yönetiliyor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
56834dfba6
commit
132efe6574
6 files changed
+141
-56
No files matched your search
+1
-1
@@ -883,7 +883,7 @@ fn paper_canvas_view<'a>(tab: &'a super::document::DocumentTab) -> Element<'a, M
|
||||
let w = rect.width.clamp(1.0, canvas_w - x);
|
||||
let h = rect.height.clamp(1.0, canvas_h - y);
|
||||
|
||||
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle, tab.render_mode))
|
||||
let vp_widget = shader(PaperViewportPane::new(scene, vp_handle))
|
||||
.width(iced::Length::Fixed(w))
|
||||
.height(iced::Length::Fixed(h));
|
||||
|
||||
|
||||
+45
-10
@@ -1028,6 +1028,7 @@ impl Pipeline {
|
||||
bg_color: [f32; 4],
|
||||
mesh_wireframe: bool,
|
||||
hidden_line: bool,
|
||||
show_3d_edges: bool,
|
||||
) {
|
||||
let vp = clip_bounds;
|
||||
let msaa = &self.msaa_view;
|
||||
@@ -1156,15 +1157,17 @@ impl Pipeline {
|
||||
});
|
||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||
// Three draw modes share this pass:
|
||||
// - Solid: `mesh_pipeline` + the triangle index buffer.
|
||||
// - Wireframe: `mesh_wireframe_pipeline` + the pre-built
|
||||
// `wire_index_buffer` (each triangle edge expanded to a
|
||||
// line segment at upload).
|
||||
// - HiddenLine: depth prepass with `mesh_depth_pipeline`
|
||||
// (writes Z, no colour), then wire-edge draw on top.
|
||||
// The wire draw's LessEqual depth test naturally hides
|
||||
// edges occluded by the prepass.
|
||||
// Four draw paths share this pass:
|
||||
// - Solid: `mesh_pipeline` + triangle index buf.
|
||||
// - Wireframe: `mesh_wireframe_pipeline` + the
|
||||
// pre-built `wire_index_buffer`.
|
||||
// - HiddenLine: depth prepass (`mesh_depth_pipeline`,
|
||||
// writes Z, no colour) → wire overlay.
|
||||
// - Solid+Edges: `mesh_pipeline` shaded fill → wire
|
||||
// overlay; LessEqual depth test on the
|
||||
// wire pass keeps the edges crisp on
|
||||
// top of the shaded surface.
|
||||
let want_solid_with_edges = !hidden_line && !mesh_wireframe && show_3d_edges;
|
||||
if hidden_line {
|
||||
pass.set_pipeline(&self.mesh_depth_pipeline);
|
||||
for (i, set) in self.gpu_meshes.iter().enumerate() {
|
||||
@@ -1242,6 +1245,36 @@ impl Pipeline {
|
||||
pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..icount, 0, 0..1);
|
||||
}
|
||||
// *WithEdges variants: overlay wire-edge segments on top
|
||||
// of the shaded fill. The LessEqual depth test on the
|
||||
// wireframe pipeline keeps the edges visible at the
|
||||
// fragments that just got written by the solid pass.
|
||||
if want_solid_with_edges {
|
||||
pass.set_pipeline(&self.mesh_wireframe_pipeline);
|
||||
for (i, set) in self.gpu_meshes.iter().enumerate() {
|
||||
if !self.mesh_visible.get(i).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let level = self
|
||||
.mesh_lod_levels
|
||||
.get(i)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.min(set.lods.len().saturating_sub(1));
|
||||
let Some(mesh) = set.lods.get(level) else {
|
||||
continue;
|
||||
};
|
||||
if mesh.wire_index_count == 0 {
|
||||
continue;
|
||||
}
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
pass.set_index_buffer(
|
||||
mesh.wire_index_buffer.slice(..),
|
||||
wgpu::IndexFormat::Uint32,
|
||||
);
|
||||
pass.draw_indexed(0..mesh.wire_index_count, 0, 0..1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1293,7 +1326,9 @@ impl Pipeline {
|
||||
}
|
||||
|
||||
// ── Pass 5b: 3DFACE edges (batched, possibly multiple chunks) ────
|
||||
if !self.gpu_face3d_edges.is_empty() {
|
||||
// FlatShaded / GouraudShaded hide the 3DFACE outline (the user
|
||||
// chose a clean shaded look); every other mode keeps it.
|
||||
if show_3d_edges && !self.gpu_face3d_edges.is_empty() {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("face3d_edges.render_pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
|
||||
@@ -14,6 +14,14 @@ pub struct Uniforms {
|
||||
/// LWDISPLAY toggle (1.0 = show lineweights, 0.0 = force 1 px).
|
||||
/// Read by the wire shader so the toggle does not require a retessellate.
|
||||
pub lwdisplay_enable: f32,
|
||||
/// 1.0 → mesh fragment shader replaces the interpolated vertex
|
||||
/// normal with `normalize(cross(dpdx(pos), dpdy(pos)))` so each
|
||||
/// triangle gets a uniform shade (FlatShaded mode); 0.0 → keeps the
|
||||
/// per-vertex normal interpolation (GouraudShaded-style).
|
||||
pub flat_shade: f32,
|
||||
/// Pads the struct to 112 B (next multiple of 16) so wgpu's uniform
|
||||
/// alignment rules are satisfied.
|
||||
pub _pad: [f32; 3],
|
||||
}
|
||||
|
||||
impl Uniforms {
|
||||
@@ -30,6 +38,8 @@ impl Uniforms {
|
||||
viewport_size: [bounds.width, bounds.height],
|
||||
world_per_pixel,
|
||||
lwdisplay_enable: if lwdisplay_enable { 1.0 } else { 0.0 },
|
||||
flat_shade: 0.0,
|
||||
_pad: [0.0; 3],
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
-10
@@ -107,12 +107,10 @@ pub struct Primitive {
|
||||
/// in `Wireframe2D` / `Wireframe3D`; on for every shaded variant. Set
|
||||
/// at the same point `view_wireframe` is computed so the two stay in
|
||||
/// lock-step for the gating logic in `prepare()`.
|
||||
#[allow(dead_code)]
|
||||
pub(super) mesh_fill: bool,
|
||||
/// Whether the active render mode wants 3D mesh / face edges
|
||||
/// rendered on top of fills. Most shaded modes turn this off; the
|
||||
/// `*WithEdges` variants and the pure wireframes leave it on.
|
||||
#[allow(dead_code)]
|
||||
pub(super) show_3d_edges: bool,
|
||||
/// HiddenLine routes 3D fills through a depth-only prepass so edges
|
||||
/// occluded by closer geometry are culled by the LessEqual depth
|
||||
@@ -138,6 +136,11 @@ pub struct RenderModeFlags {
|
||||
pub mesh_fill: bool,
|
||||
pub show_3d_edges: bool,
|
||||
pub hidden_line: bool,
|
||||
/// `true` for FlatShaded / FlatShadedWithEdges. The mesh shader
|
||||
/// reads `Uniforms.flat_shade` and replaces the smooth per-vertex
|
||||
/// normal with a per-triangle face normal so each triangle reads
|
||||
/// as a single tone.
|
||||
pub flat_shade: bool,
|
||||
}
|
||||
|
||||
pub fn render_mode_flags(
|
||||
@@ -150,24 +153,42 @@ pub fn render_mode_flags(
|
||||
mesh_fill: false,
|
||||
show_3d_edges: true,
|
||||
hidden_line: false,
|
||||
flat_shade: false,
|
||||
},
|
||||
M::HiddenLine => RenderModeFlags {
|
||||
face3d_fill: true,
|
||||
mesh_fill: true,
|
||||
show_3d_edges: true,
|
||||
hidden_line: true,
|
||||
flat_shade: false,
|
||||
},
|
||||
M::FlatShaded | M::GouraudShaded => RenderModeFlags {
|
||||
M::FlatShaded => RenderModeFlags {
|
||||
face3d_fill: true,
|
||||
mesh_fill: true,
|
||||
show_3d_edges: false,
|
||||
hidden_line: false,
|
||||
flat_shade: true,
|
||||
},
|
||||
M::FlatShadedWithEdges | M::GouraudShadedWithEdges => RenderModeFlags {
|
||||
M::GouraudShaded => RenderModeFlags {
|
||||
face3d_fill: true,
|
||||
mesh_fill: true,
|
||||
show_3d_edges: false,
|
||||
hidden_line: false,
|
||||
flat_shade: false,
|
||||
},
|
||||
M::FlatShadedWithEdges => RenderModeFlags {
|
||||
face3d_fill: true,
|
||||
mesh_fill: true,
|
||||
show_3d_edges: true,
|
||||
hidden_line: false,
|
||||
flat_shade: true,
|
||||
},
|
||||
M::GouraudShadedWithEdges => RenderModeFlags {
|
||||
face3d_fill: true,
|
||||
mesh_fill: true,
|
||||
show_3d_edges: true,
|
||||
hidden_line: false,
|
||||
flat_shade: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -267,6 +288,7 @@ impl shader::Primitive for Primitive {
|
||||
self.bg_color,
|
||||
mesh_wireframe,
|
||||
self.hidden_line,
|
||||
self.show_3d_edges,
|
||||
);
|
||||
if self.show_viewcube {
|
||||
pipeline.viewcube.render(encoder, target, *clip);
|
||||
@@ -455,6 +477,9 @@ impl Scene {
|
||||
self.paper_bg_color
|
||||
};
|
||||
|
||||
let mut uniforms = Uniforms::new(&cam, bounds, self.document.header.lineweight_display);
|
||||
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
|
||||
|
||||
Primitive {
|
||||
wires: all_wires,
|
||||
face3d_wires: Arc::new(face3d_wires),
|
||||
@@ -462,7 +487,7 @@ impl Scene {
|
||||
wipeout_hatches: self.wipeout_models_arc(),
|
||||
images: self.images_arc(),
|
||||
meshes: self.meshes_arc(),
|
||||
uniforms: Uniforms::new(&cam, bounds, self.document.header.lineweight_display),
|
||||
uniforms,
|
||||
cam_rotation: cam.view_rotation_mat(),
|
||||
hover_region,
|
||||
bg_color,
|
||||
@@ -478,14 +503,20 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Build a Primitive that renders model-space content through a specific
|
||||
/// paper-space viewport's camera, applying its layer-freeze list.
|
||||
/// paper-space viewport's camera, applying its layer-freeze list. The
|
||||
/// render mode is read from the viewport entity itself (each paper-space
|
||||
/// viewport carries its own visual style) — the model-space pick_list
|
||||
/// only governs the Model layout.
|
||||
pub(super) fn build_viewport_primitive(
|
||||
&self,
|
||||
vp_handle: Handle,
|
||||
hover_region: Option<usize>,
|
||||
bounds: Rectangle,
|
||||
render_mode: acadrust::entities::ViewportRenderMode,
|
||||
) -> Primitive {
|
||||
let render_mode = match self.document.get_entity(vp_handle) {
|
||||
Some(EntityType::Viewport(vp)) => vp.render_mode,
|
||||
_ => acadrust::entities::ViewportRenderMode::Wireframe2D,
|
||||
};
|
||||
let flags = render_mode_flags(render_mode);
|
||||
let view_wireframe = !flags.face3d_fill;
|
||||
let cam = match self.camera_for_viewport(vp_handle) {
|
||||
@@ -506,6 +537,9 @@ impl Scene {
|
||||
Arc::new(v)
|
||||
};
|
||||
|
||||
let mut uniforms = Uniforms::new(&cam, bounds, self.document.header.lineweight_display);
|
||||
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
|
||||
|
||||
Primitive {
|
||||
wires: all_wires,
|
||||
face3d_wires: Arc::new(face3d_wires),
|
||||
@@ -513,7 +547,7 @@ impl Scene {
|
||||
wipeout_hatches: self.wipeout_models_arc(),
|
||||
images: self.images_arc(),
|
||||
meshes: self.meshes_arc(),
|
||||
uniforms: Uniforms::new(&cam, bounds, self.document.header.lineweight_display),
|
||||
uniforms,
|
||||
cam_rotation: cam.view_rotation_mat(),
|
||||
hover_region,
|
||||
bg_color: self.bg_color,
|
||||
@@ -535,10 +569,9 @@ impl Scene {
|
||||
vp_handle: Handle,
|
||||
hover_region: Option<usize>,
|
||||
bounds: Rectangle,
|
||||
render_mode: acadrust::entities::ViewportRenderMode,
|
||||
) -> PaperViewportPrimitive {
|
||||
PaperViewportPrimitive(
|
||||
self.build_viewport_primitive(vp_handle, hover_region, bounds, render_mode),
|
||||
self.build_viewport_primitive(vp_handle, hover_region, bounds),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+10
-23
@@ -20,6 +20,9 @@ pub struct ViewportPane<'a> {
|
||||
pub scene: &'a Scene,
|
||||
pub mode: ViewportPaneMode,
|
||||
pub show_viewcube: bool,
|
||||
/// Render mode for the **Model** layout view. Paper-space viewports
|
||||
/// read their own `render_mode` field from the viewport entity, so
|
||||
/// this value is ignored when `mode` is `Paper`.
|
||||
pub render_mode: acadrust::entities::ViewportRenderMode,
|
||||
}
|
||||
|
||||
@@ -59,20 +62,11 @@ impl<'a> ViewportPane<'a> {
|
||||
pub struct PaperViewportPane<'a> {
|
||||
pub scene: &'a Scene,
|
||||
pub handle: Handle,
|
||||
pub render_mode: acadrust::entities::ViewportRenderMode,
|
||||
}
|
||||
|
||||
impl<'a> PaperViewportPane<'a> {
|
||||
pub fn new(
|
||||
scene: &'a Scene,
|
||||
handle: Handle,
|
||||
render_mode: acadrust::entities::ViewportRenderMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
scene,
|
||||
handle,
|
||||
render_mode,
|
||||
}
|
||||
pub fn new(scene: &'a Scene, handle: Handle) -> Self {
|
||||
Self { scene, handle }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,12 +81,7 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for PaperViewportPan
|
||||
bounds: Rectangle,
|
||||
) -> Self::Primitive {
|
||||
self.scene
|
||||
.build_active_viewport_primitive(
|
||||
self.handle,
|
||||
state.hover_region,
|
||||
bounds,
|
||||
self.render_mode,
|
||||
)
|
||||
.build_active_viewport_primitive(self.handle, state.hover_region, bounds)
|
||||
}
|
||||
|
||||
fn update(
|
||||
@@ -136,12 +125,10 @@ impl<'a, Msg: std::fmt::Debug + Clone> shader::Program<Msg> for ViewportPane<'a>
|
||||
self.show_viewcube,
|
||||
self.render_mode,
|
||||
),
|
||||
ViewportPaneMode::Paper { handle } => self.scene.build_viewport_primitive(
|
||||
*handle,
|
||||
state.hover_region,
|
||||
bounds,
|
||||
self.render_mode,
|
||||
),
|
||||
ViewportPaneMode::Paper { handle } => {
|
||||
self.scene
|
||||
.build_viewport_primitive(*handle, state.hover_region, bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-12
@@ -2,13 +2,23 @@
|
||||
//
|
||||
// Vertex layout: position [f32;3], normal [f32;3], color [f32;4] (40 bytes)
|
||||
//
|
||||
// Lighting: simple half-Lambert with a fixed directional light so solids look
|
||||
// solid regardless of camera angle. Selection highlight overrides color.
|
||||
// Lighting: simple half-Lambert with a fixed directional light. Two
|
||||
// shading paths share this shader, picked per-frame via `u.flat_shade`:
|
||||
// - 0.0 → per-vertex normals interpolated to the fragment (Gouraud).
|
||||
// - 1.0 → per-triangle face normal from screen-space derivatives
|
||||
// `cross(dpdx(pos), dpdy(pos))`, so each triangle reads as a single
|
||||
// flat shade (FlatShaded).
|
||||
|
||||
struct Uniforms {
|
||||
view_proj: mat4x4<f32>,
|
||||
viewport: vec2<f32>,
|
||||
_pad: vec2<f32>,
|
||||
view_proj: mat4x4<f32>,
|
||||
camera_pos: vec4<f32>,
|
||||
viewport_size: vec2<f32>,
|
||||
world_per_pixel: f32,
|
||||
lwdisplay_enable: f32,
|
||||
flat_shade: f32,
|
||||
_pad0: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
@@ -21,17 +31,19 @@ struct VertexIn {
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) world_pos: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(v: VertexIn) -> VertexOut {
|
||||
var out: VertexOut;
|
||||
out.clip_pos = u.view_proj * vec4<f32>(v.position, 1.0);
|
||||
out.color = v.color;
|
||||
out.normal = v.normal;
|
||||
out.clip_pos = u.view_proj * vec4<f32>(v.position, 1.0);
|
||||
out.color = v.color;
|
||||
out.normal = v.normal;
|
||||
out.world_pos = v.position;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -39,7 +51,15 @@ fn vs_main(v: VertexIn) -> VertexOut {
|
||||
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
// Fixed directional light (world space, tilted toward the viewer).
|
||||
let light_dir = normalize(vec3<f32>(0.4, 0.8, 0.6));
|
||||
let n = normalize(in.normal);
|
||||
var n: vec3<f32>;
|
||||
if (u.flat_shade > 0.5) {
|
||||
// Per-triangle face normal: derivatives of the interpolated
|
||||
// world position are constant across the primitive, so every
|
||||
// fragment in the same triangle sees the same normal.
|
||||
n = normalize(cross(dpdx(in.world_pos), dpdy(in.world_pos)));
|
||||
} else {
|
||||
n = normalize(in.normal);
|
||||
}
|
||||
// Half-Lambert: [0.5, 1.0] so the dark side isn't fully black.
|
||||
let diff = clamp(dot(n, light_dir), 0.0, 1.0) * 0.5 + 0.5;
|
||||
let rgb = in.color.rgb * diff;
|
||||
|
||||
Reference in new issue
Block a user