Skip to content

Commit 8851532

Browse files
NiseVoidalice-i-cecileIQuick143
authored
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 <[email protected]> Co-authored-by: IQuick 143 <[email protected]>
1 parent 9223201 commit 8851532

File tree

3 files changed

+684
-0
lines changed

3 files changed

+684
-0
lines changed

crates/bevy_math/src/bounding/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ mod bounded2d;
5959
pub use bounded2d::*;
6060
mod bounded3d;
6161
pub use bounded3d::*;
62+
63+
mod raytest2d;
64+
pub use raytest2d::*;
65+
mod raytest3d;
66+
pub use raytest3d::*;
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
use super::{Aabb2d, BoundingCircle, IntersectsVolume};
2+
use crate::{primitives::Direction2d, Ray2d, Vec2};
3+
4+
/// A raycast intersection test for 2D bounding volumes
5+
#[derive(Debug)]
6+
pub struct RayTest2d {
7+
/// The ray for the test
8+
pub ray: Ray2d,
9+
/// The maximum distance for the ray
10+
pub max: f32,
11+
/// The multiplicative inverse direction of the ray
12+
direction_recip: Vec2,
13+
}
14+
15+
impl RayTest2d {
16+
/// Construct a [`RayTest2d`] from an origin, [`Direction2d`] and max distance.
17+
pub fn new(origin: Vec2, direction: Direction2d, max: f32) -> Self {
18+
Self::from_ray(Ray2d { origin, direction }, max)
19+
}
20+
21+
/// Construct a [`RayTest2d`] from a [`Ray2d`] and max distance.
22+
pub fn from_ray(ray: Ray2d, max: f32) -> Self {
23+
Self {
24+
ray,
25+
direction_recip: ray.direction.recip(),
26+
max,
27+
}
28+
}
29+
30+
/// Get the cached multiplicative inverse of the direction of the ray.
31+
pub fn direction_recip(&self) -> Vec2 {
32+
self.direction_recip
33+
}
34+
35+
/// Get the distance of an intersection with an [`Aabb2d`], if any.
36+
pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option<f32> {
37+
let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() {
38+
(aabb.min.x, aabb.max.x)
39+
} else {
40+
(aabb.max.x, aabb.min.x)
41+
};
42+
let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() {
43+
(aabb.min.y, aabb.max.y)
44+
} else {
45+
(aabb.max.y, aabb.min.y)
46+
};
47+
48+
// Calculate the minimum/maximum time for each axis based on how much the direction goes that
49+
// way. These values can get arbitrarily large, or even become NaN, which is handled by the
50+
// min/max operations below
51+
let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x;
52+
let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y;
53+
let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x;
54+
let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y;
55+
56+
// An axis that is not relevant to the ray direction will be NaN. When one of the arguments
57+
// to min/max is NaN, the other argument is used.
58+
// An axis for which the direction is the wrong way will return an arbitrarily large
59+
// negative value.
60+
let tmin = tmin_x.max(tmin_y).max(0.);
61+
let tmax = tmax_y.min(tmax_x).min(self.max);
62+
63+
if tmin <= tmax {
64+
Some(tmin)
65+
} else {
66+
None
67+
}
68+
}
69+
70+
/// Get the distance of an intersection with a [`BoundingCircle`], if any.
71+
pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option<f32> {
72+
let offset = self.ray.origin - circle.center;
73+
let projected = offset.dot(*self.ray.direction);
74+
let closest_point = offset - projected * *self.ray.direction;
75+
let distance_squared = circle.radius().powi(2) - closest_point.length_squared();
76+
if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared {
77+
None
78+
} else {
79+
let toi = -projected - distance_squared.sqrt();
80+
if toi > self.max {
81+
None
82+
} else {
83+
Some(toi.max(0.))
84+
}
85+
}
86+
}
87+
}
88+
89+
impl IntersectsVolume<Aabb2d> for RayTest2d {
90+
fn intersects(&self, volume: &Aabb2d) -> bool {
91+
self.aabb_intersection_at(volume).is_some()
92+
}
93+
}
94+
95+
impl IntersectsVolume<BoundingCircle> for RayTest2d {
96+
fn intersects(&self, volume: &BoundingCircle) -> bool {
97+
self.circle_intersection_at(volume).is_some()
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use super::*;
104+
105+
const EPSILON: f32 = 0.001;
106+
107+
#[test]
108+
fn test_ray_intersection_circle_hits() {
109+
for (test, volume, expected_distance) in &[
110+
(
111+
// Hit the center of a centered bounding circle
112+
RayTest2d::new(Vec2::Y * -5., Direction2d::Y, 90.),
113+
BoundingCircle::new(Vec2::ZERO, 1.),
114+
4.,
115+
),
116+
(
117+
// Hit the center of a centered bounding circle, but from the other side
118+
RayTest2d::new(Vec2::Y * 5., -Direction2d::Y, 90.),
119+
BoundingCircle::new(Vec2::ZERO, 1.),
120+
4.,
121+
),
122+
(
123+
// Hit the center of an offset circle
124+
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 90.),
125+
BoundingCircle::new(Vec2::Y * 3., 2.),
126+
1.,
127+
),
128+
(
129+
// Just barely hit the circle before the max distance
130+
RayTest2d::new(Vec2::X, Direction2d::Y, 1.),
131+
BoundingCircle::new(Vec2::ONE, 0.01),
132+
0.99,
133+
),
134+
(
135+
// Hit a circle off-center
136+
RayTest2d::new(Vec2::X, Direction2d::Y, 90.),
137+
BoundingCircle::new(Vec2::Y * 5., 2.),
138+
3.268,
139+
),
140+
(
141+
// Barely hit a circle on the side
142+
RayTest2d::new(Vec2::X * 0.99999, Direction2d::Y, 90.),
143+
BoundingCircle::new(Vec2::Y * 5., 1.),
144+
4.996,
145+
),
146+
] {
147+
let case = format!(
148+
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
149+
test, volume, expected_distance
150+
);
151+
assert!(test.intersects(volume), "{}", case);
152+
let actual_distance = test.circle_intersection_at(volume).unwrap();
153+
assert!(
154+
(actual_distance - expected_distance).abs() < EPSILON,
155+
"{}\n Actual distance: {}",
156+
case,
157+
actual_distance
158+
);
159+
160+
let inverted_ray = RayTest2d::new(test.ray.origin, -test.ray.direction, test.max);
161+
assert!(!inverted_ray.intersects(volume), "{}", case);
162+
}
163+
}
164+
165+
#[test]
166+
fn test_ray_intersection_circle_misses() {
167+
for (test, volume) in &[
168+
(
169+
// The ray doesn't go in the right direction
170+
RayTest2d::new(Vec2::ZERO, Direction2d::X, 90.),
171+
BoundingCircle::new(Vec2::Y * 2., 1.),
172+
),
173+
(
174+
// Ray's alignment isn't enough to hit the circle
175+
RayTest2d::new(Vec2::ZERO, Direction2d::from_xy(1., 1.).unwrap(), 90.),
176+
BoundingCircle::new(Vec2::Y * 2., 1.),
177+
),
178+
(
179+
// The ray's maximum distance isn't high enough
180+
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 0.5),
181+
BoundingCircle::new(Vec2::Y * 2., 1.),
182+
),
183+
] {
184+
assert!(
185+
!test.intersects(volume),
186+
"Case:\n Test: {:?}\n Volume: {:?}",
187+
test,
188+
volume,
189+
);
190+
}
191+
}
192+
193+
#[test]
194+
fn test_ray_intersection_circle_inside() {
195+
let volume = BoundingCircle::new(Vec2::splat(0.5), 1.);
196+
for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
197+
for direction in &[
198+
Direction2d::X,
199+
Direction2d::Y,
200+
-Direction2d::X,
201+
-Direction2d::Y,
202+
] {
203+
for max in &[0., 1., 900.] {
204+
let test = RayTest2d::new(*origin, *direction, *max);
205+
206+
let case = format!(
207+
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
208+
origin, direction, max,
209+
);
210+
assert!(test.intersects(&volume), "{}", case);
211+
212+
let actual_distance = test.circle_intersection_at(&volume);
213+
assert_eq!(actual_distance, Some(0.), "{}", case);
214+
}
215+
}
216+
}
217+
}
218+
219+
#[test]
220+
fn test_ray_intersection_aabb_hits() {
221+
for (test, volume, expected_distance) in &[
222+
(
223+
// Hit the center of a centered aabb
224+
RayTest2d::new(Vec2::Y * -5., Direction2d::Y, 90.),
225+
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
226+
4.,
227+
),
228+
(
229+
// Hit the center of a centered aabb, but from the other side
230+
RayTest2d::new(Vec2::Y * 5., -Direction2d::Y, 90.),
231+
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
232+
4.,
233+
),
234+
(
235+
// Hit the center of an offset aabb
236+
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 90.),
237+
Aabb2d::new(Vec2::Y * 3., Vec2::splat(2.)),
238+
1.,
239+
),
240+
(
241+
// Just barely hit the aabb before the max distance
242+
RayTest2d::new(Vec2::X, Direction2d::Y, 1.),
243+
Aabb2d::new(Vec2::ONE, Vec2::splat(0.01)),
244+
0.99,
245+
),
246+
(
247+
// Hit an aabb off-center
248+
RayTest2d::new(Vec2::X, Direction2d::Y, 90.),
249+
Aabb2d::new(Vec2::Y * 5., Vec2::splat(2.)),
250+
3.,
251+
),
252+
(
253+
// Barely hit an aabb on corner
254+
RayTest2d::new(Vec2::X * -0.001, Direction2d::from_xy(1., 1.).unwrap(), 90.),
255+
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
256+
1.414,
257+
),
258+
] {
259+
let case = format!(
260+
"Case:\n Test: {:?}\n Volume: {:?}\n Expected distance: {:?}",
261+
test, volume, expected_distance
262+
);
263+
assert!(test.intersects(volume), "{}", case);
264+
let actual_distance = test.aabb_intersection_at(volume).unwrap();
265+
assert!(
266+
(actual_distance - expected_distance).abs() < EPSILON,
267+
"{}\n Actual distance: {}",
268+
case,
269+
actual_distance
270+
);
271+
272+
let inverted_ray = RayTest2d::new(test.ray.origin, -test.ray.direction, test.max);
273+
assert!(!inverted_ray.intersects(volume), "{}", case);
274+
}
275+
}
276+
277+
#[test]
278+
fn test_ray_intersection_aabb_misses() {
279+
for (test, volume) in &[
280+
(
281+
// The ray doesn't go in the right direction
282+
RayTest2d::new(Vec2::ZERO, Direction2d::X, 90.),
283+
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
284+
),
285+
(
286+
// Ray's alignment isn't enough to hit the aabb
287+
RayTest2d::new(Vec2::ZERO, Direction2d::from_xy(1., 0.99).unwrap(), 90.),
288+
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
289+
),
290+
(
291+
// The ray's maximum distance isn't high enough
292+
RayTest2d::new(Vec2::ZERO, Direction2d::Y, 0.5),
293+
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
294+
),
295+
] {
296+
assert!(
297+
!test.intersects(volume),
298+
"Case:\n Test: {:?}\n Volume: {:?}",
299+
test,
300+
volume,
301+
);
302+
}
303+
}
304+
305+
#[test]
306+
fn test_ray_intersection_aabb_inside() {
307+
let volume = Aabb2d::new(Vec2::splat(0.5), Vec2::ONE);
308+
for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
309+
for direction in &[
310+
Direction2d::X,
311+
Direction2d::Y,
312+
-Direction2d::X,
313+
-Direction2d::Y,
314+
] {
315+
for max in &[0., 1., 900.] {
316+
let test = RayTest2d::new(*origin, *direction, *max);
317+
318+
let case = format!(
319+
"Case:\n origin: {:?}\n Direction: {:?}\n Max: {}",
320+
origin, direction, max,
321+
);
322+
assert!(test.intersects(&volume), "{}", case);
323+
324+
let actual_distance = test.aabb_intersection_at(&volume);
325+
assert_eq!(actual_distance, Some(0.), "{}", case,);
326+
}
327+
}
328+
}
329+
}
330+
}

0 commit comments

Comments
 (0)