1use crate::collider::{Sphere, AABB};
16use crate::math::{Fix128, Vec3Fix};
17use crate::raycast::{sweep_ray_sphere, Ray};
18use crate::solver::RigidBody;
19
20#[cfg(not(feature = "std"))]
21use alloc::vec::Vec;
22
23#[cfg(feature = "parallel")]
24use rayon::prelude::*;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct ShapeCastHit {
33 pub t: Fix128,
35 pub point: Vec3Fix,
37 pub normal: Vec3Fix,
39 pub body_index: usize,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct OverlapResult {
46 pub body_index: usize,
48 pub depth: Fix128,
50}
51
52#[inline]
63#[must_use]
64pub fn sphere_cast(
65 origin: Vec3Fix,
66 radius: Fix128,
67 direction: Vec3Fix,
68 max_distance: Fix128,
69 bodies: &[RigidBody],
70 body_radius: Fix128,
71) -> Option<ShapeCastHit> {
72 let dir_len = direction.length();
73 if dir_len.is_zero() {
74 return None;
75 }
76 let dir_norm = direction / dir_len;
77 let ray = Ray::new(origin, dir_norm);
78
79 let mut closest: Option<ShapeCastHit> = None;
80 let mut best_t = max_distance;
81
82 for (i, body) in bodies.iter().enumerate() {
83 let expanded = Sphere::new(body.position, body_radius + radius);
85
86 if let Some(hit) = sweep_ray_sphere(&ray, &expanded, best_t) {
87 best_t = hit.t;
88 closest = Some(ShapeCastHit {
89 t: hit.t,
90 point: hit.point,
91 normal: hit.normal,
92 body_index: i,
93 });
94 }
95 }
96
97 closest
98}
99
100#[must_use]
117pub fn capsule_cast(
118 capsule_a: Vec3Fix,
119 capsule_b: Vec3Fix,
120 capsule_radius: Fix128,
121 direction: Vec3Fix,
122 max_distance: Fix128,
123 bodies: &[RigidBody],
124 body_radius: Fix128,
125) -> Option<ShapeCastHit> {
126 let dir_len = direction.length();
127 if dir_len.is_zero() {
128 return None;
129 }
130 let d = direction / dir_len;
131 let reach = capsule_radius + body_radius;
132 let axis = capsule_b - capsule_a;
133 let len = axis.length();
134 if len.is_zero() {
135 return sphere_cast(
136 capsule_a,
137 capsule_radius,
138 direction,
139 max_distance,
140 bodies,
141 body_radius,
142 );
143 }
144 let u = axis / len;
145
146 let mut best: Option<ShapeCastHit> = None;
147 for (i, body) in bodies.iter().enumerate() {
148 let limit = best.map_or(max_distance, |b| b.t);
149 let Some(t) = capsule_toi(capsule_a, capsule_b, u, len, reach, d, body.position, limit)
150 else {
151 continue;
152 };
153 if best.is_some_and(|b| t >= b.t) {
154 continue;
155 }
156 let c = body.position;
158 let s = ((c - capsule_a - d * t).dot(u)).max(Fix128::ZERO).min(len);
159 let point = capsule_a + d * t + u * s;
160 let to = point - c;
161 let normal = if to.length_squared().is_zero() {
162 -d
163 } else {
164 to.normalize()
165 };
166 best = Some(ShapeCastHit {
167 t,
168 point,
169 normal,
170 body_index: i,
171 });
172 }
173 best
174}
175
176#[allow(clippy::too_many_arguments)]
180fn capsule_toi(
181 a: Vec3Fix,
182 b: Vec3Fix,
183 u: Vec3Fix,
184 len: Fix128,
185 reach: Fix128,
186 d: Vec3Fix,
187 c: Vec3Fix,
188 max_t: Fix128,
189) -> Option<Fix128> {
190 let s0 = (c - a).dot(u).max(Fix128::ZERO).min(len);
195 let to_body = c - (a + u * s0);
196 if to_body.length_squared() <= reach * reach {
197 return if to_body.length_squared().is_zero() || to_body.dot(d) > Fix128::ZERO {
198 Some(Fix128::ZERO)
199 } else {
200 None
201 };
202 }
203 let ray = Ray::new(c, -d);
204 let mut best: Option<Fix128> = None;
205 let mut take = |t: Fix128| {
206 if t >= Fix128::ZERO && t <= max_t && best.is_none_or(|b| t < b) {
207 best = Some(t);
208 }
209 };
210 for end in [a, b] {
212 if let Some(h) = sweep_ray_sphere(&ray, &Sphere::new(end, reach), max_t) {
213 take(h.t);
214 }
215 }
216 let rd = -d;
218 let oa = c - a;
219 let oa_p = oa - u * oa.dot(u);
220 let rd_p = rd - u * rd.dot(u);
221 let qa = rd_p.dot(rd_p);
222 let qb = oa_p.dot(rd_p);
223 let qc = oa_p.dot(oa_p) - reach * reach;
224 let along = |t: Fix128| oa.dot(u) + rd.dot(u) * t;
225 let on_side = |t: Fix128| {
226 let y = along(t);
227 y >= Fix128::ZERO && y <= len
228 };
229 if qc > Fix128::ZERO && !qa.is_zero() {
230 let disc = qb * qb - qa * qc;
231 if disc >= Fix128::ZERO {
232 let t = (-qb - disc.sqrt()) / qa;
233 if on_side(t) {
234 take(t);
235 }
236 }
237 }
238 best
239}
240
241#[inline]
250#[must_use]
251pub fn overlap_sphere(
252 center: Vec3Fix,
253 radius: Fix128,
254 bodies: &[RigidBody],
255 body_radius: Fix128,
256) -> Vec<OverlapResult> {
257 let mut results = Vec::new();
258 let combined_radius = radius + body_radius;
259 let combined_sq = combined_radius * combined_radius;
260
261 for (i, body) in bodies.iter().enumerate() {
262 let delta = body.position - center;
263 let dist_sq = delta.length_squared();
264
265 if dist_sq < combined_sq {
266 let dist = dist_sq.sqrt();
267 let depth = combined_radius - dist;
268 results.push(OverlapResult {
269 body_index: i,
270 depth,
271 });
272 }
273 }
274
275 results
276}
277
278#[inline]
283#[must_use]
284pub fn overlap_aabb(aabb: &AABB, bodies: &[RigidBody]) -> Vec<OverlapResult> {
285 let mut results = Vec::new();
286
287 for (i, body) in bodies.iter().enumerate() {
288 let p = body.position;
289 if p.x >= aabb.min.x
290 && p.x <= aabb.max.x
291 && p.y >= aabb.min.y
292 && p.y <= aabb.max.y
293 && p.z >= aabb.min.z
294 && p.z <= aabb.max.z
295 {
296 results.push(OverlapResult {
297 body_index: i,
298 depth: Fix128::ZERO, });
300 }
301 }
302
303 results
304}
305
306#[must_use]
312pub fn overlap_aabb_expanded(
313 aabb: &AABB,
314 bodies: &[RigidBody],
315 body_radius: Fix128,
316) -> Vec<OverlapResult> {
317 let r_sq = body_radius * body_radius;
318 bodies
319 .iter()
320 .enumerate()
321 .filter(|(_, body)| {
322 let p = body.position;
323 let nearest = Vec3Fix::new(
324 p.x.max(aabb.min.x).min(aabb.max.x),
325 p.y.max(aabb.min.y).min(aabb.max.y),
326 p.z.max(aabb.min.z).min(aabb.max.z),
327 );
328 (p - nearest).length_squared() <= r_sq
329 })
330 .map(|(i, _)| OverlapResult {
331 body_index: i,
332 depth: Fix128::ZERO,
333 })
334 .collect()
335}
336
337#[derive(Clone, Copy, Debug, PartialEq, Eq)]
343pub struct BatchRayQuery {
344 pub origin: Vec3Fix,
346 pub direction: Vec3Fix,
348 pub max_distance: Fix128,
350}
351
352#[must_use]
364pub fn batch_raycast(
365 queries: &[BatchRayQuery],
366 bodies: &[RigidBody],
367 body_radius: Fix128,
368) -> Vec<Option<ShapeCastHit>> {
369 #[cfg(feature = "parallel")]
370 {
371 queries
372 .par_iter()
373 .map(|q| {
374 sphere_cast(
375 q.origin,
376 Fix128::ZERO,
377 q.direction,
378 q.max_distance,
379 bodies,
380 body_radius,
381 )
382 })
383 .collect()
384 }
385
386 #[cfg(not(feature = "parallel"))]
387 {
388 queries
389 .iter()
390 .map(|q| {
391 sphere_cast(
392 q.origin,
393 Fix128::ZERO,
394 q.direction,
395 q.max_distance,
396 bodies,
397 body_radius,
398 )
399 })
400 .collect()
401 }
402}
403
404#[must_use]
414pub fn batch_sphere_cast(
415 origins: &[Vec3Fix],
416 radius: Fix128,
417 directions: &[Vec3Fix],
418 max_distance: Fix128,
419 bodies: &[RigidBody],
420 body_radius: Fix128,
421) -> Vec<Option<ShapeCastHit>> {
422 assert_eq!(origins.len(), directions.len());
423
424 #[cfg(feature = "parallel")]
425 {
426 origins
427 .par_iter()
428 .zip(directions.par_iter())
429 .map(|(origin, direction)| {
430 sphere_cast(
431 *origin,
432 radius,
433 *direction,
434 max_distance,
435 bodies,
436 body_radius,
437 )
438 })
439 .collect()
440 }
441
442 #[cfg(not(feature = "parallel"))]
443 {
444 origins
445 .iter()
446 .zip(directions.iter())
447 .map(|(origin, direction)| {
448 sphere_cast(
449 *origin,
450 radius,
451 *direction,
452 max_distance,
453 bodies,
454 body_radius,
455 )
456 })
457 .collect()
458 }
459}
460
461#[must_use]
471pub fn overlap_sphere_bvh(
472 center: Vec3Fix,
473 radius: Fix128,
474 bodies: &[RigidBody],
475 body_radius: Fix128,
476 bvh: &crate::bvh::LinearBvh,
477) -> Vec<OverlapResult> {
478 let combined_radius = radius + body_radius;
479 let query_aabb = AABB::new(
480 Vec3Fix::new(
481 center.x - combined_radius,
482 center.y - combined_radius,
483 center.z - combined_radius,
484 ),
485 Vec3Fix::new(
486 center.x + combined_radius,
487 center.y + combined_radius,
488 center.z + combined_radius,
489 ),
490 );
491
492 let candidates = bvh.query(&query_aabb);
493 let combined_sq = combined_radius * combined_radius;
494 let mut results = Vec::new();
495
496 for &idx in &candidates {
497 let i = idx as usize;
498 if i >= bodies.len() {
499 continue;
500 }
501 let delta = bodies[i].position - center;
502 let dist_sq = delta.length_squared();
503 if dist_sq < combined_sq {
504 let dist = dist_sq.sqrt();
505 let depth = combined_radius - dist;
506 results.push(OverlapResult {
507 body_index: i,
508 depth,
509 });
510 }
511 }
512
513 results
514}
515
516#[must_use]
521pub fn overlap_aabb_bvh(
522 aabb: &AABB,
523 bodies: &[RigidBody],
524 bvh: &crate::bvh::LinearBvh,
525) -> Vec<OverlapResult> {
526 let candidates = bvh.query(aabb);
527 let mut results = Vec::new();
528
529 for &idx in &candidates {
530 let i = idx as usize;
531 if i >= bodies.len() {
532 continue;
533 }
534 let p = bodies[i].position;
535 if p.x >= aabb.min.x
536 && p.x <= aabb.max.x
537 && p.y >= aabb.min.y
538 && p.y <= aabb.max.y
539 && p.z >= aabb.min.z
540 && p.z <= aabb.max.z
541 {
542 results.push(OverlapResult {
543 body_index: i,
544 depth: Fix128::ZERO,
545 });
546 }
547 }
548
549 results
550}
551
552#[cfg(all(test, feature = "std"))]
557mod tests {
558 use super::*;
559
560 fn make_bodies() -> Vec<RigidBody> {
561 vec![
562 RigidBody::new_static(Vec3Fix::from_int(0, 0, 0)),
563 RigidBody::new_static(Vec3Fix::from_int(5, 0, 0)),
564 RigidBody::new_static(Vec3Fix::from_int(10, 0, 0)),
565 RigidBody::new_static(Vec3Fix::from_int(0, 5, 0)),
566 ]
567 }
568
569 #[test]
570 fn test_sphere_cast_hit() {
571 let bodies = make_bodies();
572 let hit = sphere_cast(
573 Vec3Fix::from_int(-10, 0, 0),
574 Fix128::from_ratio(1, 2), Vec3Fix::UNIT_X,
576 Fix128::from_int(100),
577 &bodies,
578 Fix128::ONE,
579 );
580 assert!(hit.is_some(), "Sphere cast should hit body at origin");
581 let h = hit.expect("sphere cast should hit");
582 assert_eq!(h.body_index, 0);
583 assert!(h.t > Fix128::from_int(5), "Hit should be before the body");
585 }
586
587 #[test]
588 fn test_sphere_cast_miss() {
589 let bodies = make_bodies();
590 let hit = sphere_cast(
591 Vec3Fix::from_int(-10, 10, 0), Fix128::from_ratio(1, 2),
593 Vec3Fix::UNIT_X,
594 Fix128::from_int(100),
595 &bodies,
596 Fix128::ONE,
597 );
598 assert!(hit.is_none(), "Sphere cast should miss (y offset)");
599 }
600
601 #[test]
602 fn test_capsule_cast() {
603 let bodies = make_bodies();
604 let hit = capsule_cast(
605 Vec3Fix::from_int(-10, -1, 0),
606 Vec3Fix::from_int(-10, 1, 0),
607 Fix128::from_ratio(1, 2),
608 Vec3Fix::UNIT_X,
609 Fix128::from_int(100),
610 &bodies,
611 Fix128::ONE,
612 );
613 assert!(hit.is_some(), "Capsule cast should hit");
614 }
615
616 #[test]
617 fn test_overlap_sphere() {
618 let bodies = make_bodies();
619 let results = overlap_sphere(
620 Vec3Fix::from_int(0, 0, 0),
621 Fix128::from_int(3),
622 &bodies,
623 Fix128::ONE,
624 );
625 assert_eq!(results.len(), 1);
629 assert_eq!(results[0].body_index, 0);
630 }
631
632 #[test]
633 fn test_overlap_sphere_multiple() {
634 let bodies = make_bodies();
635 let results = overlap_sphere(
636 Vec3Fix::from_int(2, 0, 0),
637 Fix128::from_int(5),
638 &bodies,
639 Fix128::ONE,
640 );
641 assert!(
646 results.len() >= 2,
647 "Should find at least 2 overlaps, got {}",
648 results.len()
649 );
650 }
651
652 #[test]
653 fn test_overlap_aabb() {
654 let bodies = make_bodies();
655 let aabb = AABB::new(Vec3Fix::from_int(-1, -1, -1), Vec3Fix::from_int(6, 1, 1));
656 let results = overlap_aabb(&aabb, &bodies);
657 assert_eq!(results.len(), 2);
662 }
663
664 #[test]
665 fn test_batch_raycast() {
666 let bodies = make_bodies();
667 let queries = vec![
668 BatchRayQuery {
669 origin: Vec3Fix::from_int(-10, 0, 0),
670 direction: Vec3Fix::UNIT_X,
671 max_distance: Fix128::from_int(100),
672 },
673 BatchRayQuery {
674 origin: Vec3Fix::from_int(-10, 10, 0), direction: Vec3Fix::UNIT_X,
676 max_distance: Fix128::from_int(100),
677 },
678 ];
679 let results = batch_raycast(&queries, &bodies, Fix128::ONE);
680 assert_eq!(results.len(), 2);
681 assert!(results[0].is_some(), "First ray should hit");
682 assert!(results[1].is_none(), "Second ray should miss");
683 }
684
685 #[test]
686 fn test_batch_sphere_cast() {
687 let bodies = make_bodies();
688 let origins = vec![Vec3Fix::from_int(-10, 0, 0), Vec3Fix::from_int(-10, 0, 0)];
689 let directions = vec![
690 Vec3Fix::UNIT_X,
691 Vec3Fix::UNIT_Y, ];
693 let results = batch_sphere_cast(
694 &origins,
695 Fix128::from_ratio(1, 2),
696 &directions,
697 Fix128::from_int(100),
698 &bodies,
699 Fix128::ONE,
700 );
701 assert_eq!(results.len(), 2);
702 assert!(results[0].is_some(), "X-direction should hit");
703 }
704
705 #[test]
706 fn test_overlap_aabb_expanded() {
707 let bodies = make_bodies();
708 let aabb = AABB::new(Vec3Fix::from_int(4, -1, -1), Vec3Fix::from_int(6, 1, 1));
709 let results = overlap_aabb_expanded(&aabb, &bodies, Fix128::from_int(2));
710 assert!(!results.is_empty(), "Should find body at (5,0,0)");
716 }
717
718 #[test]
719 fn test_overlap_sphere_bvh() {
720 use crate::bvh::{BvhPrimitive, LinearBvh};
721
722 let bodies = make_bodies();
723 let body_radius = Fix128::ONE;
724
725 let prims: Vec<BvhPrimitive> = bodies
727 .iter()
728 .enumerate()
729 .map(|(i, b)| BvhPrimitive {
730 aabb: AABB::new(
731 Vec3Fix::new(
732 b.position.x - body_radius,
733 b.position.y - body_radius,
734 b.position.z - body_radius,
735 ),
736 Vec3Fix::new(
737 b.position.x + body_radius,
738 b.position.y + body_radius,
739 b.position.z + body_radius,
740 ),
741 ),
742 index: i as u32,
743 morton: 0,
744 })
745 .collect();
746 let bvh = LinearBvh::build(prims);
747
748 let results = overlap_sphere_bvh(
749 Vec3Fix::from_int(0, 0, 0),
750 Fix128::from_int(3),
751 &bodies,
752 body_radius,
753 &bvh,
754 );
755 assert_eq!(results.len(), 1);
757 assert_eq!(results[0].body_index, 0);
758 }
759
760 #[test]
761 fn test_overlap_aabb_bvh() {
762 use crate::bvh::{BvhPrimitive, LinearBvh};
763
764 let bodies = make_bodies();
765 let body_radius = Fix128::ONE;
766
767 let prims: Vec<BvhPrimitive> = bodies
768 .iter()
769 .enumerate()
770 .map(|(i, b)| BvhPrimitive {
771 aabb: AABB::new(
772 Vec3Fix::new(
773 b.position.x - body_radius,
774 b.position.y - body_radius,
775 b.position.z - body_radius,
776 ),
777 Vec3Fix::new(
778 b.position.x + body_radius,
779 b.position.y + body_radius,
780 b.position.z + body_radius,
781 ),
782 ),
783 index: i as u32,
784 morton: 0,
785 })
786 .collect();
787 let bvh = LinearBvh::build(prims);
788
789 let aabb = AABB::new(Vec3Fix::from_int(-1, -1, -1), Vec3Fix::from_int(6, 1, 1));
790 let results = overlap_aabb_bvh(&aabb, &bodies, &bvh);
791 assert_eq!(results.len(), 2);
793 }
794
795 fn cast(direction: Vec3Fix, max: i64, bodies: &[RigidBody]) -> Option<ShapeCastHit> {
798 let half = Fix128::from_ratio(1, 2);
799 capsule_cast(
800 Vec3Fix::from_int(-1, 0, 0),
801 Vec3Fix::from_int(1, 0, 0),
802 half,
803 direction,
804 Fix128::from_int(max),
805 bodies,
806 half,
807 )
808 }
809
810 fn body_at(p: Vec3Fix) -> RigidBody {
811 RigidBody::new_static(p)
812 }
813
814 #[test]
815 fn capsule_cast_starting_in_overlap_hits_at_zero_only_when_closing() {
816 let bodies = [body_at(Vec3Fix::new(
818 Fix128::ZERO,
819 Fix128::from_ratio(4, 5),
820 Fix128::ZERO,
821 ))];
822 assert_eq!(cast(Vec3Fix::from_int(0, -1, 0), 10, &bodies), None);
824 let hit = cast(Vec3Fix::from_int(0, 1, 0), 10, &bodies).expect("closing overlap");
826 assert_eq!(hit.t, Fix128::ZERO);
827 assert_eq!(hit.point, Vec3Fix::ZERO);
828 let off = (hit.normal - Vec3Fix::from_int(0, -1, 0)).length_squared();
830 assert!(
831 off < Fix128::from_ratio(1, 1_000_000_000_000),
832 "{:?}",
833 hit.normal
834 );
835 let on_axis = [body_at(Vec3Fix::ZERO)];
837 let hit = cast(Vec3Fix::from_int(0, 0, 1), 10, &on_axis).expect("on axis");
838 assert_eq!(hit.t, Fix128::ZERO);
839 assert_eq!(hit.normal, Vec3Fix::from_int(0, 0, -1));
840 }
841
842 #[test]
843 fn capsule_cast_side_and_end_cap_times_of_impact() {
844 let side = [body_at(Vec3Fix::from_int(0, 5, 0))];
847 let hit = cast(Vec3Fix::from_int(0, 1, 0), 10, &side).expect("side hit");
848 assert_eq!(hit.t, Fix128::from_int(4));
849 assert_eq!(hit.point, Vec3Fix::from_int(0, 4, 0));
850 assert_eq!(hit.normal, Vec3Fix::from_int(0, -1, 0));
851 assert_eq!(cast(Vec3Fix::from_int(0, 1, 0), 3, &side), None);
853 let cap = [body_at(Vec3Fix::from_int(4, 0, 0))];
855 let hit = cast(Vec3Fix::from_int(1, 0, 0), 10, &cap).expect("cap hit");
856 assert_eq!(hit.t, Fix128::from_int(2));
857 assert_eq!(hit.point, Vec3Fix::from_int(3, 0, 0));
858 assert_eq!(hit.normal, Vec3Fix::from_int(-1, 0, 0));
859 }
860
861 #[test]
862 fn capsule_cast_picks_the_earliest_body_and_ties_go_to_the_lowest_index() {
863 let p = |y| body_at(Vec3Fix::from_int(0, y, 0));
864 let hit = cast(Vec3Fix::from_int(0, 1, 0), 10, &[p(8), p(5)]).expect("hit");
866 assert_eq!((hit.body_index, hit.t), (1, Fix128::from_int(4)));
867 let hit = cast(Vec3Fix::from_int(0, 1, 0), 10, &[p(5), p(5)]).expect("hit");
869 assert_eq!(hit.body_index, 0);
870 }
871
872 #[test]
873 fn capsule_cast_degenerate_inputs() {
874 let bodies = [body_at(Vec3Fix::from_int(0, 5, 0))];
875 assert_eq!(cast(Vec3Fix::ZERO, 10, &bodies), None);
877 let half = Fix128::from_ratio(1, 2);
879 let hit = capsule_cast(
880 Vec3Fix::ZERO,
881 Vec3Fix::ZERO,
882 half,
883 Vec3Fix::from_int(0, 1, 0),
884 Fix128::from_int(10),
885 &bodies,
886 half,
887 )
888 .expect("sphere fallback");
889 assert_eq!(hit.t, Fix128::from_int(4));
890 assert_eq!(
892 sphere_cast(
893 Vec3Fix::ZERO,
894 half,
895 Vec3Fix::ZERO,
896 Fix128::ONE,
897 &bodies,
898 half
899 ),
900 None
901 );
902 }
903}