1use crate::narrowphase::gjk::{Gjk, GjkResult};
7use oxiphysics_core::Transform;
8use oxiphysics_core::math::{Real, Vec3};
9use oxiphysics_geometry::Shape;
10
11const MAX_CCD_ITERATIONS: usize = 32;
13
14const CCD_TOLERANCE: Real = 1e-6;
16
17#[derive(Debug, Clone)]
19pub struct ToiResult {
20 pub toi: Real,
22 pub normal: Vec3,
24 pub witness_a: Vec3,
26 pub witness_b: Vec3,
28}
29
30pub fn time_of_impact(
36 shape_a: &dyn Shape,
37 transform_a_start: &Transform,
38 transform_a_end: &Transform,
39 shape_b: &dyn Shape,
40 transform_b_start: &Transform,
41 transform_b_end: &Transform,
42) -> Option<ToiResult> {
43 let vel_a = transform_a_end.position - transform_a_start.position;
45 let vel_b = transform_b_end.position - transform_b_start.position;
46 let relative_vel = vel_a - vel_b;
47 let rel_speed = relative_vel.norm();
48
49 if rel_speed < CCD_TOLERANCE {
50 return None;
52 }
53
54 let mut t = 0.0;
55
56 for _ in 0..MAX_CCD_ITERATIONS {
57 let ta = interpolate_transform(transform_a_start, transform_a_end, t);
59 let tb = interpolate_transform(transform_b_start, transform_b_end, t);
60
61 let result = Gjk::query(shape_a, &ta, shape_b, &tb);
63
64 match result {
65 GjkResult::Intersecting(_) => {
66 if t < CCD_TOLERANCE {
68 return None;
70 }
71 return bisect_toi(
73 shape_a,
74 transform_a_start,
75 transform_a_end,
76 shape_b,
77 transform_b_start,
78 transform_b_end,
79 (t - 0.1).max(0.0),
80 t,
81 );
82 }
83 GjkResult::Separated {
84 distance,
85 point_a,
86 point_b,
87 ..
88 } => {
89 if distance < CCD_TOLERANCE {
90 let normal = if distance > 1e-12 {
91 (point_b - point_a).normalize()
92 } else {
93 relative_vel.normalize()
94 };
95 return Some(ToiResult {
96 toi: t,
97 normal,
98 witness_a: point_a,
99 witness_b: point_b,
100 });
101 }
102
103 let upper_bound = compute_upper_bound(shape_a, shape_b);
106 let denominator = rel_speed + upper_bound;
107 if denominator < CCD_TOLERANCE {
108 return None;
109 }
110
111 let dt = distance / denominator;
112 t += dt;
113
114 if t > 1.0 {
115 return None; }
117 }
118 }
119 }
120
121 None
122}
123
124fn compute_upper_bound(shape_a: &dyn Shape, shape_b: &dyn Shape) -> Real {
126 let aabb_a = shape_a.bounding_box();
128 let aabb_b = shape_b.bounding_box();
129 let radius_a = aabb_a.half_extents().norm();
130 let radius_b = aabb_b.half_extents().norm();
131 (radius_a + radius_b) * std::f64::consts::PI * 0.1
134}
135
136fn interpolate_transform(start: &Transform, end: &Transform, t: Real) -> Transform {
138 let position = start.position * (1.0 - t) + end.position * t;
139 let rotation = start.rotation.slerp(&end.rotation, t);
140 Transform::new(position, rotation)
141}
142
143fn bisect_toi(
145 shape_a: &dyn Shape,
146 ta_start: &Transform,
147 ta_end: &Transform,
148 shape_b: &dyn Shape,
149 tb_start: &Transform,
150 tb_end: &Transform,
151 mut t_low: Real,
152 mut t_high: Real,
153) -> Option<ToiResult> {
154 for _ in 0..16 {
155 let t_mid = (t_low + t_high) * 0.5;
156 let ta = interpolate_transform(ta_start, ta_end, t_mid);
157 let tb = interpolate_transform(tb_start, tb_end, t_mid);
158
159 let result = Gjk::query(shape_a, &ta, shape_b, &tb);
160 match result {
161 GjkResult::Intersecting(_) => {
162 t_high = t_mid;
163 }
164 GjkResult::Separated {
165 distance,
166 point_a,
167 point_b,
168 ..
169 } => {
170 if distance < CCD_TOLERANCE {
171 let normal = if distance > 1e-12 {
172 (point_b - point_a).normalize()
173 } else {
174 let rel = (ta_end.position - ta_start.position)
176 - (tb_end.position - tb_start.position);
177 if rel.norm() > 1e-10 {
178 rel.normalize()
179 } else {
180 Vec3::new(0.0, 1.0, 0.0)
181 }
182 };
183 return Some(ToiResult {
184 toi: t_mid,
185 normal,
186 witness_a: point_a,
187 witness_b: point_b,
188 });
189 }
190 t_low = t_mid;
191 }
192 }
193
194 if (t_high - t_low) < CCD_TOLERANCE {
195 break;
196 }
197 }
198
199 let t = (t_low + t_high) * 0.5;
201 let ta = interpolate_transform(ta_start, ta_end, t);
202 let tb = interpolate_transform(tb_start, tb_end, t);
203 let vel = ta_end.position - ta_start.position - (tb_end.position - tb_start.position);
204 let normal = if vel.norm() > 1e-10 {
205 vel.normalize()
206 } else {
207 Vec3::new(0.0, 1.0, 0.0)
208 };
209 Some(ToiResult {
210 toi: t,
211 normal,
212 witness_a: ta.position,
213 witness_b: tb.position,
214 })
215}
216
217#[derive(Debug, Clone, PartialEq)]
223pub struct CcdResult {
224 pub toi: f64,
226 pub normal: [f64; 3],
228 pub witness_a: [f64; 3],
230 pub witness_b: [f64; 3],
232}
233
234#[inline]
237fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
238 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
239}
240
241#[inline]
242fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
243 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
244}
245
246#[inline]
247fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
248 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
249}
250
251#[inline]
252fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
253 [v[0] * s, v[1] * s, v[2] * s]
254}
255
256#[inline]
257fn len3(v: [f64; 3]) -> f64 {
258 dot3(v, v).sqrt()
259}
260
261#[inline]
262fn norm3(v: [f64; 3]) -> [f64; 3] {
263 let l = len3(v);
264 if l < 1e-300 {
265 [0.0, 0.0, 0.0]
266 } else {
267 scale3(v, 1.0 / l)
268 }
269}
270
271#[inline]
272fn neg3(v: [f64; 3]) -> [f64; 3] {
273 [-v[0], -v[1], -v[2]]
274}
275
276#[inline]
277fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
278 [
279 a[1] * b[2] - a[2] * b[1],
280 a[2] * b[0] - a[0] * b[2],
281 a[0] * b[1] - a[1] * b[0],
282 ]
283}
284
285pub fn sphere_sphere_toi(
294 center_a: [f64; 3],
295 vel_a: [f64; 3],
296 radius_a: f64,
297 center_b: [f64; 3],
298 vel_b: [f64; 3],
299 radius_b: f64,
300) -> Option<f64> {
301 let r_sum = radius_a + radius_b;
302 let dp = sub3(center_b, center_a);
304 let dv = sub3(vel_b, vel_a);
305
306 let a = dot3(dv, dv);
309 let b = dot3(dp, dv);
310 let c = dot3(dp, dp) - r_sum * r_sum;
311
312 if c <= 0.0 {
314 return Some(0.0);
315 }
316
317 if a < 1e-300 {
318 return None;
320 }
321
322 let disc = b * b - a * c;
323 if disc < 0.0 {
324 return None;
325 }
326
327 let sqrt_disc = disc.sqrt();
328 let t = (-b - sqrt_disc) / a;
330 if (0.0..=1.0).contains(&t) {
331 return Some(t);
332 }
333 let t2 = (-b + sqrt_disc) / a;
334 if (0.0..=1.0).contains(&t2) {
335 return Some(t2);
336 }
337 None
338}
339
340pub fn sphere_plane_toi(
349 center: [f64; 3],
350 vel: [f64; 3],
351 radius: f64,
352 plane_normal: [f64; 3],
353 plane_d: f64,
354) -> Option<f64> {
355 let dist = dot3(plane_normal, center) - plane_d;
357 let v_dot_n = dot3(plane_normal, vel);
358
359 if dist.abs() <= radius {
361 return Some(0.0);
362 }
363
364 if v_dot_n.abs() < 1e-300 {
365 return None;
366 }
367
368 let t = if dist > 0.0 {
371 (dist - radius) / (-v_dot_n)
372 } else {
373 (dist + radius) / (-v_dot_n)
374 };
375
376 if (0.0..=1.0).contains(&t) {
377 Some(t)
378 } else {
379 None
380 }
381}
382
383pub fn relative_velocity_bound(vel_a: [f64; 3], vel_b: [f64; 3]) -> f64 {
389 len3(sub3(vel_a, vel_b))
390}
391
392pub fn gjk_distance_approx(
402 support_a: impl Fn([f64; 3]) -> [f64; 3],
403 support_b: impl Fn([f64; 3]) -> [f64; 3],
404) -> f64 {
405 let support = |d: [f64; 3]| -> [f64; 3] { sub3(support_a(d), support_b(neg3(d))) };
407
408 const MAX_ITERS: usize = 32;
409 const EPS: f64 = 1e-7;
410
411 let mut simplex: Vec<[f64; 3]> = Vec::with_capacity(4);
413
414 let mut dir = [1.0_f64, 0.0, 0.0];
415 let first = support(dir);
416 simplex.push(first);
417 dir = neg3(first);
418
419 for _ in 0..MAX_ITERS {
420 let l = len3(dir);
421 if l < EPS {
422 return 0.0;
424 }
425 let dir_n = scale3(dir, 1.0 / l);
426 let new_pt = support(dir_n);
427
428 let proj = dot3(new_pt, dir_n);
430 if proj < -EPS {
431 return l;
433 }
434
435 simplex.push(new_pt);
436
437 let (new_dir, done, dist) = nearest_simplex(&mut simplex);
439 if done {
440 return dist;
441 }
442 dir = new_dir;
443 }
444
445 len3(dir)
446}
447
448fn nearest_simplex(simplex: &mut Vec<[f64; 3]>) -> ([f64; 3], bool, f64) {
451 match simplex.len() {
452 1 => {
453 let a = simplex[0];
454 (neg3(a), false, len3(a))
455 }
456 2 => nearest_simplex_line(simplex),
457 3 => nearest_simplex_triangle(simplex),
458 4 => nearest_simplex_tetrahedron(simplex),
459 _ => ([1.0, 0.0, 0.0], true, 0.0),
460 }
461}
462
463fn nearest_simplex_line(simplex: &mut Vec<[f64; 3]>) -> ([f64; 3], bool, f64) {
464 let b = simplex[0];
465 let a = simplex[1]; let ab = sub3(b, a);
467 let ao = neg3(a);
468 let t = dot3(ab, ao);
469 if t <= 0.0 {
470 *simplex = vec![a];
472 (ao, false, len3(ao))
473 } else {
474 let denom = dot3(ab, ab);
475 if denom < 1e-300 {
476 *simplex = vec![a];
477 return (ao, false, len3(ao));
478 }
479 let tc = t / denom;
480 if tc >= 1.0 {
481 *simplex = vec![b];
483 let bo = neg3(b);
484 (bo, false, len3(bo))
485 } else {
486 let closest = add3(a, scale3(ab, tc));
488 let dir = neg3(closest);
489 let dist = len3(closest);
490 (dir, dist < 1e-10, dist)
491 }
492 }
493}
494
495fn nearest_simplex_triangle(simplex: &mut Vec<[f64; 3]>) -> ([f64; 3], bool, f64) {
496 let c = simplex[0];
497 let b = simplex[1];
498 let a = simplex[2]; let ab = sub3(b, a);
500 let ac = sub3(c, a);
501 let ao = neg3(a);
502
503 let abc = cross3(ab, ac);
504 let ab_perp = cross3(ab, abc);
507 if dot3(ab_perp, ao) > 0.0 {
508 if dot3(ab, ao) > 0.0 {
509 *simplex = vec![b, a];
510 return nearest_simplex_line(simplex);
511 }
512 let ac_perp = cross3(abc, ac);
513 if dot3(ac_perp, ao) > 0.0 {
514 *simplex = vec![c, a];
515 return nearest_simplex_line(simplex);
516 }
517 *simplex = vec![a];
518 return (ao, false, len3(ao));
519 }
520 let ac_perp = cross3(abc, ac);
521 if dot3(ac_perp, ao) > 0.0 {
522 *simplex = vec![c, a];
523 return nearest_simplex_line(simplex);
524 }
525 let d = dot3(abc, ao);
527 if d.abs() < 1e-10 {
528 return ([0.0, 0.0, 0.0], true, 0.0);
529 }
530 if d > 0.0 {
531 (abc, false, d / len3(abc))
533 } else {
534 *simplex = vec![b, c, a];
536 (neg3(abc), false, (-d) / len3(abc))
537 }
538}
539
540fn nearest_simplex_tetrahedron(simplex: &mut Vec<[f64; 3]>) -> ([f64; 3], bool, f64) {
541 let d = simplex[0];
542 let c = simplex[1];
543 let b = simplex[2];
544 let a = simplex[3]; let ab = sub3(b, a);
547 let ac = sub3(c, a);
548 let ad = sub3(d, a);
549 let ao = neg3(a);
550
551 let abc = cross3(ab, ac);
552 let acd = cross3(ac, ad);
553 let adb = cross3(ad, ab);
554
555 let above_abc = dot3(abc, ao) > 0.0;
556 let above_acd = dot3(acd, ao) > 0.0;
557 let above_adb = dot3(adb, ao) > 0.0;
558
559 if !above_abc && !above_acd && !above_adb {
560 return ([0.0, 0.0, 0.0], true, 0.0);
562 }
563 if above_abc {
564 *simplex = vec![c, b, a];
565 return nearest_simplex_triangle(simplex);
566 }
567 if above_acd {
568 *simplex = vec![d, c, a];
569 return nearest_simplex_triangle(simplex);
570 }
571 *simplex = vec![b, d, a];
572 nearest_simplex_triangle(simplex)
573}
574
575pub fn conservative_advancement(
585 support_a: impl Fn([f64; 3]) -> [f64; 3],
586 support_b: impl Fn([f64; 3]) -> [f64; 3],
587 pos_a: [f64; 3],
588 vel_a: [f64; 3],
589 pos_b: [f64; 3],
590 vel_b: [f64; 3],
591 max_dist: f64,
592 max_iter: usize,
593) -> Option<f64> {
594 const THRESHOLD: f64 = 1e-4;
595 let rel_speed = relative_velocity_bound(vel_a, vel_b);
596 if rel_speed < 1e-12 {
597 return None;
598 }
599
600 let mut t = 0.0_f64;
601
602 for _ in 0..max_iter {
604 let ta = add3(pos_a, scale3(vel_a, t));
605 let tb = add3(pos_b, scale3(vel_b, t));
606 let offset = sub3(ta, tb);
607
608 let sa = |d: [f64; 3]| add3(support_a(d), ta);
612 let sb = |d: [f64; 3]| add3(support_b(d), tb);
613
614 let dist = gjk_shifted_distance(sa, sb, offset);
616
617 if dist <= THRESHOLD {
618 return Some(t);
619 }
620 if dist > max_dist {
621 return None;
622 }
623
624 let dt = dist / rel_speed;
625 t += dt;
626 if t > 1.0 {
627 return None;
628 }
629 }
630
631 None
632}
633
634fn gjk_shifted_distance(
636 support_a: impl Fn([f64; 3]) -> [f64; 3],
637 support_b: impl Fn([f64; 3]) -> [f64; 3],
638 _offset: [f64; 3],
639) -> f64 {
640 gjk_distance_approx(support_a, support_b)
641}
642
643fn closest_point_on_segment(p0: [f64; 3], p1: [f64; 3], q: [f64; 3]) -> [f64; 3] {
649 let d = sub3(p1, p0);
650 let len_sq = dot3(d, d);
651 if len_sq < 1e-300 {
652 return p0;
653 }
654 let t = (dot3(sub3(q, p0), d) / len_sq).clamp(0.0, 1.0);
655 add3(p0, scale3(d, t))
656}
657
658fn capsule_support(p0: [f64; 3], p1: [f64; 3], radius: f64, dir: [f64; 3]) -> [f64; 3] {
660 let d0 = dot3(p0, dir);
662 let d1 = dot3(p1, dir);
663 let axis_pt = if d0 >= d1 { p0 } else { p1 };
664 let dir_n = norm3(dir);
666 add3(axis_pt, scale3(dir_n, radius))
667}
668
669pub fn capsule_capsule_toi(
677 pa0: [f64; 3],
678 pa1: [f64; 3],
679 vel_a: [f64; 3],
680 ra: f64,
681 pb0: [f64; 3],
682 pb1: [f64; 3],
683 vel_b: [f64; 3],
684 rb: f64,
685 max_iter: usize,
686) -> Option<f64> {
687 let pos_a = scale3(add3(pa0, pa1), 0.5);
689 let pos_b = scale3(add3(pb0, pb1), 0.5);
690
691 let half_a = sub3(pa1, pos_a);
693 let half_b = sub3(pb1, pos_b);
694
695 let local_a0 = sub3(pa0, pos_a);
697 let local_a1 = sub3(pa1, pos_a);
698 let local_b0 = sub3(pb0, pos_b);
699 let local_b1 = sub3(pb1, pos_b);
700
701 let _ = (half_a, half_b, closest_point_on_segment); let max_dist = len3(sub3(pos_b, pos_a)) + ra + rb + 1.0;
704
705 conservative_advancement(
706 |d| capsule_support(local_a0, local_a1, ra, d),
707 |d| capsule_support(local_b0, local_b1, rb, d),
708 pos_a,
709 vel_a,
710 pos_b,
711 vel_b,
712 max_dist,
713 max_iter,
714 )
715}
716
717#[derive(Debug, Clone)]
726pub struct ConservativeAdvancement {
727 pub threshold: f64,
729 pub max_iterations: usize,
731 pub angular_speed_factor: f64,
733}
734
735impl Default for ConservativeAdvancement {
736 fn default() -> Self {
737 Self {
738 threshold: 1e-4,
739 max_iterations: 64,
740 angular_speed_factor: 0.1,
741 }
742 }
743}
744
745impl ConservativeAdvancement {
746 pub fn new() -> Self {
748 Self::default()
749 }
750
751 pub fn with_threshold(mut self, threshold: f64) -> Self {
753 self.threshold = threshold;
754 self
755 }
756
757 pub fn with_max_iterations(mut self, n: usize) -> Self {
759 self.max_iterations = n;
760 self
761 }
762
763 pub fn compute_toi(
774 &self,
775 support_a: impl Fn([f64; 3]) -> [f64; 3],
776 support_b: impl Fn([f64; 3]) -> [f64; 3],
777 pos_a: [f64; 3],
778 vel_a: [f64; 3],
779 bounding_radius_a: f64,
780 pos_b: [f64; 3],
781 vel_b: [f64; 3],
782 bounding_radius_b: f64,
783 ) -> Option<f64> {
784 let rel_speed = relative_velocity_bound(vel_a, vel_b);
785 let angular_contrib = (bounding_radius_a + bounding_radius_b) * self.angular_speed_factor;
787 let effective_speed = rel_speed + angular_contrib;
788
789 if effective_speed < 1e-12 {
790 return None;
791 }
792
793 let mut t = 0.0_f64;
794 let max_dist = len3(sub3(pos_b, pos_a)) + bounding_radius_a + bounding_radius_b + 1.0;
795
796 for _ in 0..self.max_iterations {
797 let ta = add3(pos_a, scale3(vel_a, t));
798 let tb = add3(pos_b, scale3(vel_b, t));
799 let sa = |d: [f64; 3]| add3(support_a(d), ta);
800 let sb = |d: [f64; 3]| add3(support_b(d), tb);
801
802 let dist = gjk_distance_approx(sa, sb);
803
804 if dist <= self.threshold {
805 return Some(t);
806 }
807 if dist > max_dist {
808 return None;
809 }
810
811 let dt = dist / effective_speed;
812 t += dt;
813 if t > 1.0 {
814 return None;
815 }
816 }
817
818 None
819 }
820
821 pub fn sphere_support(radius: f64) -> impl Fn([f64; 3]) -> [f64; 3] {
823 move |d: [f64; 3]| {
824 let dn = norm3(d);
825 scale3(dn, radius)
826 }
827 }
828
829 pub fn box_support(h: [f64; 3]) -> impl Fn([f64; 3]) -> [f64; 3] {
831 move |d: [f64; 3]| {
832 [
833 if d[0] >= 0.0 { h[0] } else { -h[0] },
834 if d[1] >= 0.0 { h[1] } else { -h[1] },
835 if d[2] >= 0.0 { h[2] } else { -h[2] },
836 ]
837 }
838 }
839}
840
841#[inline]
847pub fn integrate_linear(pos: [f64; 3], vel: [f64; 3], dt: f64) -> [f64; 3] {
848 add3(pos, scale3(vel, dt))
849}
850
851pub fn integrate_angular(q: [f64; 4], omega: [f64; 3], dt: f64) -> [f64; 4] {
858 let omega_q = [omega[0], omega[1], omega[2], 0.0];
861 let dq = quat_mul(omega_q, q);
862 let new_q = [
863 q[0] + 0.5 * dt * dq[0],
864 q[1] + 0.5 * dt * dq[1],
865 q[2] + 0.5 * dt * dq[2],
866 q[3] + 0.5 * dt * dq[3],
867 ];
868 quat_normalize(new_q)
869}
870
871#[inline]
875pub fn quat_mul(p: [f64; 4], q: [f64; 4]) -> [f64; 4] {
876 let (px, py, pz, pw) = (p[0], p[1], p[2], p[3]);
877 let (qx, qy, qz, qw) = (q[0], q[1], q[2], q[3]);
878 [
879 pw * qx + px * qw + py * qz - pz * qy,
880 pw * qy - px * qz + py * qw + pz * qx,
881 pw * qz + px * qy - py * qx + pz * qw,
882 pw * qw - px * qx - py * qy - pz * qz,
883 ]
884}
885
886#[inline]
888pub fn quat_normalize(q: [f64; 4]) -> [f64; 4] {
889 let len_sq = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
890 if len_sq < 1e-300 {
891 return [0.0, 0.0, 0.0, 1.0];
892 }
893 let inv = 1.0 / len_sq.sqrt();
894 [q[0] * inv, q[1] * inv, q[2] * inv, q[3] * inv]
895}
896
897pub fn quat_rotate(q: [f64; 4], v: [f64; 3]) -> [f64; 3] {
901 let v_q = [v[0], v[1], v[2], 0.0];
902 let q_conj = [-q[0], -q[1], -q[2], q[3]];
903 let tmp = quat_mul(q, v_q);
904 let result = quat_mul(tmp, q_conj);
905 [result[0], result[1], result[2]]
906}
907
908#[derive(Debug, Clone)]
914pub struct RigidBodyState {
915 pub position: [f64; 3],
917 pub orientation: [f64; 4],
919 pub linear_vel: [f64; 3],
921 pub angular_vel: [f64; 3],
923}
924
925impl RigidBodyState {
926 pub fn new(
928 position: [f64; 3],
929 orientation: [f64; 4],
930 linear_vel: [f64; 3],
931 angular_vel: [f64; 3],
932 ) -> Self {
933 Self {
934 position,
935 orientation: quat_normalize(orientation),
936 linear_vel,
937 angular_vel,
938 }
939 }
940
941 pub fn identity() -> Self {
943 Self {
944 position: [0.0; 3],
945 orientation: [0.0, 0.0, 0.0, 1.0],
946 linear_vel: [0.0; 3],
947 angular_vel: [0.0; 3],
948 }
949 }
950
951 pub fn integrate(&self, dt: f64) -> Self {
953 Self {
954 position: integrate_linear(self.position, self.linear_vel, dt),
955 orientation: integrate_angular(self.orientation, self.angular_vel, dt),
956 linear_vel: self.linear_vel,
957 angular_vel: self.angular_vel,
958 }
959 }
960
961 pub fn angular_speed_bound(&self, bounding_radius: f64) -> f64 {
963 let omega_mag = len3(self.angular_vel);
964 omega_mag * bounding_radius
965 }
966}
967
968#[derive(Debug, Clone, PartialEq)]
976pub struct CcdPair {
977 pub handle_a: u32,
979 pub handle_b: u32,
981 pub toi: f64,
983 pub normal: [f64; 3],
985 pub point_a: [f64; 3],
987 pub point_b: [f64; 3],
989}
990
991impl CcdPair {
992 pub fn new(
994 handle_a: u32,
995 handle_b: u32,
996 toi: f64,
997 normal: [f64; 3],
998 point_a: [f64; 3],
999 point_b: [f64; 3],
1000 ) -> Self {
1001 Self {
1002 handle_a,
1003 handle_b,
1004 toi,
1005 normal,
1006 point_a,
1007 point_b,
1008 }
1009 }
1010
1011 pub fn key(&self) -> (u32, u32) {
1013 (
1014 self.handle_a.min(self.handle_b),
1015 self.handle_a.max(self.handle_b),
1016 )
1017 }
1018}
1019
1020#[derive(Debug, Clone)]
1027pub struct CcdBodyEntry {
1028 pub handle: u32,
1030 pub pos_start: [f64; 3],
1032 pub pos_end: [f64; 3],
1034 pub orient_start: [f64; 4],
1036 pub orient_end: [f64; 4],
1038 pub linear_vel: [f64; 3],
1040 pub angular_vel: [f64; 3],
1042 pub bounding_radius: f64,
1044 pub ccd_enabled: bool,
1046}
1047
1048impl CcdBodyEntry {
1049 #[inline]
1051 pub fn displacement(&self) -> [f64; 3] {
1052 sub3(self.pos_end, self.pos_start)
1053 }
1054
1055 pub fn swept_radius(&self) -> f64 {
1059 let linear_part = len3(self.displacement());
1060 let omega = len3(self.angular_vel);
1061 linear_part + omega * self.bounding_radius
1062 }
1063}
1064
1065#[derive(Debug, Clone)]
1071pub struct CcdPipelineConfig {
1072 pub velocity_threshold: f64,
1074 pub proximity_threshold: f64,
1076 pub max_toi_iterations: usize,
1078 pub toi_tolerance: f64,
1080}
1081
1082impl Default for CcdPipelineConfig {
1083 fn default() -> Self {
1084 Self {
1085 velocity_threshold: 0.01,
1086 proximity_threshold: 0.1,
1087 max_toi_iterations: 64,
1088 toi_tolerance: 1e-4,
1089 }
1090 }
1091}
1092
1093#[derive(Default)]
1102pub struct CcdPipeline {
1103 pub config: CcdPipelineConfig,
1105}
1106
1107impl CcdPipeline {
1108 pub fn new() -> Self {
1110 Self::default()
1111 }
1112
1113 pub fn with_config(config: CcdPipelineConfig) -> Self {
1115 Self { config }
1116 }
1117
1118 pub fn detect(&self, bodies: &[CcdBodyEntry]) -> Vec<CcdPair> {
1122 let mut pairs: Vec<CcdPair> = Vec::new();
1123
1124 for i in 0..bodies.len() {
1125 for j in (i + 1)..bodies.len() {
1126 let a = &bodies[i];
1127 let b = &bodies[j];
1128
1129 if !a.ccd_enabled && !b.ccd_enabled {
1131 continue;
1132 }
1133
1134 let rel_vel = sub3(a.linear_vel, b.linear_vel);
1136 if len3(rel_vel) < self.config.velocity_threshold {
1137 continue;
1138 }
1139
1140 if !self.could_overlap(a, b) {
1142 continue;
1143 }
1144
1145 if let Some(pair) = self.compute_pair_toi(a, b) {
1147 pairs.push(pair);
1148 }
1149 }
1150 }
1151
1152 pairs.sort_by(|x, y| {
1154 x.toi
1155 .partial_cmp(&y.toi)
1156 .unwrap_or(std::cmp::Ordering::Equal)
1157 });
1158 pairs
1159 }
1160
1161 fn could_overlap(&self, a: &CcdBodyEntry, b: &CcdBodyEntry) -> bool {
1163 let aabb_a = swept_bounding_aabb(a);
1165 let aabb_b = swept_bounding_aabb(b);
1166
1167 let expand = self.config.proximity_threshold;
1169 let min_a = [aabb_a[0] - expand, aabb_a[1] - expand, aabb_a[2] - expand];
1170 let max_a = [aabb_a[3] + expand, aabb_a[4] + expand, aabb_a[5] + expand];
1171 let min_b = [aabb_b[0], aabb_b[1], aabb_b[2]];
1172 let max_b = [aabb_b[3], aabb_b[4], aabb_b[5]];
1173
1174 min_a[0] <= max_b[0]
1176 && max_a[0] >= min_b[0]
1177 && min_a[1] <= max_b[1]
1178 && max_a[1] >= min_b[1]
1179 && min_a[2] <= max_b[2]
1180 && max_a[2] >= min_b[2]
1181 }
1182
1183 fn compute_pair_toi(&self, a: &CcdBodyEntry, b: &CcdBodyEntry) -> Option<CcdPair> {
1186 let vel_a = a.displacement();
1187 let vel_b = b.displacement();
1188
1189 let toi = sphere_sphere_toi(
1190 a.pos_start,
1191 vel_a,
1192 a.bounding_radius,
1193 b.pos_start,
1194 vel_b,
1195 b.bounding_radius,
1196 )?;
1197
1198 if toi > 1.0 {
1199 return None;
1200 }
1201
1202 let pos_a_toi = add3(a.pos_start, scale3(vel_a, toi));
1204 let pos_b_toi = add3(b.pos_start, scale3(vel_b, toi));
1205
1206 let d = sub3(pos_a_toi, pos_b_toi);
1208 let dist = len3(d);
1209 let normal = if dist > 1e-10 {
1210 scale3(d, 1.0 / dist)
1211 } else {
1212 [0.0, 1.0, 0.0]
1213 };
1214
1215 let point_a = add3(pos_a_toi, scale3(scale3(normal, -1.0), a.bounding_radius));
1217 let point_b = add3(pos_b_toi, scale3(normal, b.bounding_radius));
1218
1219 Some(CcdPair::new(
1220 a.handle, b.handle, toi, normal, point_a, point_b,
1221 ))
1222 }
1223}
1224
1225fn swept_bounding_aabb(body: &CcdBodyEntry) -> [f64; 6] {
1229 let r = body.bounding_radius;
1230 let p0 = body.pos_start;
1231 let p1 = body.pos_end;
1232 [
1233 p0[0].min(p1[0]) - r,
1234 p0[1].min(p1[1]) - r,
1235 p0[2].min(p1[2]) - r,
1236 p0[0].max(p1[0]) + r,
1237 p0[1].max(p1[1]) + r,
1238 p0[2].max(p1[2]) + r,
1239 ]
1240}
1241
1242pub fn advance_body_to(body: &CcdBodyEntry, t: f64) -> ([f64; 3], [f64; 4]) {
1251 let pos = add3(body.pos_start, scale3(body.displacement(), t));
1252 let orient = slerp_quat(body.orient_start, body.orient_end, t);
1254 (pos, orient)
1255}
1256
1257pub fn slerp_quat(q0: [f64; 4], q1: [f64; 4], t: f64) -> [f64; 4] {
1262 let mut q1 = q1;
1263 let dot = q0[0] * q1[0] + q0[1] * q1[1] + q0[2] * q1[2] + q0[3] * q1[3];
1265 if dot < 0.0 {
1266 q1 = [-q1[0], -q1[1], -q1[2], -q1[3]];
1267 }
1268 let dot = dot.abs().min(1.0);
1269
1270 if dot > 0.9995 {
1271 return quat_normalize([
1273 q0[0] + t * (q1[0] - q0[0]),
1274 q0[1] + t * (q1[1] - q0[1]),
1275 q0[2] + t * (q1[2] - q0[2]),
1276 q0[3] + t * (q1[3] - q0[3]),
1277 ]);
1278 }
1279
1280 let theta_0 = dot.acos();
1281 let theta = theta_0 * t;
1282 let sin_theta = theta.sin();
1283 let sin_theta_0 = theta_0.sin();
1284
1285 let s0 = (theta_0 - theta).sin() / sin_theta_0;
1286 let s1 = sin_theta / sin_theta_0;
1287
1288 quat_normalize([
1289 s0 * q0[0] + s1 * q1[0],
1290 s0 * q0[1] + s1 * q1[1],
1291 s0 * q0[2] + s1 * q1[2],
1292 s0 * q0[3] + s1 * q1[3],
1293 ])
1294}
1295
1296#[cfg(test)]
1299mod tests {
1300 use super::*;
1301 use oxiphysics_geometry::Sphere;
1302
1303 #[test]
1304 fn test_ccd_sphere_sphere_hit() {
1305 let s1 = Sphere::new(0.5);
1306 let s2 = Sphere::new(0.5);
1307
1308 let t1_start = Transform::from_position(Vec3::new(-5.0, 0.0, 0.0));
1310 let t1_end = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1311
1312 let t2_start = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
1314 let t2_end = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1315
1316 let result = time_of_impact(&s1, &t1_start, &t1_end, &s2, &t2_start, &t2_end);
1317 assert!(result.is_some(), "Should detect impact");
1318 let toi = result.unwrap().toi;
1319 assert!(
1321 toi > 0.0 && toi <= 1.0,
1322 "TOI should be in (0,1], got {}",
1323 toi
1324 );
1325 }
1326
1327 #[test]
1328 fn test_ccd_sphere_sphere_miss() {
1329 let s1 = Sphere::new(0.5);
1330 let s2 = Sphere::new(0.5);
1331
1332 let t1_start = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1334 let t1_end = Transform::from_position(Vec3::new(10.0, 0.0, 0.0));
1335 let t2_start = Transform::from_position(Vec3::new(0.0, 5.0, 0.0));
1336 let t2_end = Transform::from_position(Vec3::new(10.0, 5.0, 0.0));
1337
1338 let result = time_of_impact(&s1, &t1_start, &t1_end, &s2, &t2_start, &t2_end);
1339 assert!(
1340 result.is_none(),
1341 "Should not detect impact for parallel motion"
1342 );
1343 }
1344
1345 #[test]
1346 fn test_interpolate_transform() {
1347 let start = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1348 let end = Transform::from_position(Vec3::new(10.0, 0.0, 0.0));
1349 let mid = interpolate_transform(&start, &end, 0.5);
1350 assert!((mid.position.x - 5.0).abs() < 1e-10);
1351 }
1352
1353 #[test]
1356 fn test_sphere_sphere_toi_head_on() {
1357 let toi = sphere_sphere_toi(
1375 [-1.5, 0.0, 0.0],
1376 [1.0, 0.0, 0.0],
1377 0.5,
1378 [1.5, 0.0, 0.0],
1379 [-1.0, 0.0, 0.0],
1380 0.5,
1381 );
1382 assert!(toi.is_some(), "head-on spheres should collide");
1383 let t = toi.unwrap();
1384 assert!((t - 1.0).abs() < 1e-9, "expected toi=1.0, got {}", t);
1385 }
1386
1387 #[test]
1388 fn test_sphere_sphere_toi_moving_away() {
1389 let toi = sphere_sphere_toi(
1390 [0.0, 0.0, 0.0],
1391 [-1.0, 0.0, 0.0],
1392 0.5,
1393 [3.0, 0.0, 0.0],
1394 [1.0, 0.0, 0.0],
1395 0.5,
1396 );
1397 assert!(
1398 toi.is_none(),
1399 "diverging spheres should not collide, got {:?}",
1400 toi
1401 );
1402 }
1403
1404 #[test]
1405 fn test_sphere_plane_toi_falling() {
1406 let toi = sphere_plane_toi(
1409 [0.0, 0.0, 5.0],
1410 [0.0, 0.0, -10.0],
1411 0.5,
1412 [0.0, 0.0, 1.0],
1413 0.0,
1414 );
1415 assert!(toi.is_some(), "falling sphere should hit plane");
1416 let t = toi.unwrap();
1417 assert!((t - 0.45).abs() < 1e-9, "expected toi=0.45, got {}", t);
1418 }
1419
1420 #[test]
1421 fn test_sphere_sphere_toi_already_overlapping() {
1422 let toi = sphere_sphere_toi(
1424 [0.0, 0.0, 0.0],
1425 [0.0, 0.0, 0.0],
1426 0.5,
1427 [0.5, 0.0, 0.0],
1428 [0.0, 0.0, 0.0],
1429 0.5,
1430 );
1431 assert_eq!(toi, Some(0.0), "already overlapping → toi=0.0");
1432 }
1433
1434 #[test]
1437 fn test_conservative_advancement_struct_sphere_hit() {
1438 let ca = ConservativeAdvancement::new().with_threshold(1e-3);
1439 let toi = ca.compute_toi(
1440 ConservativeAdvancement::sphere_support(0.5),
1441 ConservativeAdvancement::sphere_support(0.5),
1442 [-3.0, 0.0, 0.0],
1443 [2.5, 0.0, 0.0],
1444 0.5,
1445 [3.0, 0.0, 0.0],
1446 [-2.5, 0.0, 0.0],
1447 0.5,
1448 );
1449 assert!(toi.is_some(), "CA struct should detect sphere–sphere hit");
1450 let t = toi.unwrap();
1451 assert!((0.0..=1.0).contains(&t), "toi must be in [0,1], got {}", t);
1452 }
1453
1454 #[test]
1455 fn test_conservative_advancement_struct_miss() {
1456 let ca = ConservativeAdvancement::new();
1457 let toi = ca.compute_toi(
1458 ConservativeAdvancement::sphere_support(0.5),
1459 ConservativeAdvancement::sphere_support(0.5),
1460 [0.0, 0.0, 0.0],
1461 [1.0, 0.0, 0.0],
1462 0.5,
1463 [0.0, 5.0, 0.0],
1464 [1.0, 5.0, 0.0],
1465 0.5,
1466 );
1467 assert!(toi.is_none(), "parallel motion should not collide");
1468 }
1469
1470 #[test]
1473 fn test_integrate_linear() {
1474 let pos = [0.0, 0.0, 0.0];
1475 let vel = [1.0, 2.0, 3.0];
1476 let result = integrate_linear(pos, vel, 0.5);
1477 assert!((result[0] - 0.5).abs() < 1e-12);
1478 assert!((result[1] - 1.0).abs() < 1e-12);
1479 assert!((result[2] - 1.5).abs() < 1e-12);
1480 }
1481
1482 #[test]
1483 fn test_integrate_angular_identity_quaternion() {
1484 let q = [0.0, 0.0, 0.0, 1.0];
1485 let omega = [0.0, 0.0, 0.0]; let result = integrate_angular(q, omega, 0.1);
1487 let len_sq = result[0] * result[0]
1489 + result[1] * result[1]
1490 + result[2] * result[2]
1491 + result[3] * result[3];
1492 assert!(
1493 (len_sq - 1.0).abs() < 1e-9,
1494 "quaternion should be unit length"
1495 );
1496 assert!((result[3] - 1.0).abs() < 1e-9, "w component should stay ~1");
1497 }
1498
1499 #[test]
1500 fn test_quat_normalize_already_unit() {
1501 let q = [0.0, 0.0, 0.0, 1.0];
1502 let n = quat_normalize(q);
1503 assert!((n[3] - 1.0).abs() < 1e-12);
1504 }
1505
1506 #[test]
1507 fn test_quat_normalize_scales() {
1508 let q = [0.0, 0.0, 0.0, 2.0];
1509 let n = quat_normalize(q);
1510 assert!((n[3] - 1.0).abs() < 1e-12);
1511 }
1512
1513 #[test]
1514 fn test_quat_rotate_unit_vec() {
1515 let s = (std::f64::consts::FRAC_PI_4).sin();
1518 let c = (std::f64::consts::FRAC_PI_4).cos();
1519 let q = [0.0, 0.0, s, c];
1520 let v = [1.0, 0.0, 0.0];
1521 let result = quat_rotate(q, v);
1522 assert!(
1523 (result[0] - 0.0).abs() < 1e-9,
1524 "x should be ~0, got {}",
1525 result[0]
1526 );
1527 assert!(
1528 (result[1] - 1.0).abs() < 1e-9,
1529 "y should be ~1, got {}",
1530 result[1]
1531 );
1532 assert!(
1533 (result[2] - 0.0).abs() < 1e-9,
1534 "z should be ~0, got {}",
1535 result[2]
1536 );
1537 }
1538
1539 #[test]
1542 fn test_slerp_at_t0_returns_q0() {
1543 let q0 = quat_normalize([0.1, 0.2, 0.3, 0.9]);
1544 let q1 = quat_normalize([0.4, 0.1, 0.2, 0.8]);
1545 let r = slerp_quat(q0, q1, 0.0);
1546 for i in 0..4 {
1547 assert!(
1548 (r[i] - q0[i]).abs() < 1e-9,
1549 "slerp(q0,q1,0) should equal q0"
1550 );
1551 }
1552 }
1553
1554 #[test]
1555 fn test_slerp_at_t1_returns_q1() {
1556 let q0 = quat_normalize([0.1, 0.2, 0.3, 0.9]);
1557 let q1 = quat_normalize([0.4, 0.1, 0.2, 0.8]);
1558 let r = slerp_quat(q0, q1, 1.0);
1559 for i in 0..4 {
1560 assert!(
1561 (r[i] - q1[i]).abs() < 1e-9,
1562 "slerp(q0,q1,1) should equal q1"
1563 );
1564 }
1565 }
1566
1567 #[test]
1568 fn test_slerp_unit_length() {
1569 let q0 = quat_normalize([0.1, 0.2, 0.3, 0.9]);
1570 let q1 = quat_normalize([0.4, 0.1, 0.2, 0.8]);
1571 for i in 0..=10 {
1572 let t = i as f64 / 10.0;
1573 let r = slerp_quat(q0, q1, t);
1574 let len = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3]).sqrt();
1575 assert!(
1576 (len - 1.0).abs() < 1e-9,
1577 "slerp result must be unit quaternion, t={}, len={}",
1578 t,
1579 len
1580 );
1581 }
1582 }
1583
1584 #[test]
1587 fn test_rigid_body_state_integrate() {
1588 let state = RigidBodyState::new(
1589 [0.0, 0.0, 0.0],
1590 [0.0, 0.0, 0.0, 1.0],
1591 [1.0, 0.0, 0.0],
1592 [0.0, 0.0, 0.0],
1593 );
1594 let next = state.integrate(1.0);
1595 assert!((next.position[0] - 1.0).abs() < 1e-9);
1596 assert!((next.position[1] - 0.0).abs() < 1e-9);
1597 }
1598
1599 fn make_body(handle: u32, pos: [f64; 3], vel: [f64; 3], radius: f64) -> CcdBodyEntry {
1602 let pos_end = add3(pos, vel);
1603 CcdBodyEntry {
1604 handle,
1605 pos_start: pos,
1606 pos_end,
1607 orient_start: [0.0, 0.0, 0.0, 1.0],
1608 orient_end: [0.0, 0.0, 0.0, 1.0],
1609 linear_vel: vel,
1610 angular_vel: [0.0; 3],
1611 bounding_radius: radius,
1612 ccd_enabled: true,
1613 }
1614 }
1615
1616 #[test]
1617 fn test_ccd_pipeline_two_approaching_bodies() {
1618 let pipeline = CcdPipeline::new();
1619 let bodies = vec![
1620 make_body(0, [-3.0, 0.0, 0.0], [2.5, 0.0, 0.0], 0.5),
1621 make_body(1, [3.0, 0.0, 0.0], [-2.5, 0.0, 0.0], 0.5),
1622 ];
1623 let pairs = pipeline.detect(&bodies);
1624 assert!(
1625 !pairs.is_empty(),
1626 "pipeline should detect approaching bodies"
1627 );
1628 assert!(pairs[0].toi >= 0.0 && pairs[0].toi <= 1.0);
1629 }
1630
1631 #[test]
1632 fn test_ccd_pipeline_diverging_bodies_no_hit() {
1633 let pipeline = CcdPipeline::new();
1634 let bodies = vec![
1635 make_body(0, [0.0, 0.0, 0.0], [-5.0, 0.0, 0.0], 0.5),
1636 make_body(1, [3.0, 0.0, 0.0], [5.0, 0.0, 0.0], 0.5),
1637 ];
1638 let pairs = pipeline.detect(&bodies);
1639 assert!(
1640 pairs.is_empty(),
1641 "diverging bodies should not generate CCD pair"
1642 );
1643 }
1644
1645 #[test]
1646 fn test_ccd_pipeline_sorted_by_toi() {
1647 let pipeline = CcdPipeline::new();
1648 let bodies = vec![
1651 make_body(0, [-5.0, 0.0, 0.0], [5.0, 0.0, 0.0], 0.5),
1652 make_body(1, [-1.0, 0.0, 0.0], [-5.0, 0.0, 0.0], 0.5),
1653 make_body(2, [3.0, 0.0, 0.0], [-3.0, 0.0, 0.0], 0.5),
1654 ];
1655 let pairs = pipeline.detect(&bodies);
1656 for i in 1..pairs.len() {
1658 assert!(
1659 pairs[i - 1].toi <= pairs[i].toi,
1660 "pairs must be sorted by TOI"
1661 );
1662 }
1663 }
1664
1665 #[test]
1666 fn test_ccd_pipeline_ccd_disabled_body_skipped() {
1667 let pipeline = CcdPipeline::new();
1668 let mut b0 = make_body(0, [-3.0, 0.0, 0.0], [2.5, 0.0, 0.0], 0.5);
1669 let mut b1 = make_body(1, [3.0, 0.0, 0.0], [-2.5, 0.0, 0.0], 0.5);
1670 b0.ccd_enabled = false;
1671 b1.ccd_enabled = false;
1672 let bodies = vec![b0, b1];
1673 let pairs = pipeline.detect(&bodies);
1674 assert!(
1675 pairs.is_empty(),
1676 "both CCD-disabled bodies should be skipped"
1677 );
1678 }
1679
1680 #[test]
1683 fn test_advance_body_to_midpoint() {
1684 let body = make_body(0, [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], 0.5);
1685 let (pos, _orient) = advance_body_to(&body, 0.5);
1686 assert!(
1687 (pos[0] - 5.0).abs() < 1e-9,
1688 "position at t=0.5 should be 5.0"
1689 );
1690 }
1691
1692 #[test]
1693 fn test_advance_body_to_start() {
1694 let body = make_body(0, [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], 1.0);
1695 let (pos, _) = advance_body_to(&body, 0.0);
1696 assert!((pos[0] - 1.0).abs() < 1e-12);
1697 assert!((pos[1] - 2.0).abs() < 1e-12);
1698 assert!((pos[2] - 3.0).abs() < 1e-12);
1699 }
1700
1701 #[test]
1702 fn test_advance_body_to_end() {
1703 let body = make_body(0, [1.0, 0.0, 0.0], [3.0, 0.0, 0.0], 0.5);
1704 let (pos, _) = advance_body_to(&body, 1.0);
1705 assert!((pos[0] - 4.0).abs() < 1e-9);
1706 }
1707}