Skip to main content

oxiphysics_collision/ray_casting/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::f64::consts::PI;
6
7use super::functions::{
8    add, cross, dot, len, normalize, ray_aabb, ray_sphere, ray_triangle, reflect_ray, scale, sub,
9};
10
11/// The result of a successful ray-primitive intersection test.
12#[derive(Debug, Clone, Copy)]
13pub struct RayHit {
14    /// Parameter `t` along the ray at the hit point.
15    pub t: f64,
16    /// Hit point in world space.
17    pub point: [f64; 3],
18    /// Surface normal at the hit point (unit length, outward).
19    pub normal: [f64; 3],
20    /// UV texture coordinates at the hit point `(u, v)`.
21    pub uv: [f64; 2],
22    /// Index of the hit primitive (face / mesh index).
23    pub prim_id: usize,
24}
25impl RayHit {
26    /// Construct a new ray hit record.
27    pub fn new(t: f64, point: [f64; 3], normal: [f64; 3], uv: [f64; 2], prim_id: usize) -> Self {
28        Self {
29            t,
30            point,
31            normal,
32            uv,
33            prim_id,
34        }
35    }
36    /// Return `true` if this hit is closer than `other`.
37    pub fn closer_than(&self, other: &RayHit) -> bool {
38        self.t < other.t
39    }
40}
41/// Result of a batch ray-casting pass.
42#[derive(Debug, Clone)]
43pub struct BatchRayResult {
44    /// Per-ray closest hit, or `None` if the ray missed.
45    pub hits: Vec<Option<RayHit>>,
46}
47/// A node in a recursive ray tree.
48///
49/// Each ray may spawn up to two children: a reflected ray and/or a
50/// refracted ray.
51#[derive(Debug, Clone)]
52pub struct RayTreeNode {
53    /// The ray for this node.
54    pub ray: Ray,
55    /// Closest hit, if any.
56    pub hit: Option<RayHit>,
57    /// Attenuation factor for this ray's contribution.
58    pub weight: f64,
59    /// Reflected child (index into the tree node array).
60    pub reflected_child: Option<usize>,
61    /// Refracted child (index into the tree node array).
62    pub refracted_child: Option<usize>,
63}
64impl RayTreeNode {
65    /// Create a new ray tree node.
66    pub fn new(ray: Ray, weight: f64) -> Self {
67        Self {
68            ray,
69            hit: None,
70            weight,
71            reflected_child: None,
72            refracted_child: None,
73        }
74    }
75}
76/// Capsule: a cylinder with hemispherical caps.
77#[derive(Debug, Clone, Copy)]
78pub struct Capsule {
79    /// Centre of the first (bottom) hemisphere.
80    pub a: [f64; 3],
81    /// Centre of the second (top) hemisphere.
82    pub b: [f64; 3],
83    /// Capsule radius.
84    pub radius: f64,
85}
86impl Capsule {
87    /// Create a new capsule.
88    pub fn new(a: [f64; 3], b: [f64; 3], radius: f64) -> Self {
89        Self { a, b, radius }
90    }
91    /// Axis vector (unnormalised) from `a` to `b`.
92    pub fn axis(&self) -> [f64; 3] {
93        sub(self.b, self.a)
94    }
95    /// Length of the capsule (centre-to-centre).
96    pub fn length(&self) -> f64 {
97        len(self.axis())
98    }
99}
100/// Oriented Bounding Box.
101///
102/// Stored as a centre, a rotation matrix (columns are OBB axes), and
103/// half-extents along each local axis.
104#[derive(Debug, Clone, Copy)]
105pub struct Obb {
106    /// Centre of the OBB.
107    pub centre: [f64; 3],
108    /// Rotation matrix – rows are the local X, Y, Z axes in world space.
109    pub axes: [[f64; 3]; 3],
110    /// Half-extents along each local axis.
111    pub half_extents: [f64; 3],
112}
113impl Obb {
114    /// Create an OBB from centre, axes and half-extents.
115    pub fn new(centre: [f64; 3], axes: [[f64; 3]; 3], half_extents: [f64; 3]) -> Self {
116        Self {
117            centre,
118            axes,
119            half_extents,
120        }
121    }
122    /// Create an axis-aligned OBB (convenience wrapper).
123    pub fn from_aabb(aabb: &Aabb) -> Self {
124        Self::new(
125            aabb.centre(),
126            [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
127            aabb.half_extents(),
128        )
129    }
130}
131/// Sphere primitive defined by a centre and radius.
132#[derive(Debug, Clone, Copy)]
133pub struct Sphere {
134    /// Centre of the sphere.
135    pub centre: [f64; 3],
136    /// Radius of the sphere.
137    pub radius: f64,
138}
139impl Sphere {
140    /// Create a new sphere.
141    pub fn new(centre: [f64; 3], radius: f64) -> Self {
142        Self { centre, radius }
143    }
144    /// Compute the outward unit normal at a surface point.
145    pub fn normal_at(&self, p: [f64; 3]) -> [f64; 3] {
146        normalize(sub(p, self.centre))
147    }
148    /// Compute UV spherical coordinates `(u, v)` at a surface point.
149    ///
150    /// `u ∈ [0, 1]` maps azimuth, `v ∈ [0, 1]` maps elevation.
151    pub fn uv_at(&self, p: [f64; 3]) -> [f64; 2] {
152        let n = self.normal_at(p);
153        let u = 1.0 - (n[0].atan2(n[2]) + PI) / (2.0 * PI);
154        let v = (n[1].asin() + PI / 2.0) / PI;
155        [u, v]
156    }
157}
158/// A simple scene containing spheres and triangle meshes for ray casting.
159pub struct Scene {
160    /// Spheres in the scene.
161    pub spheres: Vec<Sphere>,
162    /// Triangle vertices.
163    pub vertices: Vec<[f64; 3]>,
164    /// Triangle index triples.
165    pub triangles: Vec<(usize, usize, usize)>,
166}
167impl Scene {
168    /// Create an empty scene.
169    pub fn new() -> Self {
170        Self {
171            spheres: Vec::new(),
172            vertices: Vec::new(),
173            triangles: Vec::new(),
174        }
175    }
176    /// Add a sphere to the scene.
177    pub fn add_sphere(&mut self, sphere: Sphere) {
178        self.spheres.push(sphere);
179    }
180    /// Add a triangle by vertex positions.
181    pub fn add_triangle(&mut self, v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) {
182        let base = self.vertices.len();
183        self.vertices.push(v0);
184        self.vertices.push(v1);
185        self.vertices.push(v2);
186        self.triangles.push((base, base + 1, base + 2));
187    }
188    /// Cast a ray against all primitives in the scene and return the closest hit.
189    pub fn cast(&self, ray: &Ray) -> Option<RayHit> {
190        let mut best: Option<RayHit> = None;
191        for sphere in &self.spheres {
192            if let Some(hit) = ray_sphere(ray, sphere) {
193                best = Some(match best {
194                    None => hit,
195                    Some(prev) => {
196                        if hit.t < prev.t {
197                            hit
198                        } else {
199                            prev
200                        }
201                    }
202                });
203            }
204        }
205        for &(i, j, k) in &self.triangles {
206            if let Some(hit) =
207                ray_triangle(ray, self.vertices[i], self.vertices[j], self.vertices[k], i)
208            {
209                best = Some(match best {
210                    None => hit,
211                    Some(prev) => {
212                        if hit.t < prev.t {
213                            hit
214                        } else {
215                            prev
216                        }
217                    }
218                });
219            }
220        }
221        best
222    }
223}
224/// A flat BVH built over a list of triangle primitives.
225pub struct Bvh {
226    /// Flat node array; node 0 is the root.
227    pub nodes: Vec<BvhNode>,
228    /// Primitives sorted according to the BVH build order.
229    pub prim_indices: Vec<usize>,
230    /// Vertex array shared with the scene.
231    pub vertices: Vec<[f64; 3]>,
232    /// Index triples (one per triangle).
233    pub indices: Vec<(usize, usize, usize)>,
234}
235impl Bvh {
236    /// Build a BVH over a triangle mesh using a simple median-split heuristic.
237    pub fn build(vertices: Vec<[f64; 3]>, indices: Vec<(usize, usize, usize)>) -> Self {
238        let n = indices.len();
239        let mut prim_indices: Vec<usize> = (0..n).collect();
240        let mut nodes = Vec::new();
241        Self::build_recursive(&vertices, &indices, &mut prim_indices, 0, n, &mut nodes);
242        Self {
243            nodes,
244            prim_indices,
245            vertices,
246            indices,
247        }
248    }
249    fn triangle_centroid(vertices: &[[f64; 3]], tri: (usize, usize, usize)) -> [f64; 3] {
250        let (i, j, k) = tri;
251        let v0 = vertices[i];
252        let v1 = vertices[j];
253        let v2 = vertices[k];
254        scale(add(add(v0, v1), v2), 1.0 / 3.0)
255    }
256    fn triangle_aabb(vertices: &[[f64; 3]], tri: (usize, usize, usize)) -> Aabb {
257        let (i, j, k) = tri;
258        let v0 = vertices[i];
259        let v1 = vertices[j];
260        let v2 = vertices[k];
261        let min = [
262            v0[0].min(v1[0]).min(v2[0]),
263            v0[1].min(v1[1]).min(v2[1]),
264            v0[2].min(v1[2]).min(v2[2]),
265        ];
266        let max = [
267            v0[0].max(v1[0]).max(v2[0]),
268            v0[1].max(v1[1]).max(v2[1]),
269            v0[2].max(v1[2]).max(v2[2]),
270        ];
271        Aabb::new(min, max)
272    }
273    fn build_recursive(
274        vertices: &[[f64; 3]],
275        indices: &[(usize, usize, usize)],
276        prim_indices: &mut Vec<usize>,
277        start: usize,
278        end: usize,
279        nodes: &mut Vec<BvhNode>,
280    ) -> usize {
281        let node_idx = nodes.len();
282        nodes.push(BvhNode::leaf(Aabb::new([0.0; 3], [0.0; 3]), 0, 0));
283        let mut aabb = Aabb::new([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
284        for &pi in &prim_indices[start..end] {
285            let tri_bb = Self::triangle_aabb(vertices, indices[pi]);
286            aabb = Aabb::merge(&aabb, &tri_bb);
287        }
288        let n_prims = end - start;
289        if n_prims <= 2 {
290            nodes[node_idx] = BvhNode::leaf(aabb, start, n_prims);
291            return node_idx;
292        }
293        let d = sub(aabb.max, aabb.min);
294        let axis = if d[0] >= d[1] && d[0] >= d[2] {
295            0
296        } else if d[1] >= d[2] {
297            1
298        } else {
299            2
300        };
301        prim_indices[start..end].sort_by(|&a, &b| {
302            let ca = Self::triangle_centroid(vertices, indices[a])[axis];
303            let cb = Self::triangle_centroid(vertices, indices[b])[axis];
304            ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
305        });
306        let mid = (start + end) / 2;
307        let left = Self::build_recursive(vertices, indices, prim_indices, start, mid, nodes);
308        let right = Self::build_recursive(vertices, indices, prim_indices, mid, end, nodes);
309        nodes[node_idx] = BvhNode::internal(aabb, left, right);
310        node_idx
311    }
312    /// Traverse the BVH and return the closest triangle hit.
313    pub fn intersect(&self, ray: &Ray) -> Option<RayHit> {
314        let mut best: Option<RayHit> = None;
315        let mut stack = vec![0usize];
316        while let Some(node_idx) = stack.pop() {
317            let node = &self.nodes[node_idx];
318            if ray_aabb(ray, &node.aabb).is_none() {
319                continue;
320            }
321            if node.is_leaf() {
322                for i in node.prim_start..(node.prim_start + node.prim_count) {
323                    let pi = self.prim_indices[i];
324                    let (vi, vj, vk) = self.indices[pi];
325                    if vi >= self.vertices.len()
326                        || vj >= self.vertices.len()
327                        || vk >= self.vertices.len()
328                    {
329                        continue;
330                    }
331                    if let Some(hit) = ray_triangle(
332                        ray,
333                        self.vertices[vi],
334                        self.vertices[vj],
335                        self.vertices[vk],
336                        pi,
337                    ) {
338                        best = Some(match best {
339                            None => hit,
340                            Some(prev) => {
341                                if hit.t < prev.t {
342                                    hit
343                                } else {
344                                    prev
345                                }
346                            }
347                        });
348                    }
349                }
350            } else {
351                stack.push(node.left);
352                stack.push(node.right);
353            }
354        }
355        best
356    }
357}
358/// A convex mesh represented as a list of vertices.
359///
360/// The support function finds the vertex that is most extreme along a
361/// given direction.
362#[derive(Debug, Clone)]
363pub struct ConvexMesh {
364    /// Vertex list in world space.
365    pub vertices: Vec<[f64; 3]>,
366}
367impl ConvexMesh {
368    /// Create a convex mesh from a vertex list.
369    pub fn new(vertices: Vec<[f64; 3]>) -> Self {
370        Self { vertices }
371    }
372    /// Support function: returns the vertex most extreme in direction `dir`.
373    pub fn support(&self, dir: [f64; 3]) -> [f64; 3] {
374        self.vertices
375            .iter()
376            .copied()
377            .fold(self.vertices[0], |best, v| {
378                if dot(v, dir) > dot(best, dir) {
379                    v
380                } else {
381                    best
382                }
383            })
384    }
385    /// Compute the AABB of the mesh.
386    pub fn aabb(&self) -> Aabb {
387        let mut min = [f64::INFINITY; 3];
388        let mut max = [f64::NEG_INFINITY; 3];
389        for &v in &self.vertices {
390            for i in 0..3 {
391                min[i] = min[i].min(v[i]);
392                max[i] = max[i].max(v[i]);
393            }
394        }
395        Aabb::new(min, max)
396    }
397}
398/// Axis-Aligned Bounding Box.
399#[derive(Debug, Clone, Copy)]
400pub struct Aabb {
401    /// Minimum corner.
402    pub min: [f64; 3],
403    /// Maximum corner.
404    pub max: [f64; 3],
405}
406impl Aabb {
407    /// Create a new AABB.
408    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
409        Self { min, max }
410    }
411    /// Return the AABB centre.
412    pub fn centre(&self) -> [f64; 3] {
413        scale(add(self.min, self.max), 0.5)
414    }
415    /// Half-extents of the AABB.
416    pub fn half_extents(&self) -> [f64; 3] {
417        scale(sub(self.max, self.min), 0.5)
418    }
419    /// Surface area of the AABB (used in SAH BVH cost).
420    pub fn surface_area(&self) -> f64 {
421        let d = sub(self.max, self.min);
422        2.0 * (d[0] * d[1] + d[1] * d[2] + d[2] * d[0])
423    }
424    /// Merge two AABBs into the smallest containing AABB.
425    pub fn merge(a: &Aabb, b: &Aabb) -> Aabb {
426        Aabb::new(
427            [
428                a.min[0].min(b.min[0]),
429                a.min[1].min(b.min[1]),
430                a.min[2].min(b.min[2]),
431            ],
432            [
433                a.max[0].max(b.max[0]),
434                a.max[1].max(b.max[1]),
435                a.max[2].max(b.max[2]),
436            ],
437        )
438    }
439    /// Expand the AABB to include point `p`.
440    pub fn expand(&self, p: [f64; 3]) -> Aabb {
441        Aabb::new(
442            [
443                self.min[0].min(p[0]),
444                self.min[1].min(p[1]),
445                self.min[2].min(p[2]),
446            ],
447            [
448                self.max[0].max(p[0]),
449                self.max[1].max(p[1]),
450                self.max[2].max(p[2]),
451            ],
452        )
453    }
454}
455/// A flat recursive ray tree for a single primary ray.
456///
457/// Supports up to `max_depth` bounces with reflection and refraction.
458pub struct RayTree {
459    /// Flat array of ray tree nodes.
460    pub nodes: Vec<RayTreeNode>,
461    /// Maximum allowed bounce depth.
462    pub max_depth: usize,
463}
464impl RayTree {
465    /// Create an empty ray tree.
466    pub fn new(max_depth: usize) -> Self {
467        Self {
468            nodes: Vec::new(),
469            max_depth,
470        }
471    }
472    /// Build the ray tree by tracing against a list of spheres.
473    ///
474    /// Recursively spawns reflection rays when hitting a sphere.
475    pub fn trace(&mut self, primary: Ray, spheres: &[Sphere]) {
476        self.nodes.clear();
477        self.nodes.push(RayTreeNode::new(primary, 1.0));
478        let mut i = 0;
479        while i < self.nodes.len() {
480            let depth = self.compute_depth(i);
481            if depth >= self.max_depth {
482                i += 1;
483                continue;
484            }
485            let ray = self.nodes[i].ray;
486            let weight = self.nodes[i].weight;
487            let mut best: Option<RayHit> = None;
488            for sphere in spheres {
489                if let Some(hit) = ray_sphere(&ray, sphere) {
490                    best = Some(match best {
491                        None => hit,
492                        Some(prev) => {
493                            if hit.t < prev.t {
494                                hit
495                            } else {
496                                prev
497                            }
498                        }
499                    });
500                }
501            }
502            self.nodes[i].hit = best;
503            if let Some(hit) = best
504                && weight > 0.01
505            {
506                let r_dir = reflect_ray(ray.direction, hit.normal);
507                let r_ray = ray.spawn_offset(hit.point, r_dir);
508                let child_idx = self.nodes.len();
509                self.nodes.push(RayTreeNode::new(r_ray, weight * 0.8));
510                self.nodes[i].reflected_child = Some(child_idx);
511            }
512            i += 1;
513        }
514    }
515    /// Compute approximate depth of node `i` (linear search for parent).
516    fn compute_depth(&self, target: usize) -> usize {
517        let mut depth = 0usize;
518        let mut current = target;
519        'outer: loop {
520            for (parent, node) in self.nodes.iter().enumerate() {
521                if node.reflected_child == Some(current) || node.refracted_child == Some(current) {
522                    depth += 1;
523                    current = parent;
524                    continue 'outer;
525                }
526            }
527            break;
528        }
529        depth
530    }
531}
532/// A ray with associated differentials, used for texture anti-aliasing
533/// (ray differentials method, Igehy 1999).
534#[derive(Debug, Clone, Copy)]
535pub struct RayDifferential {
536    /// Primary ray.
537    pub ray: Ray,
538    /// Offset ray slightly in the screen-X direction.
539    pub rx: Ray,
540    /// Offset ray slightly in the screen-Y direction.
541    pub ry: Ray,
542    /// Pixel footprint scale factor.
543    pub pixel_spread: f64,
544}
545impl RayDifferential {
546    /// Create a ray differential from a primary ray and two offset rays.
547    pub fn new(ray: Ray, rx: Ray, ry: Ray) -> Self {
548        Self {
549            ray,
550            rx,
551            ry,
552            pixel_spread: 1.0,
553        }
554    }
555    /// Compute the area of the projected pixel footprint at parameter `t`.
556    ///
557    /// Returns an estimate of the screen-space pixel footprint on the surface.
558    pub fn footprint_at(&self, t: f64) -> f64 {
559        let p = self.ray.at(t);
560        let px = self.rx.at(t);
561        let py = self.ry.at(t);
562        let dx = sub(px, p);
563        let dy = sub(py, p);
564        let c = cross(dx, dy);
565        len(c)
566    }
567    /// Transfer differentials to a surface hit point.
568    ///
569    /// Given the surface normal at the hit, propagates the differentials
570    /// using the plane transfer equations.
571    pub fn transfer_to_surface(&self, t: f64, normal: [f64; 3]) -> ([f64; 3], [f64; 3]) {
572        let n = normalize(normal);
573        let p = self.ray.at(t);
574        let px_world = self.rx.at(t);
575        let py_world = self.ry.at(t);
576        let transfer_point = |q: [f64; 3]| -> [f64; 3] {
577            let offset = dot(sub(q, p), n);
578            sub(q, scale(n, offset))
579        };
580        (transfer_point(px_world), transfer_point(py_world))
581    }
582}
583/// A node in a flat BVH.
584#[derive(Debug, Clone)]
585pub struct BvhNode {
586    /// AABB of this node.
587    pub aabb: Aabb,
588    /// Index of the left child, or `usize::MAX` for a leaf.
589    pub left: usize,
590    /// Index of the right child, or `usize::MAX` for a leaf.
591    pub right: usize,
592    /// Leaf: starting index in the primitive array.
593    pub prim_start: usize,
594    /// Leaf: number of primitives.
595    pub prim_count: usize,
596}
597impl BvhNode {
598    /// Create an internal node.
599    pub fn internal(aabb: Aabb, left: usize, right: usize) -> Self {
600        Self {
601            aabb,
602            left,
603            right,
604            prim_start: 0,
605            prim_count: 0,
606        }
607    }
608    /// Create a leaf node.
609    pub fn leaf(aabb: Aabb, prim_start: usize, prim_count: usize) -> Self {
610        Self {
611            aabb,
612            left: usize::MAX,
613            right: usize::MAX,
614            prim_start,
615            prim_count,
616        }
617    }
618    /// Return `true` if this is a leaf node.
619    pub fn is_leaf(&self) -> bool {
620        self.left == usize::MAX
621    }
622}
623/// A height field stored as a grid of height values.
624///
625/// The grid covers `[0, width] × [0, height]` in the XZ plane.
626pub struct Heightfield {
627    /// Height values at grid cells (row-major: `heights[row * cols + col]`).
628    pub heights: Vec<f64>,
629    /// Number of columns (X direction).
630    pub cols: usize,
631    /// Number of rows (Z direction).
632    pub rows: usize,
633    /// Grid spacing (same in both X and Z).
634    pub cell_size: f64,
635}
636impl Heightfield {
637    /// Create a new heightfield.
638    pub fn new(heights: Vec<f64>, cols: usize, rows: usize, cell_size: f64) -> Self {
639        Self {
640            heights,
641            cols,
642            rows,
643            cell_size,
644        }
645    }
646    /// Bilinear interpolation of height at `(x, z)`.
647    pub fn height_at(&self, x: f64, z: f64) -> f64 {
648        let xi = (x / self.cell_size).max(0.0);
649        let zi = (z / self.cell_size).max(0.0);
650        let ix = (xi as usize).min(self.cols - 2);
651        let iz = (zi as usize).min(self.rows - 2);
652        let fx = xi - ix as f64;
653        let fz = zi - iz as f64;
654        let h00 = self.heights[iz * self.cols + ix];
655        let h10 = self.heights[iz * self.cols + ix + 1];
656        let h01 = self.heights[(iz + 1) * self.cols + ix];
657        let h11 = self.heights[(iz + 1) * self.cols + ix + 1];
658        h00 * (1.0 - fx) * (1.0 - fz)
659            + h10 * fx * (1.0 - fz)
660            + h01 * (1.0 - fx) * fz
661            + h11 * fx * fz
662    }
663    /// Total width of the heightfield (X direction).
664    pub fn width(&self) -> f64 {
665        (self.cols - 1) as f64 * self.cell_size
666    }
667    /// Total depth of the heightfield (Z direction).
668    pub fn depth(&self) -> f64 {
669        (self.rows - 1) as f64 * self.cell_size
670    }
671}
672/// An infinite ray parameterised as `origin + t * direction`.
673///
674/// The direction should be a unit vector for correct distance computations;
675/// use [`Ray::new_normalised`] to construct one automatically.
676#[derive(Debug, Clone, Copy)]
677pub struct Ray {
678    /// Ray origin.
679    pub origin: [f64; 3],
680    /// Ray direction (unit length).
681    pub direction: [f64; 3],
682    /// Minimum valid parameter value (clipping near plane).
683    pub t_min: f64,
684    /// Maximum valid parameter value (clipping far plane).
685    pub t_max: f64,
686}
687impl Ray {
688    /// Create a ray with pre-normalised direction and default `[0, ∞)` range.
689    pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
690        Self {
691            origin,
692            direction,
693            t_min: 0.0,
694            t_max: f64::INFINITY,
695        }
696    }
697    /// Create a ray and automatically normalise the direction vector.
698    pub fn new_normalised(origin: [f64; 3], direction: [f64; 3]) -> Self {
699        Self::new(origin, normalize(direction))
700    }
701    /// Evaluate the ray at parameter `t`: `P = origin + t * direction`.
702    pub fn at(&self, t: f64) -> [f64; 3] {
703        add(self.origin, scale(self.direction, t))
704    }
705    /// Return `true` if `t` lies within the valid range `[t_min, t_max]`.
706    pub fn valid_t(&self, t: f64) -> bool {
707        t >= self.t_min && t <= self.t_max
708    }
709    /// Spawn a secondary ray offset slightly to avoid self-intersection.
710    pub fn spawn_offset(&self, origin: [f64; 3], direction: [f64; 3]) -> Ray {
711        let eps = 1e-6;
712        Ray::new(add(origin, scale(direction, eps)), normalize(direction))
713    }
714}