Skip to main content

bevy_shape/bounding/
raycast2d.rs

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