Skip to main content

damascene_core/scene/
depth.rs

1//! [`SceneDepthMap`] — a CPU-side snapshot of a scene's depth buffer,
2//! produced by the backend and consumed by the draw-op pass to occlude
3//! scene-anchored labels behind solid geometry.
4//!
5//! ## Why this exists / the frame-latency contract
6//!
7//! Labels are emitted in [`draw_ops`](crate::paint::draw_ops) (CPU,
8//! backend-neutral) *before* any GPU work, but the only thing that knows
9//! "is this world point behind the mesh?" is the scene's depth buffer,
10//! which exists during the GPU pass one step later. Rather than read a
11//! depth value back synchronously (a pipeline stall), the backend captures
12//! the depth map each frame and feeds the *previous* frame's map back into
13//! [`UiState`](crate::state::UiState). The occlusion test therefore runs
14//! against a map that is a frame (or a few) stale.
15//!
16//! Two consequences shape the design:
17//!
18//! - The map carries the [`ResolvedCamera`] and viewport rect that produced
19//!   it, and [`occludes`](SceneDepthMap::occludes) projects in *that* space
20//!   — so the test is self-consistent even while the live camera orbits.
21//!   The label is still *drawn* at the live projection; only the
22//!   visible/hidden decision uses the stale map.
23//! - When no map exists yet (first frames, just after a resize), callers
24//!   treat every label as occluded — better a momentary missing label than
25//!   a flash of labels punching through geometry.
26
27use std::sync::Arc;
28
29use glam::Vec3;
30
31use crate::scene::camera::ResolvedCamera;
32use crate::tree::Rect;
33
34/// Anchors within this normalised-depth margin of the nearest surface are
35/// treated as in front of it — keeps a label sitting just shy of geometry
36/// from flickering as the stale map and live pose drift apart.
37const DEPTH_BIAS: f32 = 1.0e-3;
38
39/// A captured scene depth buffer plus the camera/viewport that produced it.
40///
41/// Depth values are row-major, length `width * height`, in normalised
42/// device depth `[0, 1]` (`0` near, `1` far). The backend constructs one
43/// per `Scene3D` node each frame; [`UiState`](crate::state::UiState) holds
44/// the latest available map keyed by the node's `computed_id`.
45#[derive(Clone, Debug)]
46pub struct SceneDepthMap {
47    /// The resolved camera that rendered this depth map.
48    pub camera: ResolvedCamera,
49    /// The scene viewport rect (logical px) at capture time.
50    pub rect: Rect,
51    /// Depth grid width in pixels (physical resolution of the offscreen).
52    pub width: u32,
53    /// Depth grid height in pixels.
54    pub height: u32,
55    /// Row-major normalised depth, `width * height` values.
56    pub depth: Arc<[f32]>,
57}
58
59impl SceneDepthMap {
60    /// Whether `world` is hidden behind solid scene geometry, judged in the
61    /// camera/viewport this map was captured with.
62    ///
63    /// Returns `true` (occluded) for points behind the camera or projecting
64    /// outside the map — the conservative choice, matching the
65    /// "occlude until we know otherwise" contract above. Only geometry that
66    /// writes depth (meshes) occludes; points and lines do not.
67    pub fn occludes(&self, world: Vec3) -> bool {
68        if self.width == 0 || self.height == 0 || self.depth.is_empty() {
69            return true;
70        }
71        let Some((p, z)) = self.camera.project_to_screen_with_depth(world, self.rect) else {
72            return true; // behind the camera
73        };
74        let fx = (p.x - self.rect.x) / self.rect.w.max(f32::EPSILON);
75        let fy = (p.y - self.rect.y) / self.rect.h.max(f32::EPSILON);
76        if !(0.0..1.0).contains(&fx) || !(0.0..1.0).contains(&fy) {
77            return true; // outside the captured viewport
78        }
79        let tx = ((fx * self.width as f32) as u32).min(self.width - 1);
80        let ty = ((fy * self.height as f32) as u32).min(self.height - 1);
81        let Some(&surface_z) = self.depth.get((ty * self.width + tx) as usize) else {
82            return true;
83        };
84        // Occluded when the anchor is farther than the nearest surface.
85        z > surface_z + DEPTH_BIAS
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn camera() -> ResolvedCamera {
94        ResolvedCamera {
95            eye: Vec3::new(0.0, 0.0, 5.0),
96            target: Vec3::ZERO,
97            up: Vec3::Y,
98            fov_y: std::f32::consts::FRAC_PI_4,
99            near: 0.1,
100            far: 100.0,
101        }
102    }
103
104    fn rect() -> Rect {
105        Rect::new(0.0, 0.0, 200.0, 200.0)
106    }
107
108    /// A 1×1 map whose single texel sits at `surface_z`.
109    fn map_with(surface_z: f32) -> SceneDepthMap {
110        SceneDepthMap {
111            camera: camera(),
112            rect: rect(),
113            width: 1,
114            height: 1,
115            depth: Arc::from(vec![surface_z]),
116        }
117    }
118
119    #[test]
120    fn far_surface_does_not_occlude_a_point_in_front() {
121        // Origin is in front of the eye; an all-far map (empty background)
122        // never occludes it.
123        assert!(!map_with(1.0).occludes(Vec3::ZERO));
124    }
125
126    #[test]
127    fn near_surface_occludes_a_point_behind_it() {
128        // A surface at the very front (z≈0) hides the origin behind it.
129        assert!(map_with(0.0).occludes(Vec3::ZERO));
130    }
131
132    #[test]
133    fn point_behind_camera_is_occluded() {
134        // The eye looks toward -Z; a point behind it never projects.
135        assert!(map_with(1.0).occludes(Vec3::new(0.0, 0.0, 20.0)));
136    }
137
138    #[test]
139    fn point_outside_viewport_is_occluded() {
140        // Far off-axis but in front → projects outside the 1×1 map.
141        assert!(map_with(1.0).occludes(Vec3::new(100.0, 0.0, 0.0)));
142    }
143
144    #[test]
145    fn empty_map_occludes_everything() {
146        let empty = SceneDepthMap {
147            camera: camera(),
148            rect: rect(),
149            width: 0,
150            height: 0,
151            depth: Arc::from(Vec::<f32>::new()),
152        };
153        assert!(empty.occludes(Vec3::ZERO));
154    }
155}