euv_engine/raytracing/scene/fn.rs
1use super::*;
2
3/// Returns the component-wise minimum of two vectors.
4///
5/// # Arguments
6///
7/// - `Vector3D` - The first vector.
8/// - `Vector3D` - The second vector.
9///
10/// # Returns
11///
12/// - `Vector3D` - The component-wise minimum.
13fn vector3d_min(a: Vector3D, b: Vector3D) -> Vector3D {
14 Vector3D::new(
15 a.get_x().min(b.get_x()),
16 a.get_y().min(b.get_y()),
17 a.get_z().min(b.get_z()),
18 )
19}
20
21/// Returns the component-wise maximum of two vectors.
22///
23/// # Arguments
24///
25/// - `Vector3D` - The first vector.
26/// - `Vector3D` - The second vector.
27///
28/// # Returns
29///
30/// - `Vector3D` - The component-wise maximum.
31fn vector3d_max(a: Vector3D, b: Vector3D) -> Vector3D {
32 Vector3D::new(
33 a.get_x().max(b.get_x()),
34 a.get_y().max(b.get_y()),
35 a.get_z().max(b.get_z()),
36 )
37}
38
39/// Reports whether every component of `value` is a finite number.
40///
41/// Degenerate geometry and non-finite ray data must never register a hit,
42/// and comparison operators silently accept `NaN` on both sides, so every
43/// exact test filters its inputs and its resulting `t` through this
44/// predicate instead of relying on `<` and `>` alone.
45///
46/// # Arguments
47///
48/// - `Vector3D` - The vector to inspect.
49///
50/// # Returns
51///
52/// - `bool` - `true` when no component is `NaN` or infinite.
53fn is_finite_vector(value: Vector3D) -> bool {
54 value.get_x().is_finite() && value.get_y().is_finite() && value.get_z().is_finite()
55}
56
57/// Returns the inclusive parameter interval of a single AABB slab along one
58/// ray axis, or `None` when the ray runs parallel to the slab outside it.
59///
60/// A parallel axis is not rejected outright: the interval becomes
61/// `(-inf, +inf)` when the origin lies inside the slab and the caller keeps
62/// only the two axes that actually constrain the ray.
63///
64/// # Arguments
65///
66/// - `f64` - The ray origin component on this axis.
67/// - `f64` - The ray direction component on this axis.
68/// - `f64` - The slab lower bound.
69/// - `f64` - The slab upper bound.
70///
71/// # Returns
72///
73/// - `Option<(f64, f64)>` - The `(enter, exit)` parameters, or `None` when
74/// the ray is parallel to the slab and outside it.
75fn slab_interval(
76 origin_component: f64,
77 dir_component: f64,
78 lower: f64,
79 upper: f64,
80) -> Option<(f64, f64)> {
81 if dir_component.abs() < RAYTRACE_TRIANGLE_EPSILON {
82 if origin_component < lower || origin_component > upper {
83 return None;
84 }
85 return Some((f64::NEG_INFINITY, f64::INFINITY));
86 }
87 let inverse: f64 = 1.0 / dir_component;
88 let t0: f64 = (lower - origin_component) * inverse;
89 let t1: f64 = (upper - origin_component) * inverse;
90 Some((t0.min(t1), t0.max(t1)))
91}
92
93/// Returns the clipped entry parameter of a ray segment against an AABB.
94///
95/// The result is `None` when the segment provably cannot touch the box.
96/// The interval is clipped to `t_min`..=`t_max`, so a traversal that
97/// shrinks `t_max` as it discovers nearer hits also prunes the boxes
98/// behind those hits for free. Parallel slabs are handled inside
99/// [`slab_interval`], so an axis-aligned ray running along a face still
100/// reports the other two axes' constraint instead of dividing by zero.
101///
102/// # Arguments
103///
104/// - `Vector3D` - The ray origin.
105/// - `Vector3D` - The ray direction, not required to be unit length.
106/// - `Vector3D` - The AABB minimum corner.
107/// - `Vector3D` - The AABB maximum corner.
108/// - `f64` - The minimum ray parameter considered a valid hit.
109/// - `f64` - The maximum ray parameter considered a valid hit.
110///
111/// # Returns
112///
113/// - `Option<f64>` - The entry parameter clipped into the requested range,
114/// or `None` when the segment misses the box.
115fn aabb_entry_parameter(
116 origin: Vector3D,
117 dir: Vector3D,
118 lower: Vector3D,
119 upper: Vector3D,
120 t_min: f64,
121 t_max: f64,
122) -> Option<f64> {
123 if !is_finite_vector(origin)
124 || !is_finite_vector(dir)
125 || !is_finite_vector(lower)
126 || !is_finite_vector(upper)
127 {
128 return None;
129 }
130 let x: Option<(f64, f64)> =
131 slab_interval(origin.get_x(), dir.get_x(), lower.get_x(), upper.get_x());
132 let y: Option<(f64, f64)> =
133 slab_interval(origin.get_y(), dir.get_y(), lower.get_y(), upper.get_y());
134 let z: Option<(f64, f64)> =
135 slab_interval(origin.get_z(), dir.get_z(), lower.get_z(), upper.get_z());
136 let (x0, x1): (f64, f64) = x?;
137 let (y0, y1): (f64, f64) = y?;
138 let (z0, z1): (f64, f64) = z?;
139 let t_enter: f64 = x0.max(y0).max(z0);
140 let t_exit: f64 = x1.min(y1).min(z1);
141 if t_enter > t_exit {
142 return None;
143 }
144 let clipped: f64 = t_enter.max(t_min);
145 if clipped > t_exit.min(t_max) {
146 return None;
147 }
148 Some(clipped)
149}
150
151/// Intersects a ray with the triangle `v0`, `v1`, `v2` using the
152/// Moller-Trumbore algorithm.
153///
154/// The test is two-sided: a hit on either face counts, and the returned
155/// normal is flipped when needed so that it always faces against the ray
156/// direction, making back-face hits usable for shading. A ray parallel to
157/// the triangle plane yields `None`. No `t_min`/`t_max` filtering is
158/// applied here; use [`Ray::intersect_triangle`] for the range-clamped
159/// variant.
160///
161/// # Arguments
162///
163/// - `Vector3D` - The ray origin.
164/// - `Vector3D` - The ray direction (expected to be unit length).
165/// - `Vector3D` - The first triangle vertex.
166/// - `Vector3D` - The second triangle vertex.
167/// - `Vector3D` - The third triangle vertex.
168///
169/// # Returns
170///
171/// - `Option<(f64, Vector3D)>` - The hit distance along the ray and the
172/// unit surface normal oriented against the ray direction, or `None` on
173/// miss.
174pub fn intersect_triangle(
175 origin: Vector3D,
176 dir: Vector3D,
177 v0: Vector3D,
178 v1: Vector3D,
179 v2: Vector3D,
180) -> Option<(f64, Vector3D)> {
181 let edge1: Vector3D = v1 - v0;
182 let edge2: Vector3D = v2 - v0;
183 let pvec: Vector3D = dir.cross(edge2);
184 let det: f64 = edge1.dot(pvec);
185 if det.abs() < RAYTRACE_TRIANGLE_EPSILON {
186 return None;
187 }
188 let inverse_det: f64 = 1.0 / det;
189 let tvec: Vector3D = origin - v0;
190 let u: f64 = tvec.dot(pvec) * inverse_det;
191 if !(0.0..=1.0).contains(&u) {
192 return None;
193 }
194 let qvec: Vector3D = tvec.cross(edge1);
195 let v: f64 = dir.dot(qvec) * inverse_det;
196 // `is_finite` is the guard, not a `neg_cmp`: a NaN barycentric coordinate
197 // must fall through to the miss branch, and `!(x >= 0.0)` would also let a
198 // NaN through on the negation while losing that intent.
199 let v_usable: bool = v.is_finite() && (0.0..=1.0).contains(&v);
200 let edge_usable: bool = (u + v).is_finite() && (u + v <= 1.0);
201 if !v_usable || !edge_usable {
202 return None;
203 }
204 let t: f64 = edge2.dot(qvec) * inverse_det;
205 if !t.is_finite() {
206 return None;
207 }
208 let geometric: Vector3D = edge1.cross(edge2).normalized();
209 let normal: Vector3D = if geometric.dot(dir) > 0.0 {
210 -geometric
211 } else {
212 geometric
213 };
214 Some((t, normal))
215}
216
217/// Returns the AABB extents `(min, max)` of an [`Occluder`].
218///
219/// For AABB occluders this is `(center, extent)`. For sphere occluders
220/// the bounding box is computed from the center and the `.x` component of
221/// `extent` (the sphere radius). For triangle occluders the bounding box
222/// is the component-wise span of the three vertices.
223///
224/// # Arguments
225///
226/// - `&Occluder` - The occluder to bound.
227///
228/// # Returns
229///
230/// - `(Vector3D, Vector3D)` - The `(min, max)` corners of the AABB.
231fn occluder_aabb_extents(occluder: &Occluder) -> (Vector3D, Vector3D) {
232 let (mn, mx): (Vector3D, Vector3D) = match occluder.get_kind() {
233 OccluderKind::Aabb => (occluder.get_center(), occluder.get_extent()),
234 OccluderKind::Sphere => {
235 let center: Vector3D = occluder.get_center();
236 let radius: f64 = occluder.get_extent().get_x();
237 let r: Vector3D = Vector3D::new(radius, radius, radius);
238 (center - r, center + r)
239 }
240 OccluderKind::Triangle => {
241 let [v0, v1, v2]: [Vector3D; 3] = occluder.get_vertices();
242 let lo: Vector3D = vector3d_min(v0, vector3d_min(v1, v2));
243 let hi: Vector3D = vector3d_max(v0, vector3d_max(v1, v2));
244 (lo, hi)
245 }
246 };
247 (vector3d_min(mn, mx), vector3d_max(mn, mx))
248}
249
250/// Flattens every occluder into `(center, radius)` sphere tuples used by
251/// [`soft_shadow_factor`].
252///
253/// For sphere occluders the tuple is `(center, radius)`. For AABB
254/// occluders a conservative bounding sphere is computed from the AABB.
255///
256/// # Arguments
257///
258/// - `&[Occluder]` - The occluders to flatten into bounding spheres.
259///
260/// # Returns
261///
262/// - `Vec<(Vector3D, f64)>` - The `(center, radius)` sphere of each occluder.
263pub(crate) fn collect_occluder_points(occluders: &[Occluder]) -> Vec<(Vector3D, f64)> {
264 let mut out: Vec<(Vector3D, f64)> = Vec::new();
265 for occ in occluders.iter() {
266 let (mn, mx): (Vector3D, Vector3D) = occluder_aabb_extents(occ);
267 let cx: f64 = (mn.get_x() + mx.get_x()) * 0.5;
268 let cy: f64 = (mn.get_y() + mx.get_y()) * 0.5;
269 let cz: f64 = (mn.get_z() + mx.get_z()) * 0.5;
270 let ex: f64 = (mx.get_x() - mn.get_x()) * 0.5;
271 let ey: f64 = (mx.get_y() - mn.get_y()) * 0.5;
272 let ez: f64 = (mx.get_z() - mn.get_z()) * 0.5;
273 let r: f64 = (ex * ex + ey * ey + ez * ez).sqrt();
274 out.push((Vector3D::new(cx, cy, cz), r));
275 }
276 out
277}
278
279/// Returns the surface area of an AABB, the cost metric behind the
280/// surface-area-heuristic split search.
281///
282/// # Arguments
283///
284/// - `Vector3D` - The AABB minimum corner.
285/// - `Vector3D` - The AABB maximum corner.
286///
287/// # Returns
288///
289/// - `f64` - Twice the surface area, clamped to zero for flat or inverted
290/// bounds.
291fn aabb_surface_area(lower: Vector3D, upper: Vector3D) -> f64 {
292 let dx: f64 = (upper.get_x() - lower.get_x()).max(0.0);
293 let dy: f64 = (upper.get_y() - lower.get_y()).max(0.0);
294 let dz: f64 = (upper.get_z() - lower.get_z()).max(0.0);
295 2.0 * (dx * dy + dy * dz + dz * dx)
296}
297
298/// Returns the component of `value` on the axis selected by `axis`.
299///
300/// # Arguments
301///
302/// - `Vector3D` - The vector to read.
303/// - `usize` - `0` for `x`, `1` for `y`, `2` for `z`.
304///
305/// # Returns
306///
307/// - `f64` - The selected component.
308fn axis_component(value: Vector3D, axis: usize) -> f64 {
309 match axis {
310 0 => value.get_x(),
311 1 => value.get_y(),
312 _ => value.get_z(),
313 }
314}
315
316/// Intersects a ray with a sphere, honouring the ray's parameter range.
317///
318/// Unlike the free function in `lighting`, this variant solves the
319/// quadratic in general form so a non-unit direction reports true
320/// distances, and it accepts the exit root when the origin is already
321/// inside the sphere. A ray whose direction is zero, a negative radius, or
322/// any `NaN` input never reports a hit.
323///
324/// # Arguments
325///
326/// - `Vector3D` - The ray origin.
327/// - `Vector3D` - The ray direction, not required to be unit length.
328/// - `Vector3D` - The sphere center.
329/// - `f64` - The sphere radius.
330/// - `f64` - The minimum acceptable hit parameter.
331/// - `f64` - The maximum acceptable hit parameter.
332///
333/// # Returns
334///
335/// - `Option<(f64, Vector3D)>` - The hit parameter and the outward unit
336/// normal, or `None` when the range-constrained ray misses.
337fn intersect_sphere_in_range(
338 origin: Vector3D,
339 dir: Vector3D,
340 center: Vector3D,
341 radius: f64,
342 t_min: f64,
343 t_max: f64,
344) -> Option<(f64, Vector3D)> {
345 // A zero or negative radius describes no surface at all, and the
346 // normalising step below would divide by its length, so reject both
347 // before any arithmetic.
348 if !radius.is_finite() || radius <= 0.0 {
349 return None;
350 }
351 if !is_finite_vector(origin) || !is_finite_vector(dir) || !is_finite_vector(center) {
352 return None;
353 }
354 let a: f64 = dir.dot(dir);
355 // A zero-length or non-finite direction carries no ray: a NaN `a` would
356 // divide every candidate by NaN, so the guard must reject it explicitly
357 // rather than relying on a negated comparison.
358 if !a.is_finite() || a <= 0.0 {
359 return None;
360 }
361 let oc: Vector3D = origin - center;
362 let half_b: f64 = oc.dot(dir);
363 let c: f64 = oc.dot(oc) - radius * radius;
364 let discriminant: f64 = half_b * half_b - a * c;
365 // NaN and infinity must both miss: a negative discriminant is the miss
366 // case, and a non-finite one has no real root to take a square root of.
367 let discriminant_usable: bool = discriminant.is_finite() && discriminant >= 0.0;
368 if !discriminant_usable {
369 return None;
370 }
371 let root: f64 = discriminant.sqrt();
372 let near: f64 = (-half_b - root) / a;
373 let far: f64 = (-half_b + root) / a;
374 let t: f64 = if near >= t_min && near <= t_max {
375 near
376 } else if far >= t_min && far <= t_max {
377 far
378 } else {
379 return None;
380 };
381 let normal: Vector3D = (origin + dir.scaled(t) - center).normalized();
382 Some((t, normal))
383}
384
385/// Intersects a ray with an AABB, honouring the ray's parameter range.
386///
387/// The slab test branches explicitly on a parallel axis instead of
388/// dividing by the direction component, so a ray aligned with an axis is
389/// decided by the remaining two axes rather than by `0 * infinity`
390/// producing a `NaN` that comparison operators would silently accept.
391/// When the origin is inside the box the exit parameter is reported, matching
392/// the sphere behaviour so a shadow ray leaving a solid surface finds its way
393/// out instead of being rejected as a self-hit. The reported normal is the
394/// box's outward face normal, on entry and on exit alike.
395///
396/// # Arguments
397///
398/// - `Vector3D` - The ray origin.
399/// - `Vector3D` - The ray direction, not required to be unit length.
400/// - `Vector3D` - The AABB minimum corner.
401/// - `Vector3D` - The AABB maximum corner.
402/// - `f64` - The minimum acceptable hit parameter.
403/// - `f64` - The maximum acceptable hit parameter.
404///
405/// # Returns
406///
407/// - `Option<(f64, Vector3D)>` - The hit parameter and the outward unit
408/// normal, or `None` when the range-constrained ray misses.
409fn intersect_aabb_in_range(
410 origin: Vector3D,
411 dir: Vector3D,
412 lower: Vector3D,
413 upper: Vector3D,
414 t_min: f64,
415 t_max: f64,
416) -> Option<(f64, Vector3D)> {
417 if !is_finite_vector(origin) || !is_finite_vector(dir) {
418 return None;
419 }
420 if !is_finite_vector(lower) || !is_finite_vector(upper) {
421 return None;
422 }
423 let lo: Vector3D = vector3d_min(lower, upper);
424 let hi: Vector3D = vector3d_max(lower, upper);
425 // A box collapsed on any axis has no face a ray can strike: every point
426 // of its slab interval is a single plane, so reporting it would invent a
427 // surface where the caller supplied none.
428 if lo.get_x() >= hi.get_x() || lo.get_y() >= hi.get_y() || lo.get_z() >= hi.get_z() {
429 return None;
430 }
431 let origins: [f64; 3] = [origin.get_x(), origin.get_y(), origin.get_z()];
432 let dirs: [f64; 3] = [dir.get_x(), dir.get_y(), dir.get_z()];
433 let los: [f64; 3] = [lo.get_x(), lo.get_y(), lo.get_z()];
434 let his: [f64; 3] = [hi.get_x(), hi.get_y(), hi.get_z()];
435 let mut t_enter: f64 = f64::NEG_INFINITY;
436 let mut t_exit: f64 = f64::INFINITY;
437 let mut enter_axis: usize = 0;
438 let mut exit_axis: usize = 0;
439 for axis in 0..3 {
440 let d: f64 = dirs[axis];
441 let o: f64 = origins[axis];
442 if d.abs() < RAYTRACE_TRIANGLE_EPSILON {
443 if o < los[axis] || o > his[axis] {
444 return None;
445 }
446 continue;
447 }
448 let inverse: f64 = 1.0 / d;
449 let plane_a: f64 = (los[axis] - o) * inverse;
450 let plane_b: f64 = (his[axis] - o) * inverse;
451 // Dividing by a negative direction component swaps the two planes,
452 // so the pair must be ordered before it is compared against the
453 // running interval. Without this swap a ray travelling along a
454 // negative axis records the far plane as the near one, which both
455 // rejects genuine hits and reports the wrong face normal.
456 let (near, far): (f64, f64) = if plane_a <= plane_b {
457 (plane_a, plane_b)
458 } else {
459 (plane_b, plane_a)
460 };
461 if near > t_enter {
462 t_enter = near;
463 enter_axis = axis;
464 }
465 if far < t_exit {
466 t_exit = far;
467 exit_axis = axis;
468 }
469 }
470 if t_enter > t_exit {
471 return None;
472 }
473 // The origin is inside only when the whole slab interval straddles the
474 // origin. Comparing `t_enter` to `t_min` alone is wrong for a ray that
475 // begins outside the box but before it: there `t_enter` is positive yet
476 // the box is still ahead of the origin, and reporting the exit would
477 // skip the real surface. A box entirely behind the origin has
478 // `t_exit < 0`, so it also fails this test and its entry parameter is
479 // rejected by the range check below.
480 let inside: bool = t_enter <= 0.0 && t_exit >= 0.0;
481 let t: f64 = if inside { t_exit } else { t_enter };
482 if !t.is_finite() {
483 return None;
484 }
485 if t < t_min {
486 return None;
487 }
488 if t > t_max {
489 return None;
490 }
491 let axis: usize = if inside { exit_axis } else { enter_axis };
492 // The normal is the box's own outward face normal, matching the
493 // documented `Hit` contract: a ray entering through the low face meets
494 // that face's outward direction, and a ray that started inside and
495 // leaves through the high face carries that face's outward direction.
496 // So the sign follows the face the ray meets, not the ray itself: entry
497 // opposes the direction of travel, exit agrees with it.
498 let travelling_positive: bool = dirs[axis] > 0.0;
499 let sign: f64 = if inside {
500 if travelling_positive { 1.0 } else { -1.0 }
501 } else if travelling_positive {
502 -1.0
503 } else {
504 1.0
505 };
506 let normal: Vector3D = match axis {
507 0 => Vector3D::new(sign, 0.0, 0.0),
508 1 => Vector3D::new(0.0, sign, 0.0),
509 _ => Vector3D::new(0.0, 0.0, sign),
510 };
511 Some((t, normal))
512}
513
514/// Runs the exact intersection test for one occluder inside a parameter
515/// range.
516///
517/// This is the single source of truth for what counts as a hit: sphere,
518/// AABB and triangle each dispatch to their own range-aware routine, and a
519/// degenerate or non-finite occluder falls through to `None` rather than
520/// registering a spurious surface.
521///
522/// # Arguments
523///
524/// - `&Occluder` - The occluder to test.
525/// - `Vector3D` - The ray origin.
526/// - `Vector3D` - The ray direction, not required to be unit length.
527/// - `f64` - The minimum acceptable hit parameter.
528/// - `f64` - The maximum acceptable hit parameter.
529///
530/// # Returns
531///
532/// - `Option<(f64, Vector3D)>` - The hit parameter and the outward unit
533/// normal, or `None` when the occluder is missed or degenerate.
534fn intersect_occluder_in_range(
535 occluder: &Occluder,
536 origin: Vector3D,
537 dir: Vector3D,
538 t_min: f64,
539 t_max: f64,
540) -> Option<(f64, Vector3D)> {
541 match occluder.get_kind() {
542 OccluderKind::Sphere => intersect_sphere_in_range(
543 origin,
544 dir,
545 occluder.get_center(),
546 occluder.get_extent().get_x(),
547 t_min,
548 t_max,
549 ),
550 OccluderKind::Aabb => {
551 let (lower, upper): (Vector3D, Vector3D) = occluder_aabb_extents(occluder);
552 intersect_aabb_in_range(origin, dir, lower, upper, t_min, t_max)
553 }
554 OccluderKind::Triangle => {
555 let [v0, v1, v2]: [Vector3D; 3] = occluder.get_vertices();
556 match intersect_triangle(origin, dir, v0, v1, v2) {
557 Some((t, normal)) if t.is_finite() && t >= t_min && t <= t_max => Some((t, normal)),
558 _ => None,
559 }
560 }
561 }
562}
563
564/// Returns the tight AABB covering the primitives listed in
565/// `indices[start..start + count]`.
566///
567/// # Arguments
568///
569/// - `&[usize]` - The primitive indexes of the range to measure.
570/// - `&BvhBuild` - The per-primitive bounds and centroids.
571/// - `usize` - The first index of the range.
572/// - `usize` - The number of indexes in the range.
573///
574/// # Returns
575///
576/// - `(Vector3D, Vector3D)` - The `(min, max)` corners covering the range.
577fn bounds_of_range(
578 indices: &[usize],
579 build: &BvhBuild,
580 start: usize,
581 count: usize,
582) -> (Vector3D, Vector3D) {
583 let mut lower: Vector3D = Vector3D::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
584 let mut upper: Vector3D =
585 Vector3D::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
586 for slot in 0..count {
587 let primitive: usize = indices[start + slot];
588 lower = vector3d_min(lower, build.lower[primitive]);
589 upper = vector3d_max(upper, build.upper[primitive]);
590 }
591 (lower, upper)
592}
593
594/// Returns the widest centroid axis of a range together with its midpoint.
595///
596/// This is the fallback split used when the binned heuristic finds no
597/// worthwhile bin boundary. Selecting the axis by extent rather than by
598/// sorting keeps a degenerate centroid cloud halvable.
599///
600/// # Arguments
601///
602/// - `&[usize]` - The primitive indexes of the range.
603/// - `&BvhBuild` - The per-primitive bounds and centroids.
604/// - `usize` - The first index of the range.
605/// - `usize` - The number of indexes in the range.
606///
607/// # Returns
608///
609/// - `(usize, f64)` - The `(axis, midpoint)` cutting the widest centroid
610/// spread.
611fn longest_centroid_axis(
612 indices: &[usize],
613 build: &BvhBuild,
614 start: usize,
615 count: usize,
616) -> (usize, f64) {
617 let centroids: &[Vector3D] = &build.centroids;
618 let mut best_axis: usize = 0;
619 let mut best_span: f64 = -1.0;
620 let mut best_mid: f64 = 0.0;
621 for axis in 0..3 {
622 let mut min: f64 = f64::INFINITY;
623 let mut max: f64 = f64::NEG_INFINITY;
624 for slot in 0..count {
625 let value: f64 = axis_component(centroids[indices[start + slot]], axis);
626 min = min.min(value);
627 max = max.max(value);
628 }
629 let span: f64 = max - min;
630 if span > best_span {
631 best_span = span;
632 best_axis = axis;
633 best_mid = (min + max) * 0.5;
634 }
635 }
636 (best_axis, best_mid)
637}
638
639/// Partitions `indices[start..start + count]` around `split_value` on one
640/// centroid axis and reports where the left partition ends.
641///
642/// The partition is an in-place two-way scan, so one level of the build
643/// costs linear time instead of sorting every axis at every level.
644/// Degenerate centroid sets can place every element on one side; the caller
645/// detects a `0` or `count` result and falls back to halving the range,
646/// which always makes progress.
647///
648/// # Arguments
649///
650/// - `&mut [usize]` - The primitive index array, partitioned in place.
651/// - `&BvhBuild` - The per-primitive bounds and centroids.
652/// - `usize` - The first index of the range to partition.
653/// - `usize` - The number of indexes in the range.
654/// - `usize` - `0` for `x`, `1` for `y`, `2` for `z`.
655/// - `f64` - The split position on that axis.
656///
657/// # Returns
658///
659/// - `usize` - The number of entries placed on the left side.
660fn partition_range(
661 indices: &mut [usize],
662 build: &BvhBuild,
663 start: usize,
664 count: usize,
665 axis: usize,
666 split_value: f64,
667) -> usize {
668 let centroids: &[Vector3D] = &build.centroids;
669 let mut left: usize = 0;
670 let mut right: usize = count;
671 while left < right {
672 while left < right && axis_component(centroids[indices[start + left]], axis) < split_value {
673 left += 1;
674 }
675 while right > left
676 && axis_component(centroids[indices[start + right - 1]], axis) >= split_value
677 {
678 right -= 1;
679 }
680 if left >= right {
681 break;
682 }
683 indices.swap(start + left, start + right - 1);
684 left += 1;
685 right -= 1;
686 }
687 left
688}
689
690/// Searches all three axes for the binned surface-area-heuristic split.
691///
692/// Each axis is cut into [`RAYTRACE_BVH_BINS`] bins by centroid, swept left
693/// to right with a running cost, then swept right to left, so a level
694/// costs linear time in the primitive count with no sorting at all. A
695/// split is rejected when it cannot beat leaving the range as one leaf,
696/// which keeps clustered or coincident geometry from being chopped into
697/// ever thinner slices.
698///
699/// # Arguments
700///
701/// - `&[usize]` - The primitive indexes of the range to split.
702/// - `&BvhBuild` - The per-primitive bounds and centroids.
703/// - `usize` - The first index of the range.
704/// - `usize` - The number of indexes in the range.
705/// - `f64` - The surface area of the range's parent bounds.
706///
707/// # Returns
708///
709/// - `Option<(usize, f64)>` - The chosen `(axis, split position)`, or
710/// `None` when no bin boundary improves on a leaf.
711fn choose_sah_split(
712 indices: &[usize],
713 build: &BvhBuild,
714 start: usize,
715 count: usize,
716 parent_area: f64,
717) -> Option<(usize, f64)> {
718 if count < 2 || !parent_area.is_finite() || parent_area <= 0.0 {
719 return None;
720 }
721 let centroids: &[Vector3D] = &build.centroids;
722 let mut best: Option<(usize, f64, f64)> = None;
723 for axis in 0..3 {
724 let mut centroid_min: f64 = f64::INFINITY;
725 let mut centroid_max: f64 = f64::NEG_INFINITY;
726 for slot in 0..count {
727 let value: f64 = axis_component(centroids[indices[start + slot]], axis);
728 centroid_min = centroid_min.min(value);
729 centroid_max = centroid_max.max(value);
730 }
731 let span: f64 = centroid_max - centroid_min;
732 if !span.is_finite() || span <= RAYTRACE_BVH_AXIS_EPSILON {
733 continue;
734 }
735 let scale: f64 = RAYTRACE_BVH_BINS as f64 / span;
736 let mut bin_lower: [Vector3D; RAYTRACE_BVH_BINS] = [Vector3D::zero(); RAYTRACE_BVH_BINS];
737 let mut bin_upper: [Vector3D; RAYTRACE_BVH_BINS] = [Vector3D::zero(); RAYTRACE_BVH_BINS];
738 let mut bin_count: [usize; RAYTRACE_BVH_BINS] = [0; RAYTRACE_BVH_BINS];
739 for slot in 0..count {
740 let primitive: usize = indices[start + slot];
741 let scaled: f64 = (axis_component(centroids[primitive], axis) - centroid_min) * scale;
742 let mut bin: usize = scaled as usize;
743 if bin >= RAYTRACE_BVH_BINS {
744 bin = RAYTRACE_BVH_BINS - 1;
745 }
746 if bin_count[bin] == 0 {
747 bin_lower[bin] = build.lower[primitive];
748 bin_upper[bin] = build.upper[primitive];
749 } else {
750 bin_lower[bin] = vector3d_min(bin_lower[bin], build.lower[primitive]);
751 bin_upper[bin] = vector3d_max(bin_upper[bin], build.upper[primitive]);
752 }
753 bin_count[bin] += 1;
754 }
755 let mut left_area: [f64; RAYTRACE_BVH_BINS] = [0.0; RAYTRACE_BVH_BINS];
756 let mut left_count: [usize; RAYTRACE_BVH_BINS] = [0; RAYTRACE_BVH_BINS];
757 let mut running_lower: Vector3D =
758 Vector3D::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
759 let mut running_upper: Vector3D =
760 Vector3D::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
761 let mut running_count: usize = 0;
762 for bin in 0..RAYTRACE_BVH_BINS {
763 if bin_count[bin] > 0 {
764 running_lower = vector3d_min(running_lower, bin_lower[bin]);
765 running_upper = vector3d_max(running_upper, bin_upper[bin]);
766 running_count += bin_count[bin];
767 }
768 left_area[bin] = aabb_surface_area(running_lower, running_upper);
769 left_count[bin] = running_count;
770 }
771 let mut right_lower: Vector3D = Vector3D::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
772 let mut right_upper: Vector3D =
773 Vector3D::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
774 let mut right_count: usize = 0;
775 for bin in (1..RAYTRACE_BVH_BINS).rev() {
776 if bin_count[bin] > 0 {
777 right_lower = vector3d_min(right_lower, bin_lower[bin]);
778 right_upper = vector3d_max(right_upper, bin_upper[bin]);
779 right_count += bin_count[bin];
780 }
781 let left_total: usize = left_count[bin - 1];
782 if left_total == 0 || right_count == 0 {
783 continue;
784 }
785 let cost: f64 = left_area[bin - 1] * left_total as f64
786 + aabb_surface_area(right_lower, right_upper) * right_count as f64;
787 if matches!(best, Some((_, _, incumbent)) if cost >= incumbent) {
788 continue;
789 }
790 best = Some((axis, centroid_min + bin as f64 / scale, cost));
791 }
792 }
793 let (axis, boundary, cost): (usize, f64, f64) = best?;
794 let incumbent: f64 = count as f64 * parent_area;
795 if !cost.is_finite() || cost >= incumbent {
796 return None;
797 }
798 Some((axis, boundary))
799}
800
801/// Builds one BVH subtree in preorder, appending its root and descendants.
802///
803/// The root is pushed before its children so the array stays a dense
804/// preorder sequence: the left child lands immediately after the root, and
805/// the escape is back-patched once both subtrees have been emitted. A
806/// range that already fits in a leaf, or that has reached
807/// [`RAYTRACE_BVH_MAX_DEPTH`], is never split: the depth cap is what keeps
808/// the finished hierarchy inside the fixed traversal stack, because the
809/// heuristic can otherwise chain linearly on a geometric spread.
810///
811/// # Arguments
812///
813/// - `&mut Vec<RayTraceNode>` - The node array under construction.
814/// - `&mut [usize]` - The primitive index array, permuted in place.
815/// - `&BvhBuild` - The per-primitive bounds and centroids.
816/// - `(Vector3D, Vector3D)` - The bounds of this subtree.
817/// - `usize` - The first primitive index of this range.
818/// - `usize` - The number of primitive indexes in this range.
819/// - `usize` - The nesting level of this subtree, starting at zero.
820fn build_bvh_subtree(
821 nodes: &mut Vec<RayTraceNode>,
822 indices: &mut [usize],
823 build: &BvhBuild,
824 bounds: (Vector3D, Vector3D),
825 start: usize,
826 count: usize,
827 depth: usize,
828) {
829 let root: usize = nodes.len();
830 let too_small_to_split: bool = count <= RAYTRACE_BVH_LEAF_SIZE || count < 2;
831 if too_small_to_split || depth >= RAYTRACE_BVH_MAX_DEPTH {
832 nodes.push(RayTraceNode {
833 lower: bounds.0,
834 upper: bounds.1,
835 start,
836 count,
837 escape: root + 1,
838 });
839 return;
840 }
841 let (axis, boundary): (usize, f64) = match choose_sah_split(
842 indices,
843 build,
844 start,
845 count,
846 aabb_surface_area(bounds.0, bounds.1),
847 ) {
848 Some(chosen) => chosen,
849 None => longest_centroid_axis(indices, build, start, count),
850 };
851 let partitioned: usize = partition_range(indices, build, start, count, axis, boundary);
852 let left_count: usize = if partitioned == 0 || partitioned == count {
853 count / 2
854 } else {
855 partitioned
856 };
857 nodes.push(RayTraceNode {
858 lower: bounds.0,
859 upper: bounds.1,
860 start,
861 count: 0,
862 escape: root + 1,
863 });
864 let left_bounds: (Vector3D, Vector3D) = bounds_of_range(indices, build, start, left_count);
865 build_bvh_subtree(
866 nodes,
867 indices,
868 build,
869 left_bounds,
870 start,
871 left_count,
872 depth + 1,
873 );
874 let right_bounds: (Vector3D, Vector3D) =
875 bounds_of_range(indices, build, start + left_count, count - left_count);
876 build_bvh_subtree(
877 nodes,
878 indices,
879 build,
880 right_bounds,
881 start + left_count,
882 count - left_count,
883 depth + 1,
884 );
885 nodes[root].escape = nodes.len();
886}
887
888/// Builds the flat BVH for a list of occluders.
889///
890/// The result is the pair of arrays [`RayTraceScene`] stores: preorder
891/// nodes and the permuted primitive index list the leaves address. The
892/// hierarchy is built exactly once at construction, so no query pays for
893/// it.
894///
895/// # Arguments
896///
897/// - `&[Occluder]` - The occluders to bound.
898///
899/// # Returns
900///
901/// - `(Vec<RayTraceNode>, Vec<usize>)` - The preorder node array and the
902/// reordered primitive indexes; both empty for an empty scene.
903pub(crate) fn build_bvh(occluders: &[Occluder]) -> (Vec<RayTraceNode>, Vec<usize>) {
904 let count: usize = occluders.len();
905 let mut nodes: Vec<RayTraceNode> = Vec::new();
906 let mut indices: Vec<usize> = Vec::with_capacity(count);
907 if count == 0 {
908 return (nodes, indices);
909 }
910 let mut lower: Vec<Vector3D> = Vec::with_capacity(count);
911 let mut upper: Vec<Vector3D> = Vec::with_capacity(count);
912 let mut centroids: Vec<Vector3D> = Vec::with_capacity(count);
913 for (index, occluder) in occluders.iter().enumerate() {
914 let (lo, hi): (Vector3D, Vector3D) = occluder_aabb_extents(occluder);
915 let midpoint: Vector3D = (lo + hi).scaled(0.5);
916 let centroid: Vector3D = if is_finite_vector(midpoint) {
917 midpoint
918 } else {
919 Vector3D::zero()
920 };
921 lower.push(lo);
922 upper.push(hi);
923 centroids.push(centroid);
924 indices.push(index);
925 }
926 let build: BvhBuild = BvhBuild {
927 lower,
928 upper,
929 centroids,
930 };
931 let root_bounds: (Vector3D, Vector3D) = bounds_of_range(&indices, &build, 0, count);
932 nodes.reserve(count.saturating_mul(2));
933 build_bvh_subtree(&mut nodes, &mut indices, &build, root_bounds, 0, count, 0);
934 (nodes, indices)
935}
936
937/// Reports whether a ray's range and geometry are usable for a query.
938///
939/// An inverted or non-finite range, or a non-finite origin or direction,
940/// cannot produce a meaningful intersection, so both query entry points
941/// reject such rays instead of letting `NaN` comparisons decide.
942///
943/// # Arguments
944///
945/// - `&Ray` - The ray to validate.
946///
947/// # Returns
948///
949/// - `bool` - `true` when the ray may be traced.
950fn ray_is_traceable(ray: &Ray) -> bool {
951 let t_min: f64 = ray.get_t_min();
952 let t_max: f64 = ray.get_t_max();
953 if !t_min.is_finite() || !t_max.is_finite() || t_min > t_max {
954 return false;
955 }
956 is_finite_vector(ray.get_origin()) && is_finite_vector(ray.get_direction())
957}
958
959/// Returns the direction a ray may be traced with.
960///
961/// The direction is deliberately passed through unchanged. `t` is consumed
962/// by `Ray::at(t)`, which advances along the raw direction, so any
963/// rescaling here would make the reported parameter disagree with the point
964/// the caller reconstructs from it. Each shape therefore solves for `t` in
965/// the ray's own parameterisation: the sphere uses the full quadratic with
966/// `a = d . d`, and the slab test divides by each direction component.
967///
968/// # Arguments
969///
970/// - `&Ray` - The ray to validate.
971///
972/// # Returns
973///
974/// - `Option<Vector3D>` - The original direction, or `None` when the ray is
975/// degenerate and cannot be traced without a division blow-up.
976fn traceable_direction_of(ray: &Ray) -> Option<Vector3D> {
977 if !ray_is_traceable(ray) {
978 return None;
979 }
980 let dir: Vector3D = ray.get_direction();
981 let squared_length: f64 = dir.dot(dir);
982 if !squared_length.is_finite() || squared_length <= 0.0 {
983 return None;
984 }
985 Some(dir)
986}
987
988/// Runs the closest-hit query as a straight scan over every primitive.
989///
990/// Used for scenes too small for the hierarchy to pay for itself. The tie
991/// rule is identical to the traversal's: a nearer hit always wins, and an
992/// exact tie is resolved towards the lower original index, so the linear
993/// and hierarchical paths cannot disagree on the same scene.
994///
995/// # Arguments
996///
997/// - `&Ray` - The ray being traced.
998/// - `&[Occluder]` - Every primitive in the scene.
999/// - `Vector3D` - The validated ray direction.
1000///
1001/// # Returns
1002///
1003/// - `Option<(usize, f64, Vector3D, Vector3D)>` - The winning primitive
1004/// index, the hit parameter, the hit point and the surface normal.
1005fn linear_closest_hit(
1006 ray: &Ray,
1007 occluders: &[Occluder],
1008 dir: Vector3D,
1009) -> Option<(usize, f64, Vector3D, Vector3D)> {
1010 let t_min: f64 = ray.get_t_min();
1011 let mut best: Option<(usize, f64, Vector3D, Vector3D)> = None;
1012 for (index, occluder) in occluders.iter().enumerate() {
1013 let hit: Option<(f64, Vector3D)> =
1014 intersect_occluder_in_range(occluder, ray.get_origin(), dir, t_min, ray.get_t_max());
1015 let Some((t, normal)) = hit else {
1016 continue;
1017 };
1018 let nearer: bool = match &best {
1019 None => true,
1020 Some((best_index, best_t, _, _)) => {
1021 t < *best_t || (t == *best_t && index < *best_index)
1022 }
1023 };
1024 if nearer {
1025 best = Some((index, t, ray.get_origin() + dir.scaled(t), normal));
1026 }
1027 }
1028 best
1029}
1030
1031/// Runs the any-hit query as a straight scan over every primitive.
1032///
1033/// # Arguments
1034///
1035/// - `&Ray` - The ray being tested.
1036/// - `&[Occluder]` - Every primitive in the scene.
1037/// - `Vector3D` - The validated ray direction.
1038///
1039/// # Returns
1040///
1041/// - `bool` - Whether any primitive blocks the ray.
1042fn linear_occluded(ray: &Ray, occluders: &[Occluder], dir: Vector3D) -> bool {
1043 occluders.iter().any(|occluder: &Occluder| {
1044 intersect_occluder_in_range(
1045 occluder,
1046 ray.get_origin(),
1047 dir,
1048 ray.get_t_min(),
1049 ray.get_t_max(),
1050 )
1051 .is_some()
1052 })
1053}
1054
1055/// Finds the closest intersection between a ray and a scene's BVH without
1056/// touching any [`Material`].
1057///
1058/// Traversal walks the preorder node array with a fixed-size stack held on
1059/// the caller's frame, so a query allocates nothing. Each node is tested
1060/// against the current best distance, which prunes whole subtrees, and the
1061/// nearer child is pushed last so it pops first. Ties are broken by the
1062/// lowest original primitive index rather than by traversal order, so the
1063/// result is stable no matter how the tree was built.
1064///
1065/// # Arguments
1066///
1067/// - `&Ray` - The ray to test.
1068/// - `&RayTraceScene` - The scene holding the hierarchy.
1069///
1070/// # Returns
1071///
1072/// - `Option<(usize, f64, Vector3D, Vector3D)>` - The occluder index, the
1073/// hit distance `t`, the hit position, and the surface normal, or `None`
1074/// if the ray misses.
1075pub(crate) fn closest_hit_indexed(
1076 ray: &Ray,
1077 scene: &RayTraceScene,
1078) -> Option<(usize, f64, Vector3D, Vector3D)> {
1079 let dir: Vector3D = traceable_direction_of(ray)?;
1080 let nodes: &[RayTraceNode] = scene.get_nodes();
1081 if nodes.is_empty() {
1082 return None;
1083 }
1084 let occluders: &[Occluder] = scene.get_occluders();
1085 if occluders.len() <= RAYTRACE_LINEAR_MAX_PRIMITIVES {
1086 return linear_closest_hit(ray, occluders, dir);
1087 }
1088 let occluders: &[Occluder] = scene.get_occluders();
1089 let indices: &[usize] = scene.get_indices();
1090 let origin: Vector3D = ray.get_origin();
1091 let t_min: f64 = ray.get_t_min();
1092 let mut best_t: f64 = ray.get_t_max();
1093 let mut best_index: usize = 0;
1094 let mut best_normal: Vector3D = Vector3D::zero();
1095 let mut found: bool = false;
1096 let mut stack: [usize; RAYTRACE_BVH_STACK_SIZE] = [0usize; RAYTRACE_BVH_STACK_SIZE];
1097 let root: &RayTraceNode = &nodes[0];
1098 // The root's entry parameter is only a probe: a root the ray misses means
1099 // the whole subtree is out of range, so `?` on the discarded value is the
1100 // early return clippy asks for.
1101 let _root_entry: f64 = aabb_entry_parameter(
1102 origin,
1103 dir,
1104 root.get_lower(),
1105 root.get_upper(),
1106 t_min,
1107 best_t,
1108 )?;
1109 stack[0] = 0usize;
1110 let mut depth: usize = 1;
1111 while depth > 0 {
1112 depth -= 1;
1113 let node_index: usize = stack[depth];
1114 let node: RayTraceNode = nodes[node_index];
1115 if node.get_count() == 0 {
1116 let left_index: usize = node_index + 1;
1117 let right_index: usize = nodes[left_index].get_escape();
1118 let left: &RayTraceNode = &nodes[left_index];
1119 let right: &RayTraceNode = &nodes[right_index];
1120 let left_entry: Option<f64> = aabb_entry_parameter(
1121 origin,
1122 dir,
1123 left.get_lower(),
1124 left.get_upper(),
1125 t_min,
1126 best_t,
1127 );
1128 let right_entry: Option<f64> = aabb_entry_parameter(
1129 origin,
1130 dir,
1131 right.get_lower(),
1132 right.get_upper(),
1133 t_min,
1134 best_t,
1135 );
1136 match (left_entry, right_entry) {
1137 (None, None) => {}
1138 (Some(_), None) => {
1139 stack[depth] = left_index;
1140 depth += 1;
1141 }
1142 (None, Some(_)) => {
1143 stack[depth] = right_index;
1144 depth += 1;
1145 }
1146 (Some(left_t), Some(right_t)) => {
1147 let near_first: usize = if left_t <= right_t {
1148 left_index
1149 } else {
1150 right_index
1151 };
1152 stack[depth] = if left_t <= right_t {
1153 right_index
1154 } else {
1155 left_index
1156 };
1157 depth += 1;
1158 stack[depth] = near_first;
1159 depth += 1;
1160 }
1161 }
1162 continue;
1163 }
1164 let start: usize = node.get_start();
1165 for slot in 0..node.get_count() {
1166 let primitive: usize = indices[start + slot];
1167 let hit: Option<(f64, Vector3D)> =
1168 intersect_occluder_in_range(&occluders[primitive], origin, dir, t_min, best_t);
1169 if let Some((t, normal)) = hit {
1170 let nearer: bool = t < best_t;
1171 let stable_tie: bool = t == best_t && (!found || primitive < best_index);
1172 if nearer || stable_tie {
1173 best_t = t;
1174 best_index = primitive;
1175 best_normal = normal;
1176 found = true;
1177 }
1178 }
1179 }
1180 }
1181 if !found {
1182 return None;
1183 }
1184 Some((best_index, best_t, origin + dir.scaled(best_t), best_normal))
1185}
1186
1187/// Reports whether any occluder blocks the ray's parameter range.
1188///
1189/// This is the any-hit counterpart to [`closest_hit_indexed`]: it returns
1190/// as soon as one occluder is hit, so a shadow ray stops at the first
1191/// blocker instead of resolving the whole hierarchy.
1192///
1193/// # Arguments
1194///
1195/// - `&Ray` - The ray to test.
1196/// - `&RayTraceScene` - The scene holding the hierarchy.
1197///
1198/// # Returns
1199///
1200/// - `bool` - `true` when at least one occluder intersects the range.
1201pub(crate) fn occluded_indexed(ray: &Ray, scene: &RayTraceScene) -> bool {
1202 let untraceable: Option<Vector3D> = traceable_direction_of(ray);
1203 let dir: Vector3D = match untraceable {
1204 Some(direction) => direction,
1205 None => return false,
1206 };
1207 let occluders: &[Occluder] = scene.get_occluders();
1208 if occluders.len() <= RAYTRACE_LINEAR_MAX_PRIMITIVES {
1209 return linear_occluded(ray, occluders, dir);
1210 }
1211 let nodes: &[RayTraceNode] = scene.get_nodes();
1212 if nodes.is_empty() {
1213 return false;
1214 }
1215 let occluders: &[Occluder] = scene.get_occluders();
1216 let indices: &[usize] = scene.get_indices();
1217 let origin: Vector3D = ray.get_origin();
1218 let t_min: f64 = ray.get_t_min();
1219 let t_max: f64 = ray.get_t_max();
1220 let mut stack: [usize; RAYTRACE_BVH_STACK_SIZE] = [0usize; RAYTRACE_BVH_STACK_SIZE];
1221 let root: &RayTraceNode = &nodes[0];
1222 if aabb_entry_parameter(
1223 origin,
1224 dir,
1225 root.get_lower(),
1226 root.get_upper(),
1227 t_min,
1228 t_max,
1229 )
1230 .is_none()
1231 {
1232 return false;
1233 }
1234 stack[0] = 0usize;
1235 let mut depth: usize = 1;
1236 while depth > 0 {
1237 depth -= 1;
1238 let node_index: usize = stack[depth];
1239 let node: RayTraceNode = nodes[node_index];
1240 if node.get_count() == 0 {
1241 let left_index: usize = node_index + 1;
1242 let right_index: usize = nodes[left_index].get_escape();
1243 for child_index in [left_index, right_index] {
1244 let child: &RayTraceNode = &nodes[child_index];
1245 let entry: Option<f64> = aabb_entry_parameter(
1246 origin,
1247 dir,
1248 child.get_lower(),
1249 child.get_upper(),
1250 t_min,
1251 t_max,
1252 );
1253 if entry.is_some() {
1254 stack[depth] = child_index;
1255 depth += 1;
1256 }
1257 }
1258 continue;
1259 }
1260 let start: usize = node.get_start();
1261 for slot in 0..node.get_count() {
1262 let primitive: usize = indices[start + slot];
1263 let blocked: bool =
1264 intersect_occluder_in_range(&occluders[primitive], origin, dir, t_min, t_max)
1265 .is_some();
1266 if blocked {
1267 return true;
1268 }
1269 }
1270 }
1271 false
1272}
1273
1274/// Builds a reflected ray bouncing off a surface point with the given
1275/// normal.
1276///
1277/// # Arguments
1278///
1279/// - `&Ray` - The incoming ray.
1280/// - `Vector3D` - The world-space hit position.
1281/// - `Vector3D` - The outward unit normal at the hit point.
1282///
1283/// # Returns
1284///
1285/// - `Ray` - A new ray originating at the hit point with the reflected
1286/// direction, `t_min` reset to `RAYTRACE_DEFAULT_T_MIN`, `t_max` set to
1287/// `RAYTRACE_DEFAULT_T_MAX`, and `depth` incremented by one.
1288fn bounce_ray(ray: &Ray, position: Vector3D, normal: Vector3D) -> Ray {
1289 let dir: Vector3D = ray.get_direction();
1290 let dot: f64 = dir.dot(normal);
1291 let reflected_dir: Vector3D = dir - normal.scaled(2.0 * dot);
1292 Ray {
1293 origin: position,
1294 direction: reflected_dir,
1295 t_min: RAYTRACE_DEFAULT_T_MIN,
1296 t_max: RAYTRACE_DEFAULT_T_MAX,
1297 depth: ray.get_depth() + 1,
1298 }
1299}
1300
1301/// Builds the shadow visibility callback consumed by
1302/// `LightingUniforms::shade_with_visibility`.
1303///
1304/// The callback turns a light into one exact any-hit query against the
1305/// scene's BVH. The ray leaves the shading point along the surface normal
1306/// rather than the surface itself, so a lit surface never shadows itself
1307/// through its own footprint, and the segment stops short of a finite light
1308/// position so geometry enclosing the emitter does not stay dark. A
1309/// surface already facing away from the light is reported as lit without
1310/// spending a query at all.
1311///
1312/// The returned factor is the full `1.0` / `0.0` visibility the shader
1313/// path also uses, so CPU and GPU agree on what "shadowed" means.
1314///
1315/// # Arguments
1316///
1317/// - `&'a RayTraceScene` - The scene to query for blockers.
1318/// - `Vector3D` - The world-space shading position.
1319/// - `Vector3D` - The surface normal at that position.
1320///
1321/// # Returns
1322/// - `impl FnMut(&Light, Vector3D, f64) -> f64` - A callback returning the
1323/// light's visibility from the shading point.
1324pub(crate) fn shadow_visibility<'a>(
1325 scene: &'a RayTraceScene,
1326 position: Vector3D,
1327 normal: Vector3D,
1328) -> impl FnMut(&Light, Vector3D, f64) -> f64 + 'a {
1329 let origin: Vector3D = position + normal.scaled(RAYTRACE_SHADOW_SELF_OFFSET);
1330 move |light: &Light, light_direction: Vector3D, max_distance: f64| {
1331 let facing: f64 = normal.dot(light_direction);
1332 if facing <= 0.0 {
1333 return 1.0;
1334 }
1335 let length: f64 = light_direction.magnitude();
1336 let distance_usable: bool =
1337 length.is_finite() && length > 0.0 && max_distance.is_finite() && max_distance > 0.0;
1338 if !distance_usable {
1339 return 1.0;
1340 }
1341 let limit: f64 = match light.get_kind() {
1342 LightType::Directional => max_distance,
1343 LightType::Point | LightType::Spot => max_distance - RAYTRACE_SHADOW_LIGHT_EPSILON,
1344 };
1345 let limit: f64 = if limit > RAYTRACE_SHADOW_SELF_OFFSET {
1346 limit
1347 } else {
1348 return 1.0;
1349 };
1350 let dir: Vector3D = light_direction.scaled(1.0 / length);
1351 let ray: Ray = Ray::new(origin, dir).with_range(RAYTRACE_SHADOW_SELF_OFFSET, limit);
1352 let blocked: bool = occluded_indexed(&ray, scene);
1353 if blocked { 0.0 } else { 1.0 }
1354 }
1355}
1356
1357/// Iteratively traces a ray against a scene using its BVH, performing no
1358/// heap allocation per bounce.
1359///
1360/// Shadows are resolved exactly through the scene's own any-hit traversal
1361/// rather than through the legacy bounding-sphere list, so a traced hit is
1362/// shaded with the same visibility the GPU path computes. Color
1363/// contributions are accumulated with a specular throughput: each bounce
1364/// multiplies the throughput by the hit material's specular intensity, and
1365/// a miss adds the ambient color scaled by the current throughput. The
1366/// bounce loop stops when the ray misses, when `depth` reaches
1367/// `max_bounces`, or when the hit material's specular intensity is not
1368/// greater than [`EPSILON`], matching the behavior of the historical
1369/// recursive formulation.
1370///
1371/// # Arguments
1372///
1373/// - `Ray` - The ray to trace.
1374/// - `&RayTraceScene` - The scene to trace through; its BVH resolves every
1375/// closest-hit and every shadow query.
1376/// - `&LightingUniforms` - Lighting parameters used during shading.
1377/// - `u32` - The maximum number of bounces allowed for this ray.
1378///
1379/// # Returns
1380///
1381/// - `Vector3D` - The final traced color.
1382pub(crate) fn trace_bounces(
1383 ray: Ray,
1384 scene: &RayTraceScene,
1385 lights: &LightingUniforms,
1386 max_bounces: u32,
1387) -> Vector3D {
1388 let ambient: Vector3D = lights.get_ambient();
1389 let mut color: Vector3D = Vector3D::zero();
1390 let mut throughput: f64 = 1.0;
1391 let mut current: Ray = ray;
1392 loop {
1393 let (index, _t, position, normal): (usize, f64, Vector3D, Vector3D) =
1394 match closest_hit_indexed(¤t, scene) {
1395 None => {
1396 color += ambient.scaled(throughput);
1397 break;
1398 }
1399 Some(hit) => hit,
1400 };
1401 let material: &Material = scene.get_occluders()[index].get_material();
1402 let shaded: Vector3D = lights.shade_with_visibility(
1403 position,
1404 normal,
1405 material,
1406 shadow_visibility(scene, position, normal),
1407 );
1408 color += shaded.scaled(throughput);
1409 let spec: f64 = material.get_specular();
1410 if current.get_depth() >= max_bounces || spec <= EPSILON {
1411 break;
1412 }
1413 throughput *= spec;
1414 current = bounce_ray(¤t, position, normal);
1415 }
1416 color
1417}