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}