Path: blob/main/crates/bevy_math/src/bounding/raycast3d.rs
6596 views
use super::{Aabb3d, BoundingSphere, IntersectsVolume};1use crate::{2ops::{self, FloatPow},3Dir3A, Ray3d, Vec3A,4};56#[cfg(feature = "bevy_reflect")]7use bevy_reflect::Reflect;89/// A raycast intersection test for 3D bounding volumes10#[derive(Clone, Debug)]11#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]12pub struct RayCast3d {13/// The origin of the ray.14pub origin: Vec3A,15/// The direction of the ray.16pub direction: Dir3A,17/// The maximum distance for the ray18pub max: f32,19/// The multiplicative inverse direction of the ray20direction_recip: Vec3A,21}2223impl RayCast3d {24/// Construct a [`RayCast3d`] from an origin, [direction], and max distance.25///26/// [direction]: crate::direction::Dir327pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Dir3A>, max: f32) -> Self {28let direction = direction.into();29Self {30origin: origin.into(),31direction,32direction_recip: direction.recip(),33max,34}35}3637/// Construct a [`RayCast3d`] from a [`Ray3d`] and max distance.38pub fn from_ray(ray: Ray3d, max: f32) -> Self {39Self::new(ray.origin, ray.direction, max)40}4142/// Get the cached multiplicative inverse of the direction of the ray.43pub fn direction_recip(&self) -> Vec3A {44self.direction_recip45}4647/// Get the distance of an intersection with an [`Aabb3d`], if any.48pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {49let positive = self.direction.signum().cmpgt(Vec3A::ZERO);50let min = Vec3A::select(positive, aabb.min, aabb.max);51let max = Vec3A::select(positive, aabb.max, aabb.min);5253// Calculate the minimum/maximum time for each axis based on how much the direction goes that54// way. These values can get arbitrarily large, or even become NaN, which is handled by the55// min/max operations below56let tmin = (min - self.origin) * self.direction_recip;57let tmax = (max - self.origin) * self.direction_recip;5859// An axis that is not relevant to the ray direction will be NaN. When one of the arguments60// to min/max is NaN, the other argument is used.61// An axis for which the direction is the wrong way will return an arbitrarily large62// negative value.63let tmin = tmin.max_element().max(0.);64let tmax = tmax.min_element().min(self.max);6566if tmin <= tmax {67Some(tmin)68} else {69None70}71}7273/// Get the distance of an intersection with a [`BoundingSphere`], if any.74pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {75let offset = self.origin - sphere.center;76let projected = offset.dot(*self.direction);77let closest_point = offset - projected * *self.direction;78let distance_squared = sphere.radius().squared() - closest_point.length_squared();79if distance_squared < 0.80|| ops::copysign(projected.squared(), -projected) < -distance_squared81{82None83} else {84let toi = -projected - ops::sqrt(distance_squared);85if toi > self.max {86None87} else {88Some(toi.max(0.))89}90}91}92}9394impl IntersectsVolume<Aabb3d> for RayCast3d {95fn intersects(&self, volume: &Aabb3d) -> bool {96self.aabb_intersection_at(volume).is_some()97}98}99100impl IntersectsVolume<BoundingSphere> for RayCast3d {101fn intersects(&self, volume: &BoundingSphere) -> bool {102self.sphere_intersection_at(volume).is_some()103}104}105106/// An intersection test that casts an [`Aabb3d`] along a ray.107#[derive(Clone, Debug)]108#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]109pub struct AabbCast3d {110/// The ray along which to cast the bounding volume111pub ray: RayCast3d,112/// The aabb that is being cast113pub aabb: Aabb3d,114}115116impl AabbCast3d {117/// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [direction], and max distance.118///119/// [direction]: crate::direction::Dir3120pub fn new(121aabb: Aabb3d,122origin: impl Into<Vec3A>,123direction: impl Into<Dir3A>,124max: f32,125) -> Self {126Self {127ray: RayCast3d::new(origin, direction, max),128aabb,129}130}131132/// Construct an [`AabbCast3d`] from an [`Aabb3d`], [`Ray3d`], and max distance.133pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self {134Self::new(aabb, ray.origin, ray.direction, max)135}136137/// Get the distance at which the [`Aabb3d`]s collide, if at all.138pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option<f32> {139aabb.min -= self.aabb.max;140aabb.max -= self.aabb.min;141self.ray.aabb_intersection_at(&aabb)142}143}144145impl IntersectsVolume<Aabb3d> for AabbCast3d {146fn intersects(&self, volume: &Aabb3d) -> bool {147self.aabb_collision_at(*volume).is_some()148}149}150151/// An intersection test that casts a [`BoundingSphere`] along a ray.152#[derive(Clone, Debug)]153#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]154pub struct BoundingSphereCast {155/// The ray along which to cast the bounding volume156pub ray: RayCast3d,157/// The sphere that is being cast158pub sphere: BoundingSphere,159}160161impl BoundingSphereCast {162/// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [direction], and max distance.163///164/// [direction]: crate::direction::Dir3165pub fn new(166sphere: BoundingSphere,167origin: impl Into<Vec3A>,168direction: impl Into<Dir3A>,169max: f32,170) -> Self {171Self {172ray: RayCast3d::new(origin, direction, max),173sphere,174}175}176177/// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], [`Ray3d`], and max distance.178pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self {179Self::new(sphere, ray.origin, ray.direction, max)180}181182/// Get the distance at which the [`BoundingSphere`]s collide, if at all.183pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option<f32> {184sphere.center -= self.sphere.center;185sphere.sphere.radius += self.sphere.radius();186self.ray.sphere_intersection_at(&sphere)187}188}189190impl IntersectsVolume<BoundingSphere> for BoundingSphereCast {191fn intersects(&self, volume: &BoundingSphere) -> bool {192self.sphere_collision_at(*volume).is_some()193}194}195196#[cfg(test)]197mod tests {198use super::*;199use crate::{Dir3, Vec3};200201const EPSILON: f32 = 0.001;202203#[test]204fn test_ray_intersection_sphere_hits() {205for (test, volume, expected_distance) in &[206(207// Hit the center of a centered bounding sphere208RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),209BoundingSphere::new(Vec3::ZERO, 1.),2104.,211),212(213// Hit the center of a centered bounding sphere, but from the other side214RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),215BoundingSphere::new(Vec3::ZERO, 1.),2164.,217),218(219// Hit the center of an offset sphere220RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),221BoundingSphere::new(Vec3::Y * 3., 2.),2221.,223),224(225// Just barely hit the sphere before the max distance226RayCast3d::new(Vec3::X, Dir3::Y, 1.),227BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),2280.99,229),230(231// Hit a sphere off-center232RayCast3d::new(Vec3::X, Dir3::Y, 90.),233BoundingSphere::new(Vec3::Y * 5., 2.),2343.268,235),236(237// Barely hit a sphere on the side238RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.),239BoundingSphere::new(Vec3::Y * 5., 1.),2404.996,241),242] {243assert!(244test.intersects(volume),245"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",246);247let actual_distance = test.sphere_intersection_at(volume).unwrap();248assert!(249ops::abs(actual_distance - expected_distance) < EPSILON,250"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",251);252253let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);254assert!(255!inverted_ray.intersects(volume),256"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",257);258}259}260261#[test]262fn test_ray_intersection_sphere_misses() {263for (test, volume) in &[264(265// The ray doesn't go in the right direction266RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),267BoundingSphere::new(Vec3::Y * 2., 1.),268),269(270// Ray's alignment isn't enough to hit the sphere271RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),272BoundingSphere::new(Vec3::Y * 2., 1.),273),274(275// The ray's maximum distance isn't high enough276RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),277BoundingSphere::new(Vec3::Y * 2., 1.),278),279] {280assert!(281!test.intersects(volume),282"Case:\n Test: {test:?}\n Volume: {volume:?}",283);284}285}286287#[test]288fn test_ray_intersection_sphere_inside() {289let volume = BoundingSphere::new(Vec3::splat(0.5), 1.);290for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {291for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {292for max in &[0., 1., 900.] {293let test = RayCast3d::new(*origin, *direction, *max);294295assert!(296test.intersects(&volume),297"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",298);299300let actual_distance = test.sphere_intersection_at(&volume);301assert_eq!(302actual_distance,303Some(0.),304"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",305);306}307}308}309}310311#[test]312fn test_ray_intersection_aabb_hits() {313for (test, volume, expected_distance) in &[314(315// Hit the center of a centered aabb316RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),317Aabb3d::new(Vec3::ZERO, Vec3::ONE),3184.,319),320(321// Hit the center of a centered aabb, but from the other side322RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),323Aabb3d::new(Vec3::ZERO, Vec3::ONE),3244.,325),326(327// Hit the center of an offset aabb328RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),329Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),3301.,331),332(333// Just barely hit the aabb before the max distance334RayCast3d::new(Vec3::X, Dir3::Y, 1.),335Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),3360.99,337),338(339// Hit an aabb off-center340RayCast3d::new(Vec3::X, Dir3::Y, 90.),341Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),3423.,343),344(345// Barely hit an aabb on corner346RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),347Aabb3d::new(Vec3::Y * 2., Vec3::ONE),3481.732,349),350] {351assert!(352test.intersects(volume),353"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",354);355let actual_distance = test.aabb_intersection_at(volume).unwrap();356assert!(357ops::abs(actual_distance - expected_distance) < EPSILON,358"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",359);360361let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);362assert!(363!inverted_ray.intersects(volume),364"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",365);366}367}368369#[test]370fn test_ray_intersection_aabb_misses() {371for (test, volume) in &[372(373// The ray doesn't go in the right direction374RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),375Aabb3d::new(Vec3::Y * 2., Vec3::ONE),376),377(378// Ray's alignment isn't enough to hit the aabb379RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 0.99, 1.).unwrap(), 90.),380Aabb3d::new(Vec3::Y * 2., Vec3::ONE),381),382(383// The ray's maximum distance isn't high enough384RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),385Aabb3d::new(Vec3::Y * 2., Vec3::ONE),386),387] {388assert!(389!test.intersects(volume),390"Case:\n Test: {test:?}\n Volume: {volume:?}",391);392}393}394395#[test]396fn test_ray_intersection_aabb_inside() {397let volume = Aabb3d::new(Vec3::splat(0.5), Vec3::ONE);398for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {399for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {400for max in &[0., 1., 900.] {401let test = RayCast3d::new(*origin, *direction, *max);402403assert!(404test.intersects(&volume),405"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",406);407408let actual_distance = test.aabb_intersection_at(&volume);409assert_eq!(410actual_distance,411Some(0.),412"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",413);414}415}416}417}418419#[test]420fn test_aabb_cast_hits() {421for (test, volume, expected_distance) in &[422(423// Hit the center of the aabb, that a ray would've also hit424AabbCast3d::new(Aabb3d::new(Vec3::ZERO, Vec3::ONE), Vec3::ZERO, Dir3::Y, 90.),425Aabb3d::new(Vec3::Y * 5., Vec3::ONE),4263.,427),428(429// Hit the center of the aabb, but from the other side430AabbCast3d::new(431Aabb3d::new(Vec3::ZERO, Vec3::ONE),432Vec3::Y * 10.,433-Dir3::Y,43490.,435),436Aabb3d::new(Vec3::Y * 5., Vec3::ONE),4373.,438),439(440// Hit the edge of the aabb, that a ray would've missed441AabbCast3d::new(442Aabb3d::new(Vec3::ZERO, Vec3::ONE),443Vec3::X * 1.5,444Dir3::Y,44590.,446),447Aabb3d::new(Vec3::Y * 5., Vec3::ONE),4483.,449),450(451// Hit the edge of the aabb, by casting an off-center AABB452AabbCast3d::new(453Aabb3d::new(Vec3::X * -2., Vec3::ONE),454Vec3::X * 3.,455Dir3::Y,45690.,457),458Aabb3d::new(Vec3::Y * 5., Vec3::ONE),4593.,460),461] {462assert!(463test.intersects(volume),464"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",465);466let actual_distance = test.aabb_collision_at(*volume).unwrap();467assert!(468ops::abs(actual_distance - expected_distance) < EPSILON,469"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",470);471472let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);473assert!(474!inverted_ray.intersects(volume),475"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",476);477}478}479480#[test]481fn test_sphere_cast_hits() {482for (test, volume, expected_distance) in &[483(484// Hit the center of the bounding sphere, that a ray would've also hit485BoundingSphereCast::new(486BoundingSphere::new(Vec3::ZERO, 1.),487Vec3::ZERO,488Dir3::Y,48990.,490),491BoundingSphere::new(Vec3::Y * 5., 1.),4923.,493),494(495// Hit the center of the bounding sphere, but from the other side496BoundingSphereCast::new(497BoundingSphere::new(Vec3::ZERO, 1.),498Vec3::Y * 10.,499-Dir3::Y,50090.,501),502BoundingSphere::new(Vec3::Y * 5., 1.),5033.,504),505(506// Hit the bounding sphere off-center, that a ray would've missed507BoundingSphereCast::new(508BoundingSphere::new(Vec3::ZERO, 1.),509Vec3::X * 1.5,510Dir3::Y,51190.,512),513BoundingSphere::new(Vec3::Y * 5., 1.),5143.677,515),516(517// Hit the bounding sphere off-center, by casting a sphere that is off-center518BoundingSphereCast::new(519BoundingSphere::new(Vec3::X * -1.5, 1.),520Vec3::X * 3.,521Dir3::Y,52290.,523),524BoundingSphere::new(Vec3::Y * 5., 1.),5253.677,526),527] {528assert!(529test.intersects(volume),530"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",531);532let actual_distance = test.sphere_collision_at(*volume).unwrap();533assert!(534ops::abs(actual_distance - expected_distance) < EPSILON,535"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",536);537538let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);539assert!(540!inverted_ray.intersects(volume),541"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",542);543}544}545}546547548