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