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}