bevy_math/bounding/
raycast3d.rs

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