1use super::{Aabb3d, BoundingSphere, IntersectsVolume};
2use crate::Ray3d;
3use bevy_math::{
4 ops::{self, FloatPow},
5 Dir3A, Vec3A,
6};
7
8#[cfg(feature = "bevy_reflect")]
9use bevy_reflect::Reflect;
10
11#[derive(Clone, Debug)]
13#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
14pub struct RayCast3d {
15 pub origin: Vec3A,
17 pub direction: Dir3A,
19 pub max: f32,
21 direction_recip: Vec3A,
23}
24
25impl RayCast3d {
26 pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Dir3A>, max: f32) -> Self {
30 let direction = direction.into();
31 Self {
32 origin: origin.into(),
33 direction,
34 direction_recip: direction.recip(),
35 max,
36 }
37 }
38
39 pub fn from_ray(ray: Ray3d, max: f32) -> Self {
41 Self::new(ray.origin, ray.direction, max)
42 }
43
44 pub const fn direction_recip(&self) -> Vec3A {
46 self.direction_recip
47 }
48
49 pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {
51 let positive = self.direction.signum().cmpgt(Vec3A::ZERO);
52 let min = Vec3A::select(positive, aabb.min, aabb.max);
53 let max = Vec3A::select(positive, aabb.max, aabb.min);
54
55 let tmin = (min - self.origin) * self.direction_recip;
59 let tmax = (max - self.origin) * self.direction_recip;
60
61 let tmin = tmin.max_element().max(0.);
66 let tmax = tmax.min_element().min(self.max);
67
68 if tmin <= tmax {
69 Some(tmin)
70 } else {
71 None
72 }
73 }
74
75 pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
77 let offset = self.origin - sphere.center;
78 let projected = offset.dot(*self.direction);
79 let closest_point = offset - projected * *self.direction;
80 let distance_squared = sphere.radius().squared() - closest_point.length_squared();
81 if distance_squared < 0.
82 || ops::copysign(projected.squared(), -projected) < -distance_squared
83 {
84 None
85 } else {
86 let toi = -projected - ops::sqrt(distance_squared);
87 if toi > self.max {
88 None
89 } else {
90 Some(toi.max(0.))
91 }
92 }
93 }
94}
95
96impl IntersectsVolume<Aabb3d> for RayCast3d {
97 fn intersects(&self, volume: &Aabb3d) -> bool {
98 self.aabb_intersection_at(volume).is_some()
99 }
100}
101
102impl IntersectsVolume<BoundingSphere> for RayCast3d {
103 fn intersects(&self, volume: &BoundingSphere) -> bool {
104 self.sphere_intersection_at(volume).is_some()
105 }
106}
107
108#[derive(Clone, Debug)]
110#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
111pub struct AabbCast3d {
112 pub ray: RayCast3d,
114 pub aabb: Aabb3d,
116}
117
118impl AabbCast3d {
119 pub fn new(
123 aabb: Aabb3d,
124 origin: impl Into<Vec3A>,
125 direction: impl Into<Dir3A>,
126 max: f32,
127 ) -> Self {
128 Self {
129 ray: RayCast3d::new(origin, direction, max),
130 aabb,
131 }
132 }
133
134 pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self {
136 Self::new(aabb, ray.origin, ray.direction, max)
137 }
138
139 pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option<f32> {
141 aabb.min -= self.aabb.max;
142 aabb.max -= self.aabb.min;
143 self.ray.aabb_intersection_at(&aabb)
144 }
145}
146
147impl IntersectsVolume<Aabb3d> for AabbCast3d {
148 fn intersects(&self, volume: &Aabb3d) -> bool {
149 self.aabb_collision_at(*volume).is_some()
150 }
151}
152
153#[derive(Clone, Debug)]
155#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
156pub struct BoundingSphereCast {
157 pub ray: RayCast3d,
159 pub sphere: BoundingSphere,
161}
162
163impl BoundingSphereCast {
164 pub fn new(
168 sphere: BoundingSphere,
169 origin: impl Into<Vec3A>,
170 direction: impl Into<Dir3A>,
171 max: f32,
172 ) -> Self {
173 Self {
174 ray: RayCast3d::new(origin, direction, max),
175 sphere,
176 }
177 }
178
179 pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self {
181 Self::new(sphere, ray.origin, ray.direction, max)
182 }
183
184 pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option<f32> {
186 sphere.center -= self.sphere.center;
187 sphere.sphere.radius += self.sphere.radius();
188 self.ray.sphere_intersection_at(&sphere)
189 }
190}
191
192impl IntersectsVolume<BoundingSphere> for BoundingSphereCast {
193 fn intersects(&self, volume: &BoundingSphere) -> bool {
194 self.sphere_collision_at(*volume).is_some()
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use bevy_math::{Dir3, Vec3};
202
203 const EPSILON: f32 = 0.001;
204
205 #[test]
206 fn test_ray_intersection_sphere_hits() {
207 for (test, volume, expected_distance) in &[
208 (
209 RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
211 BoundingSphere::new(Vec3::ZERO, 1.),
212 4.,
213 ),
214 (
215 RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
217 BoundingSphere::new(Vec3::ZERO, 1.),
218 4.,
219 ),
220 (
221 RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
223 BoundingSphere::new(Vec3::Y * 3., 2.),
224 1.,
225 ),
226 (
227 RayCast3d::new(Vec3::X, Dir3::Y, 1.),
229 BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),
230 0.99,
231 ),
232 (
233 RayCast3d::new(Vec3::X, Dir3::Y, 90.),
235 BoundingSphere::new(Vec3::Y * 5., 2.),
236 3.268,
237 ),
238 (
239 RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.),
241 BoundingSphere::new(Vec3::Y * 5., 1.),
242 4.996,
243 ),
244 ] {
245 assert!(
246 test.intersects(volume),
247 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
248 );
249 let actual_distance = test.sphere_intersection_at(volume).unwrap();
250 assert!(
251 ops::abs(actual_distance - expected_distance) < EPSILON,
252 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
253 );
254
255 let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
256 assert!(
257 !inverted_ray.intersects(volume),
258 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
259 );
260 }
261 }
262
263 #[test]
264 fn test_ray_intersection_sphere_misses() {
265 for (test, volume) in &[
266 (
267 RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
269 BoundingSphere::new(Vec3::Y * 2., 1.),
270 ),
271 (
272 RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
274 BoundingSphere::new(Vec3::Y * 2., 1.),
275 ),
276 (
277 RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
279 BoundingSphere::new(Vec3::Y * 2., 1.),
280 ),
281 ] {
282 assert!(
283 !test.intersects(volume),
284 "Case:\n Test: {test:?}\n Volume: {volume:?}",
285 );
286 }
287 }
288
289 #[test]
290 fn test_ray_intersection_sphere_inside() {
291 let volume = BoundingSphere::new(Vec3::splat(0.5), 1.);
292 for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
293 for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
294 for max in &[0., 1., 900.] {
295 let test = RayCast3d::new(*origin, *direction, *max);
296
297 assert!(
298 test.intersects(&volume),
299 "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
300 );
301
302 let actual_distance = test.sphere_intersection_at(&volume);
303 assert_eq!(
304 actual_distance,
305 Some(0.),
306 "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
307 );
308 }
309 }
310 }
311 }
312
313 #[test]
314 fn test_ray_intersection_aabb_hits() {
315 for (test, volume, expected_distance) in &[
316 (
317 RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
319 Aabb3d::new(Vec3::ZERO, Vec3::ONE),
320 4.,
321 ),
322 (
323 RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
325 Aabb3d::new(Vec3::ZERO, Vec3::ONE),
326 4.,
327 ),
328 (
329 RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
331 Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),
332 1.,
333 ),
334 (
335 RayCast3d::new(Vec3::X, Dir3::Y, 1.),
337 Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),
338 0.99,
339 ),
340 (
341 RayCast3d::new(Vec3::X, Dir3::Y, 90.),
343 Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),
344 3.,
345 ),
346 (
347 RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
349 Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
350 1.732,
351 ),
352 ] {
353 assert!(
354 test.intersects(volume),
355 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
356 );
357 let actual_distance = test.aabb_intersection_at(volume).unwrap();
358 assert!(
359 ops::abs(actual_distance - expected_distance) < EPSILON,
360 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
361 );
362
363 let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
364 assert!(
365 !inverted_ray.intersects(volume),
366 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
367 );
368 }
369 }
370
371 #[test]
372 fn test_ray_intersection_aabb_misses() {
373 for (test, volume) in &[
374 (
375 RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
377 Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
378 ),
379 (
380 RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 0.99, 1.).unwrap(), 90.),
382 Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
383 ),
384 (
385 RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
387 Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
388 ),
389 ] {
390 assert!(
391 !test.intersects(volume),
392 "Case:\n Test: {test:?}\n Volume: {volume:?}",
393 );
394 }
395 }
396
397 #[test]
398 fn test_ray_intersection_aabb_inside() {
399 let volume = Aabb3d::new(Vec3::splat(0.5), Vec3::ONE);
400 for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
401 for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
402 for max in &[0., 1., 900.] {
403 let test = RayCast3d::new(*origin, *direction, *max);
404
405 assert!(
406 test.intersects(&volume),
407 "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
408 );
409
410 let actual_distance = test.aabb_intersection_at(&volume);
411 assert_eq!(
412 actual_distance,
413 Some(0.),
414 "Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
415 );
416 }
417 }
418 }
419 }
420
421 #[test]
422 fn test_aabb_cast_hits() {
423 for (test, volume, expected_distance) in &[
424 (
425 AabbCast3d::new(Aabb3d::new(Vec3::ZERO, Vec3::ONE), Vec3::ZERO, Dir3::Y, 90.),
427 Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
428 3.,
429 ),
430 (
431 AabbCast3d::new(
433 Aabb3d::new(Vec3::ZERO, Vec3::ONE),
434 Vec3::Y * 10.,
435 -Dir3::Y,
436 90.,
437 ),
438 Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
439 3.,
440 ),
441 (
442 AabbCast3d::new(
444 Aabb3d::new(Vec3::ZERO, Vec3::ONE),
445 Vec3::X * 1.5,
446 Dir3::Y,
447 90.,
448 ),
449 Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
450 3.,
451 ),
452 (
453 AabbCast3d::new(
455 Aabb3d::new(Vec3::X * -2., Vec3::ONE),
456 Vec3::X * 3.,
457 Dir3::Y,
458 90.,
459 ),
460 Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
461 3.,
462 ),
463 ] {
464 assert!(
465 test.intersects(volume),
466 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
467 );
468 let actual_distance = test.aabb_collision_at(*volume).unwrap();
469 assert!(
470 ops::abs(actual_distance - expected_distance) < EPSILON,
471 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
472 );
473
474 let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
475 assert!(
476 !inverted_ray.intersects(volume),
477 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
478 );
479 }
480 }
481
482 #[test]
483 fn test_sphere_cast_hits() {
484 for (test, volume, expected_distance) in &[
485 (
486 BoundingSphereCast::new(
488 BoundingSphere::new(Vec3::ZERO, 1.),
489 Vec3::ZERO,
490 Dir3::Y,
491 90.,
492 ),
493 BoundingSphere::new(Vec3::Y * 5., 1.),
494 3.,
495 ),
496 (
497 BoundingSphereCast::new(
499 BoundingSphere::new(Vec3::ZERO, 1.),
500 Vec3::Y * 10.,
501 -Dir3::Y,
502 90.,
503 ),
504 BoundingSphere::new(Vec3::Y * 5., 1.),
505 3.,
506 ),
507 (
508 BoundingSphereCast::new(
510 BoundingSphere::new(Vec3::ZERO, 1.),
511 Vec3::X * 1.5,
512 Dir3::Y,
513 90.,
514 ),
515 BoundingSphere::new(Vec3::Y * 5., 1.),
516 3.677,
517 ),
518 (
519 BoundingSphereCast::new(
521 BoundingSphere::new(Vec3::X * -1.5, 1.),
522 Vec3::X * 3.,
523 Dir3::Y,
524 90.,
525 ),
526 BoundingSphere::new(Vec3::Y * 5., 1.),
527 3.677,
528 ),
529 ] {
530 assert!(
531 test.intersects(volume),
532 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
533 );
534 let actual_distance = test.sphere_collision_at(*volume).unwrap();
535 assert!(
536 ops::abs(actual_distance - expected_distance) < EPSILON,
537 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
538 );
539
540 let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
541 assert!(
542 !inverted_ray.intersects(volume),
543 "Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
544 );
545 }
546 }
547}