rapier3d/geometry/contact_pair.rs
1use super::CollisionEvent;
2use crate::alloc_prelude::*;
3use crate::dynamics::{RigidBodyHandle, RigidBodySet};
4use crate::geometry::{ColliderHandle, ColliderSet, Contact, ContactManifold};
5use crate::math::{Pose, Real, TangentImpulse, Vector};
6use crate::pipeline::EventHandler;
7use crate::prelude::CollisionEventFlags;
8use crate::utils::ScalarType;
9use crate::utils::SolverBlock;
10use parry::math::{SIMD_WIDTH, SimdReal};
11use parry::query::ContactManifoldsWorkspace;
12// Only `relative_pose_drift`’s 2D branch needs the no-std float methods.
13#[cfg(all(not(feature = "std"), feature = "dim2"))]
14use simba::scalar::ComplexField as _;
15
16bitflags::bitflags! {
17 #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
18 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
19 /// Flags affecting the behavior of the constraints solver for a given contact manifold.
20 pub struct SolverFlags: u32 {
21 /// The constraint solver will take this contact manifold into
22 /// account for force computation.
23 const COMPUTE_IMPULSES = 0b001;
24 }
25}
26
27impl Default for SolverFlags {
28 fn default() -> Self {
29 SolverFlags::COMPUTE_IMPULSES
30 }
31}
32
33bitflags::bitflags! {
34 #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
35 #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
36 /// Event bookkeeping bits of a contact pair.
37 ///
38 /// Serialized as a single byte with the same values as the `start_event_emitted`
39 /// bool it replaces, so snapshots keep their exact byte layout.
40 pub(crate) struct PairEventStatus: u8 {
41 /// A `CollisionEvent::Started` was emitted for this pair.
42 const START_EVENT_EMITTED = 0b01;
43 /// The pair's total contact force exceeded its force-event threshold at the
44 /// previous step. [`ContactForceEvent::started`] is derived from it, and it
45 /// resets when the force drops back below the threshold or the pair stops
46 /// touching.
47 const INITIAL_FORCE_THRESHOLD_EVENT_EMITTED = 0b10;
48 }
49}
50
51#[derive(Copy, Clone, Debug)]
52#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
53/// A single contact between two collider.
54pub struct ContactData {
55 /// The impulse, along the contact normal, applied by this contact to the first collider's rigid-body.
56 ///
57 /// The impulse applied to the second collider's rigid-body is given by `-impulse`.
58 pub impulse: Real,
59 /// The friction impulse along the vector orthonormal to the contact normal, applied to the first
60 /// collider's rigid-body.
61 pub tangent_impulse: TangentImpulse<Real>,
62 /// The impulse retained for warmstarting the next simulation step.
63 pub warmstart_impulse: Real,
64 /// The friction impulse retained for warmstarting the next simulation step.
65 pub warmstart_tangent_impulse: TangentImpulse<Real>,
66 /// The twist impulse retained for warmstarting the next simulation step.
67 #[cfg(feature = "dim3")]
68 pub warmstart_twist_impulse: Real,
69 /// The friction warm-start impulse as a **world-space** vector — the canonical
70 /// value 3D friction warm-starts from,
71 /// projected onto the constraint's current tangent basis at constraint generation.
72 /// Warm-starting from the raw [`Self::warmstart_tangent_impulse`] components would
73 /// silently rotate the friction force whenever the basis changes, kicking resting stacks.
74 #[cfg(feature = "dim3")]
75 #[cfg_attr(feature = "serde-serialize", serde(default))]
76 pub warmstart_tangent_world: Vector,
77 /// The solver's lever arm for the first body: contact point relative to the body's CoM,
78 /// in **world space**, frozen at the pair's last full narrow-phase update (anchor
79 /// freezing) and used verbatim while recycled. Load-bearing for tall-stack stability:
80 /// re-linearizing the arms every step under heavy warm-started impulses is a state-
81 /// proportional energy pump (lean mode). Separations still track the bodies' rigid motion.
82 #[cfg_attr(feature = "serde-serialize", serde(default))]
83 pub solver_dp1: Vector,
84 /// The solver's lever arm for the second body (see [`Self::solver_dp1`]).
85 #[cfg_attr(feature = "serde-serialize", serde(default))]
86 pub solver_dp2: Vector,
87}
88
89impl Default for ContactData {
90 fn default() -> Self {
91 Self {
92 impulse: 0.0,
93 tangent_impulse: na::zero(),
94 warmstart_impulse: 0.0,
95 warmstart_tangent_impulse: na::zero(),
96 #[cfg(feature = "dim3")]
97 warmstart_twist_impulse: 0.0,
98 #[cfg(feature = "dim3")]
99 warmstart_tangent_world: Vector::ZERO,
100 solver_dp1: Vector::ZERO,
101 solver_dp2: Vector::ZERO,
102 }
103 }
104}
105
106#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
107#[derive(Copy, Clone, Debug)]
108/// The description of all the contacts between a pair of colliders.
109pub struct IntersectionPair {
110 /// Are the colliders intersecting?
111 pub intersecting: bool,
112 /// Was a `CollisionEvent::Started` emitted for this collider?
113 pub(crate) start_event_emitted: bool,
114}
115
116impl IntersectionPair {
117 pub(crate) fn new() -> Self {
118 Self {
119 intersecting: false,
120 start_event_emitted: false,
121 }
122 }
123
124 pub(crate) fn emit_start_event(
125 &mut self,
126 bodies: &RigidBodySet,
127 colliders: &ColliderSet,
128 collider1: ColliderHandle,
129 collider2: ColliderHandle,
130 events: &dyn EventHandler,
131 ) {
132 self.start_event_emitted = true;
133 events.handle_collision_event(
134 bodies,
135 colliders,
136 CollisionEvent::Started(collider1, collider2, CollisionEventFlags::SENSOR),
137 None,
138 );
139 }
140
141 pub(crate) fn emit_stop_event(
142 &mut self,
143 bodies: &RigidBodySet,
144 colliders: &ColliderSet,
145 collider1: ColliderHandle,
146 collider2: ColliderHandle,
147 events: &dyn EventHandler,
148 ) {
149 self.start_event_emitted = false;
150 events.handle_collision_event(
151 bodies,
152 colliders,
153 CollisionEvent::Stopped(collider1, collider2, CollisionEventFlags::SENSOR),
154 None,
155 );
156 }
157}
158
159/// Sentinel color for pairs currently holding no solver graph color.
160pub(crate) const SOLVER_COLOR_UNCOLORED: u8 = u8::MAX;
161/// Color assigned when the parallel color space is exhausted (or for extra manifolds
162/// of multi-manifold pairs); such constraints are solved sequentially.
163pub(crate) const SOLVER_COLOR_OVERFLOW: u8 = 128;
164/// Number of low colors dynamic-vs-dynamic contacts may use; `..128` is reserved for
165/// dynamic-vs-fixed so those always iterate last, giving
166/// fixed geometry the final say each sweep and reducing push-through of piled bodies.
167pub(crate) const SOLVER_DYNAMIC_COLOR_COUNT: u32 = 120;
168
169#[cfg(feature = "serde-serialize")]
170fn default_solver_color() -> u8 {
171 SOLVER_COLOR_UNCOLORED
172}
173#[cfg(feature = "serde-serialize")]
174fn default_solver_color_bodies() -> [u32; 2] {
175 [u32::MAX; 2]
176}
177
178#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
179#[derive(Clone)]
180/// All contact information between two colliding colliders.
181///
182/// When two colliders are touching, a ContactPair stores all the contact points, normals,
183/// and forces between them. You can access this through the narrow phase or in event handlers.
184///
185/// ## Contact manifolds
186///
187/// The contacts are organized into "manifolds" - groups of contact points that share similar
188/// properties (like being on the same face). Most collider pairs have 1 manifold, but complex
189/// shapes may have multiple.
190///
191/// ## Use cases
192///
193/// - Reading contact normals for custom physics
194/// - Checking penetration depth
195/// - Analyzing impact forces
196/// - Implementing custom contact responses
197///
198/// # Example
199/// ```
200/// # use rapier3d::prelude::*;
201/// # use rapier3d::geometry::ContactPair;
202/// # let contact_pair = ContactPair::default();
203/// if let Some((manifold, contact)) = contact_pair.find_deepest_contact() {
204/// println!("Deepest penetration: {}", -contact.dist);
205/// println!("Contact normal: {:?}", manifold.data.normal);
206/// }
207/// ```
208pub struct ContactPair {
209 /// The first collider involved in the contact pair.
210 pub collider1: ColliderHandle,
211 /// The second collider involved in the contact pair.
212 pub collider2: ColliderHandle,
213 /// The set of contact manifolds between the two colliders.
214 ///
215 /// All contact manifold contain themselves contact points between the colliders.
216 /// Note that contact points in the contact manifold do not take into account the
217 /// [`Collider::contact_skin`] which only affects the constraint solver and the
218 /// [`SolverContact`].
219 ///
220 /// [`Collider::contact_skin`]: crate::geometry::Collider::contact_skin
221 pub manifolds: Vec<ContactManifold>,
222 /// Cluster manifolds handed to the constraint solver instead of `manifolds` when
223 /// contact clustering applies (see [`IntegrationParameters::contact_clustering`]);
224 /// empty otherwise. They merge the points of manifolds sharing (nearly) the same
225 /// contact normal and hold the contact impulses actually applied by the solver.
226 ///
227 /// [`IntegrationParameters::contact_clustering`]: crate::dynamics::IntegrationParameters::contact_clustering
228 pub solver_clusters: Vec<ContactManifold>,
229 /// The clusters solved at the previous step, kept as the warm-start source (and
230 /// reused as scratch buffers) when rebuilding `solver_clusters` each frame.
231 #[cfg_attr(feature = "serde-serialize", serde(skip))]
232 pub(crate) solver_clusters_prev: Vec<ContactManifold>,
233 /// The persistent solver graph color of this pair: same-color active pairs never share
234 /// a rigid-body, so one color solves concurrently. Maintained incrementally on contact
235 /// start/stop; `SOLVER_COLOR_UNCOLORED` inactive, `SOLVER_COLOR_OVERFLOW` no free color.
236 #[cfg_attr(feature = "serde-serialize", serde(default = "default_solver_color"))]
237 pub(crate) solver_color: u8,
238 /// The body mask slots on which this pair's color bit is set (u32::MAX = none).
239 #[cfg_attr(
240 feature = "serde-serialize",
241 serde(default = "default_solver_color_bodies")
242 )]
243 pub(crate) solver_color_bodies: [u32; 2],
244 /// Event bookkeeping: `CollisionEvent::Started` emission and force-event
245 /// threshold status.
246 pub(crate) event_status: PairEventStatus,
247 pub(crate) workspace: Option<ContactManifoldsWorkspace>,
248 /// State cached at the last full narrow-phase update, allowing the update to be
249 /// skipped ("recycled") while the colliders' relative pose stays within
250 /// `IntegrationParameters::contact_recycling`'s drift threshold.
251 ///
252 /// Part of the snapshot: a restored pair must resume recycling from the same
253 /// reference pose, or its first update recomputes manifolds (and re-derives the
254 /// world-frozen solver anchors) where the uninterrupted run would have recycled.
255 pub(crate) recycle_state: Option<ContactRecycleState>,
256}
257
258/// The relative configuration of a contact pair at its last full narrow-phase
259/// update, used by contact recycling to bound how much the pair moved since.
260#[derive(Copy, Clone, Debug)]
261#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
262pub(crate) struct ContactRecycleState {
263 /// Pose of the second collider relative to the first at the last full update.
264 pub pos12: Pose,
265 /// World rotation of the first collider at the last full update. The frozen world-space anchors
266 /// ([`ContactData::solver_dp1`]) mean recycling must also bound each body's *absolute* rotation:
267 /// rotating rigidly together keeps the relative pose but invalidates world-frozen arms (bound: `cos Δθ > 0.98`, ~11.5°).
268 pub rot1: crate::math::Rotation,
269 /// World rotation of the second collider at the last full update.
270 pub rot2: crate::math::Rotation,
271 /// Conservative bound on the distance of any point of either shape from its
272 /// collider origin, used to convert a relative rotation into a point-drift bound.
273 pub max_extent: Real,
274 /// The maximum relative-pose drift below which this pair can be recycled,
275 /// precomputed at the last full update (it depends on whether the pair had
276 /// active contacts, which recycling doesn't change).
277 pub max_drift: Real,
278}
279
280/// `cos Δθ` between two world rotations (in 3D, computed from the quaternion dot
281/// `cos(Δθ/2)` as `2·dot² − 1`), for the per-body rotation bound of contact
282/// recycling.
283#[inline]
284pub(crate) fn relative_rot_cos(base: &crate::math::Rotation, cur: &crate::math::Rotation) -> Real {
285 #[cfg(feature = "dim2")]
286 {
287 base.dot(*cur)
288 }
289 #[cfg(feature = "dim3")]
290 {
291 let c = base.dot(*cur);
292 2.0 * c * c - 1.0
293 }
294}
295
296/// Straight-line bound on how far any point within `max_extent` of the origin moved
297/// between poses `base` and `cur`: translation delta + rotation *chord* `2·max_extent·sin(Δθ/2)`.
298/// Tighter than the arc length `max_extent·Δθ`, and no `atan2`/`acos`.
299#[inline]
300pub(crate) fn relative_pose_drift(base: &Pose, cur: &Pose, max_extent: Real) -> Real {
301 let trans = (cur.translation - base.translation).length();
302 let delta_rot = cur.rotation * base.rotation.inverse();
303 #[cfg(feature = "dim2")]
304 let rot_chord = {
305 // To avoid explicit trigonometric functions, use the identity:
306 // `sin(Δθ/2) = |sin Δθ| / sqrt(2(1 + cos Δθ))`
307 let (sin, cos) = (delta_rot.sin(), delta_rot.cos());
308 let denom = 2.0 * (1.0 + cos);
309 let half_sin = if denom > 1.0e-6 {
310 sin.abs() / denom.sqrt()
311 } else {
312 1.0
313 };
314
315 2.0 * half_sin * max_extent
316 };
317 #[cfg(feature = "dim3")]
318 let rot_chord = {
319 // A unit quaternion's vector part is already `sin(Δθ/2)` about its axis.
320 2.0 * Vector::new(delta_rot.x, delta_rot.y, delta_rot.z).length() * max_extent
321 };
322 trans + rot_chord
323}
324
325impl Default for ContactPair {
326 fn default() -> Self {
327 Self::new(ColliderHandle::invalid(), ColliderHandle::invalid())
328 }
329}
330
331impl ContactPair {
332 pub(crate) fn new(collider1: ColliderHandle, collider2: ColliderHandle) -> Self {
333 Self {
334 collider1,
335 collider2,
336 manifolds: Vec::new(),
337 solver_clusters: Vec::new(),
338 solver_clusters_prev: Vec::new(),
339 solver_color: SOLVER_COLOR_UNCOLORED,
340 solver_color_bodies: [u32::MAX; 2],
341 event_status: PairEventStatus::empty(),
342 workspace: None,
343 recycle_state: None,
344 }
345 }
346
347 /// Resets a retired pair to the exact state [`Self::new`] would produce,
348 /// keeping the (outer) buffer capacities so pooled reuse skips their
349 /// reallocation on pair-churn-heavy scenes.
350 pub(crate) fn reset_for_reuse(&mut self, collider1: ColliderHandle, collider2: ColliderHandle) {
351 self.collider1 = collider1;
352 self.collider2 = collider2;
353 self.manifolds.clear();
354 self.solver_clusters.clear();
355 self.solver_clusters_prev.clear();
356 self.solver_color = SOLVER_COLOR_UNCOLORED;
357 self.solver_color_bodies = [u32::MAX; 2];
358 self.event_status = PairEventStatus::empty();
359 self.workspace = None;
360 self.recycle_state = None;
361 }
362
363 /// The manifolds actually seen by the constraint solver: the contact clusters if
364 /// clustering applied to this pair, the plain manifolds otherwise.
365 pub fn solver_manifolds(&self) -> &[ContactManifold] {
366 if self.solver_clusters.is_empty() {
367 &self.manifolds
368 } else {
369 &self.solver_clusters
370 }
371 }
372
373 /// Mutable twin of [`Self::solver_manifolds`]: the manifolds the constraint
374 /// solver actually sees (the solver clusters if any, else the plain manifolds).
375 #[cfg_attr(feature = "parallel", allow(dead_code))] // Single-threaded solver path.
376 pub(crate) fn solver_manifolds_mut(&mut self) -> &mut [ContactManifold] {
377 if self.solver_clusters.is_empty() {
378 &mut self.manifolds
379 } else {
380 &mut self.solver_clusters
381 }
382 }
383
384 /// Is there any active contact in this contact pair?
385 pub fn has_any_active_contact(&self) -> bool {
386 self.solver_manifolds()
387 .iter()
388 .any(|m| !m.data.solver_contacts.is_empty())
389 }
390
391 /// Clears all the contacts of this contact pair.
392 pub fn clear(&mut self) {
393 self.manifolds.clear();
394 self.solver_clusters.clear();
395 self.solver_clusters_prev.clear();
396 self.workspace = None;
397 self.recycle_state = None;
398 }
399
400 // NOTE: while recycled, a pair's world-space solver data (normal, frozen lever arms — see
401 // `ContactData::solver_dp1`) keeps its last-full-update values (anchor freezing): the solver
402 // rebuilds world points/separations from body-local anchors + current poses, so no per-step refresh; user data stays stale within the recycle drift bound.
403
404 /// The total impulse (force × time) applied by all contacts.
405 ///
406 /// This is the accumulated force that pushed the colliders apart.
407 /// Useful for determining impact strength.
408 pub fn total_impulse(&self) -> Vector {
409 self.solver_manifolds()
410 .iter()
411 .map(|m| m.total_impulse() * m.data.normal)
412 .sum()
413 }
414
415 /// The total magnitude of all contact impulses (sum of lengths, not length of sum).
416 ///
417 /// This is what's compared against `contact_force_event_threshold`.
418 pub fn total_impulse_magnitude(&self) -> Real {
419 self.solver_manifolds()
420 .iter()
421 .fold(0.0, |a, m| a + m.total_impulse())
422 }
423
424 /// Finds the strongest contact impulse and its direction.
425 ///
426 /// Returns `(magnitude, normal_direction)` of the strongest individual contact.
427 pub fn max_impulse(&self) -> (Real, Vector) {
428 let mut result = (0.0, Vector::ZERO);
429
430 for m in self.solver_manifolds() {
431 let impulse = m.total_impulse();
432
433 if impulse > result.0 {
434 result = (impulse, m.data.normal);
435 }
436 }
437
438 result
439 }
440
441 /// Finds the contact point with the deepest penetration.
442 ///
443 /// When objects overlap, this returns the contact point that's penetrating the most.
444 /// Useful for:
445 /// - Finding the "worst" overlap
446 /// - Determining primary contact direction
447 /// - Custom penetration resolution
448 ///
449 /// Returns both the contact point and its parent manifold.
450 ///
451 /// # Example
452 /// ```
453 /// # use rapier3d::prelude::*;
454 /// # use rapier3d::geometry::ContactPair;
455 /// # let pair = ContactPair::default();
456 /// if let Some((manifold, contact)) = pair.find_deepest_contact() {
457 /// let penetration_depth = -contact.dist; // Negative dist = penetration
458 /// println!("Deepest penetration: {} units", penetration_depth);
459 /// }
460 /// ```
461 #[profiling::function]
462 pub fn find_deepest_contact(&self) -> Option<(&ContactManifold, &Contact)> {
463 let mut deepest = None;
464
465 for m2 in &self.manifolds {
466 let deepest_candidate = m2.find_deepest_contact();
467
468 deepest = match (deepest, deepest_candidate) {
469 (_, None) => deepest,
470 (None, Some(c2)) => Some((m2, c2)),
471 (Some((m1, c1)), Some(c2)) => {
472 if c1.dist <= c2.dist {
473 Some((m1, c1))
474 } else {
475 Some((m2, c2))
476 }
477 }
478 }
479 }
480
481 deepest
482 }
483
484 pub(crate) fn emit_start_event(
485 &mut self,
486 bodies: &RigidBodySet,
487 colliders: &ColliderSet,
488 events: &dyn EventHandler,
489 ) {
490 self.event_status
491 .insert(PairEventStatus::START_EVENT_EMITTED);
492
493 events.handle_collision_event(
494 bodies,
495 colliders,
496 CollisionEvent::Started(self.collider1, self.collider2, CollisionEventFlags::empty()),
497 Some(self),
498 );
499 }
500
501 pub(crate) fn emit_stop_event(
502 &mut self,
503 bodies: &RigidBodySet,
504 colliders: &ColliderSet,
505 events: &dyn EventHandler,
506 ) {
507 // Not touching anymore: the force-event threshold status resets with it.
508 self.event_status = PairEventStatus::empty();
509
510 events.handle_collision_event(
511 bodies,
512 colliders,
513 CollisionEvent::Stopped(self.collider1, self.collider2, CollisionEventFlags::empty()),
514 Some(self),
515 );
516 }
517}
518
519#[derive(Clone, Debug)]
520#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
521/// A contact manifold between two colliders.
522///
523/// A contact manifold describes a set of contacts between two colliders. All the contact
524/// part of the same contact manifold share the same contact normal and contact kinematics.
525pub struct ContactManifoldData {
526 // The following are set by the narrow-phase.
527 /// The first rigid-body involved in this contact manifold.
528 pub rigid_body1: Option<RigidBodyHandle>,
529 /// The second rigid-body involved in this contact manifold.
530 pub rigid_body2: Option<RigidBodyHandle>,
531 // We put the following fields here to avoids reading the colliders inside of the
532 // contact preparation method.
533 /// Flags used to control some aspects of the constraints solver for this contact manifold.
534 pub solver_flags: SolverFlags,
535 /// The solver graph color of this manifold (copied from its contact pair during
536 /// constraint selection; extra manifolds of a same pair are sent to the overflow
537 /// color since they share their bodies).
538 #[cfg_attr(feature = "serde-serialize", serde(default = "default_solver_color"))]
539 pub(crate) solver_color: u8,
540 /// The solver-body index (`active_set_id`) of each rigid-body, or `u32::MAX` for a
541 /// world-attached side (fixed, sleeping, no body). Stamped by constraint selection so
542 /// the assembly never re-reads the rigid-body set.
543 pub(crate) solver_body_ids: [u32; 2],
544 /// This manifold's persistent position (bucket + index) in the narrow-phase's
545 /// `SolverContactGraph`, maintained incrementally so the solver reads a ready color-grouped
546 /// contact list without re-selecting/re-sorting. `GraphPos::NONE` when not solver-active.
547 #[cfg_attr(feature = "parallel", allow(dead_code))] // Single-threaded solver path.
548 pub(crate) graph_pos: crate::dynamics::solver::solver_contact_graph::GraphPos,
549 /// The world-space contact normal shared by all the contact in this contact manifold.
550 // NOTE: read the comment of `solver_contacts` regarding serialization. It applies
551 // to this field as well.
552 pub normal: Vector,
553 /// The contacts that will be seen by the constraints solver for computing forces.
554 // NOTE: unfortunately, we can't ignore this field when serialize
555 // the contact manifold data. The reason is that the solver contacts
556 // won't be updated for sleeping bodies. So it means that for one
557 // frame, we won't have any solver contacts when waking up an island
558 // after a deserialization. Not only does this break post-snapshot
559 // determinism, but it will also skip constraint resolution for these
560 // contacts during one frame.
561 //
562 // An alternative would be to skip the serialization of `solver_contacts` and
563 // find a way to recompute them right after the deserialization process completes.
564 // However, this would be an expensive operation. And doing this efficiently as part
565 // of the narrow-phase update or the contact manifold collect will likely lead to tricky
566 // bugs too.
567 //
568 // So right now it is best to just serialize this field and keep it that way until it
569 // is proven to be actually problematic in real applications (in terms of snapshot size for example).
570 pub solver_contacts: SolverContacts,
571 /// The relative dominance of the bodies involved in this contact manifold.
572 pub relative_dominance: i16,
573 /// A user-defined piece of data.
574 pub user_data: u32,
575 /// The effective friction coefficient of this manifold's contacts (combined from
576 /// both colliders' materials; identical for every contact of the manifold).
577 #[cfg_attr(feature = "serde-serialize", serde(default))]
578 pub friction: Real,
579 /// The effective restitution coefficient of this manifold's contacts.
580 #[cfg_attr(feature = "serde-serialize", serde(default))]
581 pub restitution: Real,
582}
583
584/// A single solver contact.
585pub type SolverContact = SolverContactGeneric<Real, 1>;
586
587/// The container of a manifold's solver contacts. In 2D a manifold has at most 2 active
588/// contacts, so they are stored inline: the solver's contact gathers read one contiguous
589/// manifold instead of chasing a heap allocation per manifold (a dependent cache miss
590/// on every SIMD lane of every constraint, every step).
591#[cfg(feature = "dim2")]
592pub type SolverContacts = arrayvec::ArrayVec<SolverContact, 2>;
593/// The container of a manifold's solver contacts. In 3D, composite-shape manifolds can
594/// exceed the solver's per-constraint point cap, so they stay heap-allocated.
595#[cfg(feature = "dim3")]
596pub type SolverContacts = Vec<SolverContact>;
597/// A group of `SIMD_WIDTH` solver contacts stored in SoA fashion for SIMD optimizations.
598pub type SimdSolverContact = SolverContactGeneric<SimdReal, SIMD_WIDTH>;
599
600/// A contact seen by the constraints solver for computing forces.
601#[derive(Copy, Clone, Debug)]
602#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
603#[cfg_attr(
604 feature = "serde-serialize",
605 serde(bound(
606 serialize = "N: serde::Serialize, N::Vector: serde::Serialize, [ContactId; LANES]: serde::Serialize"
607 ))
608)]
609#[cfg_attr(
610 feature = "serde-serialize",
611 serde(bound(
612 deserialize = "N: serde::Deserialize<'de>, N::Vector: serde::Deserialize<'de>, [ContactId; LANES]: serde::Deserialize<'de>"
613 ))
614)]
615#[repr(C)]
616#[repr(align(16))]
617pub struct SolverContactGeneric<N: ScalarType, const LANES: usize> {
618 // IMPORTANT: don't change the fields unless `SimdSolverContactRepr` is also changed.
619 // TOTAL: 8/8 lanes in 2D (two 16B SIMD rows), 11/12 in 3D. Friction/restitution live
620 // on `ContactManifoldData`, is-new in bit 31 of `contact_id`, warm-starts on the manifold points.
621 /// The contact point on the first body's surface (contact skin baked in), in that
622 /// body's CoM-centered local frame so it rides rigidly with the body — what lets
623 /// contact recycling skip the per-frame world refresh. World-space instead for a side
624 /// without a solver body (none, or world-attached by dominance — fixed bodies included).
625 /// Inside [`PhysicsHooks::modify_solver_contacts`] this always holds the fresh
626 /// **world-space** point (hooks run before localization).
627 ///
628 /// [`PhysicsHooks::modify_solver_contacts`]: crate::pipeline::PhysicsHooks::modify_solver_contacts
629 pub anchor1: N::Vector, // 2/3
630 /// The contact point on the second body's surface, expressed like
631 /// [`Self::anchor1`] (world-space when the second side is world-attached, i.e.
632 /// `relative_dominance < 0`, or inside the contact-modification hook).
633 pub anchor2: N::Vector, // 2/3
634 /// Distance between the contact points along the normal at the last full contact
635 /// update (negative = penetration), minus the contact skins. Writable from
636 /// [`PhysicsHooks::modify_solver_contacts`] (the delta is baked into the anchors after
637 /// the hook); afterwards the solver re-derives the live separation and never reads this.
638 ///
639 /// [`PhysicsHooks::modify_solver_contacts`]: crate::pipeline::PhysicsHooks::modify_solver_contacts
640 pub dist: N, // 1/1
641 /// The desired tangent relative velocity at the contact point.
642 ///
643 /// This is set to zero by default. Set to a non-zero value to
644 /// simulate, e.g., conveyor belts.
645 pub tangent_velocity: N::Vector, // 2/3
646 /// The index of the manifold contact used to generate this solver contact, in the
647 /// low 31 bits; bit 31 ([`NEW_CONTACT_BIT`]) is set if this contact did not exist
648 /// during the last *full* contact update (recycled steps leave it untouched; the
649 /// solver derives contact newness from the warm-start state instead).
650 pub contact_id: [ContactId; LANES], // 1/1
651 #[cfg(feature = "dim3")]
652 pub(crate) padding: [N; 1],
653}
654
655/// The storage type of [`SolverContactGeneric::contact_id`]: one `Real`-sized slot
656/// per lane, so that a lane of the AoSoA struct keeps the same layout as a scalar
657/// contact. At `f32` a slot is exactly the `u32` id; at `f64` the high 32 bits are
658/// unused padding.
659#[cfg(feature = "f32")]
660pub type ContactId = u32;
661/// See [`ContactId`].
662#[cfg(feature = "f64")]
663pub type ContactId = u64;
664
665/// Bit set in [`SolverContactGeneric::contact_id`] when the contact did not exist
666/// during the previous timestep.
667pub const NEW_CONTACT_BIT: ContactId = 1 << 31;
668
669// One scalar `SolverContact` reinterpreted as fixed 128-bit blocks for the
670// AoS↔SoA gather. The blocks are always 4-wide (`SolverBlock`), independent of
671// `SIMD_WIDTH`, so this holds at both 4 and 8 lanes.
672#[repr(C)]
673#[repr(align(16))]
674pub struct SimdSolverContactRepr {
675 data0: SolverBlock,
676 data1: SolverBlock,
677 #[cfg(feature = "dim3")]
678 data2: SolverBlock,
679}
680
681// NOTE: if these assertion fail with a weird "0 - 1 would overflow" error, it means the equality doesn’t hold.
682static_assertions::const_assert_eq!(
683 align_of::<SimdSolverContactRepr>(),
684 align_of::<SolverContact>()
685);
686static_assertions::assert_eq_size!(SimdSolverContactRepr, SolverContact);
687// The SoA gather result is at least as aligned as the AoS lane array (equal at 4
688// lanes; at 8 lanes `SimdReal` is 32-byte-aligned while the scalar array is 16).
689static_assertions::const_assert_eq!(
690 align_of::<SimdSolverContact>() % align_of::<[SolverContact; SIMD_WIDTH]>(),
691 0
692);
693static_assertions::assert_eq_size!(SimdSolverContact, [SolverContact; SIMD_WIDTH]);
694
695impl SimdSolverContact {
696 /// Gathers one solver contact per lane, at a per-lane index (the lanes of a
697 /// constraint chunk may have different active-contact counts, so callers
698 /// clamp each lane's index to its own count).
699 ///
700 /// # Safety
701 ///
702 /// Every `ks[k]` must be a valid index into `contacts[k]` — the gather reads each
703 /// lane's slice unchecked.
704 pub unsafe fn gather_unchecked(
705 contacts: &[&[SolverContact]; SIMD_WIDTH],
706 ks: [usize; SIMD_WIDTH],
707 ) -> Self {
708 // TODO PERF: double-check that the compiler is using simd loads and
709 // isn’t generating useless copies.
710
711 let data_repr: &[&[SimdSolverContactRepr]; SIMD_WIDTH] =
712 unsafe { core::mem::transmute(contacts) };
713 use crate::utils::transpose_wide;
714
715 // One 128-bit block per lane, gathered at each lane's own `ks` index.
716 let aos0: [_; SIMD_WIDTH] =
717 core::array::from_fn(|k| unsafe { data_repr[k].get_unchecked(ks[k]).data0.0 });
718 let aos1: [_; SIMD_WIDTH] =
719 core::array::from_fn(|k| unsafe { data_repr[k].get_unchecked(ks[k]).data1.0 });
720 let soa0 = transpose_wide(aos0);
721 let soa1 = transpose_wide(aos1);
722
723 #[cfg(feature = "dim2")]
724 unsafe {
725 core::mem::transmute::<[[SimdReal; 4]; 2], SimdSolverContact>([soa0, soa1])
726 }
727
728 #[cfg(feature = "dim3")]
729 {
730 let aos2: [_; SIMD_WIDTH] =
731 core::array::from_fn(|k| unsafe { data_repr[k].get_unchecked(ks[k]).data2.0 });
732 let soa2 = transpose_wide(aos2);
733
734 unsafe {
735 core::mem::transmute::<[[SimdReal; 4]; 3], SimdSolverContact>([soa0, soa1, soa2])
736 }
737 }
738 }
739}
740
741impl<N: ScalarType, const LANES: usize> SolverContactGeneric<N, LANES> {
742 /// The manifold contact indices, with the is-new bit masked off.
743 ///
744 /// These indices are only valid within the timestep that produced this solver
745 /// contact: manifold points may be reordered or replaced by the next narrow-phase
746 /// update.
747 #[inline]
748 pub fn contact_indices(&self) -> [ContactId; LANES] {
749 self.contact_id.map(|id| id & !NEW_CONTACT_BIT)
750 }
751}
752
753/// Should a contact be treated as bouncy? (SIMD lanes; `1.0` = bouncy.) Restitution is
754/// per-manifold ([`ContactManifoldData::restitution`]); `is_new` is decoded from bit 31
755/// ([`NEW_CONTACT_BIT`]) of [`SolverContactGeneric::contact_id`].
756pub fn is_bouncy_simd(restitution: SimdReal, is_new: SimdReal) -> SimdReal {
757 use na::{SimdPartialOrd, SimdValue};
758
759 let one = SimdReal::splat(1.0);
760 let zero = SimdReal::splat(0.0);
761
762 // Treat new collisions as bouncing at first, unless we have zero restitution.
763 let if_new = one.select(restitution.simd_gt(zero), zero);
764
765 // If the contact is still here one step later, it is now a resting contact.
766 // The exception is very high restitutions, which can never rest
767 let if_not_new = one.select(restitution.simd_ge(one), zero);
768
769 if_new.select(is_new.simd_ne(zero), if_not_new)
770}
771
772/// Scalar variant of [`is_bouncy_simd`].
773pub fn is_bouncy(restitution: Real, is_new: bool) -> Real {
774 if is_new {
775 (restitution > 0.0) as u32 as Real
776 } else {
777 (restitution >= 1.0) as u32 as Real
778 }
779}
780
781impl Default for ContactManifoldData {
782 fn default() -> Self {
783 Self::new(None, None, SolverFlags::empty())
784 }
785}
786
787impl ContactManifoldData {
788 pub(crate) fn new(
789 rigid_body1: Option<RigidBodyHandle>,
790 rigid_body2: Option<RigidBodyHandle>,
791 solver_flags: SolverFlags,
792 ) -> ContactManifoldData {
793 Self {
794 rigid_body1,
795 rigid_body2,
796 solver_flags,
797 solver_color: SOLVER_COLOR_UNCOLORED,
798 solver_body_ids: [u32::MAX; 2],
799 graph_pos: crate::dynamics::solver::solver_contact_graph::GraphPos::NONE,
800 normal: Vector::ZERO,
801 solver_contacts: SolverContacts::new(),
802 relative_dominance: 0,
803 user_data: 0,
804 friction: 0.0,
805 restitution: 0.0,
806 }
807 }
808
809 /// Resolves the world-space contact points (one per body surface) of one solver
810 /// contact: body-local anchors ([`SolverContactGeneric::anchor1`]) are resolved through
811 /// the bodies' current poses (a world-attached side's anchor already is a world point).
812 /// The points differ by roughly the separation along the normal; their midpoint is the
813 /// effective solver contact point.
814 pub fn solver_contact_world_points(
815 &self,
816 contact: &SolverContact,
817 bodies: &crate::dynamics::RigidBodySet,
818 ) -> (Vector, Vector) {
819 let resolve =
820 |anchor: Vector, handle: Option<RigidBodyHandle>, world_attached: bool| match handle
821 .filter(|_| !world_attached)
822 .and_then(|h| bodies.get(h))
823 {
824 Some(rb) => rb.pos.position * (rb.mprops.local_mprops.local_com + anchor),
825 None => anchor,
826 };
827 (
828 resolve(
829 contact.anchor1,
830 self.rigid_body1,
831 self.relative_dominance > 0,
832 ),
833 resolve(
834 contact.anchor2,
835 self.rigid_body2,
836 self.relative_dominance < 0,
837 ),
838 )
839 }
840
841 /// Number of actives contacts, i.e., contacts that will be seen by
842 /// the constraints solver.
843 #[inline]
844 pub fn num_active_contacts(&self) -> usize {
845 self.solver_contacts.len()
846 }
847}
848
849/// Additional methods for the contact manifold.
850pub trait ContactManifoldExt {
851 /// Computes the sum of all the impulses applied by contacts from this contact manifold.
852 fn total_impulse(&self) -> Real;
853}
854
855impl ContactManifoldExt for ContactManifold {
856 fn total_impulse(&self) -> Real {
857 self.points.iter().map(|pt| pt.data.impulse).sum()
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::relative_pose_drift;
864 use crate::math::{Pose, Real, Vector};
865
866 /// A pose compared against *itself* must report no drift beyond rounding noise (the
867 /// `cur ∘ base⁻¹` product is not exactly the identity), whatever the orientation and
868 /// however far the shape reaches: it must never eat into the sleep gate's allowance.
869 #[test]
870 fn same_pose_never_drifts() {
871 // The sleep gate's per-step allowance at the default threshold and 60 Hz.
872 let allowance = 0.05 * (1.0 / 60.0) * 2.0;
873 let max_extent = 4.0;
874
875 let mut worst: Real = 0.0;
876 let mut n = 0;
877 for i in 0..40 {
878 for j in 0..10 {
879 let angle = i as Real * 0.157;
880 #[cfg(feature = "dim2")]
881 let pose = Pose::new(Vector::new(j as Real * 3.7, 1.0), angle);
882 #[cfg(feature = "dim3")]
883 let pose = Pose::new(
884 Vector::new(j as Real * 3.7, 1.0, -2.0),
885 Vector::new(0.3, -0.7, 0.15).normalize() * angle,
886 );
887 worst = worst.max(relative_pose_drift(&pose, &pose, max_extent));
888 n += 1;
889 }
890 }
891 let rounding_noise = 8.0 * Real::EPSILON * max_extent;
892 assert!(
893 worst <= rounding_noise,
894 "an unmoved pose drifted by up to {worst} over {n} orientations \
895 (rounding noise is {rounding_noise}, the sleep gate allows {allowance} per step)"
896 );
897 }
898}