avian2d/dynamics/solver/contact/
normal_part.rs

1use crate::{dynamics::solver::softness_parameters::SoftnessCoefficients, prelude::*};
2use bevy::reflect::Reflect;
3#[cfg(feature = "serialize")]
4use bevy::reflect::{ReflectDeserialize, ReflectSerialize};
5
6pub type NormalImpulse = Scalar;
7
8// TODO: Block solver for solving two contact points simultaneously
9// TODO: One-body constraint version
10/// The normal part of a [`ContactConstraintPoint`](super::ContactConstraintPoint).
11/// Aims to resolve overlap.
12#[derive(Clone, Debug, PartialEq, Reflect)]
13#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
15#[reflect(Debug, PartialEq)]
16pub struct ContactNormalPart {
17    /// The magnitude of the contact impulse along the contact normal.
18    pub impulse: NormalImpulse,
19
20    /// The total normal impulse applied across substeps and restitution.
21    /// The unit is typically N⋅s or kg⋅m/s.
22    ///
23    /// This is used by the solver to identify speculative contacts that did not generate
24    /// an impulse, but is also useful for users to determine how "strong" the contact is.
25    /// This is stored for contact manifolds as [`ContactPoint::normal_impulse`].
26    pub total_impulse: Scalar,
27
28    /// The inertial properties of the bodies projected onto the contact normal,
29    /// or in other words, the mass "seen" by the constraint along the normal.
30    pub effective_mass: Scalar,
31
32    /// The softness parameters used for tuning contact response.
33    pub softness: SoftnessCoefficients,
34}
35
36impl ContactNormalPart {
37    /// Generates a new [`ContactNormalPart`].
38    #[allow(clippy::too_many_arguments)]
39    pub fn generate(
40        effective_inverse_mass_sum: Vector,
41        inverse_angular_inertia1: &SymmetricTensor,
42        inverse_angular_inertia2: &SymmetricTensor,
43        r1: Vector,
44        r2: Vector,
45        normal: Vector,
46        warm_start_impulse: Option<NormalImpulse>,
47        softness: SoftnessCoefficients,
48    ) -> Self {
49        let i1 = inverse_angular_inertia1;
50        let i2 = inverse_angular_inertia2;
51
52        // Derivation for the projected normal mass. This is for 3D, but the 2D version is basically equivalent.
53        //
54        // The penetration constraint function is the following:
55        //
56        // C(s) = dot(p2 - p1, n) = dot(x2 + r2 - x1 - r1, n)
57        //
58        // where
59        // - p1 and p2 are world-space contact points for each body
60        // - n is the surface normal pointing from the first body towards the second (the order matters)
61        // - x1 and x2 are the centers of mass
62        // - r1 and r2 are vectors from the centers of mass to the corresponding contact points
63        //
64        // The contact constraint is satisfied when the bodies are not penetrating:
65        //
66        // C(s) >= 0
67        //
68        // We can compute the velocity constraint by getting the time derivative:
69        //
70        // C_vel(s) = d/dt(dot(x2 + r2 - x1 - r1, n))
71        //          = dot(d/dt(x2 + r2 - x1 - r1), n) + dot(x2 + r2 - x1 - r1, d/dt(n))
72        //
73        // The penetration is assumed to be small, so we can ignore the second term:
74        //
75        // C_vel(s) = dot(d/dt(x2 + r2 - x1 - r1), n)
76        //          = dot(lin_vel2 + ang_vel2 x r2 - lin_vel1 - ang_vel1 x r1, n)
77        //          = dot(lin_vel2, n) + dot(ang_vel2, r2 x n) - dot(v1, n) - dot(ang_vel1, r1 x n)
78        //
79        // By inspection, we can see that the Jacobian is the following:
80        //
81        //      linear1  angular1 linear2 angular2
82        // J = [ -normal, -(r1 x n), n, r2 x n ]
83        //
84        // From this, we can derive the effective inverse mass:
85        //
86        // K = J_x * M^-1 * J_x^T
87        //   = 1/m1 + 1/m2 + (r1 x n)^T * I1^-1 * (r1 x n) + (r2 x n)^T * I2^-1 * (r2 x n)
88        //
89        // See "Constraints Derivation for Rigid Body Simulation in 3D" section 2.1.3
90        // by Daniel Chappuis for the full derivation of the effective inverse mass.
91        //
92        // Finally, the transposes can be simplified with dot products, because a^T * b = dot(a, b),
93        // where a and b are two column vectors.
94        //
95        // K = 1/m1 + 1/m2 + dot(r1 x n, I1^-1 * (r1 x n)) + dot(r2 x n, I2^-1 * (r2 x n))
96
97        let r1_cross_n = cross(r1, normal);
98        let r2_cross_n = cross(r2, normal);
99
100        let k_linear = normal.dot(effective_inverse_mass_sum * normal);
101        #[cfg(feature = "2d")]
102        let k = k_linear + i1 * r1_cross_n * r1_cross_n + i2 * r2_cross_n * r2_cross_n;
103        #[cfg(feature = "3d")]
104        let k = k_linear + r1_cross_n.dot(i1 * r1_cross_n) + r2_cross_n.dot(i2 * r2_cross_n);
105
106        Self {
107            impulse: warm_start_impulse.unwrap_or_default(),
108            total_impulse: 0.0,
109            effective_mass: k.recip_or_zero(),
110            softness,
111        }
112    }
113
114    /// Solves the non-penetration constraint, updating the total impulse in `self` and returning
115    /// the incremental impulse to apply to each body.
116    pub fn solve_impulse(
117        &mut self,
118        separation: Scalar,
119        relative_velocity: Vector,
120        normal: Vector,
121        use_bias: bool,
122        max_overlap_solve_speed: Scalar,
123        delta_secs: Scalar,
124    ) -> Scalar {
125        // Compute the relative velocity along the normal.
126        let normal_speed = relative_velocity.dot(normal);
127
128        // Compute the incremental normal impulse.
129        let mut impulse = if separation > 0.0 {
130            // Speculative contact: Push back the part of the velocity that would cause penetration.
131            -self.effective_mass * (normal_speed + separation / delta_secs)
132        } else if use_bias {
133            // Contact using bias: Incorporate softness parameters.
134            //
135            // 1. Velocity bias: Allows the constraint to solve overlap by boosting
136            //    the constraint response a bit, taking into account the current overlap.
137            //    This is known as Baumgarte stabilization.
138            //
139            // 2. Mass coefficient: Scales the effective mass "seen" by the constraint.
140            //
141            // 3. Impulse coefficient: Scales the accumulated impulse that is subtracted
142            //    from the total impulse to prevent the total impulse from becoming too large.
143            //
144            // See the `softness_parameters` module for more details and references.
145
146            // TODO: We might want optional slop, a small amount of allowed penetration.
147            let bias = (self.softness.bias * separation).max(-max_overlap_solve_speed);
148            let scaled_mass = self.softness.mass_scale * self.effective_mass;
149            let scaled_impulse = self.softness.impulse_scale * self.impulse;
150
151            -scaled_mass * (normal_speed + bias) - scaled_impulse
152        } else {
153            // Contact without bias: Solve normally without softness parameters.
154            // This is useful for a "relaxation" phase which helps get rid of overshoot.
155            -self.effective_mass * normal_speed
156        };
157
158        // Clamp the accumulated impulse.
159        let new_impulse = (self.impulse + impulse).max(0.0);
160        impulse = new_impulse - self.impulse;
161        self.impulse = new_impulse;
162        self.total_impulse += new_impulse;
163
164        // Return the clamped incremental normal impulse.
165        impulse
166    }
167}