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:
NiseVoid 2024-01-31 21:14:15 +01:00 committed by GitHub
parent 76d32c9d5a
commit 1b98de68fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 426 additions and 10 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}