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