Add volume cast intersection tests (#11586)
# Objective - Add a basic form of shapecasting for bounding volumes ## Solution - Implement AabbCast2d, AabbCast3d, BoundingCircleCast, and BoundingSphereCast - These are really just raycasts, but they modify the volumes the ray is casting against - The tests are slightly simpler, since they just use the raycast code for the heavy lifting
This commit is contained in:
parent
76d32c9d5a
commit
1b98de68fe
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<BoundingCircle> 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<f32> {
|
||||
aabb.min -= self.aabb.max;
|
||||
aabb.max -= self.aabb.min;
|
||||
self.ray.aabb_intersection_at(&aabb)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<Aabb2d> 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<f32> {
|
||||
circle.center -= self.circle.center;
|
||||
circle.circle.radius += self.circle.radius();
|
||||
self.ray.circle_intersection_at(&circle)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<BoundingCircle> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<BoundingSphere> 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<f32> {
|
||||
aabb.min -= self.aabb.max;
|
||||
aabb.max -= self.aabb.min;
|
||||
self.ray.aabb_intersection_at(&aabb)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<Aabb3d> 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<f32> {
|
||||
sphere.center -= self.sphere.center;
|
||||
sphere.sphere.radius += self.sphere.radius();
|
||||
self.ray.sphere_intersection_at(&sphere)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<BoundingSphere> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user