1use crate::collision::ContactPoint;
27use crate::components::ColliderShape;
28use crate::gjk::Gjk;
29use gizmo_math::{Quat, Vec3};
30
31pub struct NarrowPhase;
36
37impl NarrowPhase {
38 pub fn sphere_sphere(pos_a: Vec3, r_a: f32, pos_b: Vec3, r_b: f32) -> Option<ContactPoint> {
42 let d = pos_b - pos_a;
43 let d2 = d.length_squared();
44 let rsum = r_a + r_b;
45
46 if d2 >= rsum * rsum || d2 < 1e-10 {
48 return None;
49 }
50
51 let dist = d2.sqrt();
52 let normal = d / dist; Some(mk_contact(pos_a + normal * r_a, normal, rsum - dist))
54 }
55
56 pub fn sphere_plane(
61 sph_pos: Vec3,
62 r: f32,
63 plane_n: Vec3,
64 plane_d: f32,
65 ) -> Option<ContactPoint> {
66 let signed_dist = sph_pos.dot(plane_n) - plane_d;
68 if signed_dist >= r {
69 return None; }
71 let point = sph_pos - plane_n * signed_dist;
73 Some(mk_contact(point, -plane_n, r - signed_dist))
75 }
76
77 pub fn box_plane(
81 bpos: Vec3,
82 brot: Quat,
83 half: Vec3,
84 plane_n: Vec3,
85 plane_d: f32,
86 ) -> Vec<ContactPoint> {
87 box_corners(bpos, brot, half)
88 .iter()
89 .filter_map(|&corner| {
90 let signed_dist = corner.dot(plane_n) - plane_d;
91 if signed_dist < 0.0 {
92 Some(mk_contact(
94 corner - plane_n * signed_dist,
95 -plane_n,
96 -signed_dist,
97 ))
98 } else {
99 None
100 }
101 })
102 .collect()
103 }
104
105 pub fn shape_plane(
108 shape: &ColliderShape,
109 pos: Vec3,
110 rot: Quat,
111 plane_n: Vec3,
112 plane_d: f32,
113 ) -> Option<ContactPoint> {
114 let deepest = Gjk::support_point(shape, pos, rot, -plane_n);
117 let signed_dist = deepest.dot(plane_n) - plane_d;
118 if signed_dist < 0.0 {
119 Some(mk_contact(
120 deepest - plane_n * signed_dist,
121 -plane_n,
122 -signed_dist,
123 ))
124 } else {
125 None
126 }
127 }
128
129 pub fn box_box(
136 pos_a: Vec3,
137 rot_a: Quat,
138 ha: Vec3,
139 pos_b: Vec3,
140 rot_b: Quat,
141 hb: Vec3,
142 ) -> Vec<ContactPoint> {
143 let ax = [
145 rot_a.mul_vec3(Vec3::X),
146 rot_a.mul_vec3(Vec3::Y),
147 rot_a.mul_vec3(Vec3::Z),
148 ];
149 let bx = [
150 rot_b.mul_vec3(Vec3::X),
151 rot_b.mul_vec3(Vec3::Y),
152 rot_b.mul_vec3(Vec3::Z),
153 ];
154 let ha_ = [ha.x, ha.y, ha.z];
155 let hb_ = [hb.x, hb.y, hb.z];
156 let t = pos_b - pos_a; let mut axes = [Vec3::ZERO; 15];
163 let mut n_axes = 0usize;
164
165 for &a in &ax {
166 axes[n_axes] = a;
167 n_axes += 1;
168 }
169 for &b in &bx {
170 axes[n_axes] = b;
171 n_axes += 1;
172 }
173
174 for &a in &ax {
175 for &b in &bx {
176 let c = a.cross(b);
177 let len_sq = c.length_squared();
178 if len_sq > 1e-6 {
179 axes[n_axes] = c * len_sq.sqrt().recip();
181 n_axes += 1;
182 }
183 }
184 }
185
186 let mut min_pen = f32::MAX;
188 let mut best_axis = Vec3::Y;
189 let mut flip = false;
190
191 for &axis in &axes[..n_axes] {
192 let pen = sat_penetration(&axis, &ax, &ha_, &bx, &hb_, t);
193 if pen < 0.0 {
194 return vec![]; }
196 if pen < min_pen {
197 min_pen = pen;
198 best_axis = axis;
199 flip = t.dot(axis) < 0.0;
201 }
202 }
203
204 let normal = if flip { -best_axis } else { best_axis };
205
206 let (ref_pos, ref_rot, ref_h, inc_pos, inc_rot, inc_h) = if is_face_axis(normal, &ax, 0.707)
211 {
212 (pos_a, rot_a, ha, pos_b, rot_b, hb)
213 } else if is_face_axis(normal, &bx, 0.707) {
214 (pos_b, rot_b, hb, pos_a, rot_a, ha)
215 } else {
216 let dot_a = ax
218 .iter()
219 .map(|a| a.dot(normal).abs())
220 .fold(0.0f32, f32::max);
221 let dot_b = bx
222 .iter()
223 .map(|b| b.dot(normal).abs())
224 .fold(0.0f32, f32::max);
225 if dot_a >= dot_b {
226 (pos_a, rot_a, ha, pos_b, rot_b, hb)
227 } else {
228 (pos_b, rot_b, hb, pos_a, rot_a, ha)
229 }
230 };
231
232 let mut contacts = clip_box_box(
234 normal, min_pen, ref_pos, ref_rot, ref_h, inc_pos, inc_rot, inc_h,
235 );
236
237 if contacts.is_empty() {
242 contacts = clip_box_box(
243 -normal, min_pen, inc_pos, inc_rot, inc_h, ref_pos, ref_rot, ref_h,
244 );
245 for c in &mut contacts {
246 c.normal = -c.normal; }
248 }
249
250 if contacts.is_empty() {
253 let shape_a = ColliderShape::Box(crate::components::BoxShape { half_extents: ha });
254 let shape_b = ColliderShape::Box(crate::components::BoxShape { half_extents: hb });
255 if let Some(c) = Gjk::get_contact(&shape_a, pos_a, rot_a, &shape_b, pos_b, rot_b) {
256 contacts.push(c);
257 }
258 }
259
260 contacts
261 }
262
263 pub fn test_collision(
272 shape_a: &ColliderShape,
273 pos_a: Vec3,
274 rot_a: Quat,
275 shape_b: &ColliderShape,
276 pos_b: Vec3,
277 rot_b: Quat,
278 ) -> Option<ContactPoint> {
279 let contacts = Self::test_collision_manifold(shape_a, pos_a, rot_a, shape_b, pos_b, rot_b);
280 contacts
281 .into_iter()
282 .max_by(|a, b| a.penetration.total_cmp(&b.penetration))
283 }
284
285 pub fn test_collision_manifold(
290 shape_a: &ColliderShape,
291 pos_a: Vec3,
292 rot_a: Quat,
293 shape_b: &ColliderShape,
294 pos_b: Vec3,
295 rot_b: Quat,
296 ) -> Vec<ContactPoint> {
297 if let ColliderShape::Compound(parts) = shape_a {
299 return parts
300 .iter()
301 .flat_map(|(local_t, sub)| {
302 let wp = pos_a + rot_a.mul_vec3(local_t.position);
303 let wr = rot_a * local_t.rotation;
304 Self::test_collision_manifold(sub, wp, wr, shape_b, pos_b, rot_b)
305 })
306 .collect();
307 }
308 if let ColliderShape::Compound(parts) = shape_b {
309 return parts
310 .iter()
311 .flat_map(|(local_t, sub)| {
312 let wp = pos_b + rot_b.mul_vec3(local_t.position);
313 let wr = rot_b * local_t.rotation;
314 Self::test_collision_manifold(shape_a, pos_a, rot_a, sub, wp, wr)
315 })
316 .collect();
317 }
318
319 let mut contacts: Vec<ContactPoint> = match (shape_a, shape_b) {
321 (ColliderShape::Sphere(sa), ColliderShape::Sphere(sb)) => {
323 Self::sphere_sphere(pos_a, sa.radius, pos_b, sb.radius)
324 .into_iter()
325 .collect()
326 }
327
328 (ColliderShape::Sphere(s), ColliderShape::Plane(p)) => {
330 Self::sphere_plane(pos_a, s.radius, p.normal, p.distance)
331 .into_iter()
332 .collect()
333 }
334
335 (ColliderShape::Plane(p), ColliderShape::Sphere(s)) => {
337 Self::sphere_plane(pos_b, s.radius, p.normal, p.distance)
338 .map(|mut c| {
339 c.normal = -c.normal;
340 c
341 })
342 .into_iter()
343 .collect()
344 }
345
346 (ColliderShape::Box(b), ColliderShape::Plane(p)) => {
348 Self::box_plane(pos_a, rot_a, b.half_extents, p.normal, p.distance)
349 }
350
351 (ColliderShape::Plane(p), ColliderShape::Box(b)) => {
353 let mut cs = Self::box_plane(pos_b, rot_b, b.half_extents, p.normal, p.distance);
354 for c in &mut cs {
355 c.normal = -c.normal;
356 }
357 cs
358 }
359
360 (ColliderShape::Box(ba), ColliderShape::Box(bb)) => {
362 Self::box_box(pos_a, rot_a, ba.half_extents, pos_b, rot_b, bb.half_extents)
363 }
364
365 (_, ColliderShape::Plane(p)) => {
367 Self::shape_plane(shape_a, pos_a, rot_a, p.normal, p.distance)
368 .into_iter()
369 .collect()
370 }
371
372 (ColliderShape::Plane(p), _) => {
374 Self::shape_plane(shape_b, pos_b, rot_b, p.normal, p.distance)
375 .map(|mut c| {
376 c.normal = -c.normal;
377 c
378 })
379 .into_iter()
380 .collect()
381 }
382
383 _ => Gjk::get_contact(shape_a, pos_a, rot_a, shape_b, pos_b, rot_b)
385 .into_iter()
386 .collect(),
387 };
388
389 for c in &mut contacts {
391 c.local_point_a = c.point - pos_a;
392 c.local_point_b = c.point - pos_b;
393 }
394
395 contacts
396 }
397}
398
399#[inline]
408fn sat_penetration(
409 axis: &Vec3,
410 ax: &[Vec3; 3],
411 ha: &[f32; 3],
412 bx: &[Vec3; 3],
413 hb: &[f32; 3],
414 t: Vec3,
415) -> f32 {
416 let proj_a: f32 = ax
417 .iter()
418 .zip(ha)
419 .map(|(e, &h)| e.dot(*axis).abs() * h)
420 .sum();
421 let proj_b: f32 = bx
422 .iter()
423 .zip(hb)
424 .map(|(e, &h)| e.dot(*axis).abs() * h)
425 .sum();
426 let dist = t.dot(*axis).abs();
427 proj_a + proj_b - dist
428}
429
430#[inline]
435fn is_face_axis(normal: Vec3, axes: &[Vec3; 3], threshold: f32) -> bool {
436 axes.iter().any(|a| a.dot(normal).abs() > threshold)
437}
438
439fn box_corners(pos: Vec3, rot: Quat, h: Vec3) -> [Vec3; 8] {
445 const SIGNS: [(f32, f32, f32); 8] = [
446 (1., 1., 1.),
447 (-1., 1., 1.),
448 (1., -1., 1.),
449 (-1., -1., 1.),
450 (1., 1., -1.),
451 (-1., 1., -1.),
452 (1., -1., -1.),
453 (-1., -1., -1.),
454 ];
455 SIGNS.map(|(sx, sy, sz)| pos + rot.mul_vec3(Vec3::new(sx * h.x, sy * h.y, sz * h.z)))
456}
457
458#[inline]
460fn mk_contact(point: Vec3, normal: Vec3, penetration: f32) -> ContactPoint {
461 ContactPoint {
462 point,
463 normal,
464 penetration,
465 ..Default::default()
466 }
467}
468
469fn select_4_contacts(contacts: Vec<ContactPoint>) -> Vec<ContactPoint> {
476 if contacts.len() <= 4 {
477 return contacts;
478 }
479
480 let n = contacts.len();
481
482 let i0 = (0..n)
484 .max_by(|&a, &b| contacts[a].penetration.total_cmp(&contacts[b].penetration))
485 .unwrap();
486
487 let mut chosen = vec![i0];
488
489 for _ in 0..3 {
491 if chosen.len() == n {
492 break;
493 }
494 let next = (0..n).filter(|i| !chosen.contains(i)).max_by(|&a, &b| {
495 let da = chosen
496 .iter()
497 .map(|&c| (contacts[c].point - contacts[a].point).length_squared())
498 .fold(f32::INFINITY, f32::min);
499 let db = chosen
500 .iter()
501 .map(|&c| (contacts[c].point - contacts[b].point).length_squared())
502 .fold(f32::INFINITY, f32::min);
503 da.total_cmp(&db)
504 });
505 if let Some(idx) = next {
506 chosen.push(idx);
507 }
508 }
509
510 chosen.iter().map(|&i| contacts[i]).collect()
511}
512
513fn clip_box_box(
522 normal: Vec3,
523 _min_pen: f32,
524 ref_pos: Vec3,
525 ref_rot: Quat,
526 ref_h: Vec3,
527 inc_pos: Vec3,
528 inc_rot: Quat,
529 inc_h: Vec3,
530) -> Vec<ContactPoint> {
531 let ref_axes = [
532 ref_rot.mul_vec3(Vec3::X),
533 ref_rot.mul_vec3(Vec3::Y),
534 ref_rot.mul_vec3(Vec3::Z),
535 ];
536 let ref_h_arr = [ref_h.x, ref_h.y, ref_h.z];
537
538 let (face_idx, _) = ref_axes
540 .iter()
541 .enumerate()
542 .map(|(i, a)| (i, a.dot(normal).abs()))
543 .fold(
544 (0, 0.0f32),
545 |(bi, bv), (i, v)| if v > bv { (i, v) } else { (bi, bv) },
546 );
547
548 let face_axis = ref_axes[face_idx];
549 let face_dir = if face_axis.dot(normal) > 0.0 {
551 face_axis
552 } else {
553 -face_axis
554 };
555
556 let ref_face_d = (ref_pos + face_dir * ref_h_arr[face_idx]).dot(face_dir);
558
559 let t0 = ref_axes[(face_idx + 1) % 3];
561 let t1 = ref_axes[(face_idx + 2) % 3];
562 let e0 = ref_h_arr[(face_idx + 1) % 3];
563 let e1 = ref_h_arr[(face_idx + 2) % 3];
564
565 const SLAB_TOLERANCE: f32 = 1e-3;
567
568 let contacts: Vec<ContactPoint> = box_corners(inc_pos, inc_rot, inc_h)
569 .iter()
570 .filter_map(|&corner| {
571 let signed_depth = ref_face_d - corner.dot(face_dir);
573 if signed_depth <= 0.0 {
574 return None;
575 } let local = corner - ref_pos;
579 if local.dot(t0).abs() > e0 + SLAB_TOLERANCE {
580 return None;
581 }
582 if local.dot(t1).abs() > e1 + SLAB_TOLERANCE {
583 return None;
584 }
585
586 let depth = signed_depth.max(0.0);
590
591 Some(mk_contact(corner, normal, depth))
592 })
593 .collect();
594
595 select_4_contacts(contacts)
596}
597
598#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::components::BoxShape;
606
607 fn box_shape(half: f32) -> ColliderShape {
608 ColliderShape::Box(BoxShape {
609 half_extents: Vec3::splat(half),
610 })
611 }
612
613 #[test]
616 fn sphere_sphere_overlap_produces_contact() {
617 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(1.5, 0., 0.), 1.0);
618 assert!(c.is_some(), "overlapping spheres must collide");
619 let c = c.unwrap();
620 assert!(c.penetration > 0.0, "penetration must be positive");
621 assert!(
622 (c.normal.x - 1.0).abs() < 0.01,
623 "normal must point A→B (+X)"
624 );
625 }
626
627 #[test]
628 fn sphere_sphere_separated_returns_none() {
629 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(3.0, 0., 0.), 1.0);
630 assert!(c.is_none(), "separated spheres must not collide");
631 }
632
633 #[test]
634 fn sphere_sphere_touching_returns_none() {
635 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(2.0, 0., 0.), 1.0);
637 assert!(
638 c.is_none(),
639 "just-touching spheres should not produce contact"
640 );
641 }
642
643 #[test]
646 fn sphere_plane_below_produces_contact() {
647 let c = NarrowPhase::sphere_plane(Vec3::new(0., 0.5, 0.), 1.0, Vec3::Y, 0.0);
650 assert!(c.is_some());
651 let c = c.unwrap();
652 assert!(c.penetration > 0.0);
653 assert!((c.normal.y + 1.0).abs() < 0.01, "normal should be -Y");
655 }
656
657 #[test]
658 fn sphere_plane_above_returns_none() {
659 let c = NarrowPhase::sphere_plane(Vec3::new(0., 2.0, 0.), 1.0, Vec3::Y, 0.0);
660 assert!(c.is_none());
661 }
662
663 #[test]
666 fn box_plane_four_contacts_when_flat_on_ground() {
667 let contacts = NarrowPhase::box_plane(
670 Vec3::new(0., 0.5, 0.),
671 Quat::IDENTITY,
672 Vec3::splat(1.0),
673 Vec3::Y,
674 0.0,
675 );
676 assert_eq!(contacts.len(), 4, "flat box should have 4 contacts");
677 for c in &contacts {
678 assert!(c.penetration > 0.0, "each contact must penetrate");
679 assert!(
680 (c.normal.y + 1.0).abs() < 0.01,
681 "normal must be -Y (box→plane)"
682 );
683 }
684 }
685
686 #[test]
687 fn box_plane_no_contact_when_above() {
688 let contacts = NarrowPhase::box_plane(
689 Vec3::new(0., 2.0, 0.),
690 Quat::IDENTITY,
691 Vec3::splat(1.0),
692 Vec3::Y,
693 0.0,
694 );
695 assert!(contacts.is_empty());
696 }
697
698 #[test]
701 fn box_box_overlap_produces_contacts() {
702 let contacts = NarrowPhase::box_box(
703 Vec3::ZERO,
704 Quat::IDENTITY,
705 Vec3::splat(1.0),
706 Vec3::new(1.5, 0., 0.),
707 Quat::IDENTITY,
708 Vec3::splat(1.0),
709 );
710 assert!(!contacts.is_empty(), "overlapping boxes must have contacts");
711 for c in &contacts {
712 assert!(c.penetration >= 0.0);
713 }
714 }
715
716 #[test]
717 fn box_box_separated_returns_empty() {
718 let contacts = NarrowPhase::box_box(
719 Vec3::ZERO,
720 Quat::IDENTITY,
721 Vec3::splat(1.0),
722 Vec3::new(5.0, 0., 0.),
723 Quat::IDENTITY,
724 Vec3::splat(1.0),
725 );
726 assert!(
727 contacts.is_empty(),
728 "separated boxes must not produce contacts"
729 );
730 }
731
732 #[test]
733 fn box_box_rotated_45_produces_contacts() {
734 let rot45 = Quat::from_rotation_y(std::f32::consts::FRAC_PI_4);
735 let contacts = NarrowPhase::box_box(
736 Vec3::ZERO,
737 Quat::IDENTITY,
738 Vec3::splat(0.8),
739 Vec3::new(1.0, 0., 0.),
740 rot45,
741 Vec3::splat(0.8),
742 );
743 assert!(
744 !contacts.is_empty(),
745 "rotated overlapping boxes must collide"
746 );
747 }
748
749 #[test]
750 fn box_box_face_contact_normal_is_axis_aligned() {
751 let contacts = NarrowPhase::box_box(
753 Vec3::ZERO,
754 Quat::IDENTITY,
755 Vec3::splat(1.0),
756 Vec3::new(1.5, 0., 0.),
757 Quat::IDENTITY,
758 Vec3::splat(1.0),
759 );
760 assert!(!contacts.is_empty());
761 for c in &contacts {
762 assert!(
763 c.normal.x.abs() > 0.9,
764 "face contact normal should be X-aligned, got {:?}",
765 c.normal
766 );
767 }
768 }
769
770 #[test]
771 fn box_box_contact_count_at_most_4() {
772 let contacts = NarrowPhase::box_box(
773 Vec3::ZERO,
774 Quat::IDENTITY,
775 Vec3::splat(1.0),
776 Vec3::new(1.5, 0., 0.),
777 Quat::IDENTITY,
778 Vec3::splat(1.0),
779 );
780 assert!(
781 contacts.len() <= 4,
782 "manifold must not exceed 4 contact points"
783 );
784 }
785
786 #[test]
789 fn dispatcher_box_box_finds_contact() {
790 let ba = box_shape(1.0);
791 let bb = box_shape(1.0);
792 let c = NarrowPhase::test_collision(
793 &ba,
794 Vec3::ZERO,
795 Quat::IDENTITY,
796 &bb,
797 Vec3::new(1.5, 0., 0.),
798 Quat::IDENTITY,
799 );
800 assert!(c.is_some(), "dispatcher must detect box-box overlap");
801 }
802
803 #[test]
804 fn dispatcher_manifold_populates_local_points() {
805 let ba = box_shape(1.0);
806 let bb = box_shape(1.0);
807 let contacts = NarrowPhase::test_collision_manifold(
808 &ba,
809 Vec3::ZERO,
810 Quat::IDENTITY,
811 &bb,
812 Vec3::new(1.5, 0., 0.),
813 Quat::IDENTITY,
814 );
815 assert!(!contacts.is_empty());
816 for c in &contacts {
817 let _ = c.local_point_a; let _ = c.local_point_b;
822 }
823 }
824
825 #[test]
826 fn test_collision_returns_deepest_of_manifold() {
827 let ba = box_shape(1.0);
828 let bb = box_shape(1.0);
829
830 let manifold = NarrowPhase::test_collision_manifold(
831 &ba,
832 Vec3::ZERO,
833 Quat::IDENTITY,
834 &bb,
835 Vec3::new(1.5, 0., 0.),
836 Quat::IDENTITY,
837 );
838 let single = NarrowPhase::test_collision(
839 &ba,
840 Vec3::ZERO,
841 Quat::IDENTITY,
842 &bb,
843 Vec3::new(1.5, 0., 0.),
844 Quat::IDENTITY,
845 );
846
847 if let (Some(s), Some(deepest)) = (
848 single,
849 manifold
850 .iter()
851 .max_by(|a, b| a.penetration.total_cmp(&b.penetration)),
852 ) {
853 assert!(
854 (s.penetration - deepest.penetration).abs() < 1e-5,
855 "test_collision must return the deepest manifold contact"
856 );
857 }
858 }
859
860 #[test]
863 fn sphere_sphere_normal_points_a_to_b() {
864 let c = NarrowPhase::sphere_sphere(Vec3::ZERO, 1.0, Vec3::new(1.5, 0., 0.), 1.0).unwrap();
865 assert!(
867 c.normal.dot(Vec3::new(1.5, 0., 0.)) > 0.0,
868 "normal must point from A toward B"
869 );
870 }
871
872 #[test]
873 fn box_box_normal_points_a_to_b() {
874 let contacts = NarrowPhase::box_box(
875 Vec3::ZERO,
876 Quat::IDENTITY,
877 Vec3::splat(1.0),
878 Vec3::new(1.5, 0., 0.),
879 Quat::IDENTITY,
880 Vec3::splat(1.0),
881 );
882 let d = Vec3::new(1.5, 0., 0.); for c in &contacts {
884 assert!(
885 c.normal.dot(d) > 0.0,
886 "box-box normal must point from A toward B, got {:?}",
887 c.normal
888 );
889 }
890 }
891}