1use super::{BodyType, PhysicsWorld};
99use crate::collider::Contact;
100use crate::math::{Fix128, Vec3Fix};
101#[cfg(not(feature = "std"))]
102use alloc::vec::Vec;
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123#[non_exhaustive]
124pub struct WorldCcdConfig {
125 enabled: bool,
126 motion_threshold: Fix128,
127}
128
129impl WorldCcdConfig {
130 #[must_use]
133 pub const fn new() -> Self {
134 Self {
135 enabled: false,
136 motion_threshold: Fix128::ONE,
137 }
138 }
139
140 #[must_use]
142 pub const fn on() -> Self {
143 Self::new().with_enabled(true)
144 }
145
146 #[must_use]
148 pub const fn with_enabled(mut self, enabled: bool) -> Self {
149 self.enabled = enabled;
150 self
151 }
152
153 #[must_use]
157 pub const fn with_motion_threshold(mut self, threshold: Fix128) -> Self {
158 self.motion_threshold = threshold;
159 self
160 }
161
162 #[must_use]
164 pub const fn is_enabled(&self) -> bool {
165 self.enabled
166 }
167
168 #[must_use]
170 pub const fn motion_threshold(&self) -> Fix128 {
171 self.motion_threshold
172 }
173}
174
175impl Default for WorldCcdConfig {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181#[derive(Clone, Copy, Debug)]
183pub(super) struct CcdHit {
184 body: usize,
186 other: Option<usize>,
188 t: Fix128,
190 normal: Vec3Fix,
192 point: Vec3Fix,
194}
195
196impl PhysicsWorld {
197 pub fn set_continuous_collision(&mut self, config: WorldCcdConfig) {
199 self.ccd = config;
200 }
201
202 #[must_use]
204 pub fn continuous_collision(&self) -> WorldCcdConfig {
205 self.ccd
206 }
207
208 fn ccd_movable(&self, i: usize) -> bool {
210 self.bodies[i].body_type == BodyType::Dynamic && !self.park.is_parked(i)
211 }
212
213 fn ccd_displacement(&self, i: usize) -> Vec3Fix {
216 let b = &self.bodies[i];
217 match b.body_type {
218 BodyType::Static => Vec3Fix::ZERO,
219 BodyType::Kinematic | BodyType::Dynamic if self.park.is_parked(i) => Vec3Fix::ZERO,
220 _ => b.position - b.prev_position,
221 }
222 }
223
224 fn ccd_swap_poses(&mut self) {
227 for i in 0..self.bodies.len() {
228 let b = &self.bodies[i];
229 if b.body_type == BodyType::Static || self.park.is_parked(i) {
230 continue;
231 }
232 let b = &mut self.bodies[i];
233 core::mem::swap(&mut b.position, &mut b.prev_position);
234 core::mem::swap(&mut b.rotation, &mut b.prev_rotation);
235 }
236 }
237
238 pub(super) fn ccd_sweep(&mut self) -> Vec<CcdHit> {
243 let mut hits = Vec::new();
244 if !self.ccd.enabled {
245 return hits;
246 }
247 let n = self.bodies.len();
248 let mut swept = Vec::new();
249 for i in 0..n {
250 let b = &self.bodies[i];
251 let mover = match b.body_type {
252 BodyType::Dynamic => !self.islands.is_sleeping(i),
253 BodyType::Kinematic => true,
254 BodyType::Static => false,
255 };
256 if !mover || b.is_sensor || self.park.is_parked(i) {
257 continue;
258 }
259 let Some(radius) = self.body_collision_radii.get(i).and_then(|r| *r) else {
260 continue;
261 };
262 let d = b.position - b.prev_position;
263 let Some(travel) = d.checked_length_scaled() else {
264 continue;
265 };
266 if travel.is_zero() {
267 continue;
268 }
269 let Some(limit) = self.ccd.motion_threshold.checked_mul(radius) else {
270 continue;
271 };
272 if travel > limit {
273 swept.push((i, radius, d, travel));
274 }
275 }
276 if swept.is_empty() {
277 return hits;
278 }
279
280 self.ccd_swap_poses();
281 for &(i, radius, d, travel) in &swept {
282 let cast = self.ccd_first_hit(i, radius, d, travel);
283 let pair = self.ccd_first_moving_hit(i, radius, d);
284 let first = match (cast, pair) {
285 (Some(c), Some(p)) => Some(if p.t < c.t { p } else { c }),
286 (c, p) => c.or(p),
287 };
288 if let Some(hit) = first {
289 hits.push(hit);
290 }
291 }
292 self.ccd_swap_poses();
293
294 let displacement: Vec<Vec3Fix> = (0..n).map(|i| self.ccd_displacement(i)).collect();
297 for hit in &hits {
298 self.ccd_place(hit, &displacement);
299 }
300 hits
301 }
302
303 fn ccd_first_hit(
321 &mut self,
322 i: usize,
323 radius: Fix128,
324 d: Vec3Fix,
325 travel: Fix128,
326 ) -> Option<CcdHit> {
327 let own = self.body_filter(i);
328 let kinematic = self.bodies[i].body_type == BodyType::Kinematic;
329 let filter = crate::shape_raycast::RayFilter::new()
330 .with_layer_mask(own.mask)
331 .with_sdf(false)
332 .with_static(!kinematic)
333 .excluding_body(i);
334 let start = self.bodies[i].position;
335 let mut hidden = Hidden::default();
336 for target in self.overlap_sphere(start, radius, &filter) {
337 let other = match target {
338 crate::shape_raycast::RayTarget::Body(j) => Some(j),
339 _ => None,
340 };
341 let Some(n) = self.ccd_start_normal(i, radius, target) else {
344 continue;
345 };
346 if d.dot(n) >= Fix128::ZERO || !self.ccd_accepts(i, other, kinematic) {
347 self.ccd_hide(target, &mut hidden);
348 }
349 }
350 let mut found = None;
351 while let Some(hit) = self.cast_sphere(start, radius, d, travel, &filter) {
352 match self.ccd_judge(i, radius, d, travel, kinematic, &hit) {
353 Judged::Hit(h) => {
354 found = Some(h);
355 break;
356 }
357 Judged::Hide => {
358 if !self.ccd_hide(hit.target, &mut hidden) {
359 break;
360 }
361 }
362 }
363 }
364 for (j, layer) in hidden.bodies.into_iter().rev() {
365 self.body_filters[j].layer = layer;
366 }
367 for (k, c) in hidden.statics.into_iter().rev() {
368 self.static_colliders[k] = c;
369 }
370 found
371 }
372
373 fn ccd_hide(&mut self, target: crate::shape_raycast::RayTarget, hidden: &mut Hidden) -> bool {
378 match target {
379 crate::shape_raycast::RayTarget::Body(j) => {
380 hidden.bodies.push((j, self.body_filters[j].layer));
381 self.body_filters[j].layer = 0;
382 true
383 }
384 crate::shape_raycast::RayTarget::StaticCollider(k) => {
385 let empty = crate::static_collider::StaticCollider::TriMesh(
386 crate::trimesh::TriMesh::from_indexed(&[], &[]),
387 );
388 hidden
389 .statics
390 .push((k, core::mem::replace(&mut self.static_colliders[k], empty)));
391 true
392 }
393 crate::shape_raycast::RayTarget::Sdf(_) => false,
394 }
395 }
396
397 fn ccd_judge(
400 &self,
401 i: usize,
402 radius: Fix128,
403 d: Vec3Fix,
404 travel: Fix128,
405 kinematic: bool,
406 hit: &crate::world_shape_query::WorldShapeHit,
407 ) -> Judged {
408 let start = self.bodies[i].position;
409 if hit.t.is_zero() && hit.point == start {
413 return match self.ccd_start_normal(i, radius, hit.target) {
414 Some(n) if d.dot(n) < Fix128::ZERO && self.ccd_accepts(i, hit.body, kinematic) => {
415 Judged::Hit(CcdHit {
416 body: i,
417 other: hit.body,
418 t: Fix128::ZERO,
419 normal: n,
420 point: start - n * radius,
421 })
422 }
423 _ => Judged::Hide,
424 };
425 }
426 if !self.ccd_accepts(i, hit.body, kinematic) {
427 return Judged::Hide;
428 }
429 let Some(j) = hit.body else {
430 return Judged::Hit(CcdHit {
431 body: i,
432 other: None,
433 t: fraction(hit.t, travel),
434 normal: hit.normal,
435 point: hit.point,
436 });
437 };
438 let dj = match self.bodies[j].body_type {
440 BodyType::Static => Vec3Fix::ZERO,
441 _ if self.park.is_parked(j) => Vec3Fix::ZERO,
442 _ => self.bodies[j].prev_position - self.bodies[j].position,
443 };
444 if dj == Vec3Fix::ZERO {
445 return Judged::Hit(CcdHit {
446 body: i,
447 other: Some(j),
448 t: fraction(hit.t, travel),
449 normal: hit.normal,
450 point: hit.point,
451 });
452 }
453 let Some(rj) = self.body_collision_radii.get(j).and_then(|r| *r) else {
454 return Judged::Hide;
455 };
456 match crate::ccd::sphere_sphere_toi(self.bodies[j].position, rj, dj, start, radius, d) {
459 Some(toi) => Judged::Hit(CcdHit {
460 body: i,
461 other: Some(j),
462 t: toi.t,
463 normal: toi.normal,
464 point: toi.point,
465 }),
466 None => Judged::Hide,
467 }
468 }
469
470 fn ccd_accepts(&self, i: usize, other: Option<usize>, kinematic: bool) -> bool {
474 match other {
475 None => !kinematic,
476 Some(j) => {
477 crate::filter::CollisionFilter::can_collide(
478 &self.body_filter(i),
479 &self.body_filter(j),
480 ) && (!kinematic || self.ccd_movable(j))
481 }
482 }
483 }
484
485 fn ccd_start_normal(
489 &self,
490 i: usize,
491 radius: Fix128,
492 target: crate::shape_raycast::RayTarget,
493 ) -> Option<Vec3Fix> {
494 let start = self.bodies[i].position;
495 match target {
496 crate::shape_raycast::RayTarget::Body(j) => {
497 let pose = (self.bodies[j].position, self.bodies[j].rotation);
498 match self.body_colliders.get(j).and_then(Option::as_ref) {
499 Some(c) => crate::body_collider::contact_with_sphere(
500 c,
501 pose,
502 crate::collider::Sphere::new(start, radius),
503 false,
504 )
505 .map(|c| c.normal),
506 None => (start - pose.0).try_normalize_scaled(),
507 }
508 }
509 crate::shape_raycast::RayTarget::StaticCollider(k) => self
510 .static_colliders
511 .get(k)
512 .and_then(|c| c.collide_sphere(start, radius))
513 .map(|c| c.normal),
514 crate::shape_raycast::RayTarget::Sdf(_) => None,
515 }
516 }
517
518 fn ccd_first_moving_hit(&self, i: usize, radius: Fix128, d: Vec3Fix) -> Option<CcdHit> {
524 let own = self.body_filter(i);
525 let start = self.bodies[i].position;
526 let mut best: Option<CcdHit> = None;
527 for j in 0..self.bodies.len() {
528 if j == i || self.bodies[j].is_sensor {
529 continue;
530 }
531 if self.bodies[i].body_type == BodyType::Kinematic && !self.ccd_movable(j) {
532 continue;
533 }
534 let Some(rj) = self.body_collision_radii.get(j).and_then(|r| *r) else {
535 continue;
536 };
537 let dj = match self.bodies[j].body_type {
539 BodyType::Static => continue,
540 _ if self.park.is_parked(j) => continue,
541 _ => self.bodies[j].prev_position - self.bodies[j].position,
542 };
543 if dj == Vec3Fix::ZERO {
544 continue;
545 }
546 let other = self.body_filter(j);
547 if !crate::filter::CollisionFilter::can_collide(&own, &other) {
548 continue;
549 }
550 let pj = self.bodies[j].position;
551 let gap = start - pj;
553 let reach = radius + rj;
554 if gap
555 .checked_length_squared()
556 .zip(reach.checked_mul(reach))
557 .is_some_and(|(g2, r2)| g2 <= r2)
558 {
559 continue;
560 }
561 let Some(toi) = crate::ccd::sphere_sphere_toi(pj, rj, dj, start, radius, d) else {
562 continue;
563 };
564 if best.is_none_or(|b| toi.t < b.t) {
565 best = Some(CcdHit {
566 body: i,
567 other: Some(j),
568 t: toi.t,
569 normal: toi.normal,
570 point: toi.point,
571 });
572 }
573 }
574 best
575 }
576
577 fn ccd_place(&mut self, hit: &CcdHit, displacement: &[Vec3Fix]) {
579 let n = hit.normal;
580 let rest = Fix128::ONE - hit.t;
581 let di = displacement[hit.body];
582 let (dj, wj) = match hit.other {
583 Some(j) => (
584 displacement[j],
585 if self.ccd_movable(j) {
586 self.bodies[j].inv_mass
587 } else {
588 Fix128::ZERO
589 },
590 ),
591 None => (Vec3Fix::ZERO, Fix128::ZERO),
592 };
593 let wi = self.bodies[hit.body].inv_mass;
594 let w = wi + wj;
595 let along = if w.is_zero() {
598 Fix128::ZERO
599 } else {
600 (di.dot(n) * wj + dj.dot(n) * wi) / w
601 };
602 let place = |p0: Vec3Fix, d: Vec3Fix| -> Vec3Fix {
603 let tangent = d - n * d.dot(n);
604 p0 + d * hit.t + (tangent + n * along) * rest
605 };
606 if self.ccd_movable(hit.body) {
608 let pi = place(self.bodies[hit.body].prev_position, di);
609 self.bodies[hit.body].position = pi;
610 }
611 if let Some(j) = hit.other {
612 if self.ccd_movable(j) {
613 let pj = place(self.bodies[j].prev_position, dj);
614 self.bodies[j].position = pj;
615 }
616 }
617 }
618
619 pub(super) fn ccd_add_contacts(&mut self, hits: &[CcdHit]) {
622 for (k, hit) in hits.iter().enumerate() {
623 let Some(j) = hit.other else {
624 continue;
625 };
626 let i = hit.body;
627 if hits[..k].iter().any(|h| h.body == j && h.other == Some(i)) {
629 continue;
630 }
631 let radius = self
632 .body_collision_radii
633 .get(i)
634 .and_then(|r| *r)
635 .unwrap_or(Fix128::ZERO);
636 let contact = Contact {
637 depth: Fix128::ZERO,
638 normal: hit.normal,
639 point_a: self.bodies[i].position - hit.normal * radius,
640 point_b: hit.point,
641 };
642 let rel_vel = (self.bodies[i].velocity - self.bodies[j].velocity).dot(hit.normal);
643 self.events
644 .report_contact(i, j, hit.normal, hit.point, Fix128::ZERO, rel_vel);
645 for x in [i, j] {
646 if !self.islands.is_sleeping(x) {
647 continue;
648 }
649 if self.park.is_parked(x) {
650 self.islands.wake_body(x);
651 self.park.unpark(x, &mut self.stage_work);
652 } else {
653 self.islands.wake_island(x);
654 }
655 }
656 self.add_contact_with_material(i, j, contact);
657 }
658 }
659}
660
661#[derive(Default)]
663struct Hidden {
664 bodies: Vec<(usize, u32)>,
666 statics: Vec<(usize, crate::static_collider::StaticCollider)>,
668}
669
670enum Judged {
672 Hit(CcdHit),
674 Hide,
676}
677
678fn fraction(hit: Fix128, travel: Fix128) -> Fix128 {
680 if hit >= travel {
681 Fix128::ONE
682 } else {
683 hit / travel
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
701 use crate::filter::CollisionFilter;
702 use crate::material::PhysicsMaterial;
703 use crate::plane_collider::PlaneCollider;
704 use crate::shape::Shape;
705 use crate::solver::{PhysicsConfig, RigidBody};
706 use crate::static_collider::StaticCollider;
707 use crate::trimesh::TriMesh;
708
709 fn f(x: f64) -> Fix128 {
710 Fix128::from_f64(x)
711 }
712
713 fn v3(x: f64, y: f64, z: f64) -> Vec3Fix {
714 Vec3Fix::new(f(x), f(y), f(z))
715 }
716
717 fn h() -> Fix128 {
718 Fix128::from_ratio(1, 64)
719 }
720
721 fn close(got: Vec3Fix, want: Vec3Fix, tol: f64, what: &str) {
722 let d = [got.x - want.x, got.y - want.y, got.z - want.z];
723 assert!(
724 d.iter().all(|e| e.to_f64().abs() <= tol),
725 "{what}: got ({}, {}, {}), want ({}, {}, {})",
726 got.x.to_f64(),
727 got.y.to_f64(),
728 got.z.to_f64(),
729 want.x.to_f64(),
730 want.y.to_f64(),
731 want.z.to_f64()
732 );
733 }
734
735 const POS_TOL: f64 = 1e-9;
736 const VEL_TOL: f64 = 64e-9;
737
738 fn bare(config: WorldCcdConfig) -> PhysicsWorld {
739 let mut w = PhysicsWorld::new(PhysicsConfig {
740 substeps: 1,
741 gravity: Vec3Fix::ZERO,
742 damping: Fix128::ONE,
743 ..PhysicsConfig::default()
744 });
745 w.set_continuous_collision(config);
746 w
747 }
748
749 fn half_bouncy(w: &mut PhysicsWorld) {
750 let id = w.material_table.register(PhysicsMaterial::new(
751 1,
752 Fix128::ZERO,
753 Fix128::from_ratio(1, 2),
754 ));
755 for i in 0..w.bodies.len() {
756 w.set_body_material(i, id);
757 }
758 }
759
760 fn sphere(
761 w: &mut PhysicsWorld,
762 at: Vec3Fix,
763 mass: i64,
764 radius: f64,
765 velocity: Vec3Fix,
766 ) -> usize {
767 let mut b = RigidBody::new_dynamic(at, Fix128::from_int(mass));
768 b.velocity = velocity;
769 w.add_body_with_radius(b, f(radius))
770 }
771
772 fn plate(w: &mut PhysicsWorld) -> usize {
775 let p = w.add_body(RigidBody::new_static(v3(5.0, 0.0, 0.0)));
776 w.set_body_shape(
777 p,
778 &Shape::Box {
779 half_extents: v3(1.0 / 64.0, 3.0, 3.0),
780 },
781 );
782 p
783 }
784
785 fn stop_x() -> Fix128 {
788 Fix128::from_int(5) - Fix128::from_ratio(1, 64) - Fix128::from_ratio(1, 4)
789 }
790
791 fn predict(w: &mut PhysicsWorld, moves: &[(usize, Vec3Fix)]) {
794 for b in &mut w.bodies {
795 b.prev_position = b.position;
796 b.prev_rotation = b.rotation;
797 }
798 for &(i, d) in moves {
799 w.bodies[i].position = w.bodies[i].prev_position + d;
800 }
801 }
802
803 fn x(at: Fix128) -> Vec3Fix {
804 Vec3Fix::new(at, Fix128::ZERO, Fix128::ZERO)
805 }
806
807 #[test]
812 fn off_sweeps_nothing() {
813 let mut w = bare(WorldCcdConfig::new());
814 assert_eq!(w.continuous_collision(), WorldCcdConfig::new());
815 assert!(!w.continuous_collision().is_enabled());
816 plate(&mut w);
817 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, v3(512.0, 0.0, 0.0));
818 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
819 assert!(w.ccd_sweep().is_empty());
820 assert_eq!(w.bodies[s].position, v3(8.0, 0.0, 0.0));
821 assert_eq!(w.bodies[s].prev_position, Vec3Fix::ZERO);
822
823 let mut w = bare(WorldCcdConfig::default());
824 plate(&mut w);
825 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, v3(512.0, 0.0, 0.0));
826 w.step(h());
827 assert_eq!(w.bodies[s].position, v3(8.0, 0.0, 0.0));
828 assert_eq!(w.bodies[s].velocity, v3(512.0, 0.0, 0.0));
829 assert!(w.contact_events().is_empty());
830 }
831
832 #[test]
838 fn the_threshold_selects_displacements_strictly_longer_than_threshold_times_radius() {
839 let config = WorldCcdConfig::on().with_motion_threshold(Fix128::from_int(40));
840 assert_eq!(config.motion_threshold(), Fix128::from_int(40));
841 assert!(config.is_enabled());
842
843 let mut w = bare(config);
844 plate(&mut w);
845 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
846 predict(&mut w, &[(s, v3(10.0, 0.0, 0.0))]);
847 assert!(w.ccd_sweep().is_empty(), "swept at the threshold");
848 assert_eq!(w.bodies[s].position, v3(10.0, 0.0, 0.0));
849
850 let past = Fix128::from_int(10) + Fix128::from_ratio(1, 1 << 20);
851 let mut w = bare(config);
852 let p = plate(&mut w);
853 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
854 predict(&mut w, &[(s, x(past))]);
855 let hits = w.ccd_sweep();
856 assert_eq!(hits.len(), 1);
857 assert_eq!(hits[0].body, s);
858 assert_eq!(hits[0].other, Some(p));
859 assert_eq!(hits[0].t, stop_x() / past);
860 assert_eq!(hits[0].normal, v3(-1.0, 0.0, 0.0));
861 close(w.bodies[s].position, x(stop_x()), POS_TOL, "placed");
862 }
863
864 #[test]
868 fn sensors_bodies_without_radius_still_bodies_and_overflowing_limits_are_not_swept() {
869 let run = |w: &mut PhysicsWorld, i: usize, d: Vec3Fix| {
870 predict(w, &[(i, d)]);
871 let hits = w.ccd_sweep();
872 (hits.len(), w.bodies[i].position)
873 };
874 let far = v3(8.0, 0.0, 0.0);
875
876 let mut w = bare(WorldCcdConfig::on());
877 plate(&mut w);
878 let s = w.add_body_with_radius(RigidBody::new_sensor(Vec3Fix::ZERO), f(0.25));
879 assert_eq!(run(&mut w, s, far), (0, far), "sensor");
880
881 let mut w = bare(WorldCcdConfig::on());
882 plate(&mut w);
883 let s = w.add_body(RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE));
884 assert_eq!(run(&mut w, s, far), (0, far), "no radius");
885
886 let mut w = bare(WorldCcdConfig::on());
887 plate(&mut w);
888 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
889 assert_eq!(run(&mut w, s, Vec3Fix::ZERO), (0, Vec3Fix::ZERO), "still");
890
891 let mut w = bare(WorldCcdConfig::on().with_motion_threshold(Fix128::from_int(i64::MAX)));
892 plate(&mut w);
893 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 2.0, Vec3Fix::ZERO);
894 assert_eq!(run(&mut w, s, far), (0, far), "overflowing limit");
895 }
896
897 #[test]
904 fn start_overlaps_it_does_not_move_into_are_hidden() {
905 let mut w = bare(WorldCcdConfig::on());
906 let p = plate(&mut w);
907 let lap = 1.0 / 4096.0;
908 w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
909 v3(0.0, 1.0, 0.0),
910 f(-0.25 + lap),
911 )));
912 w.add_body_with_radius(
914 RigidBody::new_static(v3(0.0, 0.0, -1.25 + lap)),
915 Fix128::ONE,
916 );
917 let b = w.add_body(RigidBody::new_static(v3(0.0, 0.0, 1.25 - lap)));
919 w.set_body_shape(
920 b,
921 &Shape::Box {
922 half_extents: v3(1.0, 1.0, 1.0),
923 },
924 );
925 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
926 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
927 let hits = w.ccd_sweep();
928 assert_eq!(hits.len(), 1);
929 assert_eq!(hits[0].other, Some(p));
930 assert_eq!(hits[0].t, stop_x() / Fix128::from_int(8));
931 assert_eq!(hits[0].normal, v3(-1.0, 0.0, 0.0));
932 close(w.bodies[s].position, x(stop_x()), POS_TOL, "placed");
933 assert_eq!(w.body_filter(b), CollisionFilter::DEFAULT);
935 assert!(matches!(w.static_colliders[0], StaticCollider::Plane(_)));
936 }
937
938 #[test]
942 fn moving_into_a_start_overlap_is_a_hit_at_t_zero() {
943 let scene = |vx: f64| {
944 let mut w = bare(WorldCcdConfig::on());
945 let o = w.add_body_with_radius(RigidBody::new_static(Vec3Fix::ZERO), Fix128::ONE);
946 let s = sphere(&mut w, v3(-1.5, 0.0, 0.0), 1, 0.5, v3(vx, 0.0, 0.0));
947 half_bouncy(&mut w);
948 (w, o, s)
949 };
950
951 let (mut w, o, s) = scene(512.0);
952 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
953 let hits = w.ccd_sweep();
954 assert_eq!(hits.len(), 1);
955 assert_eq!(hits[0].other, Some(o));
956 assert_eq!(hits[0].t, Fix128::ZERO);
957 close(hits[0].normal, v3(-1.0, 0.0, 0.0), 1e-19, "normal");
959 close(hits[0].point, v3(-1.0, 0.0, 0.0), 1e-19, "point");
960
961 let (mut w, _, s) = scene(512.0);
962 w.step(h());
963 close(w.bodies[s].position, v3(-1.5, 0.0, 0.0), POS_TOL, "into");
964 close(w.bodies[s].velocity, v3(-256.0, 0.0, 0.0), VEL_TOL, "into");
965 assert_eq!(w.contact_events().len(), 1);
966
967 let (mut w, _, s) = scene(-512.0);
968 w.step(h());
969 assert_eq!(w.bodies[s].position, v3(-9.5, 0.0, 0.0), "away");
970 assert_eq!(w.bodies[s].velocity, v3(-512.0, 0.0, 0.0), "away");
971 }
972
973 #[test]
979 fn moving_into_a_box_it_starts_inside_of_is_a_hit_at_t_zero() {
980 let y0 = 1.0 / 64.0 + 0.25 - 1.0 / 1024.0;
981 let scene = || {
982 let mut w = bare(WorldCcdConfig::on());
983 let floor = w.add_body(RigidBody::new_static(Vec3Fix::ZERO));
984 w.set_body_shape(
985 floor,
986 &Shape::Box {
987 half_extents: v3(3.0, 1.0 / 64.0, 3.0),
988 },
989 );
990 let s = sphere(&mut w, v3(0.0, y0, 0.0), 1, 0.25, v3(0.0, -512.0, 0.0));
991 half_bouncy(&mut w);
992 (w, floor, s)
993 };
994
995 let (mut w, floor, s) = scene();
996 predict(&mut w, &[(s, v3(0.0, -8.0, 0.0))]);
997 let hits = w.ccd_sweep();
998 assert_eq!(hits.len(), 1);
999 assert_eq!(hits[0].other, Some(floor));
1000 assert_eq!(hits[0].t, Fix128::ZERO);
1001 close(hits[0].normal, v3(0.0, 1.0, 0.0), 1e-7, "normal");
1004 close(hits[0].point, v3(0.0, y0 - 0.25, 0.0), 1e-7, "point");
1005 close(w.bodies[s].position, v3(0.0, y0, 0.0), 1e-6, "held");
1008
1009 let (mut w, _, s) = scene();
1010 w.step(h());
1011 let y = w.bodies[s].position.y.to_f64();
1012 assert!(y >= y0 - POS_TOL, "passed the floor: y = {y}");
1013 assert!(w.bodies[s].velocity.y.to_f64() > 0.0, "did not bounce");
1014 }
1015
1016 #[test]
1024 fn a_target_the_pair_filter_rejects_is_retried() {
1025 let group = CollisionFilter::DEFAULT.with_group(7);
1026 for at in [2.5, 0.75] {
1027 let mut w = bare(WorldCcdConfig::on());
1028 let p = plate(&mut w);
1029 let g = w.add_body_with_radius(RigidBody::new_static(v3(at, 0.0, 0.0)), Fix128::ONE);
1030 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
1031 w.set_body_filter(g, group);
1032 w.set_body_filter(s, group);
1033 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
1034 let hits = w.ccd_sweep();
1035 assert_eq!(hits.len(), 1, "at {at}");
1036 assert_eq!(hits[0].other, Some(p), "at {at}");
1037 assert_eq!(hits[0].t, stop_x() / Fix128::from_int(8), "at {at}");
1038 assert_eq!(w.body_filter(g), group, "at {at}: filter not put back");
1039 }
1040
1041 let mut w = bare(WorldCcdConfig::on());
1043 plate(&mut w);
1044 let g = w.add_body_with_radius(RigidBody::new_static(v3(2.5, 0.0, 0.0)), Fix128::ONE);
1045 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
1046 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
1047 let hits = w.ccd_sweep();
1048 assert_eq!(hits[0].other, Some(g));
1049 assert_eq!(hits[0].t, Fix128::from_ratio(5, 32));
1050 }
1051
1052 #[test]
1060 fn two_moving_spheres_are_retimed_by_their_relative_motion() {
1061 let scene = |mass_b: i64| {
1062 let mut w = bare(WorldCcdConfig::on());
1063 let a = sphere(&mut w, v3(-6.0, 0.0, 0.0), 1, 0.5, v3(512.0, 0.0, 0.0));
1064 let b = sphere(&mut w, v3(6.0, 0.0, 0.0), mass_b, 0.5, v3(-512.0, 0.0, 0.0));
1065 half_bouncy(&mut w);
1066 (w, a, b)
1067 };
1068
1069 let (mut w, a, b) = scene(1);
1070 predict(&mut w, &[(a, v3(8.0, 0.0, 0.0)), (b, v3(-8.0, 0.0, 0.0))]);
1071 let hits = w.ccd_sweep();
1072 assert_eq!(hits.len(), 2);
1073 assert_eq!((hits[0].body, hits[0].other), (a, Some(b)));
1074 assert_eq!((hits[1].body, hits[1].other), (b, Some(a)));
1075 assert_eq!(hits[0].t, Fix128::from_ratio(11, 16));
1076 assert_eq!(hits[0].normal, v3(-1.0, 0.0, 0.0));
1077 assert_eq!(w.bodies[a].position, v3(-0.5, 0.0, 0.0));
1078 assert_eq!(w.bodies[b].position, v3(0.5, 0.0, 0.0));
1079
1080 let (mut w, a, b) = scene(1);
1081 w.step(h());
1082 close(w.bodies[a].position, v3(-0.5, 0.0, 0.0), POS_TOL, "a");
1083 close(w.bodies[b].position, v3(0.5, 0.0, 0.0), POS_TOL, "b");
1084 close(w.bodies[a].velocity, v3(-256.0, 0.0, 0.0), VEL_TOL, "a");
1085 close(w.bodies[b].velocity, v3(256.0, 0.0, 0.0), VEL_TOL, "b");
1086 assert_eq!(w.contact_events().len(), 1);
1088
1089 let (mut w, a, b) = scene(3);
1090 w.step(h());
1091 close(
1092 w.bodies[a].position,
1093 v3(-0.5 - 1.25, 0.0, 0.0),
1094 POS_TOL,
1095 "a 1:3",
1096 );
1097 close(
1098 w.bodies[b].position,
1099 v3(0.5 - 1.25, 0.0, 0.0),
1100 POS_TOL,
1101 "b 1:3",
1102 );
1103 close(w.bodies[a].velocity, v3(-640.0, 0.0, 0.0), VEL_TOL, "a 1:3");
1105 close(w.bodies[b].velocity, v3(-128.0, 0.0, 0.0), VEL_TOL, "b 1:3");
1106 }
1107
1108 #[test]
1114 fn a_cast_hit_on_a_moving_body_is_retimed() {
1115 let mut w = bare(WorldCcdConfig::on());
1116 let a = sphere(&mut w, Vec3Fix::ZERO, 1, 0.5, Vec3Fix::ZERO);
1117 let b = sphere(&mut w, v3(4.0, 0.0, 0.0), 1, 0.5, Vec3Fix::ZERO);
1118 predict(&mut w, &[(a, v3(16.0, 0.0, 0.0)), (b, v3(4.0, 0.0, 0.0))]);
1119 let hits = w.ccd_sweep();
1120 assert_eq!(hits.len(), 2);
1121 assert_eq!((hits[0].body, hits[0].other), (a, Some(b)));
1122 assert_eq!(hits[0].t, Fix128::from_ratio(1, 4));
1123 assert_eq!(hits[0].normal, v3(-1.0, 0.0, 0.0));
1124 assert_eq!(w.bodies[a].position, v3(4.0 + 7.5, 0.0, 0.0));
1127 assert_eq!(w.bodies[b].position, v3(5.0 + 7.5, 0.0, 0.0));
1128
1129 let mut w = bare(WorldCcdConfig::on());
1130 let a = sphere(&mut w, Vec3Fix::ZERO, 1, 0.5, v3(512.0, 0.0, 0.0));
1131 let b = sphere(&mut w, v3(4.0, 0.0, 0.0), 1, 0.5, v3(1024.0, 0.0, 0.0));
1132 predict(&mut w, &[(a, v3(8.0, 0.0, 0.0)), (b, v3(16.0, 0.0, 0.0))]);
1133 assert!(w.ccd_sweep().is_empty());
1134 assert_eq!(w.bodies[a].position, v3(8.0, 0.0, 0.0));
1135 assert_eq!(w.bodies[b].position, v3(20.0, 0.0, 0.0));
1136 }
1137
1138 #[test]
1146 fn the_moving_pass_skips_bodies_it_cannot_answer() {
1147 let group = CollisionFilter::DEFAULT.with_group(7);
1148 let mut w = bare(WorldCcdConfig::on());
1149 let p = plate(&mut w);
1150 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
1151 let bare_body = w.add_body(RigidBody::new_dynamic(v3(2.0, -4.0, 0.0), Fix128::ONE));
1152 let grouped = sphere(&mut w, v3(4.0, -4.0, 0.0), 1, 0.25, Vec3Fix::ZERO);
1153 let lapped = sphere(&mut w, v3(0.0, 0.0, 0.25), 1, 0.25, Vec3Fix::ZERO);
1154 w.set_body_filter(s, group);
1155 w.set_body_filter(grouped, group);
1156 let up = v3(0.0, 8.0, 0.0);
1157 let back = v3(0.0, 0.0, 8.0);
1158 predict(
1159 &mut w,
1160 &[
1161 (s, v3(8.0, 0.0, 0.0)),
1162 (bare_body, up),
1163 (grouped, up),
1164 (lapped, back),
1165 ],
1166 );
1167 let hits = w.ccd_sweep();
1168 assert_eq!(hits.len(), 1);
1169 assert_eq!((hits[0].body, hits[0].other), (s, Some(p)));
1170 assert_eq!(hits[0].t, stop_x() / Fix128::from_int(8));
1171 assert_eq!(w.bodies[bare_body].position, v3(2.0, 4.0, 0.0));
1172 assert_eq!(w.bodies[grouped].position, v3(4.0, 4.0, 0.0));
1173 assert_eq!(w.bodies[lapped].position, v3(0.0, 0.0, 8.25));
1174 }
1175
1176 #[test]
1180 fn hits_add_one_contact_per_pair_and_wake_a_sleeping_body() {
1181 let mut w = bare(WorldCcdConfig::on());
1182 let a = sphere(&mut w, v3(-6.0, 0.0, 0.0), 1, 0.5, Vec3Fix::ZERO);
1183 let b = sphere(&mut w, v3(6.0, 0.0, 0.0), 1, 0.5, Vec3Fix::ZERO);
1184 predict(&mut w, &[(a, v3(8.0, 0.0, 0.0)), (b, v3(-8.0, 0.0, 0.0))]);
1185 let hits = w.ccd_sweep();
1186 assert_eq!(hits.len(), 2);
1187 w.clear_contacts();
1188 w.ccd_add_contacts(&hits);
1189 assert_eq!(w.contact_constraints.len(), 1);
1190 assert_eq!(w.contact_events().len(), 1);
1191 let c = &w.contact_constraints[0];
1192 assert_eq!((c.body_a, c.body_b), (a, b));
1193 assert_eq!(c.contact.depth, Fix128::ZERO);
1194
1195 let mut w = bare(WorldCcdConfig::on());
1197 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
1198 let still = sphere(&mut w, v3(4.0, 0.0, 0.0), 1, 0.25, Vec3Fix::ZERO);
1199 w.islands.sleep_data[still].state = crate::sleeping::SleepState::Sleeping;
1200 assert!(w.is_sleeping(still));
1201 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
1202 let hits = w.ccd_sweep();
1203 assert_eq!(hits.len(), 1);
1204 assert_eq!(hits[0].other, Some(still));
1205 assert_eq!(hits[0].t, Fix128::from_ratio(7, 16));
1206 w.clear_contacts();
1207 w.ccd_add_contacts(&hits);
1208 assert!(!w.is_sleeping(still));
1209 assert_eq!(w.contact_constraints.len(), 1);
1210
1211 let mut w = bare(WorldCcdConfig::on());
1213 w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
1214 v3(-1.0, 0.0, 0.0),
1215 f(-5.0),
1216 )));
1217 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, Vec3Fix::ZERO);
1218 predict(&mut w, &[(s, v3(8.0, 0.0, 0.0))]);
1219 let hits = w.ccd_sweep();
1220 assert_eq!(hits.len(), 1);
1221 assert_eq!(hits[0].other, None);
1222 w.clear_contacts();
1223 w.ccd_add_contacts(&hits);
1224 assert!(w.contact_constraints.is_empty());
1225 }
1226
1227 #[test]
1235 fn a_kinematic_body_sweeps_only_for_dynamic_bodies() {
1236 let scene = || {
1237 let mut w = bare(WorldCcdConfig::on());
1238 let wall = w.add_body(RigidBody::new_static(v3(-3.0, 0.0, 0.0)));
1239 w.set_body_shape(
1240 wall,
1241 &Shape::Box {
1242 half_extents: v3(1.0 / 64.0, 2.0, 2.0),
1243 },
1244 );
1245 let mut k = RigidBody::new_kinematic(v3(-6.0, 0.0, 0.0));
1246 k.kinematic_target = Some((v3(4.0, 0.0, 0.0), k.rotation));
1247 let k = w.add_body_with_radius(k, f(0.5));
1248 let d = sphere(&mut w, Vec3Fix::ZERO, 1, 0.5, Vec3Fix::ZERO);
1249 half_bouncy(&mut w);
1250 (w, k, d)
1251 };
1252
1253 let (mut w, k, d) = scene();
1254 predict(&mut w, &[(k, v3(10.0, 0.0, 0.0))]);
1255 let hits = w.ccd_sweep();
1256 assert_eq!(hits.len(), 1);
1257 assert_eq!((hits[0].body, hits[0].other), (k, Some(d)));
1258 assert_eq!(hits[0].t, Fix128::from_ratio(1, 2));
1259 assert_eq!(hits[0].normal, v3(-1.0, 0.0, 0.0));
1261 assert_eq!(w.bodies[k].position, v3(4.0, 0.0, 0.0));
1262 assert_eq!(w.bodies[d].position, v3(5.0, 0.0, 0.0));
1263
1264 let (mut w, k, d) = scene();
1265 w.step(h());
1266 assert_eq!(w.bodies[k].position, v3(4.0, 0.0, 0.0), "kinematic target");
1267 close(w.bodies[d].position, v3(5.0, 0.0, 0.0), POS_TOL, "dynamic");
1268 close(
1269 w.bodies[d].velocity,
1270 v3(960.0, 0.0, 0.0),
1271 VEL_TOL,
1272 "dynamic",
1273 );
1274 }
1275
1276 #[test]
1283 fn a_static_collider_stops_the_body_which_then_slides() {
1284 let scene = || {
1285 let mut w = bare(WorldCcdConfig::on());
1286 let tri = TriMesh::from_indexed(
1287 &[
1288 v3(5.0, -40.0, -40.0),
1289 v3(5.0, 40.0, -40.0),
1290 v3(5.0, 0.0, 40.0),
1291 ],
1292 &[0, 1, 2],
1293 );
1294 w.add_static_collider(StaticCollider::TriMesh(tri));
1295 let s = sphere(&mut w, Vec3Fix::ZERO, 1, 0.25, v3(512.0, 256.0, 0.0));
1296 (w, s)
1297 };
1298
1299 let (mut w, s) = scene();
1300 predict(&mut w, &[(s, v3(8.0, 4.0, 0.0))]);
1301 let hits = w.ccd_sweep();
1302 assert_eq!(hits.len(), 1);
1303 assert_eq!(hits[0].other, None);
1304 assert_eq!(hits[0].t, Fix128::from_ratio(19, 32));
1305 assert_eq!(hits[0].normal, v3(-1.0, 0.0, 0.0));
1306 close(w.bodies[s].position, v3(4.75, 4.0, 0.0), POS_TOL, "placed");
1307
1308 let (mut w, s) = scene();
1309 w.step(h());
1310 close(w.bodies[s].position, v3(4.75, 4.0, 0.0), POS_TOL, "stepped");
1311 assert!(w.contact_events().is_empty());
1312 }
1313}