Skip to main content

rs_math3d/
queries.rs

1// Copyright 2020-Present (c) Raja Lehtihet & Wael El Oraiby
2//
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
5//
6// 1. Redistributions of source code must retain the above copyright notice,
7// this list of conditions and the following disclaimer.
8//
9// 2. Redistributions in binary form must reproduce the above copyright notice,
10// this list of conditions and the following disclaimer in the documentation
11// and/or other materials provided with the distribution.
12//
13// 3. Neither the name of the copyright holder nor the names of its contributors
14// may be used to endorse or promote products derived from this software without
15// specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27// POSSIBILITY OF SUCH DAMAGE.
28//! Intersection and distance query traits and implementations.
29//!
30//! Boolean intersection tests are boundary-inclusive unless stated otherwise:
31//! touching counts as intersecting.
32//!
33//! The `Box3`/`Ray` query uses a slab-interval test. For ray directions that are
34//! parallel to a slab plane, the implementation checks whether the ray origin is
35//! already inside that slab instead of relying on IEEE `0/0` behaviour.
36
37use crate::primitives::*;
38use crate::scalar::*;
39use crate::vector::*;
40use num_traits::{One, Zero};
41
42////////////////////////////////////////////////////////////////////////////////
43/// Query Traits
44////////////////////////////////////////////////////////////////////////////////
45pub trait Distance<T, Other> {
46    /// Computes the distance to another value, or `None` if undefined.
47    fn distance(&self, other: &Other) -> Option<T>;
48}
49
50/// Trait for boolean intersection tests.
51pub trait Intersect<T> {
52    /// Returns `true` when two objects intersect.
53    ///
54    /// Unless an implementation documents otherwise, touching at the boundary
55    /// counts as an intersection.
56    fn intersect(&self, other: &T) -> bool;
57}
58
59/// Trait for producing intersection results.
60pub trait Intersection<T, Other> {
61    /// Returns the intersection result, or `None` if there is no intersection.
62    fn intersection(&self, other: &Other) -> Option<T>;
63}
64
65////////////////////////////////////////////////////////////////////////////////
66/// Distance Queries
67////////////////////////////////////////////////////////////////////////////////
68impl<T: FloatScalar> Distance<T, Vector3<T>> for Plane<T> {
69    fn distance(&self, other: &Vector3<T>) -> Option<T> {
70        let n = self.normal();
71        let nom = Vector3::dot(other, &n) + self.constant();
72        let denom = Vector3::dot(&n, &n);
73        if denom <= T::epsilon() {
74            return None;
75        }
76        Some(T::tabs(nom) / denom)
77    }
78}
79
80impl<T: FloatScalar> Distance<T, Vector3<T>> for Segment<T, Vector3<T>> {
81    fn distance(&self, other: &Vector3<T>) -> Option<T> {
82        Segment::distance(self, other, T::epsilon())
83    }
84}
85
86////////////////////////////////////////////////////////////////////////////////
87/// Intersect Queries
88////////////////////////////////////////////////////////////////////////////////
89// Updates the running `[tmin, tmax]` interval for one axis-aligned slab.
90//
91// For nonzero directions this is the usual slab intersection update:
92// intersect the current interval with the parameter interval where the ray lies
93// between `slab_min` and `slab_max`.
94//
95// For zero directions the ray is parallel to the slab planes. In that case there
96// is no finite entry/exit parameter, so the query reduces to whether the origin
97// already lies inside the slab.
98fn update_slab_interval<T: FloatScalar>(
99    slab_min: T,
100    slab_max: T,
101    ray_start: T,
102    ray_dir: T,
103    tmin: &mut T,
104    tmax: &mut T,
105) -> bool {
106    if ray_dir == <T as Zero>::zero() {
107        return ray_start >= slab_min && ray_start <= slab_max;
108    }
109
110    let inv_dir = <T as One>::one() / ray_dir;
111    let mut t0 = (slab_min - ray_start) * inv_dir;
112    let mut t1 = (slab_max - ray_start) * inv_dir;
113    if t0 > t1 {
114        core::mem::swap(&mut t0, &mut t1);
115    }
116
117    *tmin = T::max(*tmin, t0);
118    *tmax = T::min(*tmax, t1);
119    *tmin <= *tmax
120}
121
122impl<T: FloatScalar> Intersect<Ray<T, Vector3<T>>> for Box3<T> {
123    fn intersect(&self, other: &Ray<T, Vector3<T>>) -> bool {
124        // Slab test for an axis-aligned box.
125        //
126        // The running interval `[tmin, tmax]` contains all ray parameters `t`
127        // for which the point `other.start + t * other.direction` is inside the
128        // slabs processed so far.
129        //
130        // This is in the same family as the Williams et al. ray-box test, but we
131        // handle `direction == 0` explicitly instead of depending on direct
132        // division for every axis. That avoids `0/0 -> NaN` when a ray starts on
133        // a slab boundary and is parallel to that axis.
134        let mut tmin = -T::infinity();
135        let mut tmax = T::infinity();
136
137        if !update_slab_interval(
138            self.min.x,
139            self.max.x,
140            other.start.x,
141            other.direction.x,
142            &mut tmin,
143            &mut tmax,
144        ) {
145            return false;
146        }
147        if !update_slab_interval(
148            self.min.y,
149            self.max.y,
150            other.start.y,
151            other.direction.y,
152            &mut tmin,
153            &mut tmax,
154        ) {
155            return false;
156        }
157        if !update_slab_interval(
158            self.min.z,
159            self.max.z,
160            other.start.z,
161            other.direction.z,
162            &mut tmin,
163            &mut tmax,
164        ) {
165            return false;
166        }
167
168        // Intersect the final slab interval with the forward-ray constraint
169        // `t >= 0`. Touching the box boundary counts as an intersection.
170        tmax >= T::max(tmin, <T as Zero>::zero())
171    }
172}
173
174impl<T: FloatScalar> Intersect<Sphere3<T>> for Box3<T> {
175    fn intersect(&self, other: &Sphere3<T>) -> bool {
176        let mut dist = <T as Zero>::zero();
177
178        let c = other.center;
179        let r = other.radius;
180
181        if c.x < self.min.x {
182            dist += T::squared(c.x - self.min.x)
183        } else if c.x > self.max.x {
184            dist += T::squared(c.x - self.max.x)
185        }
186
187        if c.y < self.min.y {
188            dist += T::squared(c.y - self.min.y)
189        } else if c.y > self.max.y {
190            dist += T::squared(c.y - self.max.y)
191        }
192
193        if c.z < self.min.z {
194            dist += T::squared(c.z - self.min.z)
195        } else if c.z > self.max.z {
196            dist += T::squared(c.z - self.max.z)
197        }
198
199        r * r >= dist
200    }
201}
202
203fn is_in_0_to_1_range<T: FloatScalar>(x: T) -> bool {
204    x >= <T as Zero>::zero() && x <= <T as One>::one()
205}
206
207impl<T: FloatScalar> Intersect<Tri3<T>> for Sphere3<T> {
208    fn intersect(&self, tri: &Tri3<T>) -> bool {
209        let uvw = tri.barycentric_coordinates(&self.center);
210        let verts = tri.vertices();
211        let v0 = verts[0];
212        let v1 = verts[1];
213        let v2 = verts[2];
214        if is_in_0_to_1_range(uvw.x) && is_in_0_to_1_range(uvw.y) && is_in_0_to_1_range(uvw.z) {
215            Plane::try_from_tri(&v0, &v1, &v2, T::epsilon()).is_some_and(|p| {
216                p.distance(&self.center)
217                    .is_some_and(|dist| dist <= self.radius)
218            })
219        } else {
220            let d0 = Segment::new(&v0, &v1).distance(&self.center, T::epsilon());
221            let d1 = Segment::new(&v1, &v2).distance(&self.center, T::epsilon());
222            let d2 = Segment::new(&v2, &v0).distance(&self.center, T::epsilon());
223
224            match (d0, d1, d2) {
225                (Some(d0), Some(d1), Some(d2)) => {
226                    let m = T::min(d0, T::min(d1, d2));
227                    m <= self.radius
228                }
229                _ => false,
230            }
231        }
232    }
233}
234
235///
236/// Ray/Line-Triangle Intersection Test Routines
237/// Different optimizations of my and Ben Trumbore's
238/// code from journals of graphics tools (JGT)
239/// <http://www.acm.org/jgt/>
240/// by Tomas Moller, May 2000
241///
242enum TriangleIntersectionKind {
243    Ray,
244    Line,
245}
246
247fn triangle_intersection_from_point_dir<T: FloatScalar>(
248    start: &Vector3<T>,
249    direction: &Vector3<T>,
250    tri: &Tri3<T>,
251    epsilon: T,
252    kind: TriangleIntersectionKind,
253) -> Option<(T, Vector3<T>)> {
254    let verts = tri.vertices();
255    let v0 = verts[0];
256    let v1 = verts[1];
257    let v2 = verts[2];
258    let edge1 = v1 - v0;
259    let edge2 = v2 - v0;
260
261    let pvec = Vector3::cross(direction, &edge2);
262    let det = Vector3::dot(&edge1, &pvec);
263    if det > -epsilon && det < epsilon {
264        return None;
265    }
266
267    let tvec = *start - v0;
268    let qvec = Vector3::cross(&tvec, &edge1);
269
270    let u = Vector3::dot(&tvec, &pvec) / det;
271    if u < -epsilon || u > <T as One>::one() + epsilon {
272        return None;
273    }
274
275    let v = Vector3::dot(direction, &qvec) / det;
276    if v < -epsilon || u + v > <T as One>::one() + T::two() * epsilon {
277        return None;
278    }
279
280    let t = Vector3::dot(&edge2, &qvec) / det;
281    if matches!(kind, TriangleIntersectionKind::Ray) && t < -epsilon {
282        return None;
283    }
284
285    Some((t, *start + (*direction * t)))
286}
287
288impl<T: FloatScalar> Intersection<(T, Vector3<T>), Tri3<T>> for Ray<T, Vector3<T>> {
289    fn intersection(&self, tri: &Tri3<T>) -> Option<(T, Vector3<T>)> {
290        triangle_intersection_from_point_dir(
291            &self.start,
292            &self.direction,
293            tri,
294            T::epsilon(),
295            TriangleIntersectionKind::Ray,
296        )
297    }
298}
299
300impl<T: FloatScalar> Intersection<(T, Vector3<T>), Tri3<T>> for Line<T, Vector3<T>> {
301    fn intersection(&self, tri: &Tri3<T>) -> Option<(T, Vector3<T>)> {
302        triangle_intersection_from_point_dir(
303            &self.p,
304            &self.d,
305            tri,
306            T::epsilon(),
307            TriangleIntersectionKind::Line,
308        )
309    }
310}
311
312impl<T: FloatScalar> Intersection<(T, Vector3<T>), Ray<T, Vector3<T>>> for Tri3<T> {
313    fn intersection(&self, ray: &Ray<T, Vector3<T>>) -> Option<(T, Vector3<T>)> {
314        ray.intersection(self)
315    }
316}
317
318impl<T: FloatScalar> Intersection<(T, Vector3<T>), Line<T, Vector3<T>>> for Tri3<T> {
319    fn intersection(&self, line: &Line<T, Vector3<T>>) -> Option<(T, Vector3<T>)> {
320        line.intersection(self)
321    }
322}
323
324///
325/// Building an Orthonormal Basis from a Unit Vector
326/// John. F. Hughes & Thomas Moller
327/// Journal of Graphics Tools, 4:4, 33-35 (DOI: 10.1080/10867651.1999.10487513)
328///
329/// This implementation uses a symmetric variant of the Hughes-Moller
330/// construction: instead of branching on two specific components, it zeros the
331/// smallest-magnitude component and normalizes the resulting orthogonal helper
332/// vector. The proof sketch below applies to this variant.
333///
334/// Proof sketch for the branch construction:
335///
336/// Let `u = (x, y, z)` be the normalized input, so `||u|| = 1`.
337/// We choose the helper vector by zeroing the smallest-magnitude component:
338///
339/// - if `|x| <= |y|` and `|x| <= |z|`, choose `v0 = (0, -z, y)`
340/// - else if `|y| <= |x|` and `|y| <= |z|`, choose `v0 = (-z, 0, x)`
341/// - else choose `v0 = (-y, x, 0)`
342///
343/// In each branch, `u · v0 = 0`, so `v0` is orthogonal to `u`.
344/// The squared length is also bounded away from zero:
345///
346/// - first branch: `||v0||² = y² + z² = 1 - x²`
347/// - second branch: `||v0||² = x² + z² = 1 - y²`
348/// - third branch: `||v0||² = x² + y² = 1 - z²`
349///
350/// Because the selected component has minimal magnitude, its square is at most
351/// `1/3`. Therefore `||v0||² >= 2/3` in every branch, so `v0` is never zero for
352/// non-zero `u`, and normalizing it is always safe.
353///
354/// Let `v = normalize(v0)`. Then `u` and `v` are orthonormal. Finally,
355/// `w = normalize(u × v)` is orthogonal to both and has unit length, so the
356/// returned triple is orthonormal.
357///
358pub fn try_basis_from_unit<T: FloatScalar>(
359    unit: &Vector3<T>,
360    epsilon: T,
361) -> Option<[Vector3<T>; 3]> {
362    let u = unit.try_normalize(epsilon)?;
363    let x = u.x;
364    let y = u.y;
365    let z = u.z;
366
367    let v = if x.tabs() <= y.tabs() && x.tabs() <= z.tabs() {
368        Vector3::new(<T as Zero>::zero(), -z, y)
369    } else if y.tabs() <= x.tabs() && y.tabs() <= z.tabs() {
370        Vector3::new(-z, <T as Zero>::zero(), x)
371    } else {
372        // z.abs() < x.abs() && z.abs() <= y.abs()
373        Vector3::new(-y, x, <T as Zero>::zero())
374    };
375    let v = Vector3::normalize(&v);
376    let w = Vector3::normalize(&Vector3::cross(&u, &v));
377    Some([u, w, v])
378}
379
380/// Builds an orthonormal basis from a non-zero input direction.
381///
382/// # Panics
383/// Panics if `unit` is too small to normalize. Use
384/// [`try_basis_from_unit`] when the input may be degenerate.
385pub fn basis_from_unit<T: FloatScalar>(unit: &Vector3<T>) -> [Vector3<T>; 3] {
386    try_basis_from_unit(unit, T::epsilon()).expect("basis direction must be non-zero")
387}
388
389#[cfg(test)]
390mod tests {
391    use super::{Intersect, Intersection};
392    use crate::primitives::{Box3, Line, Ray, Sphere3, Tri3};
393    use crate::vector::{FloatVector, Vector, Vector3};
394    use crate::EPS_F32;
395
396    fn assert_orthonormal_basis(basis: [Vector3<f32>; 3]) {
397        let [u, w, v] = basis;
398        assert!((u.length() - 1.0).abs() < 0.001);
399        assert!((v.length() - 1.0).abs() < 0.001);
400        assert!((w.length() - 1.0).abs() < 0.001);
401        assert!(Vector3::dot(&u, &v).abs() < 0.001);
402        assert!(Vector3::dot(&u, &w).abs() < 0.001);
403        assert!(Vector3::dot(&v, &w).abs() < 0.001);
404    }
405
406    #[test]
407    fn test_box3_sphere_intersect_z_axis() {
408        let b = Box3::new(
409            &Vector3::new(0.0f32, 0.0, 0.0),
410            &Vector3::new(1.0, 1.0, 1.0),
411        );
412
413        let outside_z = Sphere3::new(Vector3::new(0.5, 0.5, 2.0), 0.25);
414        assert!(!b.intersect(&outside_z));
415
416        let touching_z = Sphere3::new(Vector3::new(0.5, 0.5, 2.0), 1.0);
417        assert!(b.intersect(&touching_z));
418    }
419
420    #[test]
421    fn test_box3_sphere_intersect_on_edge() {
422        let b = Box3::new(
423            &Vector3::new(0.0f32, 0.0, 0.0),
424            &Vector3::new(1.0, 1.0, 1.0),
425        );
426
427        let touching = Sphere3::new(Vector3::new(2.0, 0.5, 0.5), 1.0);
428        assert!(b.intersect(&touching));
429
430        let just_inside = Sphere3::new(Vector3::new(2.0, 0.5, 0.5), 1.0001);
431        assert!(b.intersect(&just_inside));
432    }
433
434    #[test]
435    fn test_box3_ray_intersect_touching_boundary() {
436        let b = Box3::new(
437            &Vector3::new(0.0f32, 0.0, 0.0),
438            &Vector3::new(1.0, 1.0, 1.0),
439        );
440        let ray = Ray::new(
441            &Vector3::new(1.0f32, 0.5, 0.5),
442            &Vector3::new(1.0, 0.0, 0.0),
443            EPS_F32,
444        )
445        .expect("ray should be valid");
446        assert!(b.intersect(&ray));
447    }
448
449    #[test]
450    fn test_box3_ray_intersect_parallel_on_face() {
451        let b = Box3::new(
452            &Vector3::new(0.0f32, 0.0, 0.0),
453            &Vector3::new(1.0, 1.0, 1.0),
454        );
455        let ray = Ray::new(
456            &Vector3::new(0.0f32, 0.5, 0.5),
457            &Vector3::new(0.0, 1.0, 0.0),
458            EPS_F32,
459        )
460        .expect("ray should be valid");
461        assert!(b.intersect(&ray));
462    }
463
464    #[test]
465    fn test_box3_ray_intersect_parallel_outside_slab() {
466        let b = Box3::new(
467            &Vector3::new(0.0f32, 0.0, 0.0),
468            &Vector3::new(1.0, 1.0, 1.0),
469        );
470        let ray = Ray::new(
471            &Vector3::new(2.0f32, 0.5, 0.5),
472            &Vector3::new(0.0, 1.0, 0.0),
473            EPS_F32,
474        )
475        .expect("ray should be valid");
476        assert!(!b.intersect(&ray));
477    }
478
479    #[test]
480    fn test_try_basis_from_unit_zero_none() {
481        let zero = Vector3::new(0.0f32, 0.0, 0.0);
482        assert!(super::try_basis_from_unit(&zero, EPS_F32).is_none());
483    }
484
485    #[test]
486    fn test_try_basis_from_unit_orthonormal() {
487        let basis0 = super::try_basis_from_unit(&Vector3::new(1.0f32, 2.0, 3.0), EPS_F32)
488            .expect("basis should exist");
489        let basis1 = super::try_basis_from_unit(&Vector3::new(0.1f32, 4.0, 5.0), EPS_F32)
490            .expect("basis should exist");
491        let basis2 = super::try_basis_from_unit(&Vector3::new(4.0f32, 0.1, 5.0), EPS_F32)
492            .expect("basis should exist");
493        let basis3 = super::try_basis_from_unit(&Vector3::new(4.0f32, 5.0, 0.1), EPS_F32)
494            .expect("basis should exist");
495
496        assert_orthonormal_basis(basis0);
497        assert_orthonormal_basis(basis1);
498        assert_orthonormal_basis(basis2);
499        assert_orthonormal_basis(basis3);
500    }
501
502    #[test]
503    fn test_try_basis_from_unit_axes_and_ties() {
504        let basis_x =
505            super::try_basis_from_unit(&Vector3::new(1.0f32, 0.0, 0.0), EPS_F32).expect("basis");
506        let basis_y =
507            super::try_basis_from_unit(&Vector3::new(0.0f32, 1.0, 0.0), EPS_F32).expect("basis");
508        let basis_z =
509            super::try_basis_from_unit(&Vector3::new(0.0f32, 0.0, 1.0), EPS_F32).expect("basis");
510        let basis_neg =
511            super::try_basis_from_unit(&Vector3::new(-1.0f32, -1.0, -1.0), EPS_F32).expect("basis");
512
513        assert_orthonormal_basis(basis_x);
514        assert_orthonormal_basis(basis_y);
515        assert_orthonormal_basis(basis_z);
516        assert_orthonormal_basis(basis_neg);
517    }
518
519    #[test]
520    fn test_ray_triangle_intersection_rejects_hits_behind_ray() {
521        let tri = Tri3::new([
522            Vector3::new(0.0f32, 0.0, 0.0),
523            Vector3::new(1.0, 0.0, 0.0),
524            Vector3::new(0.0, 1.0, 0.0),
525        ]);
526        let ray = Ray::new(
527            &Vector3::new(0.25f32, 0.25, 1.0),
528            &Vector3::new(0.0, 0.0, 1.0),
529            EPS_F32,
530        )
531        .expect("ray should be valid");
532
533        assert!(ray.intersection(&tri).is_none());
534        assert!(tri.intersection(&ray).is_none());
535    }
536
537    #[test]
538    fn test_ray_triangle_intersection_forward_hit() {
539        let tri = Tri3::new([
540            Vector3::new(0.0f32, 0.0, 0.0),
541            Vector3::new(1.0, 0.0, 0.0),
542            Vector3::new(0.0, 1.0, 0.0),
543        ]);
544        let ray = Ray::new(
545            &Vector3::new(0.25f32, 0.25, 1.0),
546            &Vector3::new(0.0, 0.0, -1.0),
547            EPS_F32,
548        )
549        .expect("ray should be valid");
550
551        let (t0, p0) = ray.intersection(&tri).expect("ray should hit triangle");
552        let (t1, p1) = tri.intersection(&ray).expect("triangle should hit ray");
553
554        assert!((t0 - 1.0).abs() < 0.001);
555        assert!((p0.x - 0.25).abs() < 0.001);
556        assert!((p0.y - 0.25).abs() < 0.001);
557        assert!(p0.z.abs() < 0.001);
558
559        assert!((t1 - t0).abs() < 0.001);
560        assert!((p1.x - p0.x).abs() < 0.001);
561        assert!((p1.y - p0.y).abs() < 0.001);
562        assert!((p1.z - p0.z).abs() < 0.001);
563    }
564
565    #[test]
566    fn test_line_triangle_intersection_accepts_hits_on_both_sides() {
567        let tri = Tri3::new([
568            Vector3::new(0.0f32, 0.0, 0.0),
569            Vector3::new(1.0, 0.0, 0.0),
570            Vector3::new(0.0, 1.0, 0.0),
571        ]);
572        let line = Line::new(
573            &Vector3::new(0.25f32, 0.25, 1.0),
574            &Vector3::new(0.0, 0.0, 1.0),
575            EPS_F32,
576        )
577        .expect("line should be valid");
578
579        let (t0, p0) = line.intersection(&tri).expect("line should hit triangle");
580        let (t1, p1) = tri.intersection(&line).expect("triangle should hit line");
581
582        assert!((t0 + 1.0).abs() < 0.001);
583        assert!((p0.x - 0.25).abs() < 0.001);
584        assert!((p0.y - 0.25).abs() < 0.001);
585        assert!(p0.z.abs() < 0.001);
586
587        assert!((t1 - t0).abs() < 0.001);
588        assert!((p1.x - p0.x).abs() < 0.001);
589        assert!((p1.y - p0.y).abs() < 0.001);
590        assert!((p1.z - p0.z).abs() < 0.001);
591    }
592
593    #[test]
594    fn test_box3_sphere_intersect_negative_radius_canonicalized() {
595        let b = Box3::new(
596            &Vector3::new(0.0f32, 0.0, 0.0),
597            &Vector3::new(1.0, 1.0, 1.0),
598        );
599        let touching = Sphere3::new(Vector3::new(2.0f32, 0.5, 0.5), -1.0);
600        assert!(b.intersect(&touching));
601    }
602}