rapier2d/dynamics/ccd/
ccd_solver.rs

1use super::TOIEntry;
2use crate::dynamics::{IntegrationParameters, IslandManager, RigidBodyHandle, RigidBodySet};
3use crate::geometry::{BroadPhaseBvh, ColliderParent, ColliderSet, CollisionEvent, NarrowPhase};
4use crate::math::Real;
5use crate::parry::utils::SortedPair;
6use crate::pipeline::{EventHandler, QueryFilter};
7use crate::prelude::{ActiveEvents, CollisionEventFlags};
8use parry::utils::hashmap::HashMap;
9use std::collections::BinaryHeap;
10
11pub enum PredictedImpacts {
12    Impacts(HashMap<RigidBodyHandle, Real>),
13    ImpactsAfterEndTime(Real),
14    NoImpacts,
15}
16
17/// Continuous Collision Detection solver that prevents fast objects from tunneling through geometry.
18///
19/// CCD (Continuous Collision Detection) solves the "tunneling problem" where fast-moving objects
20/// pass through thin walls because they move more than the wall's thickness in one timestep.
21///
22/// ## How it works
23///
24/// 1. Detects which bodies are moving fast enough to potentially tunnel
25/// 2. Predicts where/when they would impact during the timestep
26/// 3. Clamps their motion to stop just before impact
27/// 4. Next frame, normal collision detection handles the contact
28///
29/// ## When to use CCD
30///
31/// Enable CCD on bodies that:
32/// - Move very fast (bullets, projectiles)
33/// - Are small and hit thin geometry
34/// - Must NEVER pass through walls (gameplay-critical)
35///
36/// **Cost**: More expensive than regular collision detection. Only use when needed!
37///
38/// Enable via `RigidBodyBuilder::ccd_enabled(true)` or `body.enable_ccd(true)`.
39#[derive(Clone, Default)]
40#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
41pub struct CCDSolver;
42
43impl CCDSolver {
44    /// Initializes a new CCD solver
45    pub fn new() -> Self {
46        Self
47    }
48
49    /// Apply motion-clamping to the bodies affected by the given `impacts`.
50    ///
51    /// The `impacts` should be the result of a previous call to `self.predict_next_impacts`.
52    pub fn clamp_motions(&self, dt: Real, bodies: &mut RigidBodySet, impacts: &PredictedImpacts) {
53        if let PredictedImpacts::Impacts(tois) = impacts {
54            for (handle, toi) in tois {
55                let rb = bodies.index_mut_internal(*handle);
56                let local_com = &rb.mprops.local_mprops.local_com;
57
58                let min_toi = (rb.ccd.ccd_thickness
59                    * 0.15
60                    * crate::utils::inv(rb.ccd.max_point_velocity(&rb.ccd_vels)))
61                .min(dt);
62                // println!(
63                //     "Min toi: {}, Toi: {}, thick: {}, max_vel: {}",
64                //     min_toi,
65                //     toi,
66                //     rb.ccd.ccd_thickness,
67                //     rb.ccd.max_point_velocity(&rb.integrated_vels)
68                // );
69                let new_pos = rb
70                    .ccd_vels
71                    .integrate(toi.max(min_toi), &rb.pos.position, local_com);
72                rb.pos.next_position = new_pos;
73            }
74        }
75    }
76
77    /// Updates the set of bodies that needs CCD to be resolved.
78    ///
79    /// Returns `true` if any rigid-body must have CCD resolved.
80    pub fn update_ccd_active_flags(
81        &self,
82        islands: &IslandManager,
83        bodies: &mut RigidBodySet,
84        dt: Real,
85        include_forces: bool,
86    ) -> bool {
87        let mut ccd_active = false;
88
89        // println!("Checking CCD activation");
90        for handle in islands.active_bodies() {
91            let rb = bodies.index_mut_internal(*handle);
92
93            if rb.ccd.ccd_enabled {
94                let forces = if include_forces {
95                    Some(&rb.forces)
96                } else {
97                    None
98                };
99                let moving_fast = rb.ccd.is_moving_fast(dt, &rb.ccd_vels, forces);
100                rb.ccd.ccd_active = moving_fast;
101                ccd_active = ccd_active || moving_fast;
102            }
103        }
104
105        ccd_active
106    }
107
108    /// Find the first time a CCD-enabled body has a non-sensor collider hitting another non-sensor collider.
109    #[profiling::function]
110    pub fn find_first_impact(
111        &mut self,
112        dt: Real, // NOTE: this doesn’t necessarily match the `params.dt`.
113        params: &IntegrationParameters,
114        islands: &IslandManager,
115        bodies: &RigidBodySet,
116        colliders: &ColliderSet,
117        broad_phase: &mut BroadPhaseBvh,
118        narrow_phase: &NarrowPhase,
119    ) -> Option<Real> {
120        // Update the query pipeline with the colliders’ predicted positions.
121        for (handle, co) in colliders.iter_enabled() {
122            if let Some(co_parent) = co.parent {
123                let rb = &bodies[co_parent.handle];
124                if rb.is_ccd_active() {
125                    let predicted_pos = rb
126                        .pos
127                        .integrate_forces_and_velocities(dt, &rb.forces, &rb.vels, &rb.mprops);
128                    let next_position = predicted_pos * co_parent.pos_wrt_parent;
129                    let swept_aabb = co.shape.compute_swept_aabb(&co.pos, &next_position);
130                    broad_phase.set_aabb(params, handle, swept_aabb);
131                }
132            }
133        }
134
135        let query_pipeline = broad_phase.as_query_pipeline(
136            narrow_phase.query_dispatcher(),
137            bodies,
138            colliders,
139            QueryFilter::default(),
140        );
141
142        let mut pairs_seen = HashMap::default();
143        let mut min_toi = dt;
144
145        for handle in islands.active_bodies() {
146            let rb1 = &bodies[*handle];
147
148            if rb1.ccd.ccd_active {
149                let predicted_body_pos1 = rb1.pos.integrate_forces_and_velocities(
150                    dt,
151                    &rb1.forces,
152                    &rb1.ccd_vels,
153                    &rb1.mprops,
154                );
155
156                for ch1 in &rb1.colliders.0 {
157                    let co1 = &colliders[*ch1];
158                    let co1_parent = co1
159                        .parent
160                        .as_ref()
161                        .expect("Could not find the ColliderParent component.");
162
163                    if co1.is_sensor() {
164                        continue; // Ignore sensors.
165                    }
166
167                    let predicted_collider_pos1 = predicted_body_pos1 * co1_parent.pos_wrt_parent;
168                    let aabb1 = co1
169                        .shape
170                        .compute_swept_aabb(&co1.pos, &predicted_collider_pos1);
171
172                    for (ch2, _) in query_pipeline.intersect_aabb_conservative(aabb1) {
173                        if *ch1 == ch2 {
174                            // Ignore self-intersection.
175                            continue;
176                        }
177
178                        if pairs_seen
179                            .insert(
180                                SortedPair::new(ch1.into_raw_parts().0, ch2.into_raw_parts().0),
181                                (),
182                            )
183                            .is_none()
184                        {
185                            let co1 = &colliders[*ch1];
186                            let co2 = &colliders[ch2];
187
188                            let bh1 = co1.parent.map(|p| p.handle);
189                            let bh2 = co2.parent.map(|p| p.handle);
190
191                            // Ignore self-intersection and sensors and apply collision groups filter.
192                            if bh1 == bh2                                                       // Ignore self-intersection.
193                                    || (co1.is_sensor() || co2.is_sensor())                         // Ignore sensors.
194                                    || !co1.flags.collision_groups.test(co2.flags.collision_groups) // Apply collision groups.
195                                    || !co1.flags.solver_groups.test(co2.flags.solver_groups)
196                            // Apply solver groups.
197                            {
198                                continue;
199                            }
200
201                            let smallest_dist = narrow_phase
202                                .contact_pair(*ch1, ch2)
203                                .and_then(|p| p.find_deepest_contact())
204                                .map(|c| c.1.dist)
205                                .unwrap_or(0.0);
206
207                            let rb2 = bh2.and_then(|h| bodies.get(h));
208
209                            if let Some(toi) = TOIEntry::try_from_colliders(
210                                narrow_phase.query_dispatcher(),
211                                *ch1,
212                                ch2,
213                                co1,
214                                co2,
215                                Some(rb1),
216                                rb2,
217                                None,
218                                None,
219                                0.0,
220                                min_toi,
221                                smallest_dist,
222                            ) {
223                                min_toi = min_toi.min(toi.toi);
224                            }
225                        }
226                    }
227                }
228            }
229        }
230
231        if min_toi < dt { Some(min_toi) } else { None }
232    }
233
234    /// Outputs the set of bodies as well as their first time-of-impact event.
235    #[profiling::function]
236    pub fn predict_impacts_at_next_positions(
237        &mut self,
238        params: &IntegrationParameters,
239        islands: &IslandManager,
240        bodies: &RigidBodySet,
241        colliders: &ColliderSet,
242        broad_phase: &mut BroadPhaseBvh,
243        narrow_phase: &NarrowPhase,
244        events: &dyn EventHandler,
245    ) -> PredictedImpacts {
246        let dt = params.dt;
247        let mut frozen = HashMap::<_, Real>::default();
248        let mut all_toi = BinaryHeap::new();
249        let mut pairs_seen = HashMap::default();
250        let mut min_overstep = dt;
251
252        // Update the query pipeline with the colliders’ `next_position`.
253        for (handle, co) in colliders.iter_enabled() {
254            if let Some(co_parent) = co.parent {
255                let rb = &bodies[co_parent.handle];
256                if rb.is_ccd_active() {
257                    let rb_next_pos = &bodies[co_parent.handle].pos.next_position;
258                    let next_position = rb_next_pos * co_parent.pos_wrt_parent;
259                    let swept_aabb = co.shape.compute_swept_aabb(&co.pos, &next_position);
260                    broad_phase.set_aabb(params, handle, swept_aabb);
261                }
262            }
263        }
264
265        let query_pipeline = broad_phase.as_query_pipeline(
266            narrow_phase.query_dispatcher(),
267            bodies,
268            colliders,
269            QueryFilter::default(),
270        );
271
272        /*
273         *
274         * First, collect all TOIs.
275         *
276         */
277        // TODO: don't iterate through all the colliders.
278        for handle in islands.active_bodies() {
279            let rb1 = &bodies[*handle];
280
281            if rb1.ccd.ccd_active {
282                let predicted_body_pos1 = rb1.pos.integrate_forces_and_velocities(
283                    dt,
284                    &rb1.forces,
285                    &rb1.ccd_vels,
286                    &rb1.mprops,
287                );
288
289                for ch1 in &rb1.colliders.0 {
290                    let co1 = &colliders[*ch1];
291                    let co_parent1 = co1
292                        .parent
293                        .as_ref()
294                        .expect("Could not find the ColliderParent component.");
295
296                    let predicted_collider_pos1 = predicted_body_pos1 * co_parent1.pos_wrt_parent;
297                    let aabb1 = co1
298                        .shape
299                        .compute_swept_aabb(&co1.pos, &predicted_collider_pos1);
300
301                    for (ch2, _) in query_pipeline.intersect_aabb_conservative(aabb1) {
302                        if *ch1 == ch2 {
303                            // Ignore self-intersection.
304                            continue;
305                        }
306
307                        if pairs_seen
308                            .insert(
309                                SortedPair::new(ch1.into_raw_parts().0, ch2.into_raw_parts().0),
310                                (),
311                            )
312                            .is_none()
313                        {
314                            let co1 = &colliders[*ch1];
315                            let co2 = &colliders[ch2];
316
317                            let bh1 = co1.parent.map(|p| p.handle);
318                            let bh2 = co2.parent.map(|p| p.handle);
319
320                            // Ignore self-intersections and apply groups filter.
321                            if bh1 == bh2
322                                || !co1.flags.collision_groups.test(co2.flags.collision_groups)
323                            {
324                                continue;
325                            }
326
327                            let smallest_dist = narrow_phase
328                                .contact_pair(*ch1, ch2)
329                                .and_then(|p| p.find_deepest_contact())
330                                .map(|c| c.1.dist)
331                                .unwrap_or(0.0);
332
333                            let rb1 = bh1.map(|h| &bodies[h]);
334                            let rb2 = bh2.map(|h| &bodies[h]);
335
336                            if let Some(toi) = TOIEntry::try_from_colliders(
337                                query_pipeline.dispatcher,
338                                *ch1,
339                                ch2,
340                                co1,
341                                co2,
342                                rb1,
343                                rb2,
344                                None,
345                                None,
346                                0.0,
347                                // NOTE: we use dt here only once we know that
348                                // there is at least one TOI before dt.
349                                min_overstep,
350                                smallest_dist,
351                            ) {
352                                if toi.toi > dt {
353                                    min_overstep = min_overstep.min(toi.toi);
354                                } else {
355                                    min_overstep = dt;
356                                    all_toi.push(toi);
357                                }
358                            }
359                        }
360                    }
361                }
362            }
363        }
364
365        /*
366         *
367         * If the smallest TOI is outside of the time interval, return.
368         *
369         */
370        if min_overstep == dt && all_toi.is_empty() {
371            return PredictedImpacts::NoImpacts;
372        } else if min_overstep > dt {
373            return PredictedImpacts::ImpactsAfterEndTime(min_overstep);
374        }
375
376        // NOTE: all fixed bodies (and kinematic bodies?) should be considered as "frozen", this
377        // may avoid some resweeps.
378        let mut pseudo_intersections_to_check = vec![];
379
380        while let Some(toi) = all_toi.pop() {
381            assert!(toi.toi <= dt);
382
383            let rb1 = toi.b1.and_then(|b| bodies.get(b));
384            let rb2 = toi.b2.and_then(|b| bodies.get(b));
385
386            let mut colliders_to_check = Vec::new();
387            let should_freeze1 = rb1.is_some()
388                && rb1.unwrap().ccd.ccd_active
389                && !frozen.contains_key(&toi.b1.unwrap());
390            let should_freeze2 = rb2.is_some()
391                && rb2.unwrap().ccd.ccd_active
392                && !frozen.contains_key(&toi.b2.unwrap());
393
394            if !should_freeze1 && !should_freeze2 {
395                continue;
396            }
397
398            if toi.is_pseudo_intersection_test {
399                // NOTE: this test is redundant with the previous `if !should_freeze && ...`
400                //       but let's keep it to avoid tricky regressions if we end up swapping both
401                //       `if` for some reason in the future.
402                if should_freeze1 || should_freeze2 {
403                    // This is only an intersection so we don't have to freeze and there is no
404                    // need to resweep. However, we will need to see if we have to generate
405                    // intersection events, so push the TOI for further testing.
406                    pseudo_intersections_to_check.push(toi);
407                }
408                continue;
409            }
410
411            if should_freeze1 {
412                let _ = frozen.insert(toi.b1.unwrap(), toi.toi);
413                colliders_to_check.extend_from_slice(&rb1.unwrap().colliders.0);
414            }
415
416            if should_freeze2 {
417                let _ = frozen.insert(toi.b2.unwrap(), toi.toi);
418                colliders_to_check.extend_from_slice(&rb2.unwrap().colliders.0);
419            }
420
421            let start_time = toi.toi;
422
423            // NOTE: the 1 and 2 indices (e.g., `ch1`, `ch2`) below are unrelated to the
424            //       ones we used above.
425            for ch1 in &colliders_to_check {
426                let co1 = &colliders[*ch1];
427                let co1_parent = co1.parent.as_ref().unwrap();
428                let rb1 = &bodies[co1_parent.handle];
429
430                let co_next_pos1 = rb1.pos.next_position * co1_parent.pos_wrt_parent;
431                let aabb = co1.shape.compute_swept_aabb(&co1.pos, &co_next_pos1);
432
433                for (ch2, _) in query_pipeline.intersect_aabb_conservative(aabb) {
434                    let co2 = &colliders[ch2];
435
436                    let bh1 = co1.parent.map(|p| p.handle);
437                    let bh2 = co2.parent.map(|p| p.handle);
438
439                    // Ignore self-intersection and apply groups filter.
440                    if bh1 == bh2 || !co1.flags.collision_groups.test(co2.flags.collision_groups) {
441                        continue;
442                    }
443
444                    let frozen1 = bh1.and_then(|h| frozen.get(&h));
445                    let frozen2 = bh2.and_then(|h| frozen.get(&h));
446
447                    let rb1 = bh1.and_then(|h| bodies.get(h));
448                    let rb2 = bh2.and_then(|h| bodies.get(h));
449
450                    if (frozen1.is_some() || !rb1.map(|b| b.ccd.ccd_active).unwrap_or(false))
451                        && (frozen2.is_some() || !rb2.map(|b| b.ccd.ccd_active).unwrap_or(false))
452                    {
453                        // We already did a resweep.
454                        continue;
455                    }
456
457                    let smallest_dist = narrow_phase
458                        .contact_pair(*ch1, ch2)
459                        .and_then(|p| p.find_deepest_contact())
460                        .map(|c| c.1.dist)
461                        .unwrap_or(0.0);
462
463                    if let Some(toi) = TOIEntry::try_from_colliders(
464                        query_pipeline.dispatcher,
465                        *ch1,
466                        ch2,
467                        co1,
468                        co2,
469                        rb1,
470                        rb2,
471                        frozen1.copied(),
472                        frozen2.copied(),
473                        start_time,
474                        dt,
475                        smallest_dist,
476                    ) {
477                        all_toi.push(toi);
478                    }
479                }
480            }
481        }
482
483        for toi in pseudo_intersections_to_check {
484            // See if the intersection is still active once the bodies
485            // reach their final positions.
486            // - If the intersection is still active, don't report it yet. It will be
487            //   reported by the narrow-phase at the next timestep/substep.
488            // - If the intersection isn't active anymore, and it wasn't intersecting
489            //   before, then we need to generate one interaction-start and one interaction-stop
490            //   events because it will never be detected by the narrow-phase because of tunneling.
491            let co1 = &colliders[toi.c1];
492            let co2 = &colliders[toi.c2];
493
494            if !co1.is_sensor() && !co2.is_sensor() {
495                // TODO: this happens if we found a TOI between two non-sensor
496                //       colliders with mismatching solver_flags. It is not clear
497                //       what we should do in this case: we could report a
498                //       contact started/contact stopped event for example. But in
499                //       that case, what contact pair should be pass to these events?
500                // For now we just ignore this special case. Let's wait for an actual
501                // use-case to come up before we determine what we want to do here.
502                continue;
503            }
504
505            let co_next_pos1 = if let Some(b1) = toi.b1 {
506                let co_parent1: &ColliderParent = co1.parent.as_ref().unwrap();
507                let rb1 = &bodies[b1];
508                let local_com1 = &rb1.mprops.local_mprops.local_com;
509                let frozen1 = frozen.get(&b1);
510                let pos1 = frozen1
511                    .map(|t| rb1.ccd_vels.integrate(*t, &rb1.pos.position, local_com1))
512                    .unwrap_or(rb1.pos.next_position);
513                pos1 * co_parent1.pos_wrt_parent
514            } else {
515                co1.pos.0
516            };
517
518            let co_next_pos2 = if let Some(b2) = toi.b2 {
519                let co_parent2: &ColliderParent = co2.parent.as_ref().unwrap();
520                let rb2 = &bodies[b2];
521                let local_com2 = &rb2.mprops.local_mprops.local_com;
522                let frozen2 = frozen.get(&b2);
523                let pos2 = frozen2
524                    .map(|t| rb2.ccd_vels.integrate(*t, &rb2.pos.position, local_com2))
525                    .unwrap_or(rb2.pos.next_position);
526                pos2 * co_parent2.pos_wrt_parent
527            } else {
528                co2.pos.0
529            };
530
531            let prev_coll_pos12 = co1.pos.inv_mul(&co2.pos);
532            let next_coll_pos12 = co_next_pos1.inv_mul(&co_next_pos2);
533
534            let intersect_before = query_pipeline
535                .dispatcher
536                .intersection_test(&prev_coll_pos12, co1.shape.as_ref(), co2.shape.as_ref())
537                .unwrap_or(false);
538
539            let intersect_after = query_pipeline
540                .dispatcher
541                .intersection_test(&next_coll_pos12, co1.shape.as_ref(), co2.shape.as_ref())
542                .unwrap_or(false);
543
544            if !intersect_before
545                && !intersect_after
546                && (co1.flags.active_events | co2.flags.active_events)
547                    .contains(ActiveEvents::COLLISION_EVENTS)
548            {
549                // Emit one intersection-started and one intersection-stopped event.
550                events.handle_collision_event(
551                    bodies,
552                    colliders,
553                    CollisionEvent::Started(toi.c1, toi.c2, CollisionEventFlags::SENSOR),
554                    None,
555                );
556                events.handle_collision_event(
557                    bodies,
558                    colliders,
559                    CollisionEvent::Stopped(toi.c1, toi.c2, CollisionEventFlags::SENSOR),
560                    None,
561                );
562            }
563        }
564
565        PredictedImpacts::Impacts(frozen)
566    }
567}