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