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