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