euv_engine/raytracing/scene/impl.rs
1use super::*;
2
3/// Implements factory constructors and accessors for [`Ray`] and
4/// [`Occluder`].
5impl Ray {
6 /// Creates a new ray starting at `origin` pointing in `direction`.
7 ///
8 /// `t_min` and `t_max` default to [`RAYTRACE_DEFAULT_T_MIN`] and
9 /// [`RAYTRACE_DEFAULT_T_MAX`]. `depth` defaults to 0.
10 ///
11 /// # Arguments
12 ///
13 /// - `Vector3D` - The ray origin.
14 /// - `Vector3D` - The unit direction.
15 ///
16 /// # Returns
17 ///
18 /// - `Ray` - The new ray.
19 pub fn new(origin: Vector3D, direction: Vector3D) -> Ray {
20 Ray {
21 origin,
22 direction,
23 t_min: RAYTRACE_DEFAULT_T_MIN,
24 t_max: RAYTRACE_DEFAULT_T_MAX,
25 depth: 0,
26 }
27 }
28
29 /// Computes the world-space point at distance `t` along this ray.
30 ///
31 /// # Arguments
32 ///
33 /// - `f64` - The ray parameter.
34 ///
35 /// # Returns
36 ///
37 /// - `Vector3D` - `origin + direction * t`.
38 pub fn at(&self, t: f64) -> Vector3D {
39 self.get_origin() + self.get_direction().scaled(t)
40 }
41
42 /// Returns a clone of this ray with `depth` replaced by `depth`.
43 ///
44 /// # Arguments
45 ///
46 /// - `u32` - The new recursion depth.
47 ///
48 /// # Returns
49 ///
50 /// - `Ray` - The cloned ray with updated depth.
51 pub fn with_depth(&self, depth: u32) -> Ray {
52 Ray {
53 origin: self.get_origin(),
54 direction: self.get_direction(),
55 t_min: self.get_t_min(),
56 t_max: self.get_t_max(),
57 depth,
58 }
59 }
60
61 /// Returns a clone of this ray with its parameter range replaced.
62 ///
63 /// The new bounds are taken as given, so an inverted or non-finite
64 /// range is representable; both query entry points reject such a ray
65 /// as untraceable rather than letting `NaN` comparisons decide the
66 /// outcome.
67 ///
68 /// # Arguments
69 ///
70 /// - `f64` - The minimum accepted hit parameter.
71 /// - `f64` - The maximum accepted hit parameter.
72 ///
73 /// # Returns
74 ///
75 /// - `Ray` - The cloned ray with the new range.
76 pub fn with_range(&self, t_min: f64, t_max: f64) -> Ray {
77 Ray {
78 origin: self.get_origin(),
79 direction: self.get_direction(),
80 t_min,
81 t_max,
82 depth: self.get_depth(),
83 }
84 }
85
86 /// Intersects this ray with a triangle using the Moller-Trumbore
87 /// algorithm, keeping only hits inside this ray's `t_min`..=`t_max`
88 /// range.
89 ///
90 /// The test is two-sided: a hit from either face counts, and the
91 /// returned normal always faces against the ray direction. This is the
92 /// range-clamped counterpart of the free
93 /// [`intersect_triangle`](crate::intersect_triangle), which reports the
94 /// raw distance however far away the triangle is.
95 ///
96 /// # Arguments
97 ///
98 /// - `[Vector3D; 3]` - The three triangle vertices in winding order.
99 ///
100 /// # Returns
101 ///
102 /// - `Option<(f64, Vector3D)>` - The hit distance along the ray and the
103 /// unit normal oriented against the ray direction, or `None` on miss.
104 pub fn intersect_triangle(&self, vertices: [Vector3D; 3]) -> Option<(f64, Vector3D)> {
105 let (t, normal): (f64, Vector3D) = intersect_triangle(self, vertices)?;
106 if t < self.get_t_min() || t > self.get_t_max() {
107 return None;
108 }
109 Some((t, normal))
110 }
111}
112
113/// Implements factory constructors for [`Occluder`].
114impl Occluder {
115 /// Creates a spherical occluder centered at `center` with `radius`.
116 ///
117 /// # Arguments
118 ///
119 /// - `Vector3D` - The sphere center.
120 /// - `f64` - The sphere radius.
121 /// - `Material` - The surface material.
122 ///
123 /// # Returns
124 ///
125 /// - `Occluder` - The new sphere occluder.
126 pub fn sphere(center: Vector3D, radius: f64, material: Material) -> Occluder {
127 Occluder {
128 kind: OccluderKind::Sphere,
129 center,
130 extent: Vector3D::new(radius, radius, radius),
131 vertices: [center; 3],
132 material,
133 }
134 }
135
136 /// Creates an axis-aligned bounding-box occluder from `min` to `max`.
137 ///
138 /// # Arguments
139 ///
140 /// - `Vector3D` - The AABB minimum corner.
141 /// - `Vector3D` - The AABB maximum corner.
142 /// - `Material` - The surface material.
143 ///
144 /// # Returns
145 ///
146 /// - `Occluder` - The new AABB occluder.
147 pub fn aabb(min: Vector3D, max: Vector3D, material: Material) -> Occluder {
148 Occluder {
149 kind: OccluderKind::Aabb,
150 center: min,
151 extent: max,
152 vertices: [min; 3],
153 material,
154 }
155 }
156
157 /// Creates a triangular occluder from its three vertices.
158 ///
159 /// The vertex order fixes the geometric winding; because the
160 /// intersection test is two-sided the winding does not affect which
161 /// faces produce hits, only the raw cross-product normal before it is
162 /// oriented against the incoming ray.
163 ///
164 /// # Arguments
165 ///
166 /// - `Vector3D` - The first triangle vertex.
167 /// - `Vector3D` - The second triangle vertex.
168 /// - `Vector3D` - The third triangle vertex.
169 /// - `Material` - The surface material.
170 ///
171 /// # Returns
172 ///
173 /// - `Occluder` - The new triangle occluder.
174 pub fn triangle(v0: Vector3D, v1: Vector3D, v2: Vector3D, material: Material) -> Occluder {
175 Occluder {
176 kind: OccluderKind::Triangle,
177 center: Vector3D::zero(),
178 extent: Vector3D::zero(),
179 vertices: [v0, v1, v2],
180 material,
181 }
182 }
183
184 /// Returns a list of `(center, radius)` sphere tuples approximating
185 /// this occluder, suitable for [`soft_shadow_factor`].
186 ///
187 /// For sphere occluders this returns `(center, radius)`. For AABB
188 /// occluders the bounding sphere is computed conservatively from the
189 /// AABB extents.
190 ///
191 /// # Returns
192 ///
193 /// - `Vec<(Vector3D, f64)>` - One bounding sphere per occluder.
194 pub fn occluder_points(&self) -> Vec<(Vector3D, f64)> {
195 collect_occluder_points(from_ref(self))
196 }
197}
198
199/// Implements the constructor and zero-allocation tracing entry points for
200/// [`RayTraceScene`].
201impl RayTraceScene {
202 /// Creates a new scene taking ownership of `occluders` and precomputing
203 /// both the bounding volume hierarchy and the `(center, radius)` shadow
204 /// bounding spheres used by [`soft_shadow_factor`].
205 ///
206 /// The hierarchy is built once here; every later query reuses it, so
207 /// the cost of the BVH never appears in a per-ray measurement. The
208 /// public surface is unchanged from the pre-BVH constructor, which
209 /// takes the occluder list by value.
210 ///
211 /// # Arguments
212 ///
213 /// - `Vec<Occluder>` - All occluding surfaces in the scene.
214 ///
215 /// # Returns
216 ///
217 /// - `RayTraceScene` - The new scene with precomputed shadow data.
218 pub fn new(occluders: Vec<Occluder>) -> RayTraceScene {
219 let shadow_points: Vec<(Vector3D, f64)> = collect_occluder_points(&occluders);
220 let (nodes, indices): (Vec<RayTraceNode>, Vec<usize>) = build_bvh(&occluders);
221 RayTraceScene {
222 occluders,
223 nodes,
224 indices,
225 shadow_points,
226 }
227 }
228
229 /// Iteratively traces a ray through the scene and returns the final
230 /// shaded color, using the [`RAYTRACE_DEFAULT_MAX_BOUNCES`] constant as
231 /// the bounce limit.
232 ///
233 /// Performs no heap allocation per ray or per bounce: the shadow
234 /// bounding spheres precomputed at construction are reused, and no
235 /// [`Material`] is cloned. Use [`RayTraceScene::trace_with_bounces`] to
236 /// override the bounce limit.
237 ///
238 /// # Arguments
239 ///
240 /// - `Ray` - The ray to trace.
241 /// - `&LightingUniforms` - Lighting parameters used during shading.
242 ///
243 /// # Returns
244 ///
245 /// - `Vector3D` - The final traced color.
246 pub fn trace(&self, ray: Ray, lights: &LightingUniforms) -> Vector3D {
247 self.trace_with_bounces(ray, lights, RAYTRACE_DEFAULT_MAX_BOUNCES)
248 }
249
250 /// Iteratively traces a ray through the scene with an explicit bounce
251 /// limit and returns the final shaded color.
252 ///
253 /// On a miss the ambient color scaled by the accumulated specular
254 /// throughput is added. On a hit the surface material is evaluated with
255 /// [`LightingUniforms::shade`] and, when the hit material has a
256 /// non-zero specular component, the trace continues with a reflected
257 /// ray up to `max_bounces` times (incrementing the ray's `depth` field
258 /// per bounce).
259 ///
260 /// # Arguments
261 ///
262 /// - `Ray` - The ray to trace.
263 /// - `&LightingUniforms` - Lighting parameters used during shading.
264 /// - `u32` - The maximum number of bounces allowed for this ray.
265 ///
266 /// # Returns
267 ///
268 /// - `Vector3D` - The final traced color.
269 pub fn trace_with_bounces(
270 &self,
271 ray: Ray,
272 lights: &LightingUniforms,
273 max_bounces: u32,
274 ) -> Vector3D {
275 trace_bounces(ray, self, lights, max_bounces)
276 }
277
278 /// Finds the closest intersection between a ray and the scene
279 /// occluders.
280 ///
281 /// The winning occluder's [`Material`] is cloned exactly once, when the
282 /// returned [`Hit`] is constructed; losing candidates are never cloned.
283 /// Traversal itself resolves every candidate through the scene's
284 /// bounding volume hierarchy, so the cost scales with the parts of the
285 /// hierarchy the ray actually crosses rather than with the occluder
286 /// count.
287 ///
288 /// # Arguments
289 ///
290 /// - `&Ray` - The ray to test.
291 ///
292 /// # Returns
293 ///
294 /// - `Option<Hit>` - The closest hit, or `None` if the ray misses.
295 pub fn closest_hit(&self, ray: &Ray) -> Option<Hit> {
296 let occluders: &[Occluder] = self.get_occluders();
297 closest_hit_indexed(ray, self).map(
298 |(index, t, position, normal): (usize, f64, Vector3D, Vector3D)| Hit {
299 t,
300 position,
301 normal,
302 material: occluders[index].get_material().clone(),
303 },
304 )
305 }
306
307 /// Reports whether anything in the scene blocks the ray's parameter
308 /// range.
309 ///
310 /// This is the any-hit counterpart to [`RayTraceScene::closest_hit`],
311 /// built for shadow segments: it stops at the first blocker instead of
312 /// resolving the whole hierarchy, so a fully occluded light costs less
313 /// than a fully visible one. An empty scene reports `false`, and a ray
314 /// with an inverted or non-finite range reports `false` rather than
315 /// inventing a hit.
316 ///
317 /// # Arguments
318 ///
319 /// - `&Ray` - The ray to test.
320 ///
321 /// # Returns
322 ///
323 /// - `bool` - `true` when at least one occluder intersects the range.
324 pub fn occluded(&self, ray: &Ray) -> bool {
325 occluded_indexed(ray, self)
326 }
327}