use super::{Aabb3d, BoundingSphere, IntersectsVolume};
use crate::{
Dir3A, Ray3d, Vec3A,
ops::{self, FloatPow},
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct RayCast3d {
pub origin: Vec3A,
pub direction: Dir3A,
pub max: f32,
direction_recip: Vec3A,
}
impl RayCast3d {
pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Dir3A>, max: f32) -> Self {
let direction = direction.into();
Self {
origin: origin.into(),
direction,
direction_recip: direction.recip(),
max,
}
}
pub fn from_ray(ray: Ray3d, max: f32) -> Self {
Self::new(ray.origin, ray.direction, max)
}
pub fn direction_recip(&self) -> Vec3A {
self.direction_recip
}
pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {
let positive = self.direction.signum().cmpgt(Vec3A::ZERO);
let min = Vec3A::select(positive, aabb.min, aabb.max);
let max = Vec3A::select(positive, aabb.max, aabb.min);
let tmin = (min - self.origin) * self.direction_recip;
let tmax = (max - self.origin) * self.direction_recip;
let tmin = tmin.max_element().max(0.);
let tmax = tmax.min_element().min(self.max);
if tmin <= tmax { Some(tmin) } else { None }
}
pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
let offset = self.origin - sphere.center;
let projected = offset.dot(*self.direction);
let closest_point = offset - projected * *self.direction;
let distance_squared = sphere.radius().squared() - closest_point.length_squared();
if distance_squared < 0.
|| ops::copysign(projected.squared(), -projected) < -distance_squared
{
None
} else {
let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max {
None
} else {
Some(toi.max(0.))
}
}
}
}
impl IntersectsVolume<Aabb3d> for RayCast3d {
fn intersects(&self, volume: &Aabb3d) -> bool {
self.aabb_intersection_at(volume).is_some()
}
}
impl IntersectsVolume<BoundingSphere> for RayCast3d {
fn intersects(&self, volume: &BoundingSphere) -> bool {
self.sphere_intersection_at(volume).is_some()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct AabbCast3d {
pub ray: RayCast3d,
pub aabb: Aabb3d,
}
impl AabbCast3d {
pub fn new(
aabb: Aabb3d,
origin: impl Into<Vec3A>,
direction: impl Into<Dir3A>,
max: f32,
) -> Self {
Self {
ray: RayCast3d::new(origin, direction, max),
aabb,
}
}
pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self {
Self::new(aabb, ray.origin, ray.direction, max)
}
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()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct BoundingSphereCast {
pub ray: RayCast3d,
pub sphere: BoundingSphere,
}
impl BoundingSphereCast {
pub fn new(
sphere: BoundingSphere,
origin: impl Into<Vec3A>,
direction: impl Into<Dir3A>,
max: f32,
) -> Self {
Self {
ray: RayCast3d::new(origin, direction, max),
sphere,
}
}
pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self {
Self::new(sphere, ray.origin, ray.direction, max)
}
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::*;
use crate::{Dir3, Vec3};
const EPSILON: f32 = 0.001;
#[test]
fn test_ray_intersection_sphere_hits() {
for (test, volume, expected_distance) in &[
(
RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
BoundingSphere::new(Vec3::ZERO, 1.),
4.,
),
(
RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
BoundingSphere::new(Vec3::ZERO, 1.),
4.,
),
(
RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
BoundingSphere::new(Vec3::Y * 3., 2.),
1.,
),
(
RayCast3d::new(Vec3::X, Dir3::Y, 1.),
BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),
0.99,
),
(
RayCast3d::new(Vec3::X, Dir3::Y, 90.),
BoundingSphere::new(Vec3::Y * 5., 2.),
3.268,
),
(
RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.),
BoundingSphere::new(Vec3::Y * 5., 1.),
4.996,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.sphere_intersection_at(volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_ray_intersection_sphere_misses() {
for (test, volume) in &[
(
RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
BoundingSphere::new(Vec3::Y * 2., 1.),
),
(
RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
BoundingSphere::new(Vec3::Y * 2., 1.),
),
(
RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
BoundingSphere::new(Vec3::Y * 2., 1.),
),
] {
assert!(
!test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {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 &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max);
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
let actual_distance = test.sphere_intersection_at(&volume);
assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
}
}
}
}
#[test]
fn test_ray_intersection_aabb_hits() {
for (test, volume, expected_distance) in &[
(
RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
4.,
),
(
RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
4.,
),
(
RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),
1.,
),
(
RayCast3d::new(Vec3::X, Dir3::Y, 1.),
Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),
0.99,
),
(
RayCast3d::new(Vec3::X, Dir3::Y, 90.),
Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),
3.,
),
(
RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
1.732,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.aabb_intersection_at(volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_ray_intersection_aabb_misses() {
for (test, volume) in &[
(
RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
),
(
RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 0.99, 1.).unwrap(), 90.),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
),
(
RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
),
] {
assert!(
!test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {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 &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max);
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
let actual_distance = test.aabb_intersection_at(&volume);
assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
}
}
}
}
#[test]
fn test_aabb_cast_hits() {
for (test, volume, expected_distance) in &[
(
AabbCast3d::new(Aabb3d::new(Vec3::ZERO, Vec3::ONE), Vec3::ZERO, Dir3::Y, 90.),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
(
AabbCast3d::new(
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
Vec3::Y * 10.,
-Dir3::Y,
90.,
),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
(
AabbCast3d::new(
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
Vec3::X * 1.5,
Dir3::Y,
90.,
),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
(
AabbCast3d::new(
Aabb3d::new(Vec3::X * -2., Vec3::ONE),
Vec3::X * 3.,
Dir3::Y,
90.,
),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.aabb_collision_at(*volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_sphere_cast_hits() {
for (test, volume, expected_distance) in &[
(
BoundingSphereCast::new(
BoundingSphere::new(Vec3::ZERO, 1.),
Vec3::ZERO,
Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.,
),
(
BoundingSphereCast::new(
BoundingSphere::new(Vec3::ZERO, 1.),
Vec3::Y * 10.,
-Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.,
),
(
BoundingSphereCast::new(
BoundingSphere::new(Vec3::ZERO, 1.),
Vec3::X * 1.5,
Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.677,
),
(
BoundingSphereCast::new(
BoundingSphere::new(Vec3::X * -1.5, 1.),
Vec3::X * 3.,
Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.677,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.sphere_collision_at(*volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
}