Skip to main content

rapier2d/geometry/
contact_pair.rs

1#[cfg(doc)]
2use super::Collider;
3use super::CollisionEvent;
4use crate::dynamics::{RigidBodyHandle, RigidBodySet};
5use crate::geometry::{ColliderHandle, ColliderSet, Contact, ContactManifold};
6use crate::math::{Real, TangentImpulse, Vector};
7use crate::pipeline::EventHandler;
8use crate::prelude::CollisionEventFlags;
9use crate::utils::ScalarType;
10use parry::math::{SIMD_WIDTH, SimdReal};
11use parry::query::ContactManifoldsWorkspace;
12
13bitflags::bitflags! {
14    #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
15    #[derive(Copy, Clone, PartialEq, Eq, Debug)]
16    /// Flags affecting the behavior of the constraints solver for a given contact manifold.
17    pub struct SolverFlags: u32 {
18        /// The constraint solver will take this contact manifold into
19        /// account for force computation.
20        const COMPUTE_IMPULSES = 0b001;
21    }
22}
23
24impl Default for SolverFlags {
25    fn default() -> Self {
26        SolverFlags::COMPUTE_IMPULSES
27    }
28}
29
30#[derive(Copy, Clone, Debug)]
31#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
32/// A single contact between two collider.
33pub struct ContactData {
34    /// The impulse, along the contact normal, applied by this contact to the first collider's rigid-body.
35    ///
36    /// The impulse applied to the second collider's rigid-body is given by `-impulse`.
37    pub impulse: Real,
38    /// The friction impulse along the vector orthonormal to the contact normal, applied to the first
39    /// collider's rigid-body.
40    pub tangent_impulse: TangentImpulse<Real>,
41    /// The impulse retained for warmstarting the next simulation step.
42    pub warmstart_impulse: Real,
43    /// The friction impulse retained for warmstarting the next simulation step.
44    pub warmstart_tangent_impulse: TangentImpulse<Real>,
45    /// The twist impulse retained for warmstarting the next simulation step.
46    #[cfg(feature = "dim3")]
47    pub warmstart_twist_impulse: Real,
48}
49
50impl Default for ContactData {
51    fn default() -> Self {
52        Self {
53            impulse: 0.0,
54            tangent_impulse: na::zero(),
55            warmstart_impulse: 0.0,
56            warmstart_tangent_impulse: na::zero(),
57            #[cfg(feature = "dim3")]
58            warmstart_twist_impulse: 0.0,
59        }
60    }
61}
62
63#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
64#[derive(Copy, Clone, Debug)]
65/// The description of all the contacts between a pair of colliders.
66pub struct IntersectionPair {
67    /// Are the colliders intersecting?
68    pub intersecting: bool,
69    /// Was a `CollisionEvent::Started` emitted for this collider?
70    pub(crate) start_event_emitted: bool,
71}
72
73impl IntersectionPair {
74    pub(crate) fn new() -> Self {
75        Self {
76            intersecting: false,
77            start_event_emitted: false,
78        }
79    }
80
81    pub(crate) fn emit_start_event(
82        &mut self,
83        bodies: &RigidBodySet,
84        colliders: &ColliderSet,
85        collider1: ColliderHandle,
86        collider2: ColliderHandle,
87        events: &dyn EventHandler,
88    ) {
89        self.start_event_emitted = true;
90        events.handle_collision_event(
91            bodies,
92            colliders,
93            CollisionEvent::Started(collider1, collider2, CollisionEventFlags::SENSOR),
94            None,
95        );
96    }
97
98    pub(crate) fn emit_stop_event(
99        &mut self,
100        bodies: &RigidBodySet,
101        colliders: &ColliderSet,
102        collider1: ColliderHandle,
103        collider2: ColliderHandle,
104        events: &dyn EventHandler,
105    ) {
106        self.start_event_emitted = false;
107        events.handle_collision_event(
108            bodies,
109            colliders,
110            CollisionEvent::Stopped(collider1, collider2, CollisionEventFlags::SENSOR),
111            None,
112        );
113    }
114}
115
116#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
117#[derive(Clone)]
118/// All contact information between two colliding colliders.
119///
120/// When two colliders are touching, a ContactPair stores all the contact points, normals,
121/// and forces between them. You can access this through the narrow phase or in event handlers.
122///
123/// ## Contact manifolds
124///
125/// The contacts are organized into "manifolds" - groups of contact points that share similar
126/// properties (like being on the same face). Most collider pairs have 1 manifold, but complex
127/// shapes may have multiple.
128///
129/// ## Use cases
130///
131/// - Reading contact normals for custom physics
132/// - Checking penetration depth
133/// - Analyzing impact forces
134/// - Implementing custom contact responses
135///
136/// # Example
137/// ```
138/// # use rapier3d::prelude::*;
139/// # use rapier3d::geometry::ContactPair;
140/// # let contact_pair = ContactPair::default();
141/// if let Some((manifold, contact)) = contact_pair.find_deepest_contact() {
142///     println!("Deepest penetration: {}", -contact.dist);
143///     println!("Contact normal: {:?}", manifold.data.normal);
144/// }
145/// ```
146pub struct ContactPair {
147    /// The first collider involved in the contact pair.
148    pub collider1: ColliderHandle,
149    /// The second collider involved in the contact pair.
150    pub collider2: ColliderHandle,
151    /// The set of contact manifolds between the two colliders.
152    ///
153    /// All contact manifold contain themselves contact points between the colliders.
154    /// Note that contact points in the contact manifold do not take into account the
155    /// [`Collider::contact_skin`] which only affects the constraint solver and the
156    /// [`SolverContact`].
157    pub manifolds: Vec<ContactManifold>,
158    /// Was a `CollisionEvent::Started` emitted for this collider?
159    pub(crate) start_event_emitted: bool,
160    pub(crate) workspace: Option<ContactManifoldsWorkspace>,
161}
162
163impl Default for ContactPair {
164    fn default() -> Self {
165        Self::new(ColliderHandle::invalid(), ColliderHandle::invalid())
166    }
167}
168
169impl ContactPair {
170    pub(crate) fn new(collider1: ColliderHandle, collider2: ColliderHandle) -> Self {
171        Self {
172            collider1,
173            collider2,
174            manifolds: Vec::new(),
175            start_event_emitted: false,
176            workspace: None,
177        }
178    }
179
180    /// Is there any active contact in this contact pair?
181    pub fn has_any_active_contact(&self) -> bool {
182        self.manifolds
183            .iter()
184            .any(|m| !m.data.solver_contacts.is_empty())
185    }
186
187    /// Clears all the contacts of this contact pair.
188    pub fn clear(&mut self) {
189        self.manifolds.clear();
190        self.workspace = None;
191    }
192
193    /// The total impulse (force × time) applied by all contacts.
194    ///
195    /// This is the accumulated force that pushed the colliders apart.
196    /// Useful for determining impact strength.
197    pub fn total_impulse(&self) -> Vector {
198        self.manifolds
199            .iter()
200            .map(|m| m.total_impulse() * m.data.normal)
201            .sum()
202    }
203
204    /// The total magnitude of all contact impulses (sum of lengths, not length of sum).
205    ///
206    /// This is what's compared against `contact_force_event_threshold`.
207    pub fn total_impulse_magnitude(&self) -> Real {
208        self.manifolds
209            .iter()
210            .fold(0.0, |a, m| a + m.total_impulse())
211    }
212
213    /// Finds the strongest contact impulse and its direction.
214    ///
215    /// Returns `(magnitude, normal_direction)` of the strongest individual contact.
216    pub fn max_impulse(&self) -> (Real, Vector) {
217        let mut result = (0.0, Vector::ZERO);
218
219        for m in &self.manifolds {
220            let impulse = m.total_impulse();
221
222            if impulse > result.0 {
223                result = (impulse, m.data.normal);
224            }
225        }
226
227        result
228    }
229
230    /// Finds the contact point with the deepest penetration.
231    ///
232    /// When objects overlap, this returns the contact point that's penetrating the most.
233    /// Useful for:
234    /// - Finding the "worst" overlap
235    /// - Determining primary contact direction
236    /// - Custom penetration resolution
237    ///
238    /// Returns both the contact point and its parent manifold.
239    ///
240    /// # Example
241    /// ```
242    /// # use rapier3d::prelude::*;
243    /// # use rapier3d::geometry::ContactPair;
244    /// # let pair = ContactPair::default();
245    /// if let Some((manifold, contact)) = pair.find_deepest_contact() {
246    ///     let penetration_depth = -contact.dist;  // Negative dist = penetration
247    ///     println!("Deepest penetration: {} units", penetration_depth);
248    /// }
249    /// ```
250    #[profiling::function]
251    pub fn find_deepest_contact(&self) -> Option<(&ContactManifold, &Contact)> {
252        let mut deepest = None;
253
254        for m2 in &self.manifolds {
255            let deepest_candidate = m2.find_deepest_contact();
256
257            deepest = match (deepest, deepest_candidate) {
258                (_, None) => deepest,
259                (None, Some(c2)) => Some((m2, c2)),
260                (Some((m1, c1)), Some(c2)) => {
261                    if c1.dist <= c2.dist {
262                        Some((m1, c1))
263                    } else {
264                        Some((m2, c2))
265                    }
266                }
267            }
268        }
269
270        deepest
271    }
272
273    pub(crate) fn emit_start_event(
274        &mut self,
275        bodies: &RigidBodySet,
276        colliders: &ColliderSet,
277        events: &dyn EventHandler,
278    ) {
279        self.start_event_emitted = true;
280
281        events.handle_collision_event(
282            bodies,
283            colliders,
284            CollisionEvent::Started(self.collider1, self.collider2, CollisionEventFlags::empty()),
285            Some(self),
286        );
287    }
288
289    pub(crate) fn emit_stop_event(
290        &mut self,
291        bodies: &RigidBodySet,
292        colliders: &ColliderSet,
293        events: &dyn EventHandler,
294    ) {
295        self.start_event_emitted = false;
296
297        events.handle_collision_event(
298            bodies,
299            colliders,
300            CollisionEvent::Stopped(self.collider1, self.collider2, CollisionEventFlags::empty()),
301            Some(self),
302        );
303    }
304}
305
306#[derive(Clone, Debug)]
307#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
308/// A contact manifold between two colliders.
309///
310/// A contact manifold describes a set of contacts between two colliders. All the contact
311/// part of the same contact manifold share the same contact normal and contact kinematics.
312pub struct ContactManifoldData {
313    // The following are set by the narrow-phase.
314    /// The first rigid-body involved in this contact manifold.
315    pub rigid_body1: Option<RigidBodyHandle>,
316    /// The second rigid-body involved in this contact manifold.
317    pub rigid_body2: Option<RigidBodyHandle>,
318    // We put the following fields here to avoids reading the colliders inside of the
319    // contact preparation method.
320    /// Flags used to control some aspects of the constraints solver for this contact manifold.
321    pub solver_flags: SolverFlags,
322    /// The world-space contact normal shared by all the contact in this contact manifold.
323    // NOTE: read the comment of `solver_contacts` regarding serialization. It applies
324    // to this field as well.
325    pub normal: Vector,
326    /// The contacts that will be seen by the constraints solver for computing forces.
327    // NOTE: unfortunately, we can't ignore this field when serialize
328    // the contact manifold data. The reason is that the solver contacts
329    // won't be updated for sleeping bodies. So it means that for one
330    // frame, we won't have any solver contacts when waking up an island
331    // after a deserialization. Not only does this break post-snapshot
332    // determinism, but it will also skip constraint resolution for these
333    // contacts during one frame.
334    //
335    // An alternative would be to skip the serialization of `solver_contacts` and
336    // find a way to recompute them right after the deserialization process completes.
337    // However, this would be an expensive operation. And doing this efficiently as part
338    // of the narrow-phase update or the contact manifold collect will likely lead to tricky
339    // bugs too.
340    //
341    // So right now it is best to just serialize this field and keep it that way until it
342    // is proven to be actually problematic in real applications (in terms of snapshot size for example).
343    pub solver_contacts: Vec<SolverContact>,
344    /// The relative dominance of the bodies involved in this contact manifold.
345    pub relative_dominance: i16,
346    /// A user-defined piece of data.
347    pub user_data: u32,
348}
349
350/// A single solver contact.
351pub type SolverContact = SolverContactGeneric<Real, 1>;
352/// A group of `SIMD_WIDTH` solver contacts stored in SoA fashion for SIMD optimizations.
353pub type SimdSolverContact = SolverContactGeneric<SimdReal, SIMD_WIDTH>;
354
355/// A contact seen by the constraints solver for computing forces.
356#[derive(Copy, Clone, Debug)]
357#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
358#[cfg_attr(
359    feature = "serde-serialize",
360    serde(bound(
361        serialize = "N: serde::Serialize, N::Vector: serde::Serialize, [u32; LANES]: serde::Serialize"
362    ))
363)]
364#[cfg_attr(
365    feature = "serde-serialize",
366    serde(bound(
367        deserialize = "N: serde::Deserialize<'de>, N::Vector: serde::Deserialize<'de>, [u32; LANES]: serde::Deserialize<'de>"
368    ))
369)]
370#[repr(C)]
371#[repr(align(16))]
372pub struct SolverContactGeneric<N: ScalarType, const LANES: usize> {
373    // IMPORTANT: don’t change the fields unless `SimdSolverContactRepr` is also changed.
374    //
375    // TOTAL: 11/14 = 3*4/4*4-1
376    /// The contact point in world-space.
377    pub point: N::Vector, // 2/3
378    /// The distance between the two original contacts points along the contact normal.
379    /// If negative, this is measures the penetration depth.
380    pub dist: N, // 1/1
381    /// The effective friction coefficient at this contact point.
382    pub friction: N, // 1/1
383    /// The effective restitution coefficient at this contact point.
384    pub restitution: N, // 1/1
385    /// The desired tangent relative velocity at the contact point.
386    ///
387    /// This is set to zero by default. Set to a non-zero value to
388    /// simulate, e.g., conveyor belts.
389    pub tangent_velocity: N::Vector, // 2/3
390    /// Impulse used to warmstart the solve for the normal constraint.
391    pub warmstart_impulse: N, // 1/1
392    /// Impulse used to warmstart the solve for the friction constraints.
393    pub warmstart_tangent_impulse: TangentImpulse<N>, // 1/2
394    /// Impulse used to warmstart the solve for the twist friction constraints.
395    pub warmstart_twist_impulse: N, // 1/1
396    /// Whether this contact existed during the last timestep.
397    ///
398    /// A value of 0.0 means `false` and `1.0` means `true`.
399    /// This isn’t a bool for optimizations purpose with SIMD.
400    pub is_new: N, // 1/1
401    /// The index of the manifold contact used to generate this solver contact.
402    pub contact_id: [u32; LANES], // 1/1
403    #[cfg(feature = "dim3")]
404    pub(crate) padding: [N; 1],
405}
406
407#[repr(C)]
408#[repr(align(16))]
409pub struct SimdSolverContactRepr {
410    data0: SimdReal,
411    data1: SimdReal,
412    data2: SimdReal,
413    #[cfg(feature = "dim3")]
414    data3: SimdReal,
415}
416
417// NOTE: if these assertion fail with a weird "0 - 1 would overflow" error, it means the equality doesn’t hold.
418static_assertions::const_assert_eq!(
419    align_of::<SimdSolverContactRepr>(),
420    align_of::<SolverContact>()
421);
422#[cfg(feature = "simd-is-enabled")]
423static_assertions::assert_eq_size!(SimdSolverContactRepr, SolverContact);
424static_assertions::const_assert_eq!(
425    align_of::<SimdSolverContact>(),
426    align_of::<[SolverContact; SIMD_WIDTH]>()
427);
428#[cfg(feature = "simd-is-enabled")]
429static_assertions::assert_eq_size!(SimdSolverContact, [SolverContact; SIMD_WIDTH]);
430
431impl SimdSolverContact {
432    #[cfg(not(feature = "simd-is-enabled"))]
433    pub unsafe fn gather_unchecked(contacts: &[&[SolverContact]; SIMD_WIDTH], k: usize) -> Self {
434        contacts[0][k]
435    }
436
437    #[cfg(feature = "simd-is-enabled")]
438    pub unsafe fn gather_unchecked(contacts: &[&[SolverContact]; SIMD_WIDTH], k: usize) -> Self {
439        // TODO PERF: double-check that the compiler is using simd loads and
440        //       isn’t generating useless copies.
441
442        let data_repr: &[&[SimdSolverContactRepr]; SIMD_WIDTH] =
443            unsafe { std::mem::transmute(contacts) };
444
445        /* NOTE: this is a manual NEON implementation. To compare with what the compiler generates with `wide`.
446        unsafe {
447            use std::arch::aarch64::*;
448
449            assert!(k < SIMD_WIDTH);
450
451            // Fetch.
452            let aos0_0 = vld1q_f32(&data_repr[0][k].data0.0 as *const _ as *const f32);
453            let aos0_1 = vld1q_f32(&data_repr[1][k].data0.0 as *const _ as *const f32);
454            let aos0_2 = vld1q_f32(&data_repr[2][k].data0.0 as *const _ as *const f32);
455            let aos0_3 = vld1q_f32(&data_repr[3][k].data0.0 as *const _ as *const f32);
456
457            let aos1_0 = vld1q_f32(&data_repr[0][k].data1.0 as *const _ as *const f32);
458            let aos1_1 = vld1q_f32(&data_repr[1][k].data1.0 as *const _ as *const f32);
459            let aos1_2 = vld1q_f32(&data_repr[2][k].data1.0 as *const _ as *const f32);
460            let aos1_3 = vld1q_f32(&data_repr[3][k].data1.0 as *const _ as *const f32);
461
462            let aos2_0 = vld1q_f32(&data_repr[0][k].data2.0 as *const _ as *const f32);
463            let aos2_1 = vld1q_f32(&data_repr[1][k].data2.0 as *const _ as *const f32);
464            let aos2_2 = vld1q_f32(&data_repr[2][k].data2.0 as *const _ as *const f32);
465            let aos2_3 = vld1q_f32(&data_repr[3][k].data2.0 as *const _ as *const f32);
466
467            // Transpose.
468            let a = vzip1q_f32(aos0_0, aos0_2);
469            let b = vzip1q_f32(aos0_1, aos0_3);
470            let c = vzip2q_f32(aos0_0, aos0_2);
471            let d = vzip2q_f32(aos0_1, aos0_3);
472            let soa0_0 = vzip1q_f32(a, b);
473            let soa0_1 = vzip2q_f32(a, b);
474            let soa0_2 = vzip1q_f32(c, d);
475            let soa0_3 = vzip2q_f32(c, d);
476
477            let a = vzip1q_f32(aos1_0, aos1_2);
478            let b = vzip1q_f32(aos1_1, aos1_3);
479            let c = vzip2q_f32(aos1_0, aos1_2);
480            let d = vzip2q_f32(aos1_1, aos1_3);
481            let soa1_0 = vzip1q_f32(a, b);
482            let soa1_1 = vzip2q_f32(a, b);
483            let soa1_2 = vzip1q_f32(c, d);
484            let soa1_3 = vzip2q_f32(c, d);
485
486            let a = vzip1q_f32(aos2_0, aos2_2);
487            let b = vzip1q_f32(aos2_1, aos2_3);
488            let c = vzip2q_f32(aos2_0, aos2_2);
489            let d = vzip2q_f32(aos2_1, aos2_3);
490            let soa2_0 = vzip1q_f32(a, b);
491            let soa2_1 = vzip2q_f32(a, b);
492            let soa2_2 = vzip1q_f32(c, d);
493            let soa2_3 = vzip2q_f32(c, d);
494
495            // Return.
496            std::mem::transmute([
497                soa0_0, soa0_1, soa0_2, soa0_3, soa1_0, soa1_1, soa1_2, soa1_3, soa2_0, soa2_1,
498                soa2_2, soa2_3,
499            ])
500        }
501         */
502
503        let aos0 = [
504            unsafe { data_repr[0].get_unchecked(k).data0.0 },
505            unsafe { data_repr[1].get_unchecked(k).data0.0 },
506            unsafe { data_repr[2].get_unchecked(k).data0.0 },
507            unsafe { data_repr[3].get_unchecked(k).data0.0 },
508        ];
509        let aos1 = [
510            unsafe { data_repr[0].get_unchecked(k).data1.0 },
511            unsafe { data_repr[1].get_unchecked(k).data1.0 },
512            unsafe { data_repr[2].get_unchecked(k).data1.0 },
513            unsafe { data_repr[3].get_unchecked(k).data1.0 },
514        ];
515        let aos2 = [
516            unsafe { data_repr[0].get_unchecked(k).data2.0 },
517            unsafe { data_repr[1].get_unchecked(k).data2.0 },
518            unsafe { data_repr[2].get_unchecked(k).data2.0 },
519            unsafe { data_repr[3].get_unchecked(k).data2.0 },
520        ];
521        #[cfg(feature = "dim3")]
522        let aos3 = [
523            unsafe { data_repr[0].get_unchecked(k).data3.0 },
524            unsafe { data_repr[1].get_unchecked(k).data3.0 },
525            unsafe { data_repr[2].get_unchecked(k).data3.0 },
526            unsafe { data_repr[3].get_unchecked(k).data3.0 },
527        ];
528
529        use crate::utils::transmute_to_wide;
530        let soa0 = wide::f32x4::transpose(transmute_to_wide(aos0));
531        let soa1 = wide::f32x4::transpose(transmute_to_wide(aos1));
532        let soa2 = wide::f32x4::transpose(transmute_to_wide(aos2));
533        #[cfg(feature = "dim3")]
534        let soa3 = wide::f32x4::transpose(transmute_to_wide(aos3));
535
536        #[cfg(feature = "dim2")]
537        return unsafe {
538            std::mem::transmute::<[[wide::f32x4; 4]; 3], SolverContactGeneric<SimdReal, 4>>([
539                soa0, soa1, soa2,
540            ])
541        };
542        #[cfg(feature = "dim3")]
543        return unsafe {
544            std::mem::transmute::<[[wide::f32x4; 4]; 4], SolverContactGeneric<SimdReal, 4>>([
545                soa0, soa1, soa2, soa3,
546            ])
547        };
548    }
549}
550
551#[cfg(feature = "simd-is-enabled")]
552impl SimdSolverContact {
553    /// Should we treat this contact as a bouncy contact?
554    /// If `true`, use [`Self::restitution`].
555    pub fn is_bouncy(&self) -> SimdReal {
556        use na::{SimdPartialOrd, SimdValue};
557
558        let one = SimdReal::splat(1.0);
559        let zero = SimdReal::splat(0.0);
560
561        // Treat new collisions as bouncing at first, unless we have zero restitution.
562        let if_new = one.select(self.restitution.simd_gt(zero), zero);
563
564        // If the contact is still here one step later, it is now a resting contact.
565        // The exception is very high restitutions, which can never rest
566        let if_not_new = one.select(self.restitution.simd_ge(one), zero);
567
568        if_new.select(self.is_new.simd_ne(zero), if_not_new)
569    }
570}
571
572impl SolverContact {
573    /// Should we treat this contact as a bouncy contact?
574    /// If `true`, use [`Self::restitution`].
575    pub fn is_bouncy(&self) -> Real {
576        if self.is_new != 0.0 {
577            // Treat new collisions as bouncing at first, unless we have zero restitution.
578            (self.restitution > 0.0) as u32 as Real
579        } else {
580            // If the contact is still here one step later, it is now a resting contact.
581            // The exception is very high restitutions, which can never rest
582            (self.restitution >= 1.0) as u32 as Real
583        }
584    }
585}
586
587impl Default for ContactManifoldData {
588    fn default() -> Self {
589        Self::new(None, None, SolverFlags::empty())
590    }
591}
592
593impl ContactManifoldData {
594    pub(crate) fn new(
595        rigid_body1: Option<RigidBodyHandle>,
596        rigid_body2: Option<RigidBodyHandle>,
597        solver_flags: SolverFlags,
598    ) -> ContactManifoldData {
599        Self {
600            rigid_body1,
601            rigid_body2,
602            solver_flags,
603            normal: Vector::ZERO,
604            solver_contacts: Vec::new(),
605            relative_dominance: 0,
606            user_data: 0,
607        }
608    }
609
610    /// Number of actives contacts, i.e., contacts that will be seen by
611    /// the constraints solver.
612    #[inline]
613    pub fn num_active_contacts(&self) -> usize {
614        self.solver_contacts.len()
615    }
616}
617
618/// Additional methods for the contact manifold.
619pub trait ContactManifoldExt {
620    /// Computes the sum of all the impulses applied by contacts from this contact manifold.
621    fn total_impulse(&self) -> Real;
622}
623
624impl ContactManifoldExt for ContactManifold {
625    fn total_impulse(&self) -> Real {
626        self.points.iter().map(|pt| pt.data.impulse).sum()
627    }
628}