diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index e8c44094a9..237dda02c2 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -29,7 +29,7 @@ pub trait Bounded2d { /// A 2D axis-aligned bounding box, or bounding rectangle #[doc(alias = "BoundingRectangle")] -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct Aabb2d { /// The minimum, conventionally bottom-left, point of the box pub min: Vec2, @@ -328,7 +328,7 @@ mod aabb2d_tests { use crate::primitives::Circle; /// A bounding circle -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct BoundingCircle { /// The center of the bounding circle pub center: Vec2, @@ -425,10 +425,10 @@ impl BoundingVolume for BoundingCircle { let diff = other.center - self.center; let length = diff.length(); if self.radius() >= length + other.radius() { - return self.clone(); + return *self; } if other.radius() >= length + self.radius() { - return other.clone(); + return *other; } let dir = diff / length; Self::new( diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index bc2f9390a4..fff2b900de 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -24,7 +24,7 @@ pub trait Bounded3d { } /// A 3D axis-aligned bounding box -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct Aabb3d { /// The minimum point of the box pub min: Vec3, @@ -324,7 +324,7 @@ mod aabb3d_tests { use crate::primitives::Sphere; /// A bounding sphere -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct BoundingSphere { /// The center of the bounding sphere pub center: Vec3, @@ -417,10 +417,10 @@ impl BoundingVolume for BoundingSphere { let diff = other.center - self.center; let length = diff.length(); if self.radius() >= length + other.radius() { - return self.clone(); + return *self; } if other.radius() >= length + self.radius() { - return other.clone(); + return *other; } let dir = diff / length; Self::new( diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index 8c1780a4ef..cb0a7ea676 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -13,7 +13,7 @@ pub struct RayTest2d { } impl RayTest2d { - /// Construct a [`RayTest2d`] from an origin, [`Direction2d`] and max distance. + /// Construct a [`RayTest2d`] from an origin, [`Direction2d`], and max distance. pub fn new(origin: Vec2, direction: Direction2d, max: f32) -> Self { Self::from_ray(Ray2d { origin, direction }, max) } @@ -98,6 +98,80 @@ impl IntersectsVolume for RayTest2d { } } +/// An intersection test that casts an [`Aabb2d`] along a ray. +#[derive(Debug)] +pub struct AabbCast2d { + /// The ray along which to cast the bounding volume + pub ray: RayTest2d, + /// The aabb that is being cast + pub aabb: Aabb2d, +} + +impl AabbCast2d { + /// Construct an [`AabbCast2d`] from an [`Aabb2d`], origin, [`Direction2d`], and max distance. + pub fn new(aabb: Aabb2d, origin: Vec2, direction: Direction2d, max: f32) -> Self { + Self::from_ray(aabb, Ray2d { origin, direction }, max) + } + + /// Construct an [`AabbCast2d`] from an [`Aabb2d`], [`Ray2d`], and max distance. + pub fn from_ray(aabb: Aabb2d, ray: Ray2d, max: f32) -> Self { + Self { + ray: RayTest2d::from_ray(ray, max), + aabb, + } + } + + /// Get the distance at which the [`Aabb2d`]s collide, if at all. + pub fn aabb_collision_at(&self, mut aabb: Aabb2d) -> Option { + aabb.min -= self.aabb.max; + aabb.max -= self.aabb.min; + self.ray.aabb_intersection_at(&aabb) + } +} + +impl IntersectsVolume for AabbCast2d { + fn intersects(&self, volume: &Aabb2d) -> bool { + self.aabb_collision_at(*volume).is_some() + } +} + +/// An intersection test that casts a [`BoundingCircle`] along a ray. +#[derive(Debug)] +pub struct BoundingCircleCast { + /// The ray along which to cast the bounding volume + pub ray: RayTest2d, + /// The circle that is being cast + pub circle: BoundingCircle, +} + +impl BoundingCircleCast { + /// Construct a [`BoundingCircleCast`] from a [`BoundingCircle`], origin, [`Direction2d`], and max distance. + pub fn new(circle: BoundingCircle, origin: Vec2, direction: Direction2d, max: f32) -> Self { + Self::from_ray(circle, Ray2d { origin, direction }, max) + } + + /// Construct a [`BoundingCircleCast`] from a [`BoundingCircle`], [`Ray2d`], and max distance. + pub fn from_ray(circle: BoundingCircle, ray: Ray2d, max: f32) -> Self { + Self { + ray: RayTest2d::from_ray(ray, max), + circle, + } + } + + /// Get the distance at which the [`BoundingCircle`]s collide, if at all. + pub fn circle_collision_at(&self, mut circle: BoundingCircle) -> Option { + circle.center -= self.circle.center; + circle.circle.radius += self.circle.radius(); + self.ray.circle_intersection_at(&circle) + } +} + +impl IntersectsVolume for BoundingCircleCast { + fn intersects(&self, volume: &BoundingCircle) -> bool { + self.circle_collision_at(*volume).is_some() + } +} + #[cfg(test)] mod tests { use super::*; @@ -327,4 +401,138 @@ mod tests { } } } + + #[test] + fn test_aabb_cast_hits() { + for (test, volume, expected_distance) in &[ + ( + // Hit the center of the aabb, that a ray would've also hit + AabbCast2d::new( + Aabb2d::new(Vec2::ZERO, Vec2::ONE), + Vec2::ZERO, + Direction2d::Y, + 90., + ), + Aabb2d::new(Vec2::Y * 5., Vec2::ONE), + 3., + ), + ( + // Hit the center of the aabb, but from the other side + AabbCast2d::new( + Aabb2d::new(Vec2::ZERO, Vec2::ONE), + Vec2::Y * 10., + -Direction2d::Y, + 90., + ), + Aabb2d::new(Vec2::Y * 5., Vec2::ONE), + 3., + ), + ( + // Hit the edge of the aabb, that a ray would've missed + AabbCast2d::new( + Aabb2d::new(Vec2::ZERO, Vec2::ONE), + Vec2::X * 1.5, + Direction2d::Y, + 90., + ), + Aabb2d::new(Vec2::Y * 5., Vec2::ONE), + 3., + ), + ( + // Hit the edge of the aabb, by casting an off-center AABB + AabbCast2d::new( + Aabb2d::new(Vec2::X * -2., Vec2::ONE), + Vec2::X * 3., + Direction2d::Y, + 90., + ), + Aabb2d::new(Vec2::Y * 5., Vec2::ONE), + 3., + ), + ] { + let case = format!( + "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", + test, volume, expected_distance + ); + assert!(test.intersects(volume), "{}", case); + let actual_distance = test.aabb_collision_at(*volume).unwrap(); + assert!( + (actual_distance - expected_distance).abs() < EPSILON, + "{}\n Actual distance: {}", + case, + actual_distance + ); + + let inverted_ray = + RayTest2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); + assert!(!inverted_ray.intersects(volume), "{}", case); + } + } + + #[test] + fn test_circle_cast_hits() { + for (test, volume, expected_distance) in &[ + ( + // Hit the center of the bounding circle, that a ray would've also hit + BoundingCircleCast::new( + BoundingCircle::new(Vec2::ZERO, 1.), + Vec2::ZERO, + Direction2d::Y, + 90., + ), + BoundingCircle::new(Vec2::Y * 5., 1.), + 3., + ), + ( + // Hit the center of the bounding circle, but from the other side + BoundingCircleCast::new( + BoundingCircle::new(Vec2::ZERO, 1.), + Vec2::Y * 10., + -Direction2d::Y, + 90., + ), + BoundingCircle::new(Vec2::Y * 5., 1.), + 3., + ), + ( + // Hit the bounding circle off-center, that a ray would've missed + BoundingCircleCast::new( + BoundingCircle::new(Vec2::ZERO, 1.), + Vec2::X * 1.5, + Direction2d::Y, + 90., + ), + BoundingCircle::new(Vec2::Y * 5., 1.), + 3.677, + ), + ( + // Hit the bounding circle off-center, by casting a circle that is off-center + BoundingCircleCast::new( + BoundingCircle::new(Vec2::X * -1.5, 1.), + Vec2::X * 3., + Direction2d::Y, + 90., + ), + BoundingCircle::new(Vec2::Y * 5., 1.), + 3.677, + ), + ] { + let case = format!( + "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", + test, volume, expected_distance + ); + assert!(test.intersects(volume), "{}", case); + let actual_distance = test.circle_collision_at(*volume).unwrap(); + assert!( + (actual_distance - expected_distance).abs() < EPSILON, + "{}\n Actual distance: {}", + case, + actual_distance + ); + + let inverted_ray = + RayTest2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); + assert!(!inverted_ray.intersects(volume), "{}", case); + } + } } diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index 19ce62f00c..726dbf34f2 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -13,7 +13,7 @@ pub struct RayTest3d { } impl RayTest3d { - /// Construct a [`RayTest3d`] from an origin, [`Direction3d`] and max distance. + /// Construct a [`RayTest3d`] from an origin, [`Direction3d`], and max distance. pub fn new(origin: Vec3, direction: Direction3d, max: f32) -> Self { Self::from_ray(Ray3d { origin, direction }, max) } @@ -105,6 +105,80 @@ impl IntersectsVolume for RayTest3d { } } +/// An intersection test that casts an [`Aabb3d`] along a ray. +#[derive(Debug)] +pub struct AabbCast3d { + /// The ray along which to cast the bounding volume + pub ray: RayTest3d, + /// The aabb that is being cast + pub aabb: Aabb3d, +} + +impl AabbCast3d { + /// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [`Direction3d`], and max distance. + pub fn new(aabb: Aabb3d, origin: Vec3, direction: Direction3d, max: f32) -> Self { + Self::from_ray(aabb, Ray3d { origin, direction }, max) + } + + /// Construct an [`AabbCast3d`] from an [`Aabb3d`], [`Ray3d`], and max distance. + pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self { + Self { + ray: RayTest3d::from_ray(ray, max), + aabb, + } + } + + /// Get the distance at which the [`Aabb3d`]s collide, if at all. + pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option { + aabb.min -= self.aabb.max; + aabb.max -= self.aabb.min; + self.ray.aabb_intersection_at(&aabb) + } +} + +impl IntersectsVolume for AabbCast3d { + fn intersects(&self, volume: &Aabb3d) -> bool { + self.aabb_collision_at(*volume).is_some() + } +} + +/// An intersection test that casts a [`BoundingSphere`] along a ray. +#[derive(Debug)] +pub struct BoundingSphereCast { + /// The ray along which to cast the bounding volume + pub ray: RayTest3d, + /// The sphere that is being cast + pub sphere: BoundingSphere, +} + +impl BoundingSphereCast { + /// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [`Direction3d`], and max distance. + pub fn new(sphere: BoundingSphere, origin: Vec3, direction: Direction3d, max: f32) -> Self { + Self::from_ray(sphere, Ray3d { origin, direction }, max) + } + + /// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], [`Ray3d`], and max distance. + pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self { + Self { + ray: RayTest3d::from_ray(ray, max), + sphere, + } + } + + /// Get the distance at which the [`BoundingSphere`]s collide, if at all. + pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option { + sphere.center -= self.sphere.center; + sphere.sphere.radius += self.sphere.radius(); + self.ray.sphere_intersection_at(&sphere) + } +} + +impl IntersectsVolume for BoundingSphereCast { + fn intersects(&self, volume: &BoundingSphere) -> bool { + self.sphere_collision_at(*volume).is_some() + } +} + #[cfg(test)] mod tests { use super::*; @@ -346,4 +420,138 @@ mod tests { } } } + + #[test] + fn test_aabb_cast_hits() { + for (test, volume, expected_distance) in &[ + ( + // Hit the center of the aabb, that a ray would've also hit + AabbCast3d::new( + Aabb3d::new(Vec3::ZERO, Vec3::ONE), + Vec3::ZERO, + Direction3d::Y, + 90., + ), + Aabb3d::new(Vec3::Y * 5., Vec3::ONE), + 3., + ), + ( + // Hit the center of the aabb, but from the other side + AabbCast3d::new( + Aabb3d::new(Vec3::ZERO, Vec3::ONE), + Vec3::Y * 10., + -Direction3d::Y, + 90., + ), + Aabb3d::new(Vec3::Y * 5., Vec3::ONE), + 3., + ), + ( + // Hit the edge of the aabb, that a ray would've missed + AabbCast3d::new( + Aabb3d::new(Vec3::ZERO, Vec3::ONE), + Vec3::X * 1.5, + Direction3d::Y, + 90., + ), + Aabb3d::new(Vec3::Y * 5., Vec3::ONE), + 3., + ), + ( + // Hit the edge of the aabb, by casting an off-center AABB + AabbCast3d::new( + Aabb3d::new(Vec3::X * -2., Vec3::ONE), + Vec3::X * 3., + Direction3d::Y, + 90., + ), + Aabb3d::new(Vec3::Y * 5., Vec3::ONE), + 3., + ), + ] { + let case = format!( + "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", + test, volume, expected_distance + ); + assert!(test.intersects(volume), "{}", case); + let actual_distance = test.aabb_collision_at(*volume).unwrap(); + assert!( + (actual_distance - expected_distance).abs() < EPSILON, + "{}\n Actual distance: {}", + case, + actual_distance + ); + + let inverted_ray = + RayTest3d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); + assert!(!inverted_ray.intersects(volume), "{}", case); + } + } + + #[test] + fn test_sphere_cast_hits() { + for (test, volume, expected_distance) in &[ + ( + // Hit the center of the bounding sphere, that a ray would've also hit + BoundingSphereCast::new( + BoundingSphere::new(Vec3::ZERO, 1.), + Vec3::ZERO, + Direction3d::Y, + 90., + ), + BoundingSphere::new(Vec3::Y * 5., 1.), + 3., + ), + ( + // Hit the center of the bounding sphere, but from the other side + BoundingSphereCast::new( + BoundingSphere::new(Vec3::ZERO, 1.), + Vec3::Y * 10., + -Direction3d::Y, + 90., + ), + BoundingSphere::new(Vec3::Y * 5., 1.), + 3., + ), + ( + // Hit the bounding sphere off-center, that a ray would've missed + BoundingSphereCast::new( + BoundingSphere::new(Vec3::ZERO, 1.), + Vec3::X * 1.5, + Direction3d::Y, + 90., + ), + BoundingSphere::new(Vec3::Y * 5., 1.), + 3.677, + ), + ( + // Hit the bounding sphere off-center, by casting a sphere that is off-center + BoundingSphereCast::new( + BoundingSphere::new(Vec3::X * -1.5, 1.), + Vec3::X * 3., + Direction3d::Y, + 90., + ), + BoundingSphere::new(Vec3::Y * 5., 1.), + 3.677, + ), + ] { + let case = format!( + "Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}", + test, volume, expected_distance + ); + assert!(test.intersects(volume), "{}", case); + let actual_distance = test.sphere_collision_at(*volume).unwrap(); + assert!( + (actual_distance - expected_distance).abs() < EPSILON, + "{}\n Actual distance: {}", + case, + actual_distance + ); + + let inverted_ray = + RayTest3d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max); + assert!(!inverted_ray.intersects(volume), "{}", case); + } + } }