bevy_math/bounding/
raycast2d.rs

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