mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +00:00
Add RayTest2d and RayTest3d (#11310)
# Objective Implement a raycast intersection test for bounding volumes ## Solution - Implement RayTest2d and RayTest3d types --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: IQuick 143 <IQuick143cz@gmail.com>
This commit is contained in:
parent
9223201d54
commit
8851532890
3 changed files with 684 additions and 0 deletions
|
@ -59,3 +59,8 @@ mod bounded2d;
|
|||
pub use bounded2d::*;
|
||||
mod bounded3d;
|
||||
pub use bounded3d::*;
|
||||
|
||||
mod raytest2d;
|
||||
pub use raytest2d::*;
|
||||
mod raytest3d;
|
||||
pub use raytest3d::*;
|
||||
|
|
330
crates/bevy_math/src/bounding/raytest2d.rs
Normal file
330
crates/bevy_math/src/bounding/raytest2d.rs
Normal file
|
@ -0,0 +1,330 @@
|
|||
use super::{Aabb2d, BoundingCircle, IntersectsVolume};
|
||||
use crate::{primitives::Direction2d, Ray2d, Vec2};
|
||||
|
||||
/// A raycast intersection test for 2D bounding volumes
|
||||
#[derive(Debug)]
|
||||
pub struct RayTest2d {
|
||||
/// The ray for the test
|
||||
pub ray: Ray2d,
|
||||
/// The maximum distance for the ray
|
||||
pub max: f32,
|
||||
/// The multiplicative inverse direction of the ray
|
||||
direction_recip: Vec2,
|
||||
}
|
||||
|
||||
impl RayTest2d {
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Construct a [`RayTest2d`] from a [`Ray2d`] and max distance.
|
||||
pub fn from_ray(ray: Ray2d, max: f32) -> Self {
|
||||
Self {
|
||||
ray,
|
||||
direction_recip: ray.direction.recip(),
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cached multiplicative inverse of the direction of the ray.
|
||||
pub fn direction_recip(&self) -> Vec2 {
|
||||
self.direction_recip
|
||||
}
|
||||
|
||||
/// Get the distance of an intersection with an [`Aabb2d`], if any.
|
||||
pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option<f32> {
|
||||
let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() {
|
||||
(aabb.min.x, aabb.max.x)
|
||||
} else {
|
||||
(aabb.max.x, aabb.min.x)
|
||||
};
|
||||
let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() {
|
||||
(aabb.min.y, aabb.max.y)
|
||||
} else {
|
||||
(aabb.max.y, aabb.min.y)
|
||||
};
|
||||
|
||||
// Calculate the minimum/maximum time for each axis based on how much the direction goes that
|
||||
// way. These values can get arbitrarily large, or even become NaN, which is handled by the
|
||||
// min/max operations below
|
||||
let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x;
|
||||
let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y;
|
||||
let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x;
|
||||
let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y;
|
||||
|
||||
// An axis that is not relevant to the ray direction will be NaN. When one of the arguments
|
||||
// to min/max is NaN, the other argument is used.
|
||||
// An axis for which the direction is the wrong way will return an arbitrarily large
|
||||
// negative value.
|
||||
let tmin = tmin_x.max(tmin_y).max(0.);
|
||||
let tmax = tmax_y.min(tmax_x).min(self.max);
|
||||
|
||||
if tmin <= tmax {
|
||||
Some(tmin)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the distance of an intersection with a [`BoundingCircle`], if any.
|
||||
pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option<f32> {
|
||||
let offset = self.ray.origin - circle.center;
|
||||
let projected = offset.dot(*self.ray.direction);
|
||||
let closest_point = offset - projected * *self.ray.direction;
|
||||
let distance_squared = circle.radius().powi(2) - closest_point.length_squared();
|
||||
if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared {
|
||||
None
|
||||
} else {
|
||||
let toi = -projected - distance_squared.sqrt();
|
||||
if toi > self.max {
|
||||
None
|
||||
} else {
|
||||
Some(toi.max(0.))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<Aabb2d> for RayTest2d {
|
||||
fn intersects(&self, volume: &Aabb2d) -> bool {
|
||||
self.aabb_intersection_at(volume).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<BoundingCircle> for RayTest2d {
|
||||
fn intersects(&self, volume: &BoundingCircle) -> bool {
|
||||
self.circle_intersection_at(volume).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const EPSILON: f32 = 0.001;
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_circle_hits() {
|
||||
for (test, volume, expected_distance) in &[
|
||||
(
|
||||
// Hit the center of a centered bounding circle
|
||||
RayTest2d::new(Vec2::Y * -5., Direction2d::Y, 90.),
|
||||
BoundingCircle::new(Vec2::ZERO, 1.),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of a centered bounding circle, but from the other side
|
||||
RayTest2d::new(Vec2::Y * 5., -Direction2d::Y, 90.),
|
||||
BoundingCircle::new(Vec2::ZERO, 1.),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of an offset circle
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 90.),
|
||||
BoundingCircle::new(Vec2::Y * 3., 2.),
|
||||
1.,
|
||||
),
|
||||
(
|
||||
// Just barely hit the circle before the max distance
|
||||
RayTest2d::new(Vec2::X, Direction2d::Y, 1.),
|
||||
BoundingCircle::new(Vec2::ONE, 0.01),
|
||||
0.99,
|
||||
),
|
||||
(
|
||||
// Hit a circle off-center
|
||||
RayTest2d::new(Vec2::X, Direction2d::Y, 90.),
|
||||
BoundingCircle::new(Vec2::Y * 5., 2.),
|
||||
3.268,
|
||||
),
|
||||
(
|
||||
// Barely hit a circle on the side
|
||||
RayTest2d::new(Vec2::X * 0.99999, Direction2d::Y, 90.),
|
||||
BoundingCircle::new(Vec2::Y * 5., 1.),
|
||||
4.996,
|
||||
),
|
||||
] {
|
||||
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_intersection_at(volume).unwrap();
|
||||
assert!(
|
||||
(actual_distance - expected_distance).abs() < EPSILON,
|
||||
"{}\n Actual distance: {}",
|
||||
case,
|
||||
actual_distance
|
||||
);
|
||||
|
||||
let inverted_ray = RayTest2d::new(test.ray.origin, -test.ray.direction, test.max);
|
||||
assert!(!inverted_ray.intersects(volume), "{}", case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_circle_misses() {
|
||||
for (test, volume) in &[
|
||||
(
|
||||
// The ray doesn't go in the right direction
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::X, 90.),
|
||||
BoundingCircle::new(Vec2::Y * 2., 1.),
|
||||
),
|
||||
(
|
||||
// Ray's alignment isn't enough to hit the circle
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::from_xy(1., 1.).unwrap(), 90.),
|
||||
BoundingCircle::new(Vec2::Y * 2., 1.),
|
||||
),
|
||||
(
|
||||
// The ray's maximum distance isn't high enough
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 0.5),
|
||||
BoundingCircle::new(Vec2::Y * 2., 1.),
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
!test.intersects(volume),
|
||||
"Case:\n Test: {:?}\n Volume: {:?}",
|
||||
test,
|
||||
volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_circle_inside() {
|
||||
let volume = BoundingCircle::new(Vec2::splat(0.5), 1.);
|
||||
for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
|
||||
for direction in &[
|
||||
Direction2d::X,
|
||||
Direction2d::Y,
|
||||
-Direction2d::X,
|
||||
-Direction2d::Y,
|
||||
] {
|
||||
for max in &[0., 1., 900.] {
|
||||
let test = RayTest2d::new(*origin, *direction, *max);
|
||||
|
||||
let case = format!(
|
||||
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
|
||||
origin, direction, max,
|
||||
);
|
||||
assert!(test.intersects(&volume), "{}", case);
|
||||
|
||||
let actual_distance = test.circle_intersection_at(&volume);
|
||||
assert_eq!(actual_distance, Some(0.), "{}", case);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_aabb_hits() {
|
||||
for (test, volume, expected_distance) in &[
|
||||
(
|
||||
// Hit the center of a centered aabb
|
||||
RayTest2d::new(Vec2::Y * -5., Direction2d::Y, 90.),
|
||||
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of a centered aabb, but from the other side
|
||||
RayTest2d::new(Vec2::Y * 5., -Direction2d::Y, 90.),
|
||||
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of an offset aabb
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 90.),
|
||||
Aabb2d::new(Vec2::Y * 3., Vec2::splat(2.)),
|
||||
1.,
|
||||
),
|
||||
(
|
||||
// Just barely hit the aabb before the max distance
|
||||
RayTest2d::new(Vec2::X, Direction2d::Y, 1.),
|
||||
Aabb2d::new(Vec2::ONE, Vec2::splat(0.01)),
|
||||
0.99,
|
||||
),
|
||||
(
|
||||
// Hit an aabb off-center
|
||||
RayTest2d::new(Vec2::X, Direction2d::Y, 90.),
|
||||
Aabb2d::new(Vec2::Y * 5., Vec2::splat(2.)),
|
||||
3.,
|
||||
),
|
||||
(
|
||||
// Barely hit an aabb on corner
|
||||
RayTest2d::new(Vec2::X * -0.001, Direction2d::from_xy(1., 1.).unwrap(), 90.),
|
||||
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
|
||||
1.414,
|
||||
),
|
||||
] {
|
||||
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_intersection_at(volume).unwrap();
|
||||
assert!(
|
||||
(actual_distance - expected_distance).abs() < EPSILON,
|
||||
"{}\n Actual distance: {}",
|
||||
case,
|
||||
actual_distance
|
||||
);
|
||||
|
||||
let inverted_ray = RayTest2d::new(test.ray.origin, -test.ray.direction, test.max);
|
||||
assert!(!inverted_ray.intersects(volume), "{}", case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_aabb_misses() {
|
||||
for (test, volume) in &[
|
||||
(
|
||||
// The ray doesn't go in the right direction
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::X, 90.),
|
||||
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
|
||||
),
|
||||
(
|
||||
// Ray's alignment isn't enough to hit the aabb
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::from_xy(1., 0.99).unwrap(), 90.),
|
||||
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
|
||||
),
|
||||
(
|
||||
// The ray's maximum distance isn't high enough
|
||||
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 0.5),
|
||||
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
!test.intersects(volume),
|
||||
"Case:\n Test: {:?}\n Volume: {:?}",
|
||||
test,
|
||||
volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_aabb_inside() {
|
||||
let volume = Aabb2d::new(Vec2::splat(0.5), Vec2::ONE);
|
||||
for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
|
||||
for direction in &[
|
||||
Direction2d::X,
|
||||
Direction2d::Y,
|
||||
-Direction2d::X,
|
||||
-Direction2d::Y,
|
||||
] {
|
||||
for max in &[0., 1., 900.] {
|
||||
let test = RayTest2d::new(*origin, *direction, *max);
|
||||
|
||||
let case = format!(
|
||||
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
|
||||
origin, direction, max,
|
||||
);
|
||||
assert!(test.intersects(&volume), "{}", case);
|
||||
|
||||
let actual_distance = test.aabb_intersection_at(&volume);
|
||||
assert_eq!(actual_distance, Some(0.), "{}", case,);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
349
crates/bevy_math/src/bounding/raytest3d.rs
Normal file
349
crates/bevy_math/src/bounding/raytest3d.rs
Normal file
|
@ -0,0 +1,349 @@
|
|||
use super::{Aabb3d, BoundingSphere, IntersectsVolume};
|
||||
use crate::{primitives::Direction3d, Ray3d, Vec3};
|
||||
|
||||
/// A raycast intersection test for 3D bounding volumes
|
||||
#[derive(Debug)]
|
||||
pub struct RayTest3d {
|
||||
/// The ray for the test
|
||||
pub ray: Ray3d,
|
||||
/// The maximum distance for the ray
|
||||
pub max: f32,
|
||||
/// The multiplicative inverse direction of the ray
|
||||
direction_recip: Vec3,
|
||||
}
|
||||
|
||||
impl RayTest3d {
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Construct a [`RayTest3d`] from a [`Ray3d`] and max distance.
|
||||
pub fn from_ray(ray: Ray3d, max: f32) -> Self {
|
||||
Self {
|
||||
ray,
|
||||
direction_recip: ray.direction.recip(),
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cached multiplicative inverse of the direction of the ray.
|
||||
pub fn direction_recip(&self) -> Vec3 {
|
||||
self.direction_recip
|
||||
}
|
||||
|
||||
/// Get the distance of an intersection with an [`Aabb3d`], if any.
|
||||
pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {
|
||||
let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() {
|
||||
(aabb.min.x, aabb.max.x)
|
||||
} else {
|
||||
(aabb.max.x, aabb.min.x)
|
||||
};
|
||||
let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() {
|
||||
(aabb.min.y, aabb.max.y)
|
||||
} else {
|
||||
(aabb.max.y, aabb.min.y)
|
||||
};
|
||||
let (min_z, max_z) = if self.ray.direction.z.is_sign_positive() {
|
||||
(aabb.min.z, aabb.max.z)
|
||||
} else {
|
||||
(aabb.max.z, aabb.min.z)
|
||||
};
|
||||
|
||||
// Calculate the minimum/maximum time for each axis based on how much the direction goes that
|
||||
// way. These values can get arbitrarily large, or even become NaN, which is handled by the
|
||||
// min/max operations below
|
||||
let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x;
|
||||
let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y;
|
||||
let tmin_z = (min_z - self.ray.origin.z) * self.direction_recip.z;
|
||||
let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x;
|
||||
let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y;
|
||||
let tmax_z = (max_z - self.ray.origin.z) * self.direction_recip.z;
|
||||
|
||||
// An axis that is not relevant to the ray direction will be NaN. When one of the arguments
|
||||
// to min/max is NaN, the other argument is used.
|
||||
// An axis for which the direction is the wrong way will return an arbitrarily large
|
||||
// negative value.
|
||||
let tmin = tmin_x.max(tmin_y).max(tmin_z).max(0.);
|
||||
let tmax = tmax_z.min(tmax_y).min(tmax_x).min(self.max);
|
||||
|
||||
if tmin <= tmax {
|
||||
Some(tmin)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the distance of an intersection with a [`BoundingSphere`], if any.
|
||||
pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
|
||||
let offset = self.ray.origin - sphere.center;
|
||||
let projected = offset.dot(*self.ray.direction);
|
||||
let closest_point = offset - projected * *self.ray.direction;
|
||||
let distance_squared = sphere.radius().powi(2) - closest_point.length_squared();
|
||||
if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared {
|
||||
None
|
||||
} else {
|
||||
let toi = -projected - distance_squared.sqrt();
|
||||
if toi > self.max {
|
||||
None
|
||||
} else {
|
||||
Some(toi.max(0.))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<Aabb3d> for RayTest3d {
|
||||
fn intersects(&self, volume: &Aabb3d) -> bool {
|
||||
self.aabb_intersection_at(volume).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntersectsVolume<BoundingSphere> for RayTest3d {
|
||||
fn intersects(&self, volume: &BoundingSphere) -> bool {
|
||||
self.sphere_intersection_at(volume).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const EPSILON: f32 = 0.001;
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_sphere_hits() {
|
||||
for (test, volume, expected_distance) in &[
|
||||
(
|
||||
// Hit the center of a centered bounding sphere
|
||||
RayTest3d::new(Vec3::Y * -5., Direction3d::Y, 90.),
|
||||
BoundingSphere::new(Vec3::ZERO, 1.),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of a centered bounding sphere, but from the other side
|
||||
RayTest3d::new(Vec3::Y * 5., -Direction3d::Y, 90.),
|
||||
BoundingSphere::new(Vec3::ZERO, 1.),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of an offset sphere
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::Y, 90.),
|
||||
BoundingSphere::new(Vec3::Y * 3., 2.),
|
||||
1.,
|
||||
),
|
||||
(
|
||||
// Just barely hit the sphere before the max distance
|
||||
RayTest3d::new(Vec3::X, Direction3d::Y, 1.),
|
||||
BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),
|
||||
0.99,
|
||||
),
|
||||
(
|
||||
// Hit a sphere off-center
|
||||
RayTest3d::new(Vec3::X, Direction3d::Y, 90.),
|
||||
BoundingSphere::new(Vec3::Y * 5., 2.),
|
||||
3.268,
|
||||
),
|
||||
(
|
||||
// Barely hit a sphere on the side
|
||||
RayTest3d::new(Vec3::X * 0.99999, Direction3d::Y, 90.),
|
||||
BoundingSphere::new(Vec3::Y * 5., 1.),
|
||||
4.996,
|
||||
),
|
||||
] {
|
||||
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_intersection_at(volume).unwrap();
|
||||
assert!(
|
||||
(actual_distance - expected_distance).abs() < EPSILON,
|
||||
"{}\n Actual distance: {}",
|
||||
case,
|
||||
actual_distance
|
||||
);
|
||||
|
||||
let inverted_ray = RayTest3d::new(test.ray.origin, -test.ray.direction, test.max);
|
||||
assert!(!inverted_ray.intersects(volume), "{}", case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_sphere_misses() {
|
||||
for (test, volume) in &[
|
||||
(
|
||||
// The ray doesn't go in the right direction
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::X, 90.),
|
||||
BoundingSphere::new(Vec3::Y * 2., 1.),
|
||||
),
|
||||
(
|
||||
// Ray's alignment isn't enough to hit the sphere
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::from_xyz(1., 1., 1.).unwrap(), 90.),
|
||||
BoundingSphere::new(Vec3::Y * 2., 1.),
|
||||
),
|
||||
(
|
||||
// The ray's maximum distance isn't high enough
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::Y, 0.5),
|
||||
BoundingSphere::new(Vec3::Y * 2., 1.),
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
!test.intersects(volume),
|
||||
"Case:\n Test: {:?}\n Volume: {:?}",
|
||||
test,
|
||||
volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_sphere_inside() {
|
||||
let volume = BoundingSphere::new(Vec3::splat(0.5), 1.);
|
||||
for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
|
||||
for direction in &[
|
||||
Direction3d::X,
|
||||
Direction3d::Y,
|
||||
Direction3d::Z,
|
||||
-Direction3d::X,
|
||||
-Direction3d::Y,
|
||||
-Direction3d::Z,
|
||||
] {
|
||||
for max in &[0., 1., 900.] {
|
||||
let test = RayTest3d::new(*origin, *direction, *max);
|
||||
|
||||
let case = format!(
|
||||
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
|
||||
origin, direction, max,
|
||||
);
|
||||
assert!(test.intersects(&volume), "{}", case);
|
||||
|
||||
let actual_distance = test.sphere_intersection_at(&volume);
|
||||
assert_eq!(actual_distance, Some(0.), "{}", case,);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_aabb_hits() {
|
||||
for (test, volume, expected_distance) in &[
|
||||
(
|
||||
// Hit the center of a centered aabb
|
||||
RayTest3d::new(Vec3::Y * -5., Direction3d::Y, 90.),
|
||||
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of a centered aabb, but from the other side
|
||||
RayTest3d::new(Vec3::Y * 5., -Direction3d::Y, 90.),
|
||||
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
|
||||
4.,
|
||||
),
|
||||
(
|
||||
// Hit the center of an offset aabb
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::Y, 90.),
|
||||
Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),
|
||||
1.,
|
||||
),
|
||||
(
|
||||
// Just barely hit the aabb before the max distance
|
||||
RayTest3d::new(Vec3::X, Direction3d::Y, 1.),
|
||||
Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),
|
||||
0.99,
|
||||
),
|
||||
(
|
||||
// Hit an aabb off-center
|
||||
RayTest3d::new(Vec3::X, Direction3d::Y, 90.),
|
||||
Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),
|
||||
3.,
|
||||
),
|
||||
(
|
||||
// Barely hit an aabb on corner
|
||||
RayTest3d::new(
|
||||
Vec3::X * -0.001,
|
||||
Direction3d::from_xyz(1., 1., 1.).unwrap(),
|
||||
90.,
|
||||
),
|
||||
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
|
||||
1.732,
|
||||
),
|
||||
] {
|
||||
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_intersection_at(volume).unwrap();
|
||||
assert!(
|
||||
(actual_distance - expected_distance).abs() < EPSILON,
|
||||
"{}\n Actual distance: {}",
|
||||
case,
|
||||
actual_distance
|
||||
);
|
||||
|
||||
let inverted_ray = RayTest3d::new(test.ray.origin, -test.ray.direction, test.max);
|
||||
assert!(!inverted_ray.intersects(volume), "{}", case);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_aabb_misses() {
|
||||
for (test, volume) in &[
|
||||
(
|
||||
// The ray doesn't go in the right direction
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::X, 90.),
|
||||
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
|
||||
),
|
||||
(
|
||||
// Ray's alignment isn't enough to hit the aabb
|
||||
RayTest3d::new(
|
||||
Vec3::ZERO,
|
||||
Direction3d::from_xyz(1., 0.99, 1.).unwrap(),
|
||||
90.,
|
||||
),
|
||||
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
|
||||
),
|
||||
(
|
||||
// The ray's maximum distance isn't high enough
|
||||
RayTest3d::new(Vec3::ZERO, Direction3d::Y, 0.5),
|
||||
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
!test.intersects(volume),
|
||||
"Case:\n Test: {:?}\n Volume: {:?}",
|
||||
test,
|
||||
volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ray_intersection_aabb_inside() {
|
||||
let volume = Aabb3d::new(Vec3::splat(0.5), Vec3::ONE);
|
||||
for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
|
||||
for direction in &[
|
||||
Direction3d::X,
|
||||
Direction3d::Y,
|
||||
Direction3d::Z,
|
||||
-Direction3d::X,
|
||||
-Direction3d::Y,
|
||||
-Direction3d::Z,
|
||||
] {
|
||||
for max in &[0., 1., 900.] {
|
||||
let test = RayTest3d::new(*origin, *direction, *max);
|
||||
|
||||
let case = format!(
|
||||
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
|
||||
origin, direction, max,
|
||||
);
|
||||
assert!(test.intersects(&volume), "{}", case);
|
||||
|
||||
let actual_distance = test.aabb_intersection_at(&volume);
|
||||
assert_eq!(actual_distance, Some(0.), "{}", case,);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue