bevy_math/bounding/
raycast3d.rs

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