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}