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