Skip to main content

fast_mesh_query/
ray.rs

1//! Ray casts against a mesh, accelerated by its BVH.
2
3use fast_mesh_bvh::{Aabb, Bvh, NodeKind};
4use fast_mesh_store::{Mesh, TriangleId, par};
5
6use crate::tri::{RayPre, TriHit, ray_triangle};
7use crate::{Appender, Cull, QueryError, Result, check};
8
9/// A ray `origin + t * dir`, in mesh-local coordinates.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Ray {
12    /// Where the ray starts.
13    pub origin: [f32; 3],
14    /// The direction. It need not be normalized: `t` is measured in units of
15    /// `dir`, so it is a distance only when `dir` has unit length.
16    ///
17    /// Subnormal components are valid input. When the largest-magnitude
18    /// component is subnormal, its reciprocal overflows and no computed `t`
19    /// is finite, so the ray never hits anything (the queries return no hit,
20    /// not an error); a subnormal component beside a normal larger one
21    /// behaves like zero.
22    pub dir: [f32; 3],
23}
24
25/// Options shared by the raycasts.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct RayOptions {
28    /// Smallest `t` that counts as a hit (inclusive). Default `0.0`.
29    pub min_t: f32,
30    /// Largest `t` that counts as a hit (inclusive). Default `f32::INFINITY`.
31    pub max_t: f32,
32    /// Which faces to ignore. Default [`Cull::None`].
33    pub cull: Cull,
34}
35
36impl Default for RayOptions {
37    fn default() -> Self {
38        RayOptions {
39            min_t: 0.0,
40            max_t: f32::INFINITY,
41            cull: Cull::None,
42        }
43    }
44}
45
46/// Where a ray hit a triangle.
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct RayHit {
49    /// The triangle that was hit.
50    pub triangle: TriangleId,
51    /// The ray parameter of the hit, in units of the ray's `dir`.
52    pub t: f32,
53    /// The hit point, `origin + t * dir`.
54    pub point: [f32; 3],
55    /// Weights of the triangle's corners 0, 1 and 2 at the hit.
56    pub barycentric: [f32; 3],
57    /// The unit geometric normal, facing the side the counter-clockwise
58    /// winding is seen from.
59    pub normal: [f32; 3],
60}
61
62/// `γ₃ = 3ε / (1 − 3ε)` with `ε = f32::EPSILON / 2` (Pharr et al., Ize 2013).
63const GAMMA3: f32 = 3.0 * (f32::EPSILON / 2.0) / (1.0 - 3.0 * (f32::EPSILON / 2.0));
64/// Factor that moves a slab distance away from zero.
65const GROW: f32 = 1.0 + 2.0 * GAMMA3;
66/// Factor that moves a slab distance towards zero.
67const SHRINK: f32 = 1.0 - 2.0 * GAMMA3;
68
69/// Slab test of the ray against `b`. Returns the entry `t`, clamped to
70/// `>= min_t`, when the ray's `[min_t, max_t]` range meets the box.
71///
72/// The interval is widened by `2γ₃` relative at both ends, so `f32` rounding
73/// never culls a box the ray touches. A slab whose `dir` component is zero
74/// and whose face plane contains the origin gives `0 * ∞ = NaN`; `max`/`min`
75/// ignore NaN, so such a slab does not cull.
76///
77/// No absolute slack is needed for large coordinates (issue #19): both this
78/// test and `ray_triangle` subtract the origin from the same corner
79/// coordinates first, so the coordinate-scale rounding is common to the two
80/// and the entry stays at or below the triangle's computed `t`; what rounds
81/// past that subtraction is relative-scale, covered by the widening.
82/// `box_enter_never_exceeds_triangle_t_at_large_coords` pins the invariant.
83pub(crate) fn ray_box(pre: &RayPre, b: &Aabb, min_t: f32, max_t: f32) -> Option<f32> {
84    let mut enter = f32::NEG_INFINITY;
85    let mut exit = f32::INFINITY;
86    for i in 0..3 {
87        let inv = pre.inv_dir[i];
88        let (near, far) = if inv < 0.0 {
89            (b.max[i], b.min[i])
90        } else {
91            (b.min[i], b.max[i])
92        };
93        enter = enter.max((near - pre.origin[i]) * inv);
94        exit = exit.min((far - pre.origin[i]) * inv);
95    }
96    let enter = enter * if enter < 0.0 { GROW } else { SHRINK };
97    let exit = exit * if exit > 0.0 { GROW } else { SHRINK };
98    (enter <= exit && enter <= max_t && exit >= min_t).then_some(enter.max(min_t))
99}
100
101fn validate(origin: [f32; 3], dir: [f32; 3]) -> Result<()> {
102    if !origin.iter().all(|c| c.is_finite()) {
103        return Err(QueryError::InvalidInput("ray origin must be finite"));
104    }
105    if !dir.iter().all(|c| c.is_finite()) {
106        return Err(QueryError::InvalidInput("ray direction must be finite"));
107    }
108    if dir == [0.0; 3] {
109        return Err(QueryError::InvalidInput("ray direction must not be zero"));
110    }
111    Ok(())
112}
113
114fn validate_opts(opts: &RayOptions) -> Result<()> {
115    if opts.min_t.is_nan() || opts.max_t.is_nan() || opts.min_t > opts.max_t {
116        return Err(QueryError::InvalidInput(
117            "min_t and max_t must not be NaN, and min_t must not exceed max_t",
118        ));
119    }
120    Ok(())
121}
122
123pub(crate) fn points(mesh: &Mesh, id: TriangleId) -> [[f32; 3]; 3] {
124    let positions = mesh.positions();
125    mesh.triangles()[id as usize].map(|v| positions[v as usize])
126}
127
128fn make_hit(pre: &RayPre, p: [[f32; 3]; 3], triangle: TriangleId, h: TriHit) -> RayHit {
129    let point = std::array::from_fn(|i| pre.origin[i] + h.t * pre.dir[i]);
130    // In f64 so that tiny triangles do not underflow to a zero normal.
131    let e1: [f64; 3] = std::array::from_fn(|i| f64::from(p[1][i]) - f64::from(p[0][i]));
132    let e2: [f64; 3] = std::array::from_fn(|i| f64::from(p[2][i]) - f64::from(p[0][i]));
133    let n = [
134        e1[1] * e2[2] - e1[2] * e2[1],
135        e1[2] * e2[0] - e1[0] * e2[2],
136        e1[0] * e2[1] - e1[1] * e2[0],
137    ];
138    let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
139    RayHit {
140        triangle,
141        t: h.t,
142        point,
143        barycentric: h.barycentric,
144        normal: n.map(|c| (c / len) as f32),
145    }
146}
147
148/// Visits, nearest box first, every node whose box the ray meets within
149/// `[opts.min_t, bound()]`, calling `leaf` for each triangle of a visited leaf.
150/// `bound` is read again before each node, so it can shrink as hits are found;
151/// it must stay within `[opts.min_t, opts.max_t]`.
152fn walk(
153    bvh: &Bvh,
154    pre: &RayPre,
155    opts: &RayOptions,
156    bound: impl Fn() -> Option<f32>,
157    mut leaf: impl FnMut(TriangleId),
158) {
159    bvh.traverse_by_key(
160        |b| ray_box(pre, b, opts.min_t, opts.max_t),
161        |_, node, entry| {
162            // The box meets `[min_t, max_t]` and is entered at `entry >= min_t`,
163            // so it meets `[min_t, bound]` exactly when `entry <= bound`.
164            let Some(max_t) = bound() else {
165                return false;
166            };
167            if entry > max_t {
168                return false;
169            }
170            if let NodeKind::Leaf { triangles } = node.kind {
171                for &id in triangles {
172                    leaf(id);
173                }
174            }
175            true
176        },
177    );
178}
179
180fn closest(
181    mesh: &Mesh,
182    bvh: &Bvh,
183    origin: [f32; 3],
184    dir: [f32; 3],
185    opts: &RayOptions,
186) -> Option<RayHit> {
187    let pre = RayPre::new(origin, dir);
188    let best: std::cell::Cell<Option<(TriangleId, TriHit)>> = std::cell::Cell::new(None);
189    let best_t = || best.get().map_or(opts.max_t, |(_, h)| h.t);
190    walk(
191        bvh,
192        &pre,
193        opts,
194        // Boxes entered exactly at `best_t` are still visited, so that a tie
195        // can go to the smaller id.
196        || Some(best_t()),
197        |id| {
198            let Some(h) = ray_triangle(&pre, points(mesh, id), opts.cull, opts.min_t, best_t())
199            else {
200                return;
201            };
202            let better = match best.get() {
203                None => true,
204                Some((bid, bh)) => h.t < bh.t || (h.t == bh.t && id < bid),
205            };
206            if better {
207                best.set(Some((id, h)));
208            }
209        },
210    );
211    best.get()
212        .map(|(id, h)| make_hit(&pre, points(mesh, id), id, h))
213}
214
215/// The nearest hit with `opts.min_t <= t <= opts.max_t`, or `None`. Equal `t`
216/// goes to the smallest triangle id, so the result is deterministic.
217///
218/// A ray crossing an edge or vertex shared by several triangles hits all of
219/// them (the test is watertight); triangles with zero area are never hit.
220///
221/// # Errors
222/// [`QueryError::InvalidInput`] for a non-finite origin, a zero or non-finite
223/// direction, or `min_t` or `max_t` that is NaN, or `min_t` greater than `max_t`; then
224/// [`QueryError::MeshMismatch`] or [`QueryError::StaleBvh`] when `bvh` does not
225/// match the current `mesh`.
226pub fn raycast(mesh: &Mesh, bvh: &Bvh, ray: &Ray, opts: &RayOptions) -> Result<Option<RayHit>> {
227    validate(ray.origin, ray.dir)?;
228    validate_opts(opts)?;
229    check(mesh, bvh)?;
230    Ok(closest(mesh, bvh, ray.origin, ray.dir, opts))
231}
232
233/// Some hit with `opts.min_t <= t <= opts.max_t`, or `None`. It stops at the
234/// first hit found, so it suits occlusion tests; which hit it returns is
235/// unspecified.
236///
237/// # Errors
238/// As for [`raycast`].
239pub fn raycast_any(mesh: &Mesh, bvh: &Bvh, ray: &Ray, opts: &RayOptions) -> Result<Option<RayHit>> {
240    validate(ray.origin, ray.dir)?;
241    validate_opts(opts)?;
242    check(mesh, bvh)?;
243    let pre = RayPre::new(ray.origin, ray.dir);
244    let found: std::cell::Cell<Option<(TriangleId, TriHit)>> = std::cell::Cell::new(None);
245    walk(
246        bvh,
247        &pre,
248        opts,
249        || found.get().is_none().then_some(opts.max_t),
250        |id| {
251            if found.get().is_none()
252                && let Some(h) =
253                    ray_triangle(&pre, points(mesh, id), opts.cull, opts.min_t, opts.max_t)
254            {
255                found.set(Some((id, h)));
256            }
257        },
258    );
259    Ok(found
260        .get()
261        .map(|(id, h)| make_hit(&pre, points(mesh, id), id, h)))
262}
263
264/// Appends every hit with `opts.min_t <= t <= opts.max_t` to `out`, sorted by
265/// `(t, triangle)`, and returns how many were appended. Elements already in
266/// `out` are kept.
267///
268/// A ray through an edge shared by several triangles, or through a vertex
269/// shared by a fan, reports one hit per triangle: a vertex shared by 6
270/// triangles gives 6 hits at the same `t`. A caller that counts surface
271/// crossings, for parity or winding, must merge hits with equal `t` itself;
272/// the query keeps every hit so that no crossing is lost, and Phase 5's
273/// inside/outside tests do not count ray crossings at all: they use
274/// angle-weighted pseudonormals, or generalized winding numbers where the
275/// orientation is inconsistent, neither of which needs a parity rule for
276/// duplicate hits.
277///
278/// # Errors
279/// As for [`raycast`], and [`QueryError::OutOfMemory`] when `out` cannot grow;
280/// on any error `out` keeps its original length.
281pub fn raycast_all(
282    mesh: &Mesh,
283    bvh: &Bvh,
284    ray: &Ray,
285    opts: &RayOptions,
286    out: &mut Vec<RayHit>,
287) -> Result<usize> {
288    validate(ray.origin, ray.dir)?;
289    validate_opts(opts)?;
290    check(mesh, bvh)?;
291    let pre = RayPre::new(ray.origin, ray.dir);
292    let mut ap = Appender::new(out);
293    let failed: std::cell::Cell<Option<QueryError>> = std::cell::Cell::new(None);
294    walk(
295        bvh,
296        &pre,
297        opts,
298        || failed.get().is_none().then_some(opts.max_t),
299        |id| {
300            if failed.get().is_some() {
301                return;
302            }
303            let p = points(mesh, id);
304            if let Some(h) = ray_triangle(&pre, p, opts.cull, opts.min_t, opts.max_t)
305                && let Err(e) = ap.push(make_hit(&pre, p, id, h))
306            {
307                failed.set(Some(e));
308            }
309        },
310    );
311    if let Some(e) = failed.get() {
312        return Err(e);
313    }
314    ap.appended()
315        .sort_unstable_by(|a, b| a.t.total_cmp(&b.t).then(a.triangle.cmp(&b.triangle)));
316    Ok(ap.count())
317}
318
319/// [`raycast`] for many rays: `out[i]` gets the nearest hit of the ray
320/// `origins[i] + t * dirs[i]`.
321///
322/// # Errors
323/// [`QueryError::InvalidInput`] when the three slices differ in length or any
324/// ray is invalid, and otherwise as for [`raycast`]. Every ray is validated
325/// before anything is written, so on error `out` is unchanged.
326pub fn raycast_many(
327    mesh: &Mesh,
328    bvh: &Bvh,
329    origins: &[[f32; 3]],
330    dirs: &[[f32; 3]],
331    opts: &RayOptions,
332    out: &mut [Option<RayHit>],
333) -> Result<()> {
334    if origins.len() != dirs.len() || origins.len() != out.len() {
335        return Err(QueryError::InvalidInput(
336            "origins, dirs and out must have the same length",
337        ));
338    }
339    for (&o, &d) in origins.iter().zip(dirs) {
340        validate(o, d)?;
341    }
342    validate_opts(opts)?;
343    check(mesh, bvh)?;
344    par::for_each_chunk_mut(out, QUERY_CHUNK, PAR_MIN_QUERIES, |i, slots| {
345        let rays = origins[i * QUERY_CHUNK..]
346            .iter()
347            .zip(&dirs[i * QUERY_CHUNK..]);
348        for ((&o, &d), slot) in rays.zip(slots) {
349            *slot = closest(mesh, bvh, o, d, opts);
350        }
351    });
352    Ok(())
353}
354
355/// Queries from which [`raycast_many`] and
356/// [`closest_point_many`](crate::closest_point_many) run in parallel, in
357/// chunks of [`QUERY_CHUNK`], with the `parallel` feature. Each query is
358/// computed on its own, so the results are the same either way.
359///
360/// Measured on the i5-13600K on a 30k-triangle terrain: at 128 queries
361/// parallel `closest_point_many` is 1.5 times faster but `raycast_many` 1.2
362/// times slower; at 256 both are faster (2.6 and 1.25 times), and at 4,096
363/// 11 and 7 times.
364pub(crate) const PAR_MIN_QUERIES: usize = 256;
365
366/// Queries per parallel task of the batch queries.
367pub(crate) const QUERY_CHUNK: usize = 64;
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::test_util::{Lcg, build, wavy};
373    use crate::tri::{RayPre, ray_triangle};
374    use crate::{QueryError, fail_allocations_after};
375    use fast_mesh_bvh::{BuildOptions, Bvh, Quality};
376    use fast_mesh_store::{Mesh, TriangleId};
377
378    const T: [f32; 9] = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
379    const DOWN: [f32; 3] = [0.0, 0.0, -1.0];
380
381    fn one_triangle(p: &[f32; 9]) -> (Mesh, Bvh) {
382        let mesh = Mesh::from_buffers(p, &[0, 1, 2]).unwrap();
383        let bvh = build(&mesh, Quality::Sah);
384        (mesh, bvh)
385    }
386
387    /// Every hit in range, by testing every alive triangle.
388    fn brute_all(mesh: &Mesh, ray: &Ray, opts: &RayOptions) -> Vec<(TriangleId, f32)> {
389        let pre = RayPre::new(ray.origin, ray.dir);
390        let mut hits = Vec::new();
391        for id in 0..mesh.triangle_slots() as TriangleId {
392            if !mesh.is_triangle_alive(id) {
393                continue;
394            }
395            if let Some(h) = ray_triangle(&pre, points(mesh, id), opts.cull, opts.min_t, opts.max_t)
396            {
397                hits.push((id, h.t));
398            }
399        }
400        hits.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
401        hits
402    }
403
404    fn brute_closest(mesh: &Mesh, ray: &Ray, opts: &RayOptions) -> Option<(TriangleId, f32)> {
405        brute_all(mesh, ray, opts).first().copied()
406    }
407
408    /// 500 rays from above a `wavy(n)` grid, pointing down with some jitter.
409    fn rays(n: u32) -> Vec<Ray> {
410        let mut rng = Lcg(0x5eed);
411        let span = n as f32 + 2.0;
412        (0..500)
413            .map(|_| Ray {
414                origin: [rng.next() * span - 1.0, rng.next() * span - 1.0, 5.0],
415                dir: [
416                    rng.next() * 0.6 - 0.3,
417                    rng.next() * 0.6 - 0.3,
418                    -1.0 - rng.next(),
419                ],
420            })
421            .collect()
422    }
423
424    /// 500 nearly horizontal rays through a `wavy(n)` grid; most cross it
425    /// several times.
426    fn shallow_rays(n: u32) -> Vec<Ray> {
427        let mut rng = Lcg(0xab1e);
428        (0..500)
429            .map(|_| {
430                let a = rng.next() * std::f32::consts::TAU;
431                Ray {
432                    origin: [
433                        rng.next() * n as f32,
434                        rng.next() * n as f32,
435                        0.5 + rng.next(),
436                    ],
437                    dir: [a.cos(), a.sin(), 0.2 * rng.next() - 0.1],
438                }
439            })
440            .collect()
441    }
442
443    fn opts() -> RayOptions {
444        RayOptions::default()
445    }
446
447    #[test]
448    fn options_default() {
449        let o = RayOptions::default();
450        assert_eq!(o.min_t, 0.0);
451        assert_eq!(o.max_t, f32::INFINITY);
452        assert_eq!(o.cull, Cull::None);
453    }
454
455    #[test]
456    fn closest_matches_brute_force() {
457        let mesh = wavy(16);
458        for quality in [Quality::Sah, Quality::Fast] {
459            let bvh = build(&mesh, quality);
460            let mut hits = 0;
461            for ray in rays(16).into_iter().chain(shallow_rays(16)) {
462                let got = raycast(&mesh, &bvh, &ray, &opts())
463                    .unwrap()
464                    .map(|h| (h.triangle, h.t));
465                assert_eq!(got, brute_closest(&mesh, &ray, &opts()), "{ray:?}");
466                hits += usize::from(got.is_some());
467            }
468            assert!(hits > 300, "only {hits} rays hit");
469        }
470    }
471
472    #[test]
473    fn all_matches_brute_force_sorted() {
474        let mesh = wavy(16);
475        let bvh = build(&mesh, Quality::Sah);
476        let sentinel = RayHit {
477            triangle: 999,
478            t: -1.0,
479            point: [0.0; 3],
480            barycentric: [0.0; 3],
481            normal: [0.0; 3],
482        };
483        let mut multi = 0;
484        for ray in rays(16).into_iter().chain(shallow_rays(16)) {
485            let mut out = vec![sentinel];
486            let n = raycast_all(&mesh, &bvh, &ray, &opts(), &mut out).unwrap();
487            assert_eq!(n, out.len() - 1);
488            assert_eq!(out[0], sentinel);
489            let got: Vec<_> = out[1..].iter().map(|h| (h.triangle, h.t)).collect();
490            assert_eq!(got, brute_all(&mesh, &ray, &opts()), "{ray:?}");
491            multi += usize::from(n > 1);
492        }
493        assert!(multi > 10, "only {multi} rays hit more than once");
494    }
495
496    #[test]
497    fn any_hits_iff_closest_hits() {
498        let mesh = wavy(16);
499        let bvh = build(&mesh, Quality::Sah);
500        for ray in rays(16).into_iter().chain(shallow_rays(16)) {
501            let any = raycast_any(&mesh, &bvh, &ray, &opts()).unwrap();
502            let closest = raycast(&mesh, &bvh, &ray, &opts()).unwrap();
503            assert_eq!(any.is_some(), closest.is_some());
504            if let Some(h) = any {
505                let pre = RayPre::new(ray.origin, ray.dir);
506                let real = ray_triangle(
507                    &pre,
508                    points(&mesh, h.triangle),
509                    Cull::None,
510                    0.0,
511                    f32::INFINITY,
512                );
513                assert_eq!(real.map(|r| r.t), Some(h.t));
514            }
515        }
516    }
517
518    #[test]
519    fn shared_edge_tie_takes_smallest_id() {
520        // Triangles 0 and 1 share the diagonal from (0,0,0) to (1,1,2).
521        let mesh = wavy(4);
522        let bvh = build(&mesh, Quality::Sah);
523        let ray = Ray {
524            origin: [0.5, 0.5, 10.0],
525            dir: DOWN,
526        };
527        let h = raycast(&mesh, &bvh, &ray, &opts()).unwrap().unwrap();
528        assert_eq!((h.triangle, h.t), (0, 9.0));
529        let mut out = Vec::new();
530        assert_eq!(raycast_all(&mesh, &bvh, &ray, &opts(), &mut out), Ok(2));
531        assert_eq!(
532            out.iter().map(|h| (h.triangle, h.t)).collect::<Vec<_>>(),
533            [(0, 9.0), (1, 9.0)]
534        );
535    }
536
537    #[test]
538    fn ray_through_a_fan_vertex_hits_every_triangle() {
539        // Six triangles around the vertex 0 at the origin.
540        let mut p = vec![0.0, 0.0, 0.0];
541        for i in 0..6 {
542            let a = i as f32 * std::f32::consts::TAU / 6.0;
543            p.extend([a.cos(), a.sin(), 0.0]);
544        }
545        let idx: Vec<u32> = (0..6).flat_map(|i| [0, 1 + i, 1 + (i + 1) % 6]).collect();
546        let mesh = Mesh::from_buffers(&p, &idx).unwrap();
547        let bvh = build(&mesh, Quality::Sah);
548        let ray = Ray {
549            origin: [0.0, 0.0, 2.0],
550            dir: DOWN,
551        };
552        let mut out = Vec::new();
553        assert_eq!(raycast_all(&mesh, &bvh, &ray, &opts(), &mut out), Ok(6));
554        assert!(out.iter().all(|h| h.t == 2.0));
555        let ids: Vec<_> = out.iter().map(|h| h.triangle).collect();
556        assert_eq!(ids, [0, 1, 2, 3, 4, 5]);
557    }
558
559    #[test]
560    fn shared_edge_tie_with_reversed_ids_takes_smallest_id() {
561        // Same as above, but the triangle listed first is the one with the larger
562        // id in each leaf order the tree might pick.
563        let p = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
564        let mesh = Mesh::from_buffers(&p, &[0, 2, 3, 0, 1, 2]).unwrap();
565        for quality in [Quality::Sah, Quality::Fast] {
566            let opts1 = BuildOptions {
567                quality,
568                max_leaf_size: 1,
569                ..Default::default()
570            };
571            let bvh = Bvh::build(&mesh, &opts1).unwrap();
572            let ray = Ray {
573                origin: [0.5, 0.5, 1.0],
574                dir: DOWN,
575            };
576            let h = raycast(&mesh, &bvh, &ray, &opts()).unwrap().unwrap();
577            assert_eq!((h.triangle, h.t), (0, 1.0));
578        }
579    }
580
581    #[test]
582    fn hit_fields() {
583        let (mesh, bvh) = one_triangle(&T);
584        let ray = Ray {
585            origin: [0.25, 0.25, 1.0],
586            dir: DOWN,
587        };
588        let h = raycast(&mesh, &bvh, &ray, &opts()).unwrap().unwrap();
589        assert_eq!(h.triangle, 0);
590        assert_eq!(h.t, 1.0);
591        for (a, b) in h.point.iter().zip([0.25, 0.25, 0.0]) {
592            assert!((a - b).abs() < 1e-6);
593        }
594        for (a, b) in h.normal.iter().zip([0.0, 0.0, 1.0]) {
595            assert!((a - b).abs() < 1e-6);
596        }
597        for (a, b) in h.barycentric.iter().zip([0.5, 0.25, 0.25]) {
598            assert!((a - b).abs() < 1e-6);
599        }
600    }
601
602    #[test]
603    fn t_is_in_units_of_dir() {
604        let (mesh, bvh) = one_triangle(&T);
605        let ray = Ray {
606            origin: [0.25, 0.25, 1.0],
607            dir: [0.0, 0.0, -2.0],
608        };
609        let h = raycast(&mesh, &bvh, &ray, &opts()).unwrap().unwrap();
610        assert_eq!(h.t, 0.5);
611        for (a, b) in h.point.iter().zip([0.25, 0.25, 0.0]) {
612            assert!((a - b).abs() < 1e-6);
613        }
614    }
615
616    #[test]
617    fn ray_starting_on_the_surface_hits_at_zero() {
618        let (mesh, bvh) = one_triangle(&T);
619        let ray = Ray {
620            origin: [0.25, 0.25, 0.0],
621            dir: DOWN,
622        };
623        let h = raycast(&mesh, &bvh, &ray, &opts()).unwrap().unwrap();
624        assert_eq!(h.t, 0.0);
625    }
626
627    #[test]
628    fn axis_aligned_ray_on_a_box_face_still_hits() {
629        // The triangle's box is x = 1, y in [0.25, 1], z in [0.25, 1]: the
630        // origin lies on the planes of its y and z min faces, where dir is zero.
631        let p = [1.0, 0.25, 0.25, 1.0, 1.0, 0.25, 1.0, 0.25, 1.0];
632        let (mesh, bvh) = one_triangle(&p);
633        let ray = Ray {
634            origin: [0.0, 0.25, 0.25],
635            dir: [1.0, 0.0, 0.0],
636        };
637        let h = raycast(&mesh, &bvh, &ray, &opts()).unwrap().unwrap();
638        assert_eq!(h.t, 1.0);
639        assert!(raycast_any(&mesh, &bvh, &ray, &opts()).unwrap().is_some());
640        // The same with negative zeros in dir.
641        let ray = Ray {
642            origin: [0.0, 0.25, 0.25],
643            dir: [1.0, -0.0, -0.0],
644        };
645        assert_eq!(
646            raycast(&mesh, &bvh, &ray, &opts()).unwrap().map(|h| h.t),
647            Some(1.0)
648        );
649    }
650
651    #[test]
652    fn negative_min_t_finds_hits_behind_the_origin() {
653        let mesh = wavy(8);
654        let bvh = build(&mesh, Quality::Sah);
655        let ray = Ray {
656            origin: [3.3, 4.6, -5.0],
657            dir: DOWN,
658        };
659        let o = RayOptions {
660            min_t: f32::NEG_INFINITY,
661            ..opts()
662        };
663        let got = raycast(&mesh, &bvh, &ray, &o)
664            .unwrap()
665            .map(|h| (h.triangle, h.t));
666        assert!(got.is_some());
667        assert_eq!(got, brute_closest(&mesh, &ray, &o));
668        assert_eq!(raycast(&mesh, &bvh, &ray, &opts()), Ok(None));
669    }
670
671    #[test]
672    fn max_t_and_cull_are_honored() {
673        let mesh = wavy(16);
674        let bvh = build(&mesh, Quality::Sah);
675        for o in [
676            RayOptions {
677                min_t: 3.5,
678                max_t: 4.5,
679                cull: Cull::None,
680            },
681            RayOptions {
682                cull: Cull::Back,
683                ..opts()
684            },
685            RayOptions {
686                cull: Cull::Front,
687                ..opts()
688            },
689        ] {
690            for ray in rays(16).into_iter().chain(shallow_rays(16)) {
691                let got = raycast(&mesh, &bvh, &ray, &o)
692                    .unwrap()
693                    .map(|h| (h.triangle, h.t));
694                assert_eq!(got, brute_closest(&mesh, &ray, &o));
695                let mut out = Vec::new();
696                raycast_all(&mesh, &bvh, &ray, &o, &mut out).unwrap();
697                let all: Vec<_> = out.iter().map(|h| (h.triangle, h.t)).collect();
698                assert_eq!(all, brute_all(&mesh, &ray, &o));
699            }
700        }
701    }
702
703    #[test]
704    fn empty_mesh_returns_nothing() {
705        let ray = Ray {
706            origin: [0.5, 0.5, 5.0],
707            dir: DOWN,
708        };
709        let check_empty = |mesh: &Mesh, bvh: &Bvh| {
710            assert_eq!(raycast(mesh, bvh, &ray, &opts()), Ok(None));
711            assert_eq!(raycast_any(mesh, bvh, &ray, &opts()), Ok(None));
712            let mut out = Vec::new();
713            assert_eq!(raycast_all(mesh, bvh, &ray, &opts(), &mut out), Ok(0));
714            assert!(out.is_empty());
715        };
716        let mesh = Mesh::new();
717        let bvh = build(&mesh, Quality::Sah);
718        check_empty(&mesh, &bvh);
719
720        let mut mesh = wavy(4);
721        let mut bvh = build(&mesh, Quality::Sah);
722        let all: Vec<TriangleId> = (0..mesh.triangle_slots() as TriangleId).collect();
723        let mut tx = mesh.begin_edit();
724        tx.remove_triangles(&all).unwrap();
725        let p = tx.commit().unwrap();
726        bvh.update(&mut mesh, p.changes()).unwrap();
727        check_empty(&mesh, &bvh);
728    }
729
730    #[test]
731    fn invalid_input() {
732        let (mesh, bvh) = one_triangle(&T);
733        let good = Ray {
734            origin: [0.25, 0.25, 1.0],
735            dir: DOWN,
736        };
737        let bad_rays = [
738            Ray {
739                dir: [0.0; 3],
740                ..good
741            },
742            Ray {
743                dir: [0.0, f32::INFINITY, -1.0],
744                ..good
745            },
746            Ray {
747                origin: [f32::NAN, 0.0, 0.0],
748                ..good
749            },
750        ];
751        let bad_opts = [
752            RayOptions {
753                min_t: 2.0,
754                max_t: 1.0,
755                ..opts()
756            },
757            RayOptions {
758                min_t: f32::NAN,
759                ..opts()
760            },
761        ];
762        let cases = bad_rays
763            .iter()
764            .map(|r| (*r, opts()))
765            .chain(bad_opts.iter().map(|o| (good, *o)));
766        for (ray, o) in cases {
767            let is_invalid =
768                |r: Result<Option<RayHit>>| matches!(r, Err(QueryError::InvalidInput(_)));
769            assert!(is_invalid(raycast(&mesh, &bvh, &ray, &o)));
770            assert!(is_invalid(raycast_any(&mesh, &bvh, &ray, &o)));
771            let mut out = Vec::new();
772            assert!(matches!(
773                raycast_all(&mesh, &bvh, &ray, &o, &mut out),
774                Err(QueryError::InvalidInput(_))
775            ));
776            let mut many = [None];
777            assert!(matches!(
778                raycast_many(&mesh, &bvh, &[ray.origin], &[ray.dir], &o, &mut many),
779                Err(QueryError::InvalidInput(_))
780            ));
781        }
782        let mut out = [None, None];
783        assert!(matches!(
784            raycast_many(
785                &mesh,
786                &bvh,
787                &[good.origin; 2],
788                &[good.dir],
789                &opts(),
790                &mut out
791            ),
792            Err(QueryError::InvalidInput(_))
793        ));
794        let mut out = [None];
795        assert!(matches!(
796            raycast_many(
797                &mesh,
798                &bvh,
799                &[good.origin; 2],
800                &[good.dir; 2],
801                &opts(),
802                &mut out
803            ),
804            Err(QueryError::InvalidInput(_))
805        ));
806    }
807
808    #[test]
809    fn nan_max_t_is_invalid() {
810        let (mesh, bvh) = one_triangle(&T);
811        let ray = Ray {
812            origin: [0.25, 0.25, 1.0],
813            dir: DOWN,
814        };
815        let o = RayOptions {
816            max_t: f32::NAN,
817            ..opts()
818        };
819        assert!(matches!(
820            raycast(&mesh, &bvh, &ray, &o),
821            Err(QueryError::InvalidInput(_))
822        ));
823        assert!(matches!(
824            raycast_any(&mesh, &bvh, &ray, &o),
825            Err(QueryError::InvalidInput(_))
826        ));
827        let mut out = Vec::new();
828        assert!(matches!(
829            raycast_all(&mesh, &bvh, &ray, &o, &mut out),
830            Err(QueryError::InvalidInput(_))
831        ));
832        let mut many = [None];
833        assert!(matches!(
834            raycast_many(&mesh, &bvh, &[ray.origin], &[ray.dir], &o, &mut many),
835            Err(QueryError::InvalidInput(_))
836        ));
837    }
838
839    #[test]
840    fn invalid_input_wins_over_stale() {
841        let (mut mesh, bvh) = one_triangle(&T);
842        let mut tx = mesh.begin_edit();
843        tx.set_positions(&[0], &[[0.0, 0.0, 1.0]]).unwrap();
844        tx.commit().unwrap();
845        let ray = Ray {
846            origin: [0.25, 0.25, 1.0],
847            dir: [0.0; 3],
848        };
849        assert!(matches!(
850            raycast(&mesh, &bvh, &ray, &opts()),
851            Err(QueryError::InvalidInput(_))
852        ));
853    }
854
855    #[test]
856    fn stale_and_mismatch_are_rejected() {
857        let (mut mesh, bvh) = one_triangle(&T);
858        let (other, _) = one_triangle(&T);
859        let ray = Ray {
860            origin: [0.25, 0.25, 1.0],
861            dir: DOWN,
862        };
863        let mut out = Vec::new();
864        let mut many = [None];
865        assert_eq!(
866            raycast(&other, &bvh, &ray, &opts()),
867            Err(QueryError::MeshMismatch)
868        );
869
870        let mut tx = mesh.begin_edit();
871        tx.set_positions(&[0], &[[0.0, 0.0, 1.0]]).unwrap();
872        tx.commit().unwrap();
873        let stale = Err(QueryError::StaleBvh);
874        assert_eq!(raycast(&mesh, &bvh, &ray, &opts()), stale);
875        assert_eq!(raycast_any(&mesh, &bvh, &ray, &opts()), stale);
876        assert_eq!(
877            raycast_all(&mesh, &bvh, &ray, &opts(), &mut out),
878            Err(QueryError::StaleBvh)
879        );
880        assert_eq!(
881            raycast_many(&mesh, &bvh, &[ray.origin], &[ray.dir], &opts(), &mut many),
882            Err(QueryError::StaleBvh)
883        );
884    }
885
886    #[test]
887    fn raycast_many_matches_raycast() {
888        let mesh = wavy(16);
889        let bvh = build(&mesh, Quality::Sah);
890        let rs = rays(16);
891        let origins: Vec<_> = rs.iter().map(|r| r.origin).collect();
892        let dirs: Vec<_> = rs.iter().map(|r| r.dir).collect();
893        let mut out = vec![None; rs.len()];
894        raycast_many(&mesh, &bvh, &origins, &dirs, &opts(), &mut out).unwrap();
895        for (ray, got) in rs.iter().zip(&out) {
896            assert_eq!(*got, raycast(&mesh, &bvh, ray, &opts()).unwrap());
897        }
898    }
899
900    #[test]
901    fn raycast_all_oom_restores_out() {
902        let (mesh, bvh) = one_triangle(&T);
903        let ray = Ray {
904            origin: [0.25, 0.25, 1.0],
905            dir: DOWN,
906        };
907        let sentinel = RayHit {
908            triangle: 7,
909            t: 0.0,
910            point: [0.0; 3],
911            barycentric: [0.0; 3],
912            normal: [0.0; 3],
913        };
914        let mut out = vec![sentinel];
915        fail_allocations_after(Some(0));
916        let r = raycast_all(&mesh, &bvh, &ray, &opts(), &mut out);
917        fail_allocations_after(None);
918        assert_eq!(r, Err(QueryError::OutOfMemory));
919        assert_eq!(out, vec![sentinel]);
920    }
921
922    /// A flat 6-triangle fan around vertex 0 at the origin.
923    fn fan() -> (Mesh, Bvh) {
924        let mut p = vec![0.0, 0.0, 0.0];
925        for k in 0..6 {
926            let a = std::f64::consts::TAU * f64::from(k) / 6.0;
927            p.extend([a.cos() as f32, a.sin() as f32, 0.0]);
928        }
929        let mut idx = Vec::new();
930        for k in 0..6 {
931            idx.extend([0, 1 + k, 1 + (k + 1) % 6]);
932        }
933        let mesh = Mesh::from_buffers(&p, &idx).unwrap();
934        let bvh = build(&mesh, Quality::Sah);
935        (mesh, bvh)
936    }
937
938    #[test]
939    fn shared_vertex_and_edge_report_one_hit_per_triangle() {
940        // The crossing-count contract of `raycast_all` (issue #18): a ray
941        // through the shared vertex hits every triangle of the fan, at equal
942        // `t`, sorted by triangle id; a ray through a shared edge hits both.
943        let (mesh, bvh) = fan();
944        let mut out = Vec::new();
945        let down = Ray {
946            origin: [0.0, 0.0, 1.0],
947            dir: DOWN,
948        };
949        let n = raycast_all(&mesh, &bvh, &down, &opts(), &mut out).unwrap();
950        assert_eq!(n, 6);
951        assert!(out.iter().all(|h| h.t == 1.0));
952        assert_eq!(
953            out.iter().map(|h| h.triangle).collect::<Vec<_>>(),
954            vec![0, 1, 2, 3, 4, 5]
955        );
956        // Through the midpoint of the edge 0-1, shared by triangles 0 and 5.
957        let edge = Ray {
958            origin: [0.5, 0.0, 1.0],
959            dir: DOWN,
960        };
961        out.clear();
962        let n = raycast_all(&mesh, &bvh, &edge, &opts(), &mut out).unwrap();
963        assert_eq!(n, 2);
964        assert!(out.iter().all(|h| h.t == 1.0));
965        assert_eq!(
966            out.iter().map(|h| h.triangle).collect::<Vec<_>>(),
967            vec![0, 5]
968        );
969    }
970
971    #[test]
972    fn box_enter_never_exceeds_triangle_t_at_large_coords() {
973        // Soundness of pruning with the smallest-id tie rule (issue #19): a
974        // box entered above its own triangle's computed `t` could be pruned
975        // while that triangle ties or beats the best. Shared-edge pairs of
976        // unit triangles at large coordinates, where `t` is tiny relative to
977        // them, rays aimed at the edge from outside the boxes.
978        let mut rng = Lcg(0x19e7);
979        let mut hits = 0;
980        for scale in [1e4f32, 1e7, 1e10] {
981            for _ in 0..600 {
982                let base = [
983                    (rng.next() * 2.0 - 1.0) * scale,
984                    (rng.next() * 2.0 - 1.0) * scale,
985                    (rng.next() * 2.0 - 1.0) * scale,
986                ];
987                let corner = |rng: &mut Lcg| -> [f32; 3] {
988                    std::array::from_fn(|k| base[k] + (rng.next() * 2.0 - 1.0))
989                };
990                let (e0, e1, a, b) = (
991                    corner(&mut rng),
992                    corner(&mut rng),
993                    corner(&mut rng),
994                    corner(&mut rng),
995                );
996                let mp: [f32; 3] = if rng.next() < 0.5 {
997                    // The shared edge midpoint: ties are common.
998                    std::array::from_fn(|k| (e0[k] + e1[k]) / 2.0)
999                } else {
1000                    // Inside the first triangle: hits are common.
1001                    std::array::from_fn(|k| (e0[k] + e1[k] + a[k]) / 3.0)
1002                };
1003                let dh: [f32; 3] = std::array::from_fn(|_| rng.next() * 2.0 - 1.0);
1004                let len = (dh[0] * dh[0] + dh[1] * dh[1] + dh[2] * dh[2]).sqrt();
1005                if len == 0.0 {
1006                    continue;
1007                }
1008                let dist = 1.0 + rng.next() * 4.0;
1009                let origin = std::array::from_fn(|k| mp[k] - dh[k] / len * dist);
1010                let dir = std::array::from_fn(|k| dh[k] / len);
1011                let pre = RayPre::new(origin, dir);
1012                for p in [[e0, e1, a], [e0, e1, b]] {
1013                    let Some(h) = ray_triangle(&pre, p, Cull::None, 0.0, f32::INFINITY) else {
1014                        continue;
1015                    };
1016                    hits += 1;
1017                    let (mn, mx) = p.iter().fold(
1018                        ([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]),
1019                        |(mn, mx), q| {
1020                            (
1021                                std::array::from_fn(|k| mn[k].min(q[k])),
1022                                std::array::from_fn(|k| mx[k].max(q[k])),
1023                            )
1024                        },
1025                    );
1026                    let b = fast_mesh_bvh::Aabb::new(mn, mx);
1027                    let e = ray_box(&pre, &b, 0.0, f32::INFINITY)
1028                        .expect("box of a hit triangle is culled");
1029                    assert!(e <= h.t, "enter {e} above t {} at scale {scale:e}", h.t);
1030                }
1031            }
1032        }
1033        assert!(hits > 1000, "only {hits} hits");
1034    }
1035
1036    #[test]
1037    fn subnormal_direction_components() {
1038        // Issue #19: subnormal components are valid. A subnormal largest
1039        // component overflows the reciprocal, so nothing hits; beside a
1040        // normal larger one it behaves like zero and hits as usual.
1041        let (mesh, bvh) = one_triangle(&T);
1042        let o = [0.25, 0.25, 1.0];
1043        let tiny = Ray {
1044            origin: o,
1045            dir: [1e-40, 0.0, 0.0],
1046        };
1047        assert_eq!(raycast(&mesh, &bvh, &tiny, &opts()), Ok(None));
1048        assert_eq!(raycast_any(&mesh, &bvh, &tiny, &opts()), Ok(None));
1049        let mut out = Vec::new();
1050        assert_eq!(raycast_all(&mesh, &bvh, &tiny, &opts(), &mut out), Ok(0));
1051        let sideways = Ray {
1052            origin: o,
1053            dir: [1e-40, 0.0, -1.0],
1054        };
1055        assert_eq!(
1056            raycast(&mesh, &bvh, &sideways, &opts())
1057                .unwrap()
1058                .map(|h| h.t),
1059            Some(1.0)
1060        );
1061    }
1062
1063    #[test]
1064    fn ray_box_is_conservative_and_clamps_entry() {
1065        use fast_mesh_bvh::Aabb;
1066        let b = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1067        let pre = RayPre::new([0.5, 0.5, 3.0], DOWN);
1068        let e = ray_box(&pre, &b, 0.0, f32::INFINITY).unwrap();
1069        assert!(e <= 2.0 && e > 1.99);
1070        assert_eq!(ray_box(&pre, &b, 0.0, 1.5), None);
1071        assert_eq!(ray_box(&pre, &b, 3.5, f32::INFINITY), None);
1072        // Inside the box: the entry is clamped to min_t.
1073        let pre = RayPre::new([0.5, 0.5, 0.5], DOWN);
1074        assert_eq!(ray_box(&pre, &b, 0.0, f32::INFINITY), Some(0.0));
1075        // Pointing away from it.
1076        let pre = RayPre::new([0.5, 0.5, 3.0], [0.0, 0.0, 1.0]);
1077        assert_eq!(ray_box(&pre, &b, 0.0, f32::INFINITY), None);
1078        // Grazing a face of a flat box.
1079        let flat = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 0.0]);
1080        let pre = RayPre::new([-1.0, 0.5, 0.0], [1.0, 0.0, 0.0]);
1081        assert!(ray_box(&pre, &flat, 0.0, f32::INFINITY).is_some());
1082    }
1083}