feat(solid3d): view-dependent silhouettes (DISPSILH) on curved faces
ISOLINES show a curved solid from any angle, but its outline still cut across whichever fixed longitudinal lines happened to face the eye. DISPSILH draws the true silhouette — the two lines where a cylinder (or cone) turns edge-on to the view — and it moves with the camera, so it cannot be a baked edge. Tessellation stores a world-space generator per cone/cylinder face on the mesh set (axis, radial frame, radius, cone taper, extent), sharing the face-extent extraction with the ISOLINES collector. Each frame `upload_silhouettes` walks those generators, solves `du·cosθ + dv·sinθ = -tanα·da` for the two edge-on angles (`φ ± acos(...)`, `φ ± π/2` for a cylinder), and builds a line list in the mesh vertex format — double-single, so it stays precise at UTM. It draws through the existing wireframe / edge pipelines in the wireframe and shaded-with-edges passes. Gated on the document's DISPSILH header flag, which defaults off — so the per-frame pass is a no-op unless the drawing asks for it. Scoped to top-level solids: a block instance's generators aren't transformed by the INSTANCE pose yet, so they are cleared on the block path (its isolines and feature edges still draw). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0ce150411a
commit
ae5231427b
5 changed files with 329 additions and 51 deletions
|
|
@ -171,6 +171,8 @@ fn tessellate_acis(
|
|||
/// double-single `edge_verts` / `edge_verts_low`.
|
||||
fn attach_feature_edges(set: &mut MeshLodSet, sat: &SatDocument, isolines: usize) {
|
||||
let xform = body_transform(sat);
|
||||
// World-space curved-face generators for the per-frame silhouette pass.
|
||||
set.curved_gens = collect_curved_gens(sat, xform);
|
||||
let mut seg_pts = collect_feature_edges(sat);
|
||||
// ISOLINES ride the same line list and the same body transform as the
|
||||
// feature edges, so they inherit the offset / per-INSTANCE re-split for free.
|
||||
|
|
@ -199,76 +201,159 @@ fn attach_feature_edges(set: &mut MeshLodSet, sat: &SatDocument, isolines: usize
|
|||
/// rather than showing only its two rim circles. Points are body-local
|
||||
/// (pre-transform), matching [`collect_feature_edges`], so the caller applies
|
||||
/// the body transform uniformly.
|
||||
/// Body-local geometry of one cone/cylinder face: its frame, radius, cone taper
|
||||
/// and the height/angular extent recovered the same way `tess_cone_face` does.
|
||||
/// Shared by the ISOLINES and silhouette-generator collectors.
|
||||
struct ConeFaceGeom {
|
||||
center: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
u_dir: [f64; 3],
|
||||
v_dir: [f64; 3],
|
||||
radius: f64,
|
||||
tan_a: f64,
|
||||
h_min: f64,
|
||||
h_max: f64,
|
||||
theta_min: f64,
|
||||
theta_span: f64,
|
||||
full: bool,
|
||||
}
|
||||
|
||||
fn cone_face_geom(sat: &SatDocument, face: &SatFace) -> Option<ConeFaceGeom> {
|
||||
let surf_rec = sat.resolve(face.surface())?;
|
||||
if surf_rec.entity_type != "cone-surface" {
|
||||
return None;
|
||||
}
|
||||
let cone = SatConeSurface::from_record(surf_rec)?;
|
||||
let (cx, cy, cz) = cone.center();
|
||||
let (ax, ay, az) = cone.axis();
|
||||
let (ux, uy, uz) = cone.major_axis();
|
||||
let radius = cone.radius();
|
||||
let sin_a = cone.sin_half_angle();
|
||||
let cos_a = cone.cos_half_angle();
|
||||
let axis = norm3([ax, ay, az]);
|
||||
let u_dir = norm3([ux, uy, uz]);
|
||||
let v_dir = cross3(axis, u_dir);
|
||||
|
||||
let poly = collect_face_polygon(sat, face, 48);
|
||||
let (mut h_min, mut h_max, mut theta_min, mut theta_max, full) =
|
||||
angular_range(cx, cy, cz, axis, u_dir, v_dir, &poly);
|
||||
if (h_max - h_min).abs() < 1e-9 {
|
||||
if let Some((vmin, vmax)) = cone_axis_span(sat, &cone, axis, [cx, cy, cz]) {
|
||||
h_min = vmin;
|
||||
h_max = vmax;
|
||||
if full {
|
||||
theta_min = 0.0;
|
||||
theta_max = TAU;
|
||||
}
|
||||
}
|
||||
}
|
||||
let theta_span = if full { TAU } else { theta_max - theta_min };
|
||||
if (h_max - h_min).abs() < 1e-10 || theta_span.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let tan_a = if cos_a.abs() > 1e-9 { sin_a / cos_a } else { 0.0 };
|
||||
Some(ConeFaceGeom {
|
||||
center: [cx, cy, cz],
|
||||
axis,
|
||||
u_dir,
|
||||
v_dir,
|
||||
radius,
|
||||
tan_a,
|
||||
h_min,
|
||||
h_max,
|
||||
theta_min,
|
||||
theta_span,
|
||||
full,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_isolines(sat: &SatDocument, count: usize) -> Vec<[f64; 3]> {
|
||||
if count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out: Vec<[f64; 3]> = Vec::new();
|
||||
for face in sat.faces() {
|
||||
let Some(surf_rec) = sat.resolve(face.surface()) else {
|
||||
let Some(g) = cone_face_geom(sat, &face) else {
|
||||
continue;
|
||||
};
|
||||
if surf_rec.entity_type != "cone-surface" {
|
||||
continue;
|
||||
}
|
||||
let Some(cone) = SatConeSurface::from_record(surf_rec) else {
|
||||
continue;
|
||||
};
|
||||
let (cx, cy, cz) = cone.center();
|
||||
let (ax, ay, az) = cone.axis();
|
||||
let (ux, uy, uz) = cone.major_axis();
|
||||
let radius = cone.radius();
|
||||
let sin_a = cone.sin_half_angle();
|
||||
let cos_a = cone.cos_half_angle();
|
||||
let axis = norm3([ax, ay, az]);
|
||||
let u_dir = norm3([ux, uy, uz]);
|
||||
let v_dir = cross3(axis, u_dir);
|
||||
|
||||
// Same face-extent recovery as `tess_cone_face`: read the height and
|
||||
// angular span from the boundary, and when the boundary is a single
|
||||
// closed rim (no height span), recover the top/bottom from the coaxial
|
||||
// circle rims.
|
||||
let poly = collect_face_polygon(sat, &face, 48);
|
||||
let (mut h_min, mut h_max, mut theta_min, mut theta_max, full_circle) =
|
||||
angular_range(cx, cy, cz, axis, u_dir, v_dir, &poly);
|
||||
if (h_max - h_min).abs() < 1e-9 {
|
||||
if let Some((vmin, vmax)) = cone_axis_span(sat, &cone, axis, [cx, cy, cz]) {
|
||||
h_min = vmin;
|
||||
h_max = vmax;
|
||||
if full_circle {
|
||||
theta_min = 0.0;
|
||||
theta_max = TAU;
|
||||
}
|
||||
}
|
||||
}
|
||||
let theta_span = if full_circle { TAU } else { theta_max - theta_min };
|
||||
if (h_max - h_min).abs() < 1e-10 || theta_span.abs() < 1e-10 {
|
||||
continue;
|
||||
}
|
||||
let r_at = |h: f64| {
|
||||
if cos_a.abs() > 1e-9 {
|
||||
radius + h * sin_a / cos_a
|
||||
} else {
|
||||
radius
|
||||
}
|
||||
};
|
||||
let (r0, r1) = (r_at(h_min), r_at(h_max));
|
||||
let [cx, cy, cz] = g.center;
|
||||
let (r0, r1) = (g.radius + g.h_min * g.tan_a, g.radius + g.h_max * g.tan_a);
|
||||
// A closed rim's line at theta and theta+TAU coincide, so step across
|
||||
// [0, TAU) with `count` divisions; a bounded arc gets interior lines
|
||||
// spanning its own arc (its two ends are already drawn as rim edges).
|
||||
for k in 0..count {
|
||||
let a = if full_circle {
|
||||
theta_min + theta_span * (k as f64 / count as f64)
|
||||
let a = if g.full {
|
||||
g.theta_min + g.theta_span * (k as f64 / count as f64)
|
||||
} else {
|
||||
theta_min + theta_span * ((k as f64 + 1.0) / (count as f64 + 1.0))
|
||||
g.theta_min + g.theta_span * ((k as f64 + 1.0) / (count as f64 + 1.0))
|
||||
};
|
||||
out.push(cone_pt(cx, cy, cz, axis, u_dir, v_dir, r0, a, h_min));
|
||||
out.push(cone_pt(cx, cy, cz, axis, u_dir, v_dir, r1, a, h_max));
|
||||
out.push(cone_pt(cx, cy, cz, g.axis, g.u_dir, g.v_dir, r0, a, g.h_min));
|
||||
out.push(cone_pt(cx, cy, cz, g.axis, g.u_dir, g.v_dir, r1, a, g.h_max));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// World-space silhouette generators for each cone/cylinder face — the params a
|
||||
/// per-frame DISPSILH pass needs. `xform` is the solid's body transform (or
|
||||
/// `None`); directions are rotated by it, the base point is placed by it and
|
||||
/// split into the double-single pair.
|
||||
fn collect_curved_gens(
|
||||
sat: &SatDocument,
|
||||
xform: Option<([f64; 9], [f64; 3], f64)>,
|
||||
) -> Vec<crate::scene::model::mesh_model::CurvedGen> {
|
||||
use crate::scene::model::mesh_model::CurvedGen;
|
||||
let rot_dir = |d: [f64; 3]| -> [f32; 3] {
|
||||
let w = match xform {
|
||||
Some((m, _, _)) => norm3([
|
||||
d[0] * m[0] + d[1] * m[3] + d[2] * m[6],
|
||||
d[0] * m[1] + d[1] * m[4] + d[2] * m[7],
|
||||
d[0] * m[2] + d[1] * m[5] + d[2] * m[8],
|
||||
]),
|
||||
None => d,
|
||||
};
|
||||
[w[0] as f32, w[1] as f32, w[2] as f32]
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for face in sat.faces() {
|
||||
let Some(g) = cone_face_geom(sat, &face) else {
|
||||
continue;
|
||||
};
|
||||
// Base circle centre in world space (double-single) — heights are along
|
||||
// the unit axis, so the base sits at h_min.
|
||||
let base_local = [
|
||||
g.center[0] + g.h_min * g.axis[0],
|
||||
g.center[1] + g.h_min * g.axis[1],
|
||||
g.center[2] + g.h_min * g.axis[2],
|
||||
];
|
||||
let (bx, by, bz, scale) = match xform {
|
||||
Some((m, tr, s)) => (
|
||||
s * (base_local[0] * m[0] + base_local[1] * m[3] + base_local[2] * m[6]) + tr[0],
|
||||
s * (base_local[0] * m[1] + base_local[1] * m[4] + base_local[2] * m[7]) + tr[1],
|
||||
s * (base_local[0] * m[2] + base_local[1] * m[5] + base_local[2] * m[8]) + tr[2],
|
||||
s,
|
||||
),
|
||||
None => (base_local[0], base_local[1], base_local[2], 1.0),
|
||||
};
|
||||
let (hx, hy, hz) = (bx as f32, by as f32, bz as f32);
|
||||
out.push(CurvedGen {
|
||||
base: [hx, hy, hz],
|
||||
base_low: [(bx - hx as f64) as f32, (by - hy as f64) as f32, (bz - hz as f64) as f32],
|
||||
axis: rot_dir(g.axis),
|
||||
u_dir: rot_dir(g.u_dir),
|
||||
v_dir: rot_dir(g.v_dir),
|
||||
radius: (g.radius * scale) as f32,
|
||||
tan_a: g.tan_a as f32,
|
||||
h_min: 0.0,
|
||||
h_max: ((g.h_max - g.h_min) * scale) as f32,
|
||||
theta_min: g.theta_min as f32,
|
||||
theta_span: g.theta_span as f32,
|
||||
full: g.full,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Line-list endpoints (pairs) for every `edge` record: straight edges emit
|
||||
/// their two vertex endpoints; ellipse/circle edges are sampled along their
|
||||
/// bounded parametric arc. Points are in body-local space (pre-transform).
|
||||
|
|
|
|||
|
|
@ -693,6 +693,12 @@ fn transform_block_mesh_lod_set(
|
|||
) -> MeshLodSet {
|
||||
use acadrust::types::Vector3;
|
||||
let mut out = set.clone();
|
||||
// Silhouette generators are world-space and untransformed here, so a block
|
||||
// instance would silhouette against the block-local pose. Drop them: a
|
||||
// block-internal solid keeps its baked isolines and feature edges, just not
|
||||
// the per-frame silhouette (deferred until the generators track the INSTANCE
|
||||
// transform).
|
||||
out.curved_gens.clear();
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,36 @@ pub struct MeshModel {
|
|||
///
|
||||
/// `lods` holds up to one MeshModel per LOD level (high → low). Empty
|
||||
/// slots fall back to the nearest available LOD at render time.
|
||||
/// A curved face's generator, kept so a view-dependent silhouette (DISPSILH)
|
||||
/// can be computed per frame — the silhouette is where the surface turns away
|
||||
/// from the eye, which no baked edge can capture. World-space, post body
|
||||
/// transform; the base point carries a double-single low half so it stays
|
||||
/// precise at UTM scale like the mesh verts.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CurvedGen {
|
||||
/// Base-circle centre (world), high half of the double-single.
|
||||
pub base: [f32; 3],
|
||||
/// Low residual paired with `base`.
|
||||
pub base_low: [f32; 3],
|
||||
/// Unit axis direction (world).
|
||||
pub axis: [f32; 3],
|
||||
/// Radial frame: `u` is the θ=0 direction, `v = axis × u`.
|
||||
pub u_dir: [f32; 3],
|
||||
pub v_dir: [f32; 3],
|
||||
/// Radius at the base (`h = 0`).
|
||||
pub radius: f32,
|
||||
/// `tan(half-angle)`: radius at height `h` is `radius + h * tan_a`
|
||||
/// (0 for a cylinder, non-zero for a cone).
|
||||
pub tan_a: f32,
|
||||
/// Height span along the axis the face covers.
|
||||
pub h_min: f32,
|
||||
pub h_max: f32,
|
||||
/// Angular span the face covers; `full` = a closed revolution.
|
||||
pub theta_min: f32,
|
||||
pub theta_span: f32,
|
||||
pub full: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MeshLodSet {
|
||||
pub lods: Vec<MeshModel>,
|
||||
|
|
@ -46,6 +76,9 @@ pub struct MeshLodSet {
|
|||
pub edge_verts: Vec<[f32; 3]>,
|
||||
/// Low residual paired with `edge_verts`.
|
||||
pub edge_verts_low: Vec<[f32; 3]>,
|
||||
/// Curved-face generators for per-frame silhouette (DISPSILH). Empty for a
|
||||
/// solid with no curved faces, or when silhouettes aren't wanted.
|
||||
pub curved_gens: Vec<CurvedGen>,
|
||||
/// World XY AABB `[min_x, min_y, max_x, max_y]` of the mesh — used
|
||||
/// by the per-frame LOD selector to compute the projected pixel
|
||||
/// diagonal.
|
||||
|
|
@ -87,6 +120,7 @@ impl MeshLodSet {
|
|||
lods,
|
||||
edge_verts: Vec::new(),
|
||||
edge_verts_low: Vec::new(),
|
||||
curved_gens: Vec::new(),
|
||||
world_aabb,
|
||||
z_aabb,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ pub struct Pipeline {
|
|||
/// it back into the base set (issue #316).
|
||||
text_preview_vbuf: Option<wgpu::Buffer>,
|
||||
text_preview_vcount: u32,
|
||||
/// Per-frame DISPSILH silhouette line list — rebuilt every prepare() from
|
||||
/// the mesh sets' curved-face generators and the current view direction, so
|
||||
/// the outline tracks the camera. Reuses the mesh vertex format / pipeline.
|
||||
silhouette_vbuf: Option<wgpu::Buffer>,
|
||||
silhouette_vcount: u32,
|
||||
/// Last requested render size (the full viewport rect, in pixels). The
|
||||
/// geometry passes render at this size; the blit UV is scaled by
|
||||
/// `depth_texture_size / alloc_size` so it samples only the filled region.
|
||||
|
|
@ -1295,6 +1300,8 @@ impl Pipeline {
|
|||
text_highlight_vcount: 0,
|
||||
text_preview_vbuf: None,
|
||||
text_preview_vcount: 0,
|
||||
silhouette_vbuf: None,
|
||||
silhouette_vcount: 0,
|
||||
mesh_pipeline,
|
||||
mesh_transparent_pipeline,
|
||||
mesh_highlight_pipeline,
|
||||
|
|
@ -1567,6 +1574,102 @@ impl Pipeline {
|
|||
self.text_preview_vcount = verts.len() as u32;
|
||||
}
|
||||
|
||||
// The math lives outside the GPU method so it can be unit-tested; see the
|
||||
// module test below.
|
||||
|
||||
/// Rebuild the per-frame DISPSILH silhouette line list from the mesh sets'
|
||||
/// curved-face generators and the current eye. For each cone/cylinder face
|
||||
/// the silhouette runs at the two angles where the surface turns edge-on to
|
||||
/// the view — `θ = φ ± acos(-tanα·(view·axis) / |view⊥|)`, which reduces to
|
||||
/// `φ ± π/2` for a cylinder. Segments are uploaded in the mesh vertex format
|
||||
/// so they draw through the existing wireframe pipeline.
|
||||
pub fn upload_silhouettes(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
sets: &[crate::scene::model::mesh_model::MeshLodSet],
|
||||
eye: glam::DVec3,
|
||||
) {
|
||||
use crate::scene::pipeline::mesh_gpu::MeshVertex;
|
||||
let mut verts: Vec<MeshVertex> = Vec::new();
|
||||
let dot = |a: [f32; 3], b: glam::DVec3| {
|
||||
a[0] as f64 * b.x + a[1] as f64 * b.y + a[2] as f64 * b.z
|
||||
};
|
||||
for set in sets {
|
||||
for g in &set.curved_gens {
|
||||
let base = glam::DVec3::new(
|
||||
g.base[0] as f64 + g.base_low[0] as f64,
|
||||
g.base[1] as f64 + g.base_low[1] as f64,
|
||||
g.base[2] as f64 + g.base_low[2] as f64,
|
||||
);
|
||||
// View direction from eye toward the face (accurate for both
|
||||
// orthographic and perspective).
|
||||
let view = (base - eye).normalize();
|
||||
let Some((t0, t1)) = silhouette_thetas(
|
||||
dot(g.u_dir, view),
|
||||
dot(g.v_dir, view),
|
||||
dot(g.axis, view),
|
||||
g.tan_a as f64,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let r0 = g.radius as f64;
|
||||
let r1 = g.radius as f64 + g.h_max as f64 * g.tan_a as f64;
|
||||
for theta in [t0, t1] {
|
||||
if !g.full {
|
||||
// Only within the face's own arc; normalise the offset
|
||||
// from theta_min into [0, TAU).
|
||||
let mut off = theta - g.theta_min as f64;
|
||||
let tau = std::f64::consts::TAU;
|
||||
off = off.rem_euclid(tau);
|
||||
if off > g.theta_span as f64 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (c, s) = (theta.cos(), theta.sin());
|
||||
let radial = |r: f64| {
|
||||
[
|
||||
g.u_dir[0] as f64 * r * c + g.v_dir[0] as f64 * r * s,
|
||||
g.u_dir[1] as f64 * r * c + g.v_dir[1] as f64 * r * s,
|
||||
g.u_dir[2] as f64 * r * c + g.v_dir[2] as f64 * r * s,
|
||||
]
|
||||
};
|
||||
let rb = radial(r0);
|
||||
let rt = radial(r1);
|
||||
let mk = |off: [f64; 3], h: f64| -> MeshVertex {
|
||||
let wx = base.x + off[0] + h * g.axis[0] as f64;
|
||||
let wy = base.y + off[1] + h * g.axis[1] as f64;
|
||||
let wz = base.z + off[2] + h * g.axis[2] as f64;
|
||||
let (hx, hy, hz) = (wx as f32, wy as f32, wz as f32);
|
||||
MeshVertex {
|
||||
position: [hx, hy, hz],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
color: set.lods.first().map(|m| m.color).unwrap_or([0.0, 0.0, 0.0, 1.0]),
|
||||
position_low: [
|
||||
(wx - hx as f64) as f32,
|
||||
(wy - hy as f64) as f32,
|
||||
(wz - hz as f64) as f32,
|
||||
],
|
||||
}
|
||||
};
|
||||
verts.push(mk(rb, 0.0));
|
||||
verts.push(mk(rt, g.h_max as f64));
|
||||
}
|
||||
}
|
||||
}
|
||||
if verts.is_empty() {
|
||||
self.silhouette_vbuf = None;
|
||||
self.silhouette_vcount = 0;
|
||||
return;
|
||||
}
|
||||
use wgpu::util::DeviceExt;
|
||||
self.silhouette_vbuf = Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("mesh.silhouette.vbuf"),
|
||||
contents: bytemuck::cast_slice(&verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
}));
|
||||
self.silhouette_vcount = verts.len() as u32;
|
||||
}
|
||||
|
||||
/// Recompute pixel scissor rects for viewport-clipped wires from the current view_proj.
|
||||
/// Called every frame from prepare() because scissor pixels shift with pan/zoom.
|
||||
pub fn compute_wire_scissors(&mut self, view_rot: glam::Mat4, eye: glam::DVec3, clip_w: u32, clip_h: u32) {
|
||||
|
|
@ -2090,6 +2193,11 @@ impl Pipeline {
|
|||
pass.draw(0..c.edge_vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
// DISPSILH silhouettes (whole batch, one buffer).
|
||||
if let Some(ref vb) = self.silhouette_vbuf {
|
||||
pass.set_vertex_buffer(0, vb.slice(..));
|
||||
pass.draw(0..self.silhouette_vcount, 0..1);
|
||||
}
|
||||
} else {
|
||||
if mesh_wireframe {
|
||||
pass.set_pipeline(&self.mesh_wireframe_pipeline);
|
||||
|
|
@ -2107,6 +2215,11 @@ impl Pipeline {
|
|||
pass.draw(0..c.edge_vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
// DISPSILH silhouettes.
|
||||
if let Some(ref vb) = self.silhouette_vbuf {
|
||||
pass.set_vertex_buffer(0, vb.slice(..));
|
||||
pass.draw(0..self.silhouette_vcount, 0..1);
|
||||
}
|
||||
} else {
|
||||
// Opaque fills first (they write depth).
|
||||
pass.set_pipeline(&self.mesh_pipeline);
|
||||
|
|
@ -2167,6 +2280,11 @@ impl Pipeline {
|
|||
pass.draw(0..c.edge_vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
// DISPSILH silhouettes over the shaded fill.
|
||||
if let Some(ref vb) = self.silhouette_vbuf {
|
||||
pass.set_vertex_buffer(0, vb.slice(..));
|
||||
pass.draw(0..self.silhouette_vcount, 0..1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2939,3 +3057,26 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The two silhouette angles of a cone/cylinder face for a view direction,
|
||||
/// expressed in the face's `(u, v, axis)` frame via the view's components on
|
||||
/// each: `du = view·u`, `dv = view·v`, `da = view·axis`. `tan_a` is the cone
|
||||
/// taper (0 for a cylinder).
|
||||
///
|
||||
/// The outward normal is edge-on to the view where `du·cosθ + dv·sinθ =
|
||||
/// -tanα·da`, i.e. `θ = φ ± acos(-tanα·da / |view⊥|)` with `φ = atan2(dv, du)`.
|
||||
/// `None` when the view runs down the axis (no outline) or the whole cone faces
|
||||
/// toward/away (`|arg| > 1`).
|
||||
fn silhouette_thetas(du: f64, dv: f64, da: f64, tan_a: f64) -> Option<(f64, f64)> {
|
||||
let r_perp = (du * du + dv * dv).sqrt();
|
||||
if r_perp < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let arg = -tan_a * da / r_perp;
|
||||
if arg.abs() > 1.0 {
|
||||
return None;
|
||||
}
|
||||
let phi = dv.atan2(du);
|
||||
let delta = arg.acos();
|
||||
Some((phi + delta, phi - delta))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ pub struct ViewportData {
|
|||
/// rendered on top of fills. Most shaded modes turn this off; the
|
||||
/// `*WithEdges` variants and the pure wireframes leave it on.
|
||||
pub(in crate::scene) show_3d_edges: bool,
|
||||
/// DISPSILH — draw view-dependent silhouette outlines on curved solid faces.
|
||||
/// A document-header global (default off), orthogonal to the visual style.
|
||||
pub(in crate::scene) display_silhouette: bool,
|
||||
/// HiddenLine routes 3D fills through a depth-only prepass so edges
|
||||
/// occluded by closer geometry are culled by the LessEqual depth
|
||||
/// test on the wire passes that follow.
|
||||
|
|
@ -555,6 +558,14 @@ impl shader::Primitive for Primitive {
|
|||
vp.uniforms.eye_high[1] as f64 + vp.uniforms.eye_low[1] as f64,
|
||||
vp.uniforms.eye_high[2] as f64 + vp.uniforms.eye_low[2] as f64,
|
||||
);
|
||||
// DISPSILH: rebuild view-dependent silhouettes each frame (off by
|
||||
// default, so this is a no-op unless the drawing enables it). Only
|
||||
// in modes that draw edges — pure shaded hides them.
|
||||
if vp.display_silhouette && (vp.view_wireframe || vp.show_3d_edges) {
|
||||
inner.upload_silhouettes(device, &vp.meshes[..], eye);
|
||||
} else {
|
||||
inner.upload_silhouettes(device, &[], eye);
|
||||
}
|
||||
inner.compute_wire_scissors(view_rot, eye, clip_size.width, clip_size.height);
|
||||
inner.compute_wipeout_scissors(view_rot, eye, clip_size.width, clip_size.height);
|
||||
inner.compute_image_scissors(view_rot, eye, clip_size.width, clip_size.height);
|
||||
|
|
@ -1362,6 +1373,7 @@ impl Scene {
|
|||
view_wireframe,
|
||||
mesh_fill: flags.mesh_fill,
|
||||
show_3d_edges: flags.show_3d_edges,
|
||||
display_silhouette: self.document.header.display_silhouette,
|
||||
hidden_line: flags.hidden_line,
|
||||
// Interaction LOD: suppress the costly hatch pass while the view is
|
||||
// actively moving; the scene-render cache holds the full-quality
|
||||
|
|
|
|||
Loading…
Reference in a new issue