bevy_math/bounding/
raycast2d.rs

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