rapier3d/dynamics/ccd/
ccd_solver.rs

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