Skip to main content

bevy_shape/bounding/
raycast3d.rs

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