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