bevy_math/bounding/
raycast3d.rs

1use super::{Aabb3d, BoundingSphere, IntersectsVolume};
2use crate::{
3    ops::{self, FloatPow},
4    Dir3A, Ray3d, Vec3A,
5};
6
7#[cfg(feature = "bevy_reflect")]
8use bevy_reflect::Reflect;
9
10/// A raycast intersection test for 3D bounding volumes
11#[derive(Clone, Debug)]
12#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
13pub struct RayCast3d {
14    /// The origin of the ray.
15    pub origin: Vec3A,
16    /// The direction of the ray.
17    pub direction: Dir3A,
18    /// The maximum distance for the ray
19    pub max: f32,
20    /// The multiplicative inverse direction of the ray
21    direction_recip: Vec3A,
22}
23
24impl RayCast3d {
25    /// Construct a [`RayCast3d`] from an origin, [direction], and max distance.
26    ///
27    /// [direction]: crate::direction::Dir3
28    pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Dir3A>, max: f32) -> Self {
29        let direction = direction.into();
30        Self {
31            origin: origin.into(),
32            direction,
33            direction_recip: direction.recip(),
34            max,
35        }
36    }
37
38    /// Construct a [`RayCast3d`] from a [`Ray3d`] and max distance.
39    pub fn from_ray(ray: Ray3d, max: f32) -> Self {
40        Self::new(ray.origin, ray.direction, max)
41    }
42
43    /// Get the cached multiplicative inverse of the direction of the ray.
44    pub fn direction_recip(&self) -> Vec3A {
45        self.direction_recip
46    }
47
48    /// Get the distance of an intersection with an [`Aabb3d`], if any.
49    pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {
50        let positive = self.direction.signum().cmpgt(Vec3A::ZERO);
51        let min = Vec3A::select(positive, aabb.min, aabb.max);
52        let max = Vec3A::select(positive, aabb.max, aabb.min);
53
54        // Calculate the minimum/maximum time for each axis based on how much the direction goes that
55        // way. These values can get arbitrarily large, or even become NaN, which is handled by the
56        // min/max operations below
57        let tmin = (min - self.origin) * self.direction_recip;
58        let tmax = (max - self.origin) * self.direction_recip;
59
60        // An axis that is not relevant to the ray direction will be NaN. When one of the arguments
61        // to min/max is NaN, the other argument is used.
62        // An axis for which the direction is the wrong way will return an arbitrarily large
63        // negative value.
64        let tmin = tmin.max_element().max(0.);
65        let tmax = tmax.min_element().min(self.max);
66
67        if tmin <= tmax {
68            Some(tmin)
69        } else {
70            None
71        }
72    }
73
74    /// Get the distance of an intersection with a [`BoundingSphere`], if any.
75    pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
76        let offset = self.origin - sphere.center;
77        let projected = offset.dot(*self.direction);
78        let closest_point = offset - projected * *self.direction;
79        let distance_squared = sphere.radius().squared() - closest_point.length_squared();
80        if distance_squared < 0.
81            || ops::copysign(projected.squared(), -projected) < -distance_squared
82        {
83            None
84        } else {
85            let toi = -projected - ops::sqrt(distance_squared);
86            if toi > self.max {
87                None
88            } else {
89                Some(toi.max(0.))
90            }
91        }
92    }
93}
94
95impl IntersectsVolume<Aabb3d> for RayCast3d {
96    fn intersects(&self, volume: &Aabb3d) -> bool {
97        self.aabb_intersection_at(volume).is_some()
98    }
99}
100
101impl IntersectsVolume<BoundingSphere> for RayCast3d {
102    fn intersects(&self, volume: &BoundingSphere) -> bool {
103        self.sphere_intersection_at(volume).is_some()
104    }
105}
106
107/// An intersection test that casts an [`Aabb3d`] along a ray.
108#[derive(Clone, Debug)]
109#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
110pub struct AabbCast3d {
111    /// The ray along which to cast the bounding volume
112    pub ray: RayCast3d,
113    /// The aabb that is being cast
114    pub aabb: Aabb3d,
115}
116
117impl AabbCast3d {
118    /// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [direction], and max distance.
119    ///
120    /// [direction]: crate::direction::Dir3
121    pub fn new(
122        aabb: Aabb3d,
123        origin: impl Into<Vec3A>,
124        direction: impl Into<Dir3A>,
125        max: f32,
126    ) -> Self {
127        Self {
128            ray: RayCast3d::new(origin, direction, max),
129            aabb,
130        }
131    }
132
133    /// Construct an [`AabbCast3d`] from an [`Aabb3d`], [`Ray3d`], and max distance.
134    pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self {
135        Self::new(aabb, ray.origin, ray.direction, max)
136    }
137
138    /// Get the distance at which the [`Aabb3d`]s collide, if at all.
139    pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option<f32> {
140        aabb.min -= self.aabb.max;
141        aabb.max -= self.aabb.min;
142        self.ray.aabb_intersection_at(&aabb)
143    }
144}
145
146impl IntersectsVolume<Aabb3d> for AabbCast3d {
147    fn intersects(&self, volume: &Aabb3d) -> bool {
148        self.aabb_collision_at(*volume).is_some()
149    }
150}
151
152/// An intersection test that casts a [`BoundingSphere`] along a ray.
153#[derive(Clone, Debug)]
154#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
155pub struct BoundingSphereCast {
156    /// The ray along which to cast the bounding volume
157    pub ray: RayCast3d,
158    /// The sphere that is being cast
159    pub sphere: BoundingSphere,
160}
161
162impl BoundingSphereCast {
163    /// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [direction], and max distance.
164    ///
165    /// [direction]: crate::direction::Dir3
166    pub fn new(
167        sphere: BoundingSphere,
168        origin: impl Into<Vec3A>,
169        direction: impl Into<Dir3A>,
170        max: f32,
171    ) -> Self {
172        Self {
173            ray: RayCast3d::new(origin, direction, max),
174            sphere,
175        }
176    }
177
178    /// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], [`Ray3d`], and max distance.
179    pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self {
180        Self::new(sphere, ray.origin, ray.direction, max)
181    }
182
183    /// Get the distance at which the [`BoundingSphere`]s collide, if at all.
184    pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option<f32> {
185        sphere.center -= self.sphere.center;
186        sphere.sphere.radius += self.sphere.radius();
187        self.ray.sphere_intersection_at(&sphere)
188    }
189}
190
191impl IntersectsVolume<BoundingSphere> for BoundingSphereCast {
192    fn intersects(&self, volume: &BoundingSphere) -> bool {
193        self.sphere_collision_at(*volume).is_some()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::{Dir3, Vec3};
201
202    const EPSILON: f32 = 0.001;
203
204    #[test]
205    fn test_ray_intersection_sphere_hits() {
206        for (test, volume, expected_distance) in &[
207            (
208                // Hit the center of a centered bounding sphere
209                RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
210                BoundingSphere::new(Vec3::ZERO, 1.),
211                4.,
212            ),
213            (
214                // Hit the center of a centered bounding sphere, but from the other side
215                RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
216                BoundingSphere::new(Vec3::ZERO, 1.),
217                4.,
218            ),
219            (
220                // Hit the center of an offset sphere
221                RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
222                BoundingSphere::new(Vec3::Y * 3., 2.),
223                1.,
224            ),
225            (
226                // Just barely hit the sphere before the max distance
227                RayCast3d::new(Vec3::X, Dir3::Y, 1.),
228                BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),
229                0.99,
230            ),
231            (
232                // Hit a sphere off-center
233                RayCast3d::new(Vec3::X, Dir3::Y, 90.),
234                BoundingSphere::new(Vec3::Y * 5., 2.),
235                3.268,
236            ),
237            (
238                // Barely hit a sphere on the side
239                RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.),
240                BoundingSphere::new(Vec3::Y * 5., 1.),
241                4.996,
242            ),
243        ] {
244            assert!(
245                test.intersects(volume),
246                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
247            );
248            let actual_distance = test.sphere_intersection_at(volume).unwrap();
249            assert!(
250                ops::abs(actual_distance - expected_distance) < EPSILON,
251                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}\n  Actual distance: {actual_distance}",
252            );
253
254            let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
255            assert!(
256                !inverted_ray.intersects(volume),
257                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
258            );
259        }
260    }
261
262    #[test]
263    fn test_ray_intersection_sphere_misses() {
264        for (test, volume) in &[
265            (
266                // The ray doesn't go in the right direction
267                RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
268                BoundingSphere::new(Vec3::Y * 2., 1.),
269            ),
270            (
271                // Ray's alignment isn't enough to hit the sphere
272                RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
273                BoundingSphere::new(Vec3::Y * 2., 1.),
274            ),
275            (
276                // The ray's maximum distance isn't high enough
277                RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
278                BoundingSphere::new(Vec3::Y * 2., 1.),
279            ),
280        ] {
281            assert!(
282                !test.intersects(volume),
283                "Case:\n  Test: {test:?}\n  Volume: {volume:?}",
284            );
285        }
286    }
287
288    #[test]
289    fn test_ray_intersection_sphere_inside() {
290        let volume = BoundingSphere::new(Vec3::splat(0.5), 1.);
291        for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
292            for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
293                for max in &[0., 1., 900.] {
294                    let test = RayCast3d::new(*origin, *direction, *max);
295
296                    assert!(
297                        test.intersects(&volume),
298                        "Case:\n  origin: {origin:?}\n  Direction: {direction:?}\n  Max: {max}",
299                    );
300
301                    let actual_distance = test.sphere_intersection_at(&volume);
302                    assert_eq!(
303                        actual_distance,
304                        Some(0.),
305                        "Case:\n  origin: {origin:?}\n  Direction: {direction:?}\n  Max: {max}",
306                    );
307                }
308            }
309        }
310    }
311
312    #[test]
313    fn test_ray_intersection_aabb_hits() {
314        for (test, volume, expected_distance) in &[
315            (
316                // Hit the center of a centered aabb
317                RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
318                Aabb3d::new(Vec3::ZERO, Vec3::ONE),
319                4.,
320            ),
321            (
322                // Hit the center of a centered aabb, but from the other side
323                RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
324                Aabb3d::new(Vec3::ZERO, Vec3::ONE),
325                4.,
326            ),
327            (
328                // Hit the center of an offset aabb
329                RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
330                Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),
331                1.,
332            ),
333            (
334                // Just barely hit the aabb before the max distance
335                RayCast3d::new(Vec3::X, Dir3::Y, 1.),
336                Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),
337                0.99,
338            ),
339            (
340                // Hit an aabb off-center
341                RayCast3d::new(Vec3::X, Dir3::Y, 90.),
342                Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),
343                3.,
344            ),
345            (
346                // Barely hit an aabb on corner
347                RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
348                Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
349                1.732,
350            ),
351        ] {
352            assert!(
353                test.intersects(volume),
354                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
355            );
356            let actual_distance = test.aabb_intersection_at(volume).unwrap();
357            assert!(
358                ops::abs(actual_distance - expected_distance) < EPSILON,
359                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}\n  Actual distance: {actual_distance}",
360            );
361
362            let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
363            assert!(
364                !inverted_ray.intersects(volume),
365                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
366            );
367        }
368    }
369
370    #[test]
371    fn test_ray_intersection_aabb_misses() {
372        for (test, volume) in &[
373            (
374                // The ray doesn't go in the right direction
375                RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
376                Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
377            ),
378            (
379                // Ray's alignment isn't enough to hit the aabb
380                RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 0.99, 1.).unwrap(), 90.),
381                Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
382            ),
383            (
384                // The ray's maximum distance isn't high enough
385                RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
386                Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
387            ),
388        ] {
389            assert!(
390                !test.intersects(volume),
391                "Case:\n  Test: {test:?}\n  Volume: {volume:?}",
392            );
393        }
394    }
395
396    #[test]
397    fn test_ray_intersection_aabb_inside() {
398        let volume = Aabb3d::new(Vec3::splat(0.5), Vec3::ONE);
399        for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
400            for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
401                for max in &[0., 1., 900.] {
402                    let test = RayCast3d::new(*origin, *direction, *max);
403
404                    assert!(
405                        test.intersects(&volume),
406                        "Case:\n  origin: {origin:?}\n  Direction: {direction:?}\n  Max: {max}",
407                    );
408
409                    let actual_distance = test.aabb_intersection_at(&volume);
410                    assert_eq!(
411                        actual_distance,
412                        Some(0.),
413                        "Case:\n  origin: {origin:?}\n  Direction: {direction:?}\n  Max: {max}",
414                    );
415                }
416            }
417        }
418    }
419
420    #[test]
421    fn test_aabb_cast_hits() {
422        for (test, volume, expected_distance) in &[
423            (
424                // Hit the center of the aabb, that a ray would've also hit
425                AabbCast3d::new(Aabb3d::new(Vec3::ZERO, Vec3::ONE), Vec3::ZERO, Dir3::Y, 90.),
426                Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
427                3.,
428            ),
429            (
430                // Hit the center of the aabb, but from the other side
431                AabbCast3d::new(
432                    Aabb3d::new(Vec3::ZERO, Vec3::ONE),
433                    Vec3::Y * 10.,
434                    -Dir3::Y,
435                    90.,
436                ),
437                Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
438                3.,
439            ),
440            (
441                // Hit the edge of the aabb, that a ray would've missed
442                AabbCast3d::new(
443                    Aabb3d::new(Vec3::ZERO, Vec3::ONE),
444                    Vec3::X * 1.5,
445                    Dir3::Y,
446                    90.,
447                ),
448                Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
449                3.,
450            ),
451            (
452                // Hit the edge of the aabb, by casting an off-center AABB
453                AabbCast3d::new(
454                    Aabb3d::new(Vec3::X * -2., Vec3::ONE),
455                    Vec3::X * 3.,
456                    Dir3::Y,
457                    90.,
458                ),
459                Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
460                3.,
461            ),
462        ] {
463            assert!(
464                test.intersects(volume),
465                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
466            );
467            let actual_distance = test.aabb_collision_at(*volume).unwrap();
468            assert!(
469                ops::abs(actual_distance - expected_distance) < EPSILON,
470                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}\n  Actual distance: {actual_distance}",
471            );
472
473            let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
474            assert!(
475                !inverted_ray.intersects(volume),
476                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
477            );
478        }
479    }
480
481    #[test]
482    fn test_sphere_cast_hits() {
483        for (test, volume, expected_distance) in &[
484            (
485                // Hit the center of the bounding sphere, that a ray would've also hit
486                BoundingSphereCast::new(
487                    BoundingSphere::new(Vec3::ZERO, 1.),
488                    Vec3::ZERO,
489                    Dir3::Y,
490                    90.,
491                ),
492                BoundingSphere::new(Vec3::Y * 5., 1.),
493                3.,
494            ),
495            (
496                // Hit the center of the bounding sphere, but from the other side
497                BoundingSphereCast::new(
498                    BoundingSphere::new(Vec3::ZERO, 1.),
499                    Vec3::Y * 10.,
500                    -Dir3::Y,
501                    90.,
502                ),
503                BoundingSphere::new(Vec3::Y * 5., 1.),
504                3.,
505            ),
506            (
507                // Hit the bounding sphere off-center, that a ray would've missed
508                BoundingSphereCast::new(
509                    BoundingSphere::new(Vec3::ZERO, 1.),
510                    Vec3::X * 1.5,
511                    Dir3::Y,
512                    90.,
513                ),
514                BoundingSphere::new(Vec3::Y * 5., 1.),
515                3.677,
516            ),
517            (
518                // Hit the bounding sphere off-center, by casting a sphere that is off-center
519                BoundingSphereCast::new(
520                    BoundingSphere::new(Vec3::X * -1.5, 1.),
521                    Vec3::X * 3.,
522                    Dir3::Y,
523                    90.,
524                ),
525                BoundingSphere::new(Vec3::Y * 5., 1.),
526                3.677,
527            ),
528        ] {
529            assert!(
530                test.intersects(volume),
531                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
532            );
533            let actual_distance = test.sphere_collision_at(*volume).unwrap();
534            assert!(
535                ops::abs(actual_distance - expected_distance) < EPSILON,
536                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}\n  Actual distance: {actual_distance}",
537            );
538
539            let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
540            assert!(
541                !inverted_ray.intersects(volume),
542                "Case:\n  Test: {test:?}\n  Volume: {volume:?}\n  Expected distance: {expected_distance:?}",
543            );
544        }
545    }
546}