From 345c0882cddb6520ecedd48c4ed88d6275e26af8 Mon Sep 17 00:00:00 2001 From: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:28:31 +0200 Subject: [PATCH] Renders hide edges just behind a face; lines on steep faces draw whole (#983) The CPU renderer's line depth test allowed a fixed share of the scene's depth range (2e-4), which on large designs or close views let edges a millimetre or more behind thin sheet show through it. Each pixel now keeps the screen depth slope of the surface it shows, and a line passes where it is no further behind than that slope spans across the line's width, plus f32 rounding. Section caps use their plane's slope. Edges lying on steep faces now draw their full width; edges behind a face stay hidden. Only crates/render/src/raster.rs changes; the GPU viewport is unchanged. Co-authored-by: NOWN-DEV --- crates/render/src/raster.rs | 111 +++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index b140a086..55b9a1a8 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -60,6 +60,9 @@ pub struct Canvas { const SS: usize = 2; const MAX_DIM: usize = 8192; +/// The rounding the lines' depth test allows for, relative to the depth (a few steps of the +/// `f32` depth buffer). +const DEPTH_EPS: f64 = 4.0 * f32::EPSILON as f64; impl Canvas { fn new(w: usize, h: usize) -> Canvas { @@ -116,6 +119,9 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas { // glass, while holes through the cut stay open (as the viewport does with its stencil). let cap_z = scene.clip.and_then(|(n, d)| cap_plane(cam, n, d, scene.radius.max(1.0), &project)); let mut cap_depth = if cap_z.is_some() { vec![f32::INFINITY; sw * sh] } else { Vec::new() }; + // The screen depth slope (per canvas pixel) of the surface each pixel shows, for the lines' + // depth test. + let mut slope = vec![0f32; sw * sh]; let cap_tint = scene.cap.unwrap_or(SECTION_CAP).f(); for sm in &scene.meshes { let m = &sm.mesh; @@ -148,6 +154,9 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas { if area.abs() < 1e-12 { continue; } + let gx = ((b[2] - a[2]) * (cc[1] - a[1]) - (cc[2] - a[2]) * (b[1] - a[1])) / area; + let gy = ((cc[2] - a[2]) * (b[0] - a[0]) - (b[2] - a[2]) * (cc[0] - a[0])) / area; + let tri_slope = gx.hypot(gy) as f32; for y in y0 as usize..=y1 as usize { for x in x0 as usize..=x1 as usize { let (px, py) = (x as f64 + 0.5, y as f64 + 0.5); @@ -171,6 +180,9 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas { if let Some(d) = c.depth.get_mut(i) { *d = z; } + if let Some(s) = slope.get_mut(i) { + *s = tri_slope; + } let n = (ns[0] * p0 + ns[1] * p1 + ns[2] * p2).normalized().unwrap_or(Vec3::Z); let cut = scene.clip.is_some() && inside; if let Some(d) = cap_depth.get_mut(i) { @@ -201,9 +213,11 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas { } } } - for (d, cz) in c.depth.iter_mut().zip(&cap_depth) { + let cap_slope = cap_z.map_or(0.0, |[a, b, _]| a.hypot(b) as f32); + for ((d, s), cz) in c.depth.iter_mut().zip(slope.iter_mut()).zip(&cap_depth) { if cz.is_finite() { *d = *cz; + *s = s.max(cap_slope); } } for l in &scene.lines { @@ -233,7 +247,14 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas { continue; } let i = iy as usize * sw + ix as usize; - let visible = l.on_top || c.depth.get(i).is_some_and(|dd| (z as f32) <= *dd + 2e-4); + // A line on a face lies at the face's depth, but the pixel holds the + // face's depth at its centre, `d` away: allow what the face's slope + // spans over that distance, and the rounding. (A fixed allowance in + // screen depth is a share of the scene's depth range, a millimetre or + // more on a large design: close views showed the far side of thin + // sheet, and parts just behind it, through it.) + let visible = l.on_top + || c.depth.get(i).zip(slope.get(i)).is_some_and(|(dd, s)| z <= f64::from(*dd) + f64::from(*s) * d + z.abs() * DEPTH_EPS); if visible { c.put(ix as usize, iy as usize, col, cov); } @@ -380,4 +401,90 @@ mod tests { // Hostile sizes are clamped. assert_eq!(render(&scene, &cam, 0, usize::MAX).w, 1); } + + /// A sheet facing the front camera, turned back by `tilt` about X: the plane y = z·tan(tilt) + /// over x in ±15, z in ±`h`. + fn sheet(tilt: f64, h: f64) -> Mesh { + let t = tilt.tan(); + let c = [(-15.0, -h), (15.0, -h), (15.0, h), (-15.0, h)].map(|(x, z)| Vec3::new(x, z * t, z)); + Mesh { + positions: c.to_vec(), + normals: vec![Vec3::new(0.0, -tilt.cos(), tilt.sin()); 4], + triangles: vec![[0, 1, 2], [0, 2, 3]], + tri_face: vec![0, 0], + ..Default::default() + } + } + + /// A close front view: 40 mm high on 64 px (128 canvas px, 0.3125 mm each), of a design of + /// radius `radius`. + fn close_view(mesh: Mesh, lines: Vec, radius: f64, fov: f64) -> (Scene, Camera) { + let scene = Scene { + meshes: vec![SceneMesh { mesh: Arc::new(mesh), color: Rgb(160, 165, 175), face_colors: Vec::new() }], + lines, + radius, + ..Default::default() + }; + let mut cam = Camera { distance: if fov > 0.0 { 20.0 / (fov * 0.5).tan() } else { 50.0 }, fov, ..Default::default() }; + cam.set_view(StandardView::Front); + (scene, cam) + } + + fn red_pixels(scene: &Scene, cam: &Camera) -> usize { + render(scene, cam, 64, 64).rgba.chunks(4).filter(|p| p[0] > 150 && p[1] < 100 && p[2] < 100).count() + } + + /// An edge 1 mm behind a sheet is hidden in a close view of a large design. The old test + /// allowed 2e-4 of the depth range, 2e-4 · (8 · 1200 + 2) = 1.92 mm here (orthographic; far + /// more in perspective), so it was drawn on the sheet. Now the allowance is what the sheet's + /// slope spans across the line's width: none facing the camera; tilted 45°, the edge lies + /// 1 / cos 45° = 1.414 mm behind along the view, and the sheet's depth changes 0.3125 mm + /// per pixel, at most 2 · 1.7 · 0.3125 = 1.06 mm over a 1.2 px line (1.7 px from its centre + /// line to the edge of its anti-aliasing). + #[test] + fn edges_behind_a_thin_sheet_stay_hidden_in_close_views() { + for fov in [0.0, 0.6] { + for tilt in [0.0, 45f64.to_radians()] { + // 1 mm behind the sheet, along its normal, across the middle. + let back = Vec3::new(0.0, tilt.cos(), -tilt.sin()); + let edge = SceneLine { + points: vec![Vec3::new(-10.0, 0.0, 0.0) + back, Vec3::new(10.0, 0.0, 0.0) + back], + color: Rgb(255, 0, 0), + width: 1.2, + on_top: false, + }; + let (mut scene, cam) = close_view(sheet(tilt, 12.0), vec![edge], 1200.0, fov); + assert_eq!(red_pixels(&scene, &cam), 0, "fov {fov}, tilt {tilt}: the edge behind the sheet shows through it"); + scene.lines[0].on_top = true; + assert!(red_pixels(&scene, &cam) > 20, "fov {fov}, tilt {tilt}: the edge is in the picture"); + } + } + } + + /// Edges lying on a face are drawn whole, however steep the face and whatever the design's + /// size: the outline of a sheet turned 70° away from the camera (its depth changes + /// tan 70° · 0.3125 = 0.86 mm per pixel) and a line across it draw exactly as they do + /// drawn over everything. + #[test] + fn edges_on_a_steep_face_draw_whole() { + let tilt = 70f64.to_radians(); + let on = |x: f64, z: f64| Vec3::new(x, z * tilt.tan(), z); + let line = |points: Vec| SceneLine { points, color: Rgb(255, 0, 0), width: 1.2, on_top: false }; + for fov in [0.0, 0.6] { + for radius in [30.0, 1200.0] { + let lines = vec![ + line(vec![on(-15.0, -5.0), on(15.0, -5.0), on(15.0, 5.0), on(-15.0, 5.0), on(-15.0, -5.0)]), + line(vec![on(-10.0, 1.5), on(10.0, 1.5)]), + ]; + let (mut scene, cam) = close_view(sheet(tilt, 5.0), lines, radius, fov); + let drawn = red_pixels(&scene, &cam); + for l in &mut scene.lines { + l.on_top = true; + } + let whole = red_pixels(&scene, &cam); + assert!(whole > 100, "fov {fov}, radius {radius}: {whole} line pixels"); + assert_eq!(drawn, whole, "fov {fov}, radius {radius}: parts of the lines on the face are hidden"); + } + } + } }