Skip to main content

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}