Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RayTest2d and RayTest3d #11310

Merged
merged 13 commits into from
Jan 28, 2024
5 changes: 5 additions & 0 deletions crates/bevy_math/src/bounding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ mod bounded2d;
pub use bounded2d::*;
mod bounded3d;
pub use bounded3d::*;

mod raytest2d;
pub use raytest2d::*;
mod raytest3d;
pub use raytest3d::*;
98 changes: 98 additions & 0 deletions crates/bevy_math/src/bounding/raytest2d.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use super::{Aabb2d, BoundingCircle, IntersectsVolume};
use crate::{primitives::Direction2d, Ray2d, Vec2};

/// A raycast intersection test for 2D bounding volumes
pub struct RayTest2d {
/// The ray for the test
pub ray: Ray2d,
/// The maximum time of impact of the ray
pub max: f32,
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// The multiplicative inverse direction of the ray
dir_recip: Vec2,
}

impl RayTest2d {
/// Construct a [`RayTest2d`] from an origin, [`Direction2d`] and max time of impact.
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 time of impact.
pub fn from_ray(ray: Ray2d, max: f32) -> Self {
Self {
ray,
dir_recip: ray.direction.recip(),
max,
}
}

/// Get the cached multiplicate inverse of the direction of the ray
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn dir_recip(&self) -> Vec2 {
self.dir_recip
}
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved

/// Get the time of impact for 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 based on how much the direction goes that
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
// way. These values van get arbitrarily large, or even become NaN, which is handled by the
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
// min/max operations below
let tmin_x = (min_x - self.ray.origin.x) * self.dir_recip.x;
let tmin_y = (min_y - self.ray.origin.y) * self.dir_recip.y;
let tmax_x = (max_x - self.ray.origin.x) * self.dir_recip.x;
let tmax_y = (max_y - self.ray.origin.y) * self.dir_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 time of impact for an intersection with a [`BoundingCircle`], if any.
pub fn circle_intersection_at(&self, sphere: &BoundingCircle) -> 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this value is actually a squared distance, though I can't figure out a better name for it.

Copy link
Contributor Author

@NiseVoid NiseVoid Jan 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, distance is a bit of a weird term ... It's the distance from the outside surface to the projected point. It's like a reversed signed distance 🙃

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()
}
}
105 changes: 105 additions & 0 deletions crates/bevy_math/src/bounding/raytest3d.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use super::{Aabb3d, BoundingSphere, IntersectsVolume};
use crate::{primitives::Direction3d, Ray3d, Vec3};

/// A raycast intersection test for 3D bounding volumes
pub struct RayTest3d {
/// The ray for the test
pub ray: Ray3d,
/// The maximum time of impact of the ray
pub max: f32,
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
/// The multiplicative inverse direction of the ray
dir_recip: Vec3,
}

impl RayTest3d {
/// Construct a [`RayTest3d`] from an origin, [`Direction3d`] and max time of impact.
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 time of impact.
pub fn from_ray(ray: Ray3d, max: f32) -> Self {
Self {
ray,
dir_recip: ray.direction.recip(),
max,
}
}

/// Get the cached multiplicate inverse of the direction of the ray
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
pub fn dir_recip(&self) -> Vec3 {
self.dir_recip
}

/// Get the time of impact for 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 based on how much the direction goes that
// way. These values van get arbitrarily large, or even become NaN, which is handled by the
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
// min/max operations below
let tmin_x = (min_x - self.ray.origin.x) * self.dir_recip.x;
let tmin_y = (min_y - self.ray.origin.y) * self.dir_recip.y;
let tmin_z = (min_z - self.ray.origin.z) * self.dir_recip.z;
let tmax_x = (max_x - self.ray.origin.x) * self.dir_recip.x;
let tmax_y = (max_y - self.ray.origin.y) * self.dir_recip.y;
let tmax_z = (max_z - self.ray.origin.z) * self.dir_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.);
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
let tmax = tmax_z.min(tmax_y).min(tmax_x).min(self.max);

if tmin <= tmax {
Some(tmin)
} else {
None
}
}

/// Get the time of impact for an intersection with a [`BoundingSphere`], if any.
pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
NiseVoid marked this conversation as resolved.
Show resolved Hide resolved
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()
}
}
Loading