Skip to main content

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(&current, 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(&current, position, normal);
1415    }
1416    color
1417}