1use crate::collision::ContactPoint;
2use crate::components::{BoxShape, CapsuleShape, ColliderShape, SphereShape};
3use gizmo_math::Vec3;
4
5const EPA_TOLERANCE: f32 = 0.001;
6const EPA_MAX_ITERATIONS: usize = 32;
7
8pub struct Gjk;
9
10impl Gjk {
11 pub fn test_collision(
13 shape_a: &ColliderShape,
14 pos_a: Vec3,
15 rot_a: gizmo_math::Quat,
16 shape_b: &ColliderShape,
17 pos_b: Vec3,
18 rot_b: gizmo_math::Quat,
19 ) -> bool {
20 let support = |dir: Vec3| {
21 let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
22 let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
23 sa - sb
24 };
25
26 Self::gjk_with_simplex(support).is_some()
27 }
28
29 pub fn get_contact(
31 shape_a: &ColliderShape,
32 pos_a: Vec3,
33 rot_a: gizmo_math::Quat,
34 shape_b: &ColliderShape,
35 pos_b: Vec3,
36 rot_b: gizmo_math::Quat,
37 ) -> Option<ContactPoint> {
38 let support = |dir: Vec3| {
39 let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
40 let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
41 sa - sb
42 };
43
44 if let Some(simplex) = Self::gjk_with_simplex(support) {
45 if let Some(contact) = Self::epa(simplex, shape_a, pos_a, rot_a, shape_b, pos_b, rot_b)
46 {
47 Some(contact)
48 } else {
49 Some(ContactPoint {
52 point: (pos_a + pos_b) * 0.5,
53 normal: (pos_b - pos_a).try_normalize().unwrap_or(Vec3::Y),
54 penetration: 0.01,
55 ..Default::default()
56 })
57 }
58 } else {
59 None
60 }
61 }
62
63 fn gjk_with_simplex<F>(support: F) -> Option<Vec<Vec3>>
65 where
66 F: Fn(Vec3) -> Vec3,
67 {
68 let mut simplex = Vec::with_capacity(4);
69 let mut direction = Vec3::new(1.0, 0.0, 0.0);
70
71 simplex.push(support(direction));
73 direction = -simplex[0];
74
75 const MAX_ITERATIONS: usize = 32;
78 for _ in 0..MAX_ITERATIONS {
79 direction = direction.try_normalize().unwrap_or_else(|| {
80 if simplex.len() >= 2 {
82 let edge = simplex[simplex.len() - 1] - simplex[0];
83 let perp = if edge.x.abs() <= edge.y.abs() && edge.x.abs() <= edge.z.abs() {
84 Vec3::new(1.0, 0.0, 0.0)
85 } else if edge.y.abs() <= edge.z.abs() {
86 Vec3::new(0.0, 1.0, 0.0)
87 } else {
88 Vec3::new(0.0, 0.0, 1.0)
89 };
90 edge.cross(perp).try_normalize().unwrap_or(Vec3::X)
91 } else {
92 Vec3::X
93 }
94 });
95
96 let a = support(direction);
97
98 if a.dot(direction) < 0.0 {
99 return None; }
101
102 simplex.push(a);
103
104 if Self::handle_simplex(&mut simplex, &mut direction) {
105 return Some(simplex); }
107 }
108
109 None
110 }
111
112 pub fn distance<F>(support: F) -> Option<(f32, Vec3)>
115 where
116 F: Fn(Vec3) -> Vec3,
117 {
118 let mut simplex = Vec::with_capacity(4);
119 let mut direction = Vec3::X;
120
121 let p = support(direction);
122 simplex.push(p);
123
124 let mut closest_point = p;
125 let mut min_dist_sq = p.length_squared();
126
127 for _ in 0..32 {
128 if min_dist_sq < 1e-8 {
129 return Some((0.0, Vec3::X)); }
131
132 direction = -closest_point;
133 let a = support(direction.normalize());
134
135 let current_dist = min_dist_sq.sqrt();
137 let lower_bound = -a.dot(direction) / current_dist;
138 if current_dist - lower_bound < 0.0001 {
139 break;
140 }
141
142 simplex.push(a);
143 closest_point = Self::closest_point_on_simplex(&mut simplex);
144 min_dist_sq = closest_point.length_squared();
145 }
146
147 let dist = min_dist_sq.sqrt();
148 let normal = if dist > 1e-6 {
149 closest_point / dist
150 } else {
151 Vec3::X
152 };
153 Some((dist, normal))
154 }
155
156 fn closest_point_on_simplex(simplex: &mut Vec<Vec3>) -> Vec3 {
157 match simplex.len() {
158 1 => simplex[0],
159 2 => {
160 let b = simplex[0];
161 let a = simplex[1];
162 let ab = b - a;
163 let ao = -a;
164 let t = ao.dot(ab) / ab.length_squared().max(1e-8);
165 if t <= 0.0 {
166 simplex.remove(0);
167 a
168 } else if t >= 1.0 {
169 simplex.remove(1);
170 b
171 } else {
172 a + ab * t
173 }
174 }
175 3 => {
176 let c = simplex[0];
177 let b = simplex[1];
178 let a = simplex[2];
179
180 let ab = b - a;
181 let ac = c - a;
182 let ap = -a;
183
184 let d1 = ab.dot(ap);
185 let d2 = ac.dot(ap);
186 if d1 <= 0.0 && d2 <= 0.0 {
187 *simplex = vec![a];
188 return a;
189 }
190
191 let bp = -b;
192 let d3 = ab.dot(bp);
193 let d4 = ac.dot(bp);
194 if d3 >= 0.0 && d4 <= d3 {
195 *simplex = vec![b];
196 return b;
197 }
198
199 let cp = -c;
200 let d5 = ab.dot(cp);
201 let d6 = ac.dot(cp);
202 if d6 >= 0.0 && d5 <= d6 {
203 *simplex = vec![c];
204 return c;
205 }
206
207 let vc = d1 * d4 - d3 * d2;
208 if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
209 let v = d1 / (d1 - d3);
210 *simplex = vec![b, a];
211 return a + ab * v;
212 }
213
214 let vb = d5 * d2 - d1 * d6;
215 if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
216 let w = d2 / (d2 - d6);
217 *simplex = vec![c, a];
218 return a + ac * w;
219 }
220
221 let va = d3 * d6 - d5 * d4;
222 if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
223 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
224 *simplex = vec![c, b];
225 return b + (c - b) * w;
226 }
227
228 let denom = 1.0 / (va + vb + vc);
229 let v = vb * denom;
230 let w = vc * denom;
231 a + ab * v + ac * w
232 }
233 4 => {
234 let d = simplex[0];
235 let c = simplex[1];
236 let b = simplex[2];
237 let a = simplex[3];
238
239 let abc = (b - a).cross(c - a);
240 let acd = (c - a).cross(d - a);
241 let adb = (d - a).cross(b - a);
242 let bdc = (c - b).cross(d - b);
243
244 if abc.dot(-a) > 0.0 {
245 *simplex = vec![c, b, a];
246 return Self::closest_point_on_simplex(simplex);
247 }
248 if acd.dot(-a) > 0.0 {
249 *simplex = vec![d, c, a];
250 return Self::closest_point_on_simplex(simplex);
251 }
252 if adb.dot(-a) > 0.0 {
253 *simplex = vec![b, d, a];
254 return Self::closest_point_on_simplex(simplex);
255 }
256 if bdc.dot(-b) > 0.0 {
257 *simplex = vec![d, c, b];
258 return Self::closest_point_on_simplex(simplex);
259 }
260
261 Vec3::ZERO
262 }
263 _ => Vec3::ZERO,
264 }
265 }
266
267 pub fn conservative_advancement(
269 shape_a: &ColliderShape,
270 mut pos_a: Vec3,
271 rot_a: gizmo_math::Quat,
272 vel_a: Vec3,
273 shape_b: &ColliderShape,
274 mut pos_b: Vec3,
275 rot_b: gizmo_math::Quat,
276 vel_b: Vec3,
277 max_t: f32,
278 ) -> Option<(f32, Vec3)> {
279 let mut t = 0.0;
280 let rel_vel = vel_a - vel_b;
281
282 if rel_vel.length_squared() < 1e-6 {
283 return None;
284 }
285
286 for _ in 0..32 {
287 let support = |dir: Vec3| {
288 let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
289 let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
290 sa - sb
291 };
292
293 if let Some((dist, normal)) = Self::distance(support) {
294 if dist < 0.001 {
295 return Some((t, normal));
296 }
297
298 let closing_vel = -rel_vel.dot(normal);
299
300 if closing_vel <= 0.0 {
301 return None;
302 }
303
304 let delta_t = dist / closing_vel;
305 t += delta_t;
306
307 if t > max_t {
308 return None;
309 }
310
311 pos_a += vel_a * delta_t;
312 pos_b += vel_b * delta_t;
313 } else {
314 return None;
315 }
316 }
317
318 None
319 }
320
321 pub fn speculative_contact(
323 shape_a: &ColliderShape,
324 pos_a: Vec3,
325 rot_a: gizmo_math::Quat,
326 vel_a: Vec3,
327 shape_b: &ColliderShape,
328 pos_b: Vec3,
329 rot_b: gizmo_math::Quat,
330 vel_b: Vec3,
331 dt: f32,
332 ) -> Option<ContactPoint> {
333 if let Some((t, _ca_normal)) = Self::conservative_advancement(
334 shape_a, pos_a, rot_a, vel_a, shape_b, pos_b, rot_b, vel_b, dt,
335 ) {
336 let hit_pos_a = pos_a + vel_a * t;
337 let hit_pos_b = pos_b + vel_b * t;
338
339 let normal_a_to_b = (hit_pos_b - hit_pos_a)
342 .try_normalize()
343 .unwrap_or(Vec3::X);
344
345 let contact_point = (hit_pos_a + hit_pos_b) * 0.5;
346
347 return Some(ContactPoint {
348 point: contact_point,
349 normal: normal_a_to_b,
350 penetration: 0.0, local_point_a: contact_point - pos_a,
352 local_point_b: contact_point - pos_b,
353 normal_impulse: 0.0,
354 tangent_impulse: Vec3::ZERO,
355 });
356 }
357 None
358 }
359
360 fn handle_simplex(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
361 match simplex.len() {
362 2 => Self::line_case(simplex, direction),
363 3 => Self::triangle_case(simplex, direction),
364 4 => Self::tetrahedron_case(simplex, direction),
365 _ => false,
366 }
367 }
368
369 fn line_case(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
370 let a = simplex[1];
371 let b = simplex[0];
372
373 let ab = b - a;
374 let ao = -a;
375
376 if ab.dot(ao) > 0.0 {
377 let mut cross = ab.cross(ao);
378 if cross.length_squared() < 1e-6 {
379 cross = if ab.x.abs() > ab.y.abs() {
380 Vec3::new(ab.y, -ab.x, 0.0)
381 } else {
382 Vec3::new(0.0, ab.z, -ab.y)
383 };
384 }
385 *direction = cross.cross(ab);
386 } else {
387 simplex.remove(0);
388 *direction = ao;
389 }
390
391 false
392 }
393
394 fn triangle_case(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
395 let a = simplex[2];
396 let b = simplex[1];
397 let c = simplex[0];
398
399 let ab = b - a;
400 let ac = c - a;
401 let ao = -a;
402
403 let abc = ab.cross(ac);
404
405 if abc.cross(ac).dot(ao) > 0.0 {
406 if ac.dot(ao) > 0.0 {
407 *simplex = vec![c, a];
408 let mut cross = ac.cross(ao);
409 if cross.length_squared() < 1e-6 {
410 cross = if ac.x.abs() > ac.y.abs() {
411 Vec3::new(ac.y, -ac.x, 0.0)
412 } else {
413 Vec3::new(0.0, ac.z, -ac.y)
414 };
415 }
416 *direction = cross.cross(ac);
417 } else {
418 *simplex = vec![b, a];
419 let mut cross = ab.cross(ao);
420 if cross.length_squared() < 1e-6 {
421 cross = if ab.x.abs() > ab.y.abs() {
422 Vec3::new(ab.y, -ab.x, 0.0)
423 } else {
424 Vec3::new(0.0, ab.z, -ab.y)
425 };
426 }
427 *direction = cross.cross(ab);
428 }
429 } else if ab.cross(abc).dot(ao) > 0.0 {
430 *simplex = vec![b, a];
431 let mut cross = ab.cross(ao);
432 if cross.length_squared() < 1e-6 {
433 cross = if ab.x.abs() > ab.y.abs() {
434 Vec3::new(ab.y, -ab.x, 0.0)
435 } else {
436 Vec3::new(0.0, ab.z, -ab.y)
437 };
438 }
439 *direction = cross.cross(ab);
440 } else {
441 if abc.dot(ao) > 0.0 {
442 *direction = abc;
443 } else {
444 simplex.swap(0, 1);
445 *direction = -abc;
446 }
447 }
448
449 false
450 }
451
452 fn tetrahedron_case(simplex: &mut Vec<Vec3>, direction: &mut Vec3) -> bool {
453 let a = simplex[3];
454 let b = simplex[2];
455 let c = simplex[1];
456 let d = simplex[0];
457
458 let ab = b - a;
459 let ac = c - a;
460 let ad = d - a;
461 let ao = -a;
462
463 let abc = ab.cross(ac);
464 let acd = ac.cross(ad);
465 let adb = ad.cross(ab);
466
467 if abc.dot(ao) > 0.0 {
468 simplex.remove(0);
469 return Self::triangle_case(simplex, direction);
470 }
471
472 if acd.dot(ao) > 0.0 {
473 simplex.remove(2);
474 simplex.swap(0, 1);
475 return Self::triangle_case(simplex, direction);
476 }
477
478 if adb.dot(ao) > 0.0 {
479 simplex.remove(1);
480 return Self::triangle_case(simplex, direction);
481 }
482
483 true
484 }
485
486 fn epa(
488 mut simplex: Vec<Vec3>,
489 shape_a: &ColliderShape,
490 pos_a: Vec3,
491 rot_a: gizmo_math::Quat,
492 shape_b: &ColliderShape,
493 pos_b: Vec3,
494 rot_b: gizmo_math::Quat,
495 ) -> Option<ContactPoint> {
496 let support = |dir: Vec3| {
497 let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
498 let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
499 sa - sb
500 };
501
502 let mut faces: Vec<(usize, usize, usize)> = Vec::new();
503 let mut edges = Vec::new();
504
505 if simplex.len() == 4 {
506 let initial_faces = [(0, 1, 2), (0, 3, 1), (0, 2, 3), (1, 3, 2)];
510 for (a, b, c) in initial_faces {
511 let n = Self::compute_face_normal(&simplex, a, b, c);
512 if n.dot(simplex[a]) >= 0.0 {
514 faces.push((a, b, c));
515 } else {
516 faces.push((a, c, b)); }
518 }
519 } else {
520 return None;
521 }
522
523 for _ in 0..EPA_MAX_ITERATIONS {
524 let (_closest_face_idx, normal, distance) = Self::find_closest_face(&simplex, &faces)?;
525
526 let support_point = support(normal);
527 let support_distance = support_point.dot(normal);
528
529 if support_distance - distance < EPA_TOLERANCE {
530 break;
531 }
532
533 let new_point_idx = simplex.len();
534 simplex.push(support_point);
535
536 edges.clear();
537 let mut i = 0;
538 while i < faces.len() {
539 let (a, b, c) = faces[i];
540 let face_normal = Self::compute_face_normal(&simplex, a, b, c);
541 let to_point = simplex[new_point_idx] - simplex[a];
542
543 if face_normal.dot(to_point) > 0.0 {
544 Self::add_edge(&mut edges, a, b);
545 Self::add_edge(&mut edges, b, c);
546 Self::add_edge(&mut edges, c, a);
547 faces.swap_remove(i);
548 } else {
549 i += 1;
550 }
551 }
552
553 for (e1, e2) in &edges {
555 let a = *e1;
556 let b = *e2;
557 let c = new_point_idx;
558 let n = Self::compute_face_normal(&simplex, a, b, c);
559 if n.dot(simplex[a]) >= 0.0 {
560 faces.push((a, b, c));
561 } else {
562 faces.push((b, a, c)); }
564 }
565 }
566
567 let (closest_idx, normal, penetration) = Self::find_closest_face(&simplex, &faces)?;
568
569 let pt_a = Self::support_point(shape_a, pos_a, rot_a, normal); let pt_b = Self::support_point(shape_b, pos_b, rot_b, -normal); let (fa, fb, fc) = faces[closest_idx];
580 let contact_point = Self::barycentric_contact_point(
581 &simplex, fa, fb, fc, normal, shape_a, pos_a, rot_a, shape_b, pos_b, rot_b,
582 )
583 .unwrap_or_else(|| (pt_a + pt_b) * 0.5);
584
585 Some(ContactPoint {
586 point: contact_point,
587 normal,
588 penetration,
589 local_point_a: contact_point - pos_a,
590 local_point_b: contact_point - pos_b,
591 normal_impulse: 0.0,
592 tangent_impulse: Vec3::ZERO,
593 })
594 }
595
596 fn barycentric_contact_point(
601 simplex: &[Vec3],
602 a: usize,
603 b: usize,
604 c: usize,
605 _normal: Vec3,
606 shape_a: &ColliderShape,
607 pos_a: Vec3,
608 rot_a: gizmo_math::Quat,
609 shape_b: &ColliderShape,
610 pos_b: Vec3,
611 rot_b: gizmo_math::Quat,
612 ) -> Option<Vec3> {
613 let va = simplex[a];
614 let vb = simplex[b];
615 let vc = simplex[c];
616
617 let (u, v, w) = Self::barycentric_coords(va, vb, vc, Vec3::ZERO)?;
619
620 let dir_a = va.try_normalize().unwrap_or(Vec3::X);
624 let dir_b = vb.try_normalize().unwrap_or(Vec3::X);
625 let dir_c = vc.try_normalize().unwrap_or(Vec3::X);
626
627 let pt_a = u * Self::support_point(shape_a, pos_a, rot_a, dir_a)
628 + v * Self::support_point(shape_a, pos_a, rot_a, dir_b)
629 + w * Self::support_point(shape_a, pos_a, rot_a, dir_c);
630 let pt_b = u * Self::support_point(shape_b, pos_b, rot_b, -dir_a)
631 + v * Self::support_point(shape_b, pos_b, rot_b, -dir_b)
632 + w * Self::support_point(shape_b, pos_b, rot_b, -dir_c);
633
634 Some((pt_a + pt_b) * 0.5)
635 }
636
637 fn barycentric_coords(a: Vec3, b: Vec3, c: Vec3, p: Vec3) -> Option<(f32, f32, f32)> {
640 let ab = b - a;
641 let ac = c - a;
642
643 let d3 = ab.dot(b - a); let d4 = ac.dot(b - a);
645 let d5 = ab.dot(c - a);
646 let d6 = ac.dot(c - a); let denom = d3 * d6 - d4 * d5;
649 if denom.abs() < 1e-8 {
650 return None; }
652
653 let ab = b - a;
655 let ac = c - a;
656 let ap = p - a;
657
658 let d00 = ab.dot(ab);
659 let d01 = ab.dot(ac);
660 let d11 = ac.dot(ac);
661 let d20 = ap.dot(ab);
662 let d21 = ap.dot(ac);
663
664 let denom = d00 * d11 - d01 * d01;
665 if denom.abs() < 1e-8 {
666 return None;
667 }
668
669 let v = (d11 * d20 - d01 * d21) / denom;
670 let w = (d00 * d21 - d01 * d20) / denom;
671 let u = 1.0 - v - w;
672
673 Some((u, v, w))
674 }
675
676 fn find_closest_face(
677 simplex: &[Vec3],
678 faces: &[(usize, usize, usize)],
679 ) -> Option<(usize, Vec3, f32)> {
680 let mut min_distance = f32::INFINITY;
681 let mut closest_idx = 0;
682 let mut closest_normal = Vec3::ZERO;
683
684 for (i, &(a, b, c)) in faces.iter().enumerate() {
685 let normal = Self::compute_face_normal(simplex, a, b, c);
686 let distance = normal.dot(simplex[a]);
687
688 if distance < min_distance {
689 min_distance = distance;
690 closest_idx = i;
691 closest_normal = normal;
692 }
693 }
694
695 if min_distance == f32::INFINITY {
696 None
697 } else {
698 Some((closest_idx, closest_normal, min_distance))
699 }
700 }
701
702 fn compute_face_normal(simplex: &[Vec3], a: usize, b: usize, c: usize) -> Vec3 {
703 let ab = simplex[b] - simplex[a];
704 let ac = simplex[c] - simplex[a];
705 let normal_raw = ab.cross(ac);
706 if normal_raw.dot(simplex[a]) < 0.0 {
708 -normal_raw.try_normalize().unwrap_or(Vec3::X)
709 } else {
710 normal_raw.try_normalize().unwrap_or(Vec3::X)
711 }
712 }
713
714 fn add_edge(edges: &mut Vec<(usize, usize)>, a: usize, b: usize) {
715 let reverse = (b, a);
716 let forward = (a, b);
717
718 if let Some(pos) = edges.iter().position(|&e| e == reverse) {
719 edges.swap_remove(pos);
720 } else if let Some(pos) = edges.iter().position(|&e| e == forward) {
721 edges.swap_remove(pos);
722 } else {
723 edges.push((a, b));
724 }
725 }
726
727 pub fn support_point(
729 shape: &ColliderShape,
730 pos: Vec3,
731 rot: gizmo_math::Quat,
732 dir: Vec3,
733 ) -> Vec3 {
734 let local_dir = rot.inverse() * dir;
735
736 let local_support = match shape {
737 ColliderShape::Sphere(s) => Self::sphere_support(s, local_dir),
738 ColliderShape::Box(b) => Self::box_support(b, local_dir),
739 ColliderShape::Capsule(c) => Self::capsule_support(c, local_dir),
740 ColliderShape::Plane(_) => {
741 debug_assert!(false, "Plane shapes must use separate collision detection");
742 Vec3::ZERO
743 }
744 ColliderShape::TriMesh(tm) => {
745 let mut best_dot = f32::NEG_INFINITY;
746 let mut best_pt = Vec3::ZERO;
747
748 if !tm.bvh.nodes.is_empty() {
749 let mut stack = Vec::with_capacity(64);
750 stack.push(0);
751
752 let abs_dir = gizmo_math::Vec3A::new(
753 local_dir.x.abs(),
754 local_dir.y.abs(),
755 local_dir.z.abs(),
756 );
757 let dir_a = gizmo_math::Vec3A::new(local_dir.x, local_dir.y, local_dir.z);
758
759 while let Some(node_idx) = stack.pop() {
760 let node = &tm.bvh.nodes[node_idx];
761
762 let center = node.aabb.center();
763 let half_extents = node.aabb.half_extents();
764
765 let max_node_dot = center.dot(dir_a)
766 + half_extents.x * abs_dir.x
767 + half_extents.y * abs_dir.y
768 + half_extents.z * abs_dir.z;
769
770 if max_node_dot < best_dot {
771 continue;
772 }
773
774 if node.is_leaf() {
775 let start = (node.first_tri_index * 3) as usize;
776 let end = start + (node.tri_count * 3) as usize;
777 for i in start..end {
778 let v = tm.vertices[tm.indices[i] as usize];
779 let d = v.dot(local_dir);
780 if d > best_dot {
781 best_dot = d;
782 best_pt = v;
783 }
784 }
785 } else {
786 if node.left_child >= 0 {
787 stack.push(node.left_child as usize);
788 }
789 if node.right_child >= 0 {
790 stack.push(node.right_child as usize);
791 }
792 }
793 }
794 } else {
795 for v in tm.vertices.iter() {
796 let d = v.dot(local_dir);
797 if d > best_dot {
798 best_dot = d;
799 best_pt = *v;
800 }
801 }
802 }
803 best_pt
804 }
805 ColliderShape::ConvexHull(ch) => {
806 let mut best_dot = f32::NEG_INFINITY;
807 let mut best_pt = Vec3::ZERO;
808 for v in ch.vertices.iter() {
809 let d = v.dot(local_dir);
810 if d > best_dot {
811 best_dot = d;
812 best_pt = *v;
813 }
814 }
815 best_pt
816 }
817 crate::components::ColliderShape::Compound(_) => {
818 debug_assert!(
819 false,
820 "Compound shapes must use separate collision detection"
821 );
822 Vec3::ZERO
823 }
824 };
825
826 pos + rot * local_support
827 }
828
829 fn sphere_support(sphere: &SphereShape, dir: Vec3) -> Vec3 {
830 dir.try_normalize().unwrap_or(Vec3::X) * sphere.radius
831 }
832
833 fn box_support(box_shape: &BoxShape, dir: Vec3) -> Vec3 {
834 Vec3::new(
835 if dir.x > 0.0 {
836 box_shape.half_extents.x
837 } else {
838 -box_shape.half_extents.x
839 },
840 if dir.y > 0.0 {
841 box_shape.half_extents.y
842 } else {
843 -box_shape.half_extents.y
844 },
845 if dir.z > 0.0 {
846 box_shape.half_extents.z
847 } else {
848 -box_shape.half_extents.z
849 },
850 )
851 }
852
853 fn capsule_support(capsule: &CapsuleShape, dir: Vec3) -> Vec3 {
854 let dir_normalized = dir.try_normalize().unwrap_or(Vec3::X);
855 let sphere_center = if dir_normalized.y > 0.0 {
856 Vec3::new(0.0, capsule.half_height, 0.0)
857 } else {
858 Vec3::new(0.0, -capsule.half_height, 0.0)
859 };
860 sphere_center + dir_normalized * capsule.radius
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867 use gizmo_math::{Quat, Vec3};
868
869 #[test]
870 fn test_sphere_vs_sphere_collision() {
871 let shape = ColliderShape::Sphere(SphereShape { radius: 1.0 });
872
873 assert!(Gjk::test_collision(
874 &shape,
875 Vec3::ZERO,
876 Quat::IDENTITY,
877 &shape,
878 Vec3::new(1.5, 0.0, 0.0),
879 Quat::IDENTITY
880 ));
881 assert!(!Gjk::test_collision(
882 &shape,
883 Vec3::ZERO,
884 Quat::IDENTITY,
885 &shape,
886 Vec3::new(2.5, 0.0, 0.0),
887 Quat::IDENTITY
888 ));
889 }
890
891 #[test]
892 fn test_box_vs_box_collision() {
893 let shape = ColliderShape::Box(BoxShape {
894 half_extents: Vec3::new(1.0, 1.0, 1.0),
895 });
896
897 assert!(Gjk::test_collision(
898 &shape,
899 Vec3::ZERO,
900 Quat::IDENTITY,
901 &shape,
902 Vec3::new(1.5, 0.0, 0.0),
903 Quat::IDENTITY
904 ));
905 assert!(!Gjk::test_collision(
906 &shape,
907 Vec3::ZERO,
908 Quat::IDENTITY,
909 &shape,
910 Vec3::new(2.5, 0.0, 0.0),
911 Quat::IDENTITY
912 ));
913 }
914
915 #[test]
916 fn test_epa_contact_generation() {
917 let shape_a = ColliderShape::Box(BoxShape {
918 half_extents: Vec3::new(1.0, 1.0, 1.0),
919 });
920 let shape_b = ColliderShape::Box(BoxShape {
921 half_extents: Vec3::new(1.0, 1.0, 1.0),
922 });
923
924 let contact = Gjk::get_contact(
925 &shape_a,
926 Vec3::ZERO,
927 Quat::IDENTITY,
928 &shape_b,
929 Vec3::new(1.5, 0.0, 0.0),
930 Quat::IDENTITY,
931 );
932
933 assert!(contact.is_some(), "EPA failed to generate contact");
934 let contact = contact.unwrap();
935
936 assert!(
937 (contact.penetration - 0.5).abs() < 0.001,
938 "Penetration depth is wrong: {}",
939 contact.penetration
940 );
941 assert!(
942 (contact.normal.x.abs() - 1.0).abs() < 0.001,
943 "Normal is wrong: {:?}",
944 contact.normal
945 );
946 }
947
948 #[test]
949 fn test_speculative_contact_approaching() {
950 let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
952
953 let contact = Gjk::speculative_contact(
954 &shape,
955 Vec3::new(-5.0, 0.0, 0.0),
956 Quat::IDENTITY,
957 Vec3::new(10.0, 0.0, 0.0),
958 &shape,
959 Vec3::new(5.0, 0.0, 0.0),
960 Quat::IDENTITY,
961 Vec3::new(-10.0, 0.0, 0.0),
962 1.0,
963 );
964
965 assert!(
966 contact.is_some(),
967 "Speculative contact missed approaching spheres"
968 );
969 }
970
971 #[test]
972 fn test_speculative_contact_separating() {
973 let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
975
976 let contact = Gjk::speculative_contact(
977 &shape,
978 Vec3::new(-5.0, 0.0, 0.0),
979 Quat::IDENTITY,
980 Vec3::new(-10.0, 0.0, 0.0),
981 &shape,
982 Vec3::new(5.0, 0.0, 0.0),
983 Quat::IDENTITY,
984 Vec3::new(10.0, 0.0, 0.0),
985 1.0,
986 );
987
988 assert!(
989 contact.is_none(),
990 "Speculative contact incorrectly fired for separating shapes"
991 );
992 }
993}