Path: blob/main/crates/bevy_math/src/bounding/raycast2d.rs
6596 views
use super::{Aabb2d, BoundingCircle, IntersectsVolume};1use crate::{2ops::{self, FloatPow},3Dir2, Ray2d, Vec2,4};56#[cfg(feature = "bevy_reflect")]7use bevy_reflect::Reflect;89/// A raycast intersection test for 2D bounding volumes10#[derive(Clone, Debug)]11#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]12pub struct RayCast2d {13/// The ray for the test14pub ray: Ray2d,15/// The maximum distance for the ray16pub max: f32,17/// The multiplicative inverse direction of the ray18direction_recip: Vec2,19}2021impl RayCast2d {22/// Construct a [`RayCast2d`] from an origin, [`Dir2`], and max distance.23pub fn new(origin: Vec2, direction: Dir2, max: f32) -> Self {24Self::from_ray(Ray2d { origin, direction }, max)25}2627/// Construct a [`RayCast2d`] from a [`Ray2d`] and max distance.28pub fn from_ray(ray: Ray2d, max: f32) -> Self {29Self {30ray,31direction_recip: ray.direction.recip(),32max,33}34}3536/// Get the cached multiplicative inverse of the direction of the ray.37pub fn direction_recip(&self) -> Vec2 {38self.direction_recip39}4041/// Get the distance of an intersection with an [`Aabb2d`], if any.42pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option<f32> {43let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() {44(aabb.min.x, aabb.max.x)45} else {46(aabb.max.x, aabb.min.x)47};48let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() {49(aabb.min.y, aabb.max.y)50} else {51(aabb.max.y, aabb.min.y)52};5354// Calculate the minimum/maximum time for each axis based on how much the direction goes that55// way. These values can get arbitrarily large, or even become NaN, which is handled by the56// min/max operations below57let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x;58let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y;59let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x;60let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y;6162// An axis that is not relevant to the ray direction will be NaN. When one of the arguments63// to min/max is NaN, the other argument is used.64// An axis for which the direction is the wrong way will return an arbitrarily large65// negative value.66let tmin = tmin_x.max(tmin_y).max(0.);67let tmax = tmax_y.min(tmax_x).min(self.max);6869if tmin <= tmax {70Some(tmin)71} else {72None73}74}7576/// Get the distance of an intersection with a [`BoundingCircle`], if any.77pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option<f32> {78let offset = self.ray.origin - circle.center;79let projected = offset.dot(*self.ray.direction);80let cross = offset.perp_dot(*self.ray.direction);81let distance_squared = circle.radius().squared() - cross.squared();82if distance_squared < 0.83|| ops::copysign(projected.squared(), -projected) < -distance_squared84{85None86} else {87let toi = -projected - ops::sqrt(distance_squared);88if toi > self.max {89None90} else {91Some(toi.max(0.))92}93}94}95}9697impl IntersectsVolume<Aabb2d> for RayCast2d {98fn intersects(&self, volume: &Aabb2d) -> bool {99self.aabb_intersection_at(volume).is_some()100}101}102103impl IntersectsVolume<BoundingCircle> for RayCast2d {104fn intersects(&self, volume: &BoundingCircle) -> bool {105self.circle_intersection_at(volume).is_some()106}107}108109/// An intersection test that casts an [`Aabb2d`] along a ray.110#[derive(Clone, Debug)]111#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]112pub struct AabbCast2d {113/// The ray along which to cast the bounding volume114pub ray: RayCast2d,115/// The aabb that is being cast116pub aabb: Aabb2d,117}118119impl AabbCast2d {120/// Construct an [`AabbCast2d`] from an [`Aabb2d`], origin, [`Dir2`], and max distance.121pub fn new(aabb: Aabb2d, origin: Vec2, direction: Dir2, max: f32) -> Self {122Self::from_ray(aabb, Ray2d { origin, direction }, max)123}124125/// Construct an [`AabbCast2d`] from an [`Aabb2d`], [`Ray2d`], and max distance.126pub fn from_ray(aabb: Aabb2d, ray: Ray2d, max: f32) -> Self {127Self {128ray: RayCast2d::from_ray(ray, max),129aabb,130}131}132133/// Get the distance at which the [`Aabb2d`]s collide, if at all.134pub fn aabb_collision_at(&self, mut aabb: Aabb2d) -> Option<f32> {135aabb.min -= self.aabb.max;136aabb.max -= self.aabb.min;137self.ray.aabb_intersection_at(&aabb)138}139}140141impl IntersectsVolume<Aabb2d> for AabbCast2d {142fn intersects(&self, volume: &Aabb2d) -> bool {143self.aabb_collision_at(*volume).is_some()144}145}146147/// An intersection test that casts a [`BoundingCircle`] along a ray.148#[derive(Clone, Debug)]149#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]150pub struct BoundingCircleCast {151/// The ray along which to cast the bounding volume152pub ray: RayCast2d,153/// The circle that is being cast154pub circle: BoundingCircle,155}156157impl BoundingCircleCast {158/// Construct a [`BoundingCircleCast`] from a [`BoundingCircle`], origin, [`Dir2`], and max distance.159pub fn new(circle: BoundingCircle, origin: Vec2, direction: Dir2, max: f32) -> Self {160Self::from_ray(circle, Ray2d { origin, direction }, max)161}162163/// Construct a [`BoundingCircleCast`] from a [`BoundingCircle`], [`Ray2d`], and max distance.164pub fn from_ray(circle: BoundingCircle, ray: Ray2d, max: f32) -> Self {165Self {166ray: RayCast2d::from_ray(ray, max),167circle,168}169}170171/// Get the distance at which the [`BoundingCircle`]s collide, if at all.172pub fn circle_collision_at(&self, mut circle: BoundingCircle) -> Option<f32> {173circle.center -= self.circle.center;174circle.circle.radius += self.circle.radius();175self.ray.circle_intersection_at(&circle)176}177}178179impl IntersectsVolume<BoundingCircle> for BoundingCircleCast {180fn intersects(&self, volume: &BoundingCircle) -> bool {181self.circle_collision_at(*volume).is_some()182}183}184185#[cfg(test)]186mod tests {187use super::*;188189const EPSILON: f32 = 0.001;190191#[test]192fn test_ray_intersection_circle_hits() {193for (test, volume, expected_distance) in &[194(195// Hit the center of a centered bounding circle196RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.),197BoundingCircle::new(Vec2::ZERO, 1.),1984.,199),200(201// Hit the center of a centered bounding circle, but from the other side202RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.),203BoundingCircle::new(Vec2::ZERO, 1.),2044.,205),206(207// Hit the center of an offset circle208RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.),209BoundingCircle::new(Vec2::Y * 3., 2.),2101.,211),212(213// Just barely hit the circle before the max distance214RayCast2d::new(Vec2::X, Dir2::Y, 1.),215BoundingCircle::new(Vec2::ONE, 0.01),2160.99,217),218(219// Hit a circle off-center220RayCast2d::new(Vec2::X, Dir2::Y, 90.),221BoundingCircle::new(Vec2::Y * 5., 2.),2223.268,223),224(225// Barely hit a circle on the side226RayCast2d::new(Vec2::X * 0.99999, Dir2::Y, 90.),227BoundingCircle::new(Vec2::Y * 5., 1.),2284.996,229),230] {231assert!(232test.intersects(volume),233"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",234);235let actual_distance = test.circle_intersection_at(volume).unwrap();236assert!(237ops::abs(actual_distance - expected_distance) < EPSILON,238"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",239);240241let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);242assert!(243!inverted_ray.intersects(volume),244"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",245);246}247}248249#[test]250fn test_ray_intersection_circle_misses() {251for (test, volume) in &[252(253// The ray doesn't go in the right direction254RayCast2d::new(Vec2::ZERO, Dir2::X, 90.),255BoundingCircle::new(Vec2::Y * 2., 1.),256),257(258// Ray's alignment isn't enough to hit the circle259RayCast2d::new(Vec2::ZERO, Dir2::from_xy(1., 1.).unwrap(), 90.),260BoundingCircle::new(Vec2::Y * 2., 1.),261),262(263// The ray's maximum distance isn't high enough264RayCast2d::new(Vec2::ZERO, Dir2::Y, 0.5),265BoundingCircle::new(Vec2::Y * 2., 1.),266),267] {268assert!(269!test.intersects(volume),270"Case:\n Test: {test:?}\n Volume: {volume:?}",271);272}273}274275#[test]276fn test_ray_intersection_circle_inside() {277let volume = BoundingCircle::new(Vec2::splat(0.5), 1.);278for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {279for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] {280for max in &[0., 1., 900.] {281let test = RayCast2d::new(*origin, *direction, *max);282283assert!(284test.intersects(&volume),285"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",286);287288let actual_distance = test.circle_intersection_at(&volume);289assert_eq!(290actual_distance,291Some(0.),292"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",293);294}295}296}297}298299#[test]300fn test_ray_intersection_aabb_hits() {301for (test, volume, expected_distance) in &[302(303// Hit the center of a centered aabb304RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.),305Aabb2d::new(Vec2::ZERO, Vec2::ONE),3064.,307),308(309// Hit the center of a centered aabb, but from the other side310RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.),311Aabb2d::new(Vec2::ZERO, Vec2::ONE),3124.,313),314(315// Hit the center of an offset aabb316RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.),317Aabb2d::new(Vec2::Y * 3., Vec2::splat(2.)),3181.,319),320(321// Just barely hit the aabb before the max distance322RayCast2d::new(Vec2::X, Dir2::Y, 1.),323Aabb2d::new(Vec2::ONE, Vec2::splat(0.01)),3240.99,325),326(327// Hit an aabb off-center328RayCast2d::new(Vec2::X, Dir2::Y, 90.),329Aabb2d::new(Vec2::Y * 5., Vec2::splat(2.)),3303.,331),332(333// Barely hit an aabb on corner334RayCast2d::new(Vec2::X * -0.001, Dir2::from_xy(1., 1.).unwrap(), 90.),335Aabb2d::new(Vec2::Y * 2., Vec2::ONE),3361.414,337),338] {339assert!(340test.intersects(volume),341"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",342);343let actual_distance = test.aabb_intersection_at(volume).unwrap();344assert!(345ops::abs(actual_distance - expected_distance) < EPSILON,346"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",347);348349let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);350assert!(351!inverted_ray.intersects(volume),352"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",353);354}355}356357#[test]358fn test_ray_intersection_aabb_misses() {359for (test, volume) in &[360(361// The ray doesn't go in the right direction362RayCast2d::new(Vec2::ZERO, Dir2::X, 90.),363Aabb2d::new(Vec2::Y * 2., Vec2::ONE),364),365(366// Ray's alignment isn't enough to hit the aabb367RayCast2d::new(Vec2::ZERO, Dir2::from_xy(1., 0.99).unwrap(), 90.),368Aabb2d::new(Vec2::Y * 2., Vec2::ONE),369),370(371// The ray's maximum distance isn't high enough372RayCast2d::new(Vec2::ZERO, Dir2::Y, 0.5),373Aabb2d::new(Vec2::Y * 2., Vec2::ONE),374),375] {376assert!(377!test.intersects(volume),378"Case:\n Test: {test:?}\n Volume: {volume:?}",379);380}381}382383#[test]384fn test_ray_intersection_aabb_inside() {385let volume = Aabb2d::new(Vec2::splat(0.5), Vec2::ONE);386for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {387for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] {388for max in &[0., 1., 900.] {389let test = RayCast2d::new(*origin, *direction, *max);390391assert!(392test.intersects(&volume),393"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",394);395396let actual_distance = test.aabb_intersection_at(&volume);397assert_eq!(398actual_distance,399Some(0.),400"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",401);402}403}404}405}406407#[test]408fn test_aabb_cast_hits() {409for (test, volume, expected_distance) in &[410(411// Hit the center of the aabb, that a ray would've also hit412AabbCast2d::new(Aabb2d::new(Vec2::ZERO, Vec2::ONE), Vec2::ZERO, Dir2::Y, 90.),413Aabb2d::new(Vec2::Y * 5., Vec2::ONE),4143.,415),416(417// Hit the center of the aabb, but from the other side418AabbCast2d::new(419Aabb2d::new(Vec2::ZERO, Vec2::ONE),420Vec2::Y * 10.,421-Dir2::Y,42290.,423),424Aabb2d::new(Vec2::Y * 5., Vec2::ONE),4253.,426),427(428// Hit the edge of the aabb, that a ray would've missed429AabbCast2d::new(430Aabb2d::new(Vec2::ZERO, Vec2::ONE),431Vec2::X * 1.5,432Dir2::Y,43390.,434),435Aabb2d::new(Vec2::Y * 5., Vec2::ONE),4363.,437),438(439// Hit the edge of the aabb, by casting an off-center AABB440AabbCast2d::new(441Aabb2d::new(Vec2::X * -2., Vec2::ONE),442Vec2::X * 3.,443Dir2::Y,44490.,445),446Aabb2d::new(Vec2::Y * 5., Vec2::ONE),4473.,448),449] {450assert!(451test.intersects(volume),452"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",453);454let actual_distance = test.aabb_collision_at(*volume).unwrap();455assert!(456ops::abs(actual_distance - expected_distance) < EPSILON,457"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",458);459460let inverted_ray =461RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);462assert!(463!inverted_ray.intersects(volume),464"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",465);466}467}468469#[test]470fn test_circle_cast_hits() {471for (test, volume, expected_distance) in &[472(473// Hit the center of the bounding circle, that a ray would've also hit474BoundingCircleCast::new(475BoundingCircle::new(Vec2::ZERO, 1.),476Vec2::ZERO,477Dir2::Y,47890.,479),480BoundingCircle::new(Vec2::Y * 5., 1.),4813.,482),483(484// Hit the center of the bounding circle, but from the other side485BoundingCircleCast::new(486BoundingCircle::new(Vec2::ZERO, 1.),487Vec2::Y * 10.,488-Dir2::Y,48990.,490),491BoundingCircle::new(Vec2::Y * 5., 1.),4923.,493),494(495// Hit the bounding circle off-center, that a ray would've missed496BoundingCircleCast::new(497BoundingCircle::new(Vec2::ZERO, 1.),498Vec2::X * 1.5,499Dir2::Y,50090.,501),502BoundingCircle::new(Vec2::Y * 5., 1.),5033.677,504),505(506// Hit the bounding circle off-center, by casting a circle that is off-center507BoundingCircleCast::new(508BoundingCircle::new(Vec2::X * -1.5, 1.),509Vec2::X * 3.,510Dir2::Y,51190.,512),513BoundingCircle::new(Vec2::Y * 5., 1.),5143.677,515),516] {517assert!(518test.intersects(volume),519"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",520);521let actual_distance = test.circle_collision_at(*volume).unwrap();522assert!(523ops::abs(actual_distance - expected_distance) < EPSILON,524"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",525);526527let inverted_ray =528RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);529assert!(530!inverted_ray.intersects(volume),531"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",532);533}534}535}536537538