Viewport: silhouette outlines on curved surfaces (back faces pushed out on screen, drawn behind the model) in the styles with edges; new visual style Shaded with Hidden Edges (edges behind the model drawn faintly)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
aee1e941bf
commit
54c7690271
4 files changed
+82
-5
No files matched your search
@@ -27,6 +27,8 @@ pub struct GpuScene {
|
||||
pub glass: Vec<u8>,
|
||||
/// Depth-tested lines.
|
||||
pub lines: Vec<u8>,
|
||||
/// Lines drawn only where the model hides them (hidden edges).
|
||||
pub hidden: Vec<u8>,
|
||||
/// Lines drawn over everything (active sketch, highlights).
|
||||
pub overlay: Vec<u8>,
|
||||
/// Translucent triangles pushed slightly back in depth, so they only show where nothing
|
||||
@@ -52,6 +54,14 @@ impl GpuScene {
|
||||
}
|
||||
self.trans.extend_from_slice(&c);
|
||||
}
|
||||
/// A line shown only where the model hides it.
|
||||
pub fn hidden_line(&mut self, a: [f32; 3], b: [f32; 3], c: [u8; 4], width: f32) {
|
||||
for v in a.iter().chain(&b) {
|
||||
self.hidden.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
self.hidden.extend_from_slice(&c);
|
||||
self.hidden.extend_from_slice(&width.to_le_bytes());
|
||||
}
|
||||
pub fn glass_tri(&mut self, p: [f32; 3], n: [f32; 3], c: [u8; 4]) {
|
||||
for v in p.iter().chain(&n) {
|
||||
self.glass.extend_from_slice(&v.to_le_bytes());
|
||||
@@ -134,6 +144,8 @@ pub struct ViewportCallback {
|
||||
pub analysis: [f32; 8],
|
||||
/// Accessibility: the depth map seen from the access direction.
|
||||
pub access: Option<AccessMap>,
|
||||
/// Draw silhouette outlines around the model (visual styles with edges).
|
||||
pub outline: bool,
|
||||
}
|
||||
|
||||
/// Heights of the model seen from a direction (an orthographic depth map): a point is reachable
|
||||
@@ -162,6 +174,7 @@ struct Batches {
|
||||
trans: Option<Batch>,
|
||||
glass: Option<Batch>,
|
||||
lines: Option<Batch>,
|
||||
hidden: Option<Batch>,
|
||||
overlays: Option<Batch>,
|
||||
ghost: Option<Batch>,
|
||||
xray: Option<Batch>,
|
||||
@@ -178,6 +191,7 @@ impl Batches {
|
||||
self.trans = upload(device, "sc_trans", &sc.trans, TRI_SIZE);
|
||||
self.glass = upload(device, "sc_glass", &sc.glass, TRI_SIZE);
|
||||
self.lines = upload(device, "sc_lines", &sc.lines, LINE_SIZE);
|
||||
self.hidden = upload(device, "sc_hidden", &sc.hidden, LINE_SIZE);
|
||||
self.overlays = upload(device, "sc_overlay", &sc.overlay, LINE_SIZE);
|
||||
self.ghost = upload(device, "sc_ghost", &sc.ghost, TRI_SIZE);
|
||||
self.xray = upload(device, "sc_xray", &sc.xray, TRI_SIZE);
|
||||
@@ -198,6 +212,10 @@ struct Resources {
|
||||
ghost: wgpu::RenderPipeline,
|
||||
xray: wgpu::RenderPipeline,
|
||||
line: wgpu::RenderPipeline,
|
||||
/// Lines behind the model (depth test >).
|
||||
line_hidden: wgpu::RenderPipeline,
|
||||
/// Silhouettes: back faces pushed out a little on screen, drawn dark behind the model.
|
||||
outline: wgpu::RenderPipeline,
|
||||
overlay: wgpu::RenderPipeline,
|
||||
uniform: wgpu::Buffer,
|
||||
bind: wgpu::BindGroup,
|
||||
@@ -447,6 +465,28 @@ fn fs_line(i: LOut) -> @location(0) vec4<f32> {
|
||||
return out_color(i.c);
|
||||
}
|
||||
|
||||
/// Silhouettes: back faces pushed out along their screen-space normal by about 1.5 px.
|
||||
@vertex
|
||||
fn vs_outline(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
let nc = (u.vp * vec4<f32>(n, 0.0)).xy * u.screen.xy;
|
||||
if (length(nc) > 1e-6) {
|
||||
let d = normalize(nc) * 1.5 * 2.0 / u.screen.xy;
|
||||
o.pos = vec4<f32>(o.pos.xy + d * o.pos.w, o.pos.zw);
|
||||
}
|
||||
o.n = vec3<f32>(0.0);
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_outline(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return out_color(vec4<f32>(0.07, 0.08, 0.10, 1.0));
|
||||
}
|
||||
|
||||
struct IOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@@ -701,6 +741,32 @@ impl Resources {
|
||||
ghost: pipeline("sc_ghost", "vs_ghost", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
xray: pipeline("sc_xray", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
||||
line: pipeline("sc_lines", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
line_hidden: pipeline("sc_lines_hidden", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::Greater, false), alpha),
|
||||
outline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("sc_outline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs_outline"),
|
||||
buffers: &[Some(tri_layout())],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
cull_mode: Some(wgpu::Face::Front),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: depth(wgpu::CompareFunction::Less, false),
|
||||
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs_outline"),
|
||||
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: alpha, write_mask: wgpu::ColorWrites::ALL })],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
}),
|
||||
overlay: pipeline("sc_overlay", "vs_line", "fs_line_all", line_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
||||
uniform,
|
||||
bind,
|
||||
@@ -834,7 +900,11 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
tris(pass, &res.tri, &m.tris);
|
||||
tris(pass, &res.tri, &pv.tris);
|
||||
tris(pass, &res.tri_hl, &h.tris);
|
||||
if self.outline {
|
||||
tris(pass, &res.outline, &m.tris);
|
||||
}
|
||||
lines(pass, &res.line, &m.lines);
|
||||
lines(pass, &res.line_hidden, &m.hidden);
|
||||
lines(pass, &res.line, &pv.lines);
|
||||
lines(pass, &res.line, &h.lines);
|
||||
for (id, q) in &res.quads {
|
||||
|
||||
@@ -300,7 +300,7 @@ impl SolveApp {
|
||||
self.autosave_minutes = m.clamp(0.0, 600.0);
|
||||
}
|
||||
if let Some(s) = v.get("visual_style").and_then(Value::as_u64) {
|
||||
self.ui.visual_style = u8::try_from(s.min(2)).unwrap_or(0);
|
||||
self.ui.visual_style = u8::try_from(s.min(3)).unwrap_or(0);
|
||||
}
|
||||
// Browser folders the user opened or closed.
|
||||
let keys = |k: &str| -> std::collections::BTreeSet<String> {
|
||||
|
||||
@@ -284,8 +284,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.checkbox(&mut app.ui.perspective, "");
|
||||
ui.end_row();
|
||||
ui.label("Visual style");
|
||||
let styles = ["Shaded with edges", "Shaded", "Wireframe"];
|
||||
egui::ComboBox::from_id_salt("pref_style").selected_text(styles[usize::from(app.ui.visual_style.min(2))]).show_ui(
|
||||
let styles = ["Shaded with edges", "Shaded", "Wireframe", "Shaded with hidden edges"];
|
||||
egui::ComboBox::from_id_salt("pref_style").selected_text(styles[usize::from(app.ui.visual_style.min(3))]).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (i, s) in styles.iter().enumerate() {
|
||||
|
||||
@@ -226,6 +226,9 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
_ => (true, true),
|
||||
};
|
||||
let edge_col = if shaded { c4(tk.body_edge) } else { c4(tk.text) };
|
||||
// Shaded with hidden edges: edges behind the model show faintly.
|
||||
let hidden_edges = app.ui.visual_style == 3;
|
||||
let hidden_col = c4(tk.text.gamma_multiply(0.35));
|
||||
// Appearances: the body's opacity, and faces with looks of their own.
|
||||
let opacity = s.doc.body_look(&b.name, b.feature).map_or(1.0, |l| l.opacity);
|
||||
let face_looks: std::collections::HashMap<usize, solvecraft_engine::doc::appearance::Look> =
|
||||
@@ -261,6 +264,9 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
}
|
||||
for w in e.windows(2) {
|
||||
sc.line(w[0].to_f32(), w[1].to_f32(), edge_col, 1.3, false);
|
||||
if hidden_edges {
|
||||
sc.hidden_line(w[0].to_f32(), w[1].to_f32(), hidden_col, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,6 +1229,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
images: crate::ref_images::gpu_images(app),
|
||||
analysis: analysis_uniform(app),
|
||||
access: crate::access::map(app),
|
||||
outline: matches!(app.ui.visual_style, 0 | 3),
|
||||
};
|
||||
painter.add(egui_wgpu::Callback::new_paint_callback(rect, cb));
|
||||
} else {
|
||||
@@ -1852,7 +1859,7 @@ fn nav_bar(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
("lookat", "Look At the selected face or plane (or the active sketch)"),
|
||||
("home", "Home view"),
|
||||
("perspective", "Perspective / orthographic"),
|
||||
("style", "Visual style: shaded with edges, shaded, wireframe"),
|
||||
("style", "Visual style: shaded with edges, shaded, wireframe, shaded with hidden edges"),
|
||||
("settings", "Grid on/off"),
|
||||
];
|
||||
let w = items.len() as f32 * 30.0 + 10.0;
|
||||
@@ -1883,7 +1890,7 @@ fn nav_bar(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
"fit" => app.animate_view("fit"),
|
||||
"home" => app.animate_view("home"),
|
||||
"lookat" => look_at_selection(app),
|
||||
"style" => app.ui.visual_style = (app.ui.visual_style + 1) % 3,
|
||||
"style" => app.ui.visual_style = (app.ui.visual_style + 1) % 4,
|
||||
"perspective" => app.ui.perspective = !app.ui.perspective,
|
||||
_ => app.ui.show_grid = !app.ui.show_grid,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user