From e65139a30c2aac0394ef2064418df1bfebb75403 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Fri, 12 Jan 2024 01:56:10 +0100 Subject: [PATCH 01/12] Add RayTest2d and RayTest3d --- crates/bevy_math/src/bounding/mod.rs | 5 ++ crates/bevy_math/src/bounding/raytest2d.rs | 78 ++++++++++++++++++++ crates/bevy_math/src/bounding/raytest3d.rs | 85 ++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 crates/bevy_math/src/bounding/raytest2d.rs create mode 100644 crates/bevy_math/src/bounding/raytest3d.rs diff --git a/crates/bevy_math/src/bounding/mod.rs b/crates/bevy_math/src/bounding/mod.rs index 7a07626fddba7..f7a7b2235c61d 100644 --- a/crates/bevy_math/src/bounding/mod.rs +++ b/crates/bevy_math/src/bounding/mod.rs @@ -59,3 +59,8 @@ mod bounded2d; pub use bounded2d::*; mod bounded3d; pub use bounded3d::*; + +mod raytest2d; +pub use raytest2d::*; +mod raytest3d; +pub use raytest3d::*; diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs new file mode 100644 index 0000000000000..d2b5898dd9edd --- /dev/null +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -0,0 +1,78 @@ +use super::{Aabb2d, BoundingCircle, IntersectsVolume}; +use crate::{primitives::Direction2d, Vec2}; + +/// A raycast intersection test for 2D bounding volumes +pub struct RayTest2d { + /// The origin of the ray + pub origin: Vec2, + /// The direction of the ray + pub dir: Direction2d, + /// The inverse direction of the ray + pub inv_dir: Vec2, + /// The maximum time of impact of the ray + pub max: f32, +} + +impl RayTest2d { + /// Construct a [`RayTest2d`] from an origin, [`Direction2d`] and max time of impact. + pub fn new(origin: Vec2, dir: Direction2d, max: f32) -> Self { + Self { + origin, + inv_dir: Vec2::ONE / *dir, + dir, + max, + } + } + + /// Get the time of impact for an intersection with an [`Aabb2d`], if any. + pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option { + let (min_x, max_x) = if self.inv_dir.x.is_sign_positive() { + (aabb.min.x, aabb.max.x) + } else { + (aabb.max.x, aabb.min.x) + }; + let (min_y, max_y) = if self.inv_dir.y.is_sign_positive() { + (aabb.min.y, aabb.max.y) + } else { + (aabb.max.y, aabb.min.y) + }; + let tmin_x = (min_x - self.origin.x) * self.inv_dir.x; + let tmin_y = (min_y - self.origin.y) * self.inv_dir.y; + let tmax_x = (max_x - self.origin.x) * self.inv_dir.x; + let tmax_y = (max_y - self.origin.y) * self.inv_dir.y; + + 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 sphere_intersection_at(&self, sphere: &BoundingCircle) -> Option { + let oc = self.origin - sphere.center; + let b = oc.dot(*self.dir); + let qc = oc - b * *self.dir; + let h = sphere.radius().powi(2) - qc.dot(qc); + if h < 0. || h > -b { + None + } else { + Some(-b - h) + } + } +} + +impl IntersectsVolume for RayTest2d { + fn intersects(&self, volume: &Aabb2d) -> bool { + self.aabb_intersection_at(volume).is_some() + } +} + +impl IntersectsVolume for RayTest2d { + fn intersects(&self, volume: &BoundingCircle) -> bool { + self.sphere_intersection_at(volume).is_some() + } +} diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs new file mode 100644 index 0000000000000..62438edb30621 --- /dev/null +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -0,0 +1,85 @@ +use super::{Aabb3d, BoundingSphere, IntersectsVolume}; +use crate::{primitives::Direction3d, Vec3}; + +/// A raycast intersection test for 3D bounding volumes +pub struct RayTest3d { + /// The origin of the ray + pub origin: Vec3, + /// The direction of the ray + pub dir: Direction3d, + /// The inverse direction of the ray + pub inv_dir: Vec3, + /// The maximum time of impact of the ray + pub max: f32, +} + +impl RayTest3d { + /// Construct a [`RayTest3d`] from an origin, [`Direction3d`] and max time of impact. + pub fn new(origin: Vec3, dir: Direction3d, max: f32) -> Self { + Self { + origin, + inv_dir: Vec3::ONE / *dir, + dir, + max, + } + } + + /// Get the time of impact for an intersection with an [`Aabb3d`], if any. + pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option { + let (min_x, max_x) = if self.inv_dir.x.is_sign_positive() { + (aabb.min.x, aabb.max.x) + } else { + (aabb.max.x, aabb.min.x) + }; + let (min_y, max_y) = if self.inv_dir.y.is_sign_positive() { + (aabb.min.y, aabb.max.y) + } else { + (aabb.max.y, aabb.min.y) + }; + let (min_z, max_z) = if self.inv_dir.z.is_sign_positive() { + (aabb.min.z, aabb.max.z) + } else { + (aabb.max.z, aabb.min.z) + }; + let tmin_x = (min_x - self.origin.x) * self.inv_dir.x; + let tmin_y = (min_y - self.origin.y) * self.inv_dir.y; + let tmin_z = (min_z - self.origin.z) * self.inv_dir.z; + let tmax_x = (max_x - self.origin.x) * self.inv_dir.x; + let tmax_y = (max_y - self.origin.y) * self.inv_dir.y; + let tmax_z = (max_z - self.origin.z) * self.inv_dir.z; + + 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 time of impact for an intersection with a [`BoundingSphere`], if any. + pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option { + let oc = self.origin - sphere.center; + let b = oc.dot(*self.dir); + let qc = oc - b * *self.dir; + let h = sphere.radius().powi(2) - qc.dot(qc); + if h < 0. || h > -b { + None + } else { + Some(-b - h) + } + } +} + +impl IntersectsVolume for RayTest3d { + fn intersects(&self, volume: &Aabb3d) -> bool { + self.aabb_intersection_at(volume).is_some() + } +} + +impl IntersectsVolume for RayTest3d { + fn intersects(&self, volume: &BoundingSphere) -> bool { + self.sphere_intersection_at(volume).is_some() + } +} From 74d0393365a7b476daf3255e8c0887e118e2e319 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Mon, 22 Jan 2024 15:09:59 +0100 Subject: [PATCH 02/12] Rename variables in circle/sphere intersection code --- crates/bevy_math/src/bounding/raytest2d.rs | 16 ++++++++-------- crates/bevy_math/src/bounding/raytest3d.rs | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index d2b5898dd9edd..f2feb5318e93a 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -52,15 +52,15 @@ impl RayTest2d { } /// Get the time of impact for an intersection with a [`BoundingCircle`], if any. - pub fn sphere_intersection_at(&self, sphere: &BoundingCircle) -> Option { - let oc = self.origin - sphere.center; - let b = oc.dot(*self.dir); - let qc = oc - b * *self.dir; - let h = sphere.radius().powi(2) - qc.dot(qc); - if h < 0. || h > -b { + pub fn circle_intersection_at(&self, sphere: &BoundingCircle) -> Option { + let offset = self.origin - sphere.center; + let projected = offset.dot(*self.dir); + let closest_point = offset - projected * *self.dir; + let distance_squared = sphere.radius().powi(2) - closest_point.length_squared(); + if distance_squared < 0. || projected.powi(2).copysign(-projected) < distance_squared { None } else { - Some(-b - h) + Some(-projected - distance_squared.sqrt()) } } } @@ -73,6 +73,6 @@ impl IntersectsVolume for RayTest2d { impl IntersectsVolume for RayTest2d { fn intersects(&self, volume: &BoundingCircle) -> bool { - self.sphere_intersection_at(volume).is_some() + self.circle_intersection_at(volume).is_some() } } diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index 62438edb30621..962e601ede478 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -60,14 +60,14 @@ impl RayTest3d { /// Get the time of impact for an intersection with a [`BoundingSphere`], if any. pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option { - let oc = self.origin - sphere.center; - let b = oc.dot(*self.dir); - let qc = oc - b * *self.dir; - let h = sphere.radius().powi(2) - qc.dot(qc); - if h < 0. || h > -b { + let offset = self.origin - sphere.center; + let projected = offset.dot(*self.dir); + let closest_point = offset - projected * *self.dir; + let distance_squared = sphere.radius().powi(2) - closest_point.length_squared(); + if distance_squared < 0. || projected.powi(2).copysign(-projected) < distance_squared { None } else { - Some(-b - h) + Some(-projected - distance_squared.sqrt()) } } } From c8cba8f1d78bc036eb29316bc3f0f6a7f9b30cf6 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Mon, 22 Jan 2024 15:13:51 +0100 Subject: [PATCH 03/12] Rename inv_dir to dir_recip --- crates/bevy_math/src/bounding/raytest2d.rs | 18 ++++++++-------- crates/bevy_math/src/bounding/raytest3d.rs | 24 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index f2feb5318e93a..dd39386cf7b17 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -7,8 +7,8 @@ pub struct RayTest2d { pub origin: Vec2, /// The direction of the ray pub dir: Direction2d, - /// The inverse direction of the ray - pub inv_dir: Vec2, + /// The multiplicative inverse direction of the ray + pub dir_recip: Vec2, /// The maximum time of impact of the ray pub max: f32, } @@ -18,7 +18,7 @@ impl RayTest2d { pub fn new(origin: Vec2, dir: Direction2d, max: f32) -> Self { Self { origin, - inv_dir: Vec2::ONE / *dir, + dir_recip: dir.recip(), dir, max, } @@ -26,20 +26,20 @@ impl RayTest2d { /// Get the time of impact for an intersection with an [`Aabb2d`], if any. pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option { - let (min_x, max_x) = if self.inv_dir.x.is_sign_positive() { + let (min_x, max_x) = if self.dir.x.is_sign_positive() { (aabb.min.x, aabb.max.x) } else { (aabb.max.x, aabb.min.x) }; - let (min_y, max_y) = if self.inv_dir.y.is_sign_positive() { + let (min_y, max_y) = if self.dir.y.is_sign_positive() { (aabb.min.y, aabb.max.y) } else { (aabb.max.y, aabb.min.y) }; - let tmin_x = (min_x - self.origin.x) * self.inv_dir.x; - let tmin_y = (min_y - self.origin.y) * self.inv_dir.y; - let tmax_x = (max_x - self.origin.x) * self.inv_dir.x; - let tmax_y = (max_y - self.origin.y) * self.inv_dir.y; + let tmin_x = (min_x - self.origin.x) * self.dir_recip.x; + let tmin_y = (min_y - self.origin.y) * self.dir_recip.y; + let tmax_x = (max_x - self.origin.x) * self.dir_recip.x; + let tmax_y = (max_y - self.origin.y) * self.dir_recip.y; let tmin = tmin_x.max(tmin_y).max(0.); let tmax = tmax_y.min(tmax_x).min(self.max); diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index 962e601ede478..0fa59d11d5cce 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -7,8 +7,8 @@ pub struct RayTest3d { pub origin: Vec3, /// The direction of the ray pub dir: Direction3d, - /// The inverse direction of the ray - pub inv_dir: Vec3, + /// The multiplicative inverse direction of the ray + pub dir_recip: Vec3, /// The maximum time of impact of the ray pub max: f32, } @@ -18,7 +18,7 @@ impl RayTest3d { pub fn new(origin: Vec3, dir: Direction3d, max: f32) -> Self { Self { origin, - inv_dir: Vec3::ONE / *dir, + dir_recip: dir.recip(), dir, max, } @@ -26,27 +26,27 @@ impl RayTest3d { /// Get the time of impact for an intersection with an [`Aabb3d`], if any. pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option { - let (min_x, max_x) = if self.inv_dir.x.is_sign_positive() { + let (min_x, max_x) = if self.dir.x.is_sign_positive() { (aabb.min.x, aabb.max.x) } else { (aabb.max.x, aabb.min.x) }; - let (min_y, max_y) = if self.inv_dir.y.is_sign_positive() { + let (min_y, max_y) = if self.dir.y.is_sign_positive() { (aabb.min.y, aabb.max.y) } else { (aabb.max.y, aabb.min.y) }; - let (min_z, max_z) = if self.inv_dir.z.is_sign_positive() { + let (min_z, max_z) = if self.dir.z.is_sign_positive() { (aabb.min.z, aabb.max.z) } else { (aabb.max.z, aabb.min.z) }; - let tmin_x = (min_x - self.origin.x) * self.inv_dir.x; - let tmin_y = (min_y - self.origin.y) * self.inv_dir.y; - let tmin_z = (min_z - self.origin.z) * self.inv_dir.z; - let tmax_x = (max_x - self.origin.x) * self.inv_dir.x; - let tmax_y = (max_y - self.origin.y) * self.inv_dir.y; - let tmax_z = (max_z - self.origin.z) * self.inv_dir.z; + let tmin_x = (min_x - self.origin.x) * self.dir_recip.x; + let tmin_y = (min_y - self.origin.y) * self.dir_recip.y; + let tmin_z = (min_z - self.origin.z) * self.dir_recip.z; + let tmax_x = (max_x - self.origin.x) * self.dir_recip.x; + let tmax_y = (max_y - self.origin.y) * self.dir_recip.y; + let tmax_z = (max_z - self.origin.z) * self.dir_recip.z; 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); From 0e22710423cf3c2b314d603391332486a0963f35 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Mon, 22 Jan 2024 15:20:46 +0100 Subject: [PATCH 04/12] Add some comments to aabb_intersection_at explaining some details --- crates/bevy_math/src/bounding/raytest2d.rs | 8 ++++++++ crates/bevy_math/src/bounding/raytest3d.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index dd39386cf7b17..81b9e21bb110e 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -36,11 +36,19 @@ impl RayTest2d { } else { (aabb.max.y, aabb.min.y) }; + + // 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 + // min/max operations below let tmin_x = (min_x - self.origin.x) * self.dir_recip.x; let tmin_y = (min_y - self.origin.y) * self.dir_recip.y; let tmax_x = (max_x - self.origin.x) * self.dir_recip.x; let tmax_y = (max_y - self.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); diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index 0fa59d11d5cce..aed44d777f975 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -41,6 +41,10 @@ impl RayTest3d { } 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 + // min/max operations below let tmin_x = (min_x - self.origin.x) * self.dir_recip.x; let tmin_y = (min_y - self.origin.y) * self.dir_recip.y; let tmin_z = (min_z - self.origin.z) * self.dir_recip.z; @@ -48,6 +52,10 @@ impl RayTest3d { let tmax_y = (max_y - self.origin.y) * self.dir_recip.y; let tmax_z = (max_z - self.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.); let tmax = tmax_z.min(tmax_y).min(tmax_x).min(self.max); From 0c0a9acfab8117845f4bc20b8ca7c0ac231e6fa8 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Mon, 22 Jan 2024 15:28:29 +0100 Subject: [PATCH 05/12] Use Ray2d/3d in RayTest2d/3d --- crates/bevy_math/src/bounding/raytest2d.rs | 47 +++++++++++-------- crates/bevy_math/src/bounding/raytest3d.rs | 53 ++++++++++++---------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index 81b9e21bb110e..8a4fb32a4034f 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -1,37 +1,44 @@ use super::{Aabb2d, BoundingCircle, IntersectsVolume}; -use crate::{primitives::Direction2d, Vec2}; +use crate::{primitives::Direction2d, Ray2d, Vec2}; /// A raycast intersection test for 2D bounding volumes pub struct RayTest2d { - /// The origin of the ray - pub origin: Vec2, - /// The direction of the ray - pub dir: Direction2d, - /// The multiplicative inverse direction of the ray - pub dir_recip: Vec2, + /// The ray for the test + pub ray: Ray2d, /// The maximum time of impact of the ray pub max: f32, + /// 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, dir: Direction2d, max: f32) -> Self { + pub fn new(origin: Vec2, direction: Direction2d, max: f32) -> Self { + Self::from_ray(Ray2d { origin, direction }, max) + } + + /// Construct a [`RayTest3d`] from a [`Ray3d`] and max time of impact. + pub fn from_ray(ray: Ray2d, max: f32) -> Self { Self { - origin, - dir_recip: dir.recip(), - dir, + ray, + dir_recip: ray.direction.recip(), max, } } + /// Get the cached multiplicate inverse of the direction of the ray + pub fn dir_recip(&self) -> Vec2 { + self.dir_recip + } + /// Get the time of impact for an intersection with an [`Aabb2d`], if any. pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option { - let (min_x, max_x) = if self.dir.x.is_sign_positive() { + 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.dir.y.is_sign_positive() { + 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) @@ -40,10 +47,10 @@ impl RayTest2d { // 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 // min/max operations below - let tmin_x = (min_x - self.origin.x) * self.dir_recip.x; - let tmin_y = (min_y - self.origin.y) * self.dir_recip.y; - let tmax_x = (max_x - self.origin.x) * self.dir_recip.x; - let tmax_y = (max_y - self.origin.y) * self.dir_recip.y; + 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. @@ -61,9 +68,9 @@ impl RayTest2d { /// Get the time of impact for an intersection with a [`BoundingCircle`], if any. pub fn circle_intersection_at(&self, sphere: &BoundingCircle) -> Option { - let offset = self.origin - sphere.center; - let projected = offset.dot(*self.dir); - let closest_point = offset - projected * *self.dir; + 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 diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index aed44d777f975..d4f85f2263ff0 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -1,42 +1,49 @@ use super::{Aabb3d, BoundingSphere, IntersectsVolume}; -use crate::{primitives::Direction3d, Vec3}; +use crate::{primitives::Direction3d, Ray3d, Vec3}; /// A raycast intersection test for 3D bounding volumes pub struct RayTest3d { - /// The origin of the ray - pub origin: Vec3, - /// The direction of the ray - pub dir: Direction3d, - /// The multiplicative inverse direction of the ray - pub dir_recip: Vec3, + /// The ray for the test + pub ray: Ray3d, /// The maximum time of impact of the ray pub max: f32, + /// 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, dir: Direction3d, max: f32) -> Self { + 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 { - origin, - dir_recip: dir.recip(), - dir, + ray, + dir_recip: ray.direction.recip(), max, } } + /// Get the cached multiplicate inverse of the direction of the ray + 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 { - let (min_x, max_x) = if self.dir.x.is_sign_positive() { + 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.dir.y.is_sign_positive() { + 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.dir.z.is_sign_positive() { + 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) @@ -45,12 +52,12 @@ impl RayTest3d { // 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 // min/max operations below - let tmin_x = (min_x - self.origin.x) * self.dir_recip.x; - let tmin_y = (min_y - self.origin.y) * self.dir_recip.y; - let tmin_z = (min_z - self.origin.z) * self.dir_recip.z; - let tmax_x = (max_x - self.origin.x) * self.dir_recip.x; - let tmax_y = (max_y - self.origin.y) * self.dir_recip.y; - let tmax_z = (max_z - self.origin.z) * self.dir_recip.z; + 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. @@ -68,9 +75,9 @@ impl RayTest3d { /// Get the time of impact for an intersection with a [`BoundingSphere`], if any. pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option { - let offset = self.origin - sphere.center; - let projected = offset.dot(*self.dir); - let closest_point = offset - projected * *self.dir; + 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 From 25d5d429787e93600004b99408fd6d3e55f5dc62 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sat, 27 Jan 2024 13:31:00 +0100 Subject: [PATCH 06/12] Fix RayTest2d::from_ray comment --- crates/bevy_math/src/bounding/raytest2d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index 8a4fb32a4034f..f9857eeba80f8 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -17,7 +17,7 @@ impl RayTest2d { Self::from_ray(Ray2d { origin, direction }, max) } - /// Construct a [`RayTest3d`] from a [`Ray3d`] and max time of impact. + /// Construct a [`RayTest2d`] from a [`Ray2d`] and max time of impact. pub fn from_ray(ray: Ray2d, max: f32) -> Self { Self { ray, From 1729772a2dd6b0229bffef1da3f0f12220fa57ad Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sat, 27 Jan 2024 14:41:46 +0100 Subject: [PATCH 07/12] Fix circle/sphere origin penetration --- crates/bevy_math/src/bounding/raytest2d.rs | 9 +++++++-- crates/bevy_math/src/bounding/raytest3d.rs | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index f9857eeba80f8..735371b10c1c8 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -72,10 +72,15 @@ impl RayTest2d { 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 { + if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared { None } else { - Some(-projected - distance_squared.sqrt()) + let toi = -projected - distance_squared.sqrt(); + if toi > self.max { + None + } else { + Some(toi.max(0.)) + } } } } diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index d4f85f2263ff0..0990b347151fe 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -79,10 +79,15 @@ impl RayTest3d { 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 { + if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared { None } else { - Some(-projected - distance_squared.sqrt()) + let toi = -projected - distance_squared.sqrt(); + if toi > self.max { + None + } else { + Some(toi.max(0.)) + } } } } From da4daef912a21ee2d5784363e3bbde080925c98e Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sat, 27 Jan 2024 19:05:54 +0100 Subject: [PATCH 08/12] Apply review suggestions --- crates/bevy_math/src/bounding/raytest2d.rs | 32 +++++++++---------- crates/bevy_math/src/bounding/raytest3d.rs | 36 +++++++++++----------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index 735371b10c1c8..0c8239bc4c356 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -5,33 +5,33 @@ use crate::{primitives::Direction2d, Ray2d, Vec2}; pub struct RayTest2d { /// The ray for the test pub ray: Ray2d, - /// The maximum time of impact of the ray + /// The maximum distance for the ray pub max: f32, /// The multiplicative inverse direction of the ray - dir_recip: Vec2, + direction_recip: Vec2, } impl RayTest2d { - /// Construct a [`RayTest2d`] from an origin, [`Direction2d`] and max time of impact. + /// 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 time of impact. + /// Construct a [`RayTest2d`] from a [`Ray2d`] and max distance. pub fn from_ray(ray: Ray2d, max: f32) -> Self { Self { ray, - dir_recip: ray.direction.recip(), + direction_recip: ray.direction.recip(), max, } } - /// Get the cached multiplicate inverse of the direction of the ray - pub fn dir_recip(&self) -> Vec2 { - self.dir_recip + /// Get the cached multiplicative inverse of the direction of the ray. + pub fn direction_recip(&self) -> Vec2 { + self.direction_recip } - /// Get the time of impact for an intersection with an [`Aabb2d`], if any. + /// Get the distance of an intersection with an [`Aabb2d`], if any. pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option { let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() { (aabb.min.x, aabb.max.x) @@ -44,13 +44,13 @@ impl RayTest2d { (aabb.max.y, aabb.min.y) }; - // 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 + // 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.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; + 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. @@ -66,7 +66,7 @@ impl RayTest2d { } } - /// Get the time of impact for an intersection with a [`BoundingCircle`], if any. + /// Get the distance of an intersection with a [`BoundingCircle`], if any. pub fn circle_intersection_at(&self, sphere: &BoundingCircle) -> Option { let offset = self.ray.origin - sphere.center; let projected = offset.dot(*self.ray.direction); diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index 0990b347151fe..e90f14b36a87b 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -5,33 +5,33 @@ use crate::{primitives::Direction3d, Ray3d, Vec3}; pub struct RayTest3d { /// The ray for the test pub ray: Ray3d, - /// The maximum time of impact of the ray + /// The maximum distance for the ray pub max: f32, /// The multiplicative inverse direction of the ray - dir_recip: Vec3, + direction_recip: Vec3, } impl RayTest3d { - /// Construct a [`RayTest3d`] from an origin, [`Direction3d`] and max time of impact. + /// 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 time of impact. + /// Construct a [`RayTest3d`] from a [`Ray3d`] and max distance. pub fn from_ray(ray: Ray3d, max: f32) -> Self { Self { ray, - dir_recip: ray.direction.recip(), + direction_recip: ray.direction.recip(), max, } } - /// Get the cached multiplicate inverse of the direction of the ray - pub fn dir_recip(&self) -> Vec3 { - self.dir_recip + /// Get the cached multiplicative inverse of the direction of the ray. + pub fn direction_recip(&self) -> Vec3 { + self.direction_recip } - /// Get the time of impact for an intersection with an [`Aabb3d`], if any. + /// Get the distance of an intersection with an [`Aabb3d`], if any. pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option { let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() { (aabb.min.x, aabb.max.x) @@ -49,15 +49,15 @@ impl RayTest3d { (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 + // 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.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; + 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. @@ -73,7 +73,7 @@ impl RayTest3d { } } - /// Get the time of impact for an intersection with a [`BoundingSphere`], if any. + /// Get the distance of an intersection with a [`BoundingSphere`], if any. pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option { let offset = self.ray.origin - sphere.center; let projected = offset.dot(*self.ray.direction); From 56c05f723b743a093336e4850e88a522b7dc0a42 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sun, 28 Jan 2024 19:13:26 +0100 Subject: [PATCH 09/12] Add tests for RayTest2d/3d --- crates/bevy_math/src/bounding/raytest2d.rs | 232 ++++++++++++++++++++ crates/bevy_math/src/bounding/raytest3d.rs | 240 +++++++++++++++++++++ 2 files changed, 472 insertions(+) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index 0c8239bc4c356..4522fb643dd25 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -2,6 +2,7 @@ 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, @@ -96,3 +97,234 @@ impl IntersectsVolume for RayTest2d { 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,); + } + } + } + } +} diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index e90f14b36a87b..d3bfdd301bef8 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -2,6 +2,7 @@ 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, @@ -103,3 +104,242 @@ impl IntersectsVolume for RayTest3d { 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::X, + -Direction3d::Y, + ] { + 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::X, + -Direction3d::Y, + ] { + 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,); + } + } + } + } +} From 845fbb2c30c862bbcae3bc3642570304a980ca9f Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sun, 28 Jan 2024 19:14:26 +0100 Subject: [PATCH 10/12] Rename stray sphere variable in RayTest2d code --- crates/bevy_math/src/bounding/raytest2d.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index 4522fb643dd25..cb8353c121fac 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -68,11 +68,11 @@ impl RayTest2d { } /// Get the distance of an intersection with a [`BoundingCircle`], if any. - pub fn circle_intersection_at(&self, sphere: &BoundingCircle) -> Option { - let offset = self.ray.origin - sphere.center; + pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option { + 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 = sphere.radius().powi(2) - closest_point.length_squared(); + let distance_squared = circle.radius().powi(2) - closest_point.length_squared(); if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared { None } else { From 65e0b0097b7febfa55179cef38ebfa723809d104 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Sun, 28 Jan 2024 14:22:45 -0500 Subject: [PATCH 11/12] Typo Co-authored-by: IQuick 143 --- crates/bevy_math/src/bounding/raytest2d.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_math/src/bounding/raytest2d.rs b/crates/bevy_math/src/bounding/raytest2d.rs index cb8353c121fac..8c1780a4efdee 100644 --- a/crates/bevy_math/src/bounding/raytest2d.rs +++ b/crates/bevy_math/src/bounding/raytest2d.rs @@ -210,7 +210,7 @@ mod tests { assert!(test.intersects(&volume), "{}", case); let actual_distance = test.circle_intersection_at(&volume); - assert_eq!(actual_distance, Some(0.), "{}", case,); + assert_eq!(actual_distance, Some(0.), "{}", case); } } } From a56f653c9bd4a648a327e3fbb00af7bc7b567445 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sun, 28 Jan 2024 20:58:54 +0100 Subject: [PATCH 12/12] Also check Z/-Z directions in test_ray_intersection_X_inside --- crates/bevy_math/src/bounding/raytest3d.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_math/src/bounding/raytest3d.rs b/crates/bevy_math/src/bounding/raytest3d.rs index d3bfdd301bef8..19ce62f00cc6e 100644 --- a/crates/bevy_math/src/bounding/raytest3d.rs +++ b/crates/bevy_math/src/bounding/raytest3d.rs @@ -204,8 +204,10 @@ mod tests { 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); @@ -324,8 +326,10 @@ mod tests { 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);