Skip to main content

avian3d/dynamics/solver/contact/
tangent_part.rs

1use crate::prelude::*;
2use bevy::reflect::Reflect;
3#[cfg(feature = "serialize")]
4use bevy::reflect::{ReflectDeserialize, ReflectSerialize};
5#[cfg(feature = "3d")]
6use bevy_math::Vec2;
7
8#[cfg(feature = "2d")]
9pub type TangentImpulse = f32;
10#[cfg(feature = "3d")]
11pub type TangentImpulse = Vec2;
12
13// TODO: One-body constraint version
14/// The tangential friction part of a [`ContactConstraintPoint`](super::ContactConstraintPoint).
15#[derive(Clone, Debug, Default, PartialEq, Reflect)]
16#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
18#[reflect(Debug, PartialEq)]
19pub struct ContactTangentPart {
20    /// The contact impulse magnitude along the contact tangent.
21    ///
22    /// This corresponds to the magnitude of the friction impulse.
23    pub impulse: TangentImpulse,
24
25    /// The inertial properties of the bodies projected onto the contact tangent,
26    /// or in other words, the mass "seen" by the constraint along the tangent.
27    #[cfg(feature = "2d")]
28    pub effective_mass: f32,
29    /// The inverse of the inertial properties of the bodies projected onto the contact tangents,
30    /// or in other words, the inverse mass "seen" by the constraint along the tangents.
31    #[cfg(feature = "3d")]
32    pub effective_inverse_mass: [f32; 3],
33}
34
35impl ContactTangentPart {
36    /// Generates a new [`ContactTangentPart`].
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        tangents: [Vector; DIM - 1],
44        warm_start_impulse: Option<TangentImpulse>,
45    ) -> Self {
46        let i1 = inverse_angular_inertia1;
47        let i2 = inverse_angular_inertia2;
48
49        let mut part = Self {
50            impulse: warm_start_impulse.unwrap_or_default(),
51            #[cfg(feature = "2d")]
52            effective_mass: 0.0,
53            #[cfg(feature = "3d")]
54            effective_inverse_mass: [0.0; 3],
55        };
56
57        // Derivation for the projected tangent masses. This is for 3D, but the 2D version is largely the same.
58        //
59        // Friction constraints aim to prevent relative tangential motion at contact points.
60        // The velocity constraint is satisfied when the relative velocity along the tangent
61        // is equal to zero.
62        //
63        // In 3D, there are two tangent directions and therefore two constraints:
64        //
65        // dot(lin_vel1_p, tangent_x) = dot(lin_vel2_p, tangent_x)
66        // dot(lin_vel1_p, tangent_y) = dot(lin_vel2_p, tangent_y)
67        //
68        // where lin_vel1_p and lin_vel2_p are the velocities of the bodies at the contact point p:
69        //
70        // lin_vel1_p = lin_vel1 + ang_vel1 x r1
71        // lin_vel2_p = lin_vel2 + ang_vel2 x r2
72        //
73        // Based on this, we get:
74        //
75        // dot(lin_vel1_p, tangent_x) = dot(lin_vel1, tangent_x) + dot(ang_vel1 x r1, tangent_x)
76        //                            = dot(lin_vel1, tangent_x) + dot(r1 x tangent_x, ang_vel1)
77        //
78        // Restating the original constraints with the derived formula:
79        //
80        // dot(lin_vel1, tangent_x) + dot(r1 x tangent_x, ang_vel1) = dot(lin_vel2, tangent_x) + dot(r2 x tangent_x, ang_vel2)
81        // dot(lin_vel1, tangent_y) + dot(r1 x tangent_y, ang_vel1) = dot(lin_vel2, tangent_y) + dot(r2 x tangent_y, ang_vel2)
82        //
83        // Finally, moving the right-hand side to the left:
84        //
85        // dot(lin_vel1, tangent_x) + dot(r1 x tangent_x, ang_vel1) - dot(lin_vel2, tangent_x) - dot(r2 x tangent_x, ang_vel2) = 0
86        // dot(lin_vel1, tangent_y) + dot(r1 x tangent_y, ang_vel1) - dot(lin_vel2, tangent_y) - dot(r2 x tangent_y, ang_vel2) = 0
87        //
88        // By inspection, we can see that the Jacobian is the following:
89        //
90        //          linear1      angular1       linear2       angular2
91        // J_x = [ -tangent_x, -(r1 x tangent_x), tangent_x, r2 x tangent_x ]
92        // J_y = [ -tangent_y, -(r1 x tangent_y), tangent_y, r2 x tangent_y ]
93        //
94        // From this, we can derive the effective inverse mass for both tangent directions:
95        //
96        // K_x = J_x * M^-1 * J_x^T
97        //     = m1 + m2 + (r1 x tangent_x)^T * I1 * (r1 x tangent_x) + (r2 x tangent_x)^T * I2 * (r2 x tangent_x)
98        // K_y = J_y * M^-1 * J_y^T
99        //     = m1 + m2 + (r1 x tangent_y)^T * I1 * (r1 x tangent_y) + (r2 x tangent_y)^T * I2 * (r2 x tangent_y)
100        //
101        // See "Constraints Derivation for Rigid Body Simulation in 3D" section 2.1.3
102        // by Daniel Chappuis for the full derivation of the effective inverse mass.
103        //
104        // Finally, the transposes can be simplified with dot products, because a^T * b = dot(a, b),
105        // where a and b are two column vectors.
106        //
107        // K_x = m1 + m2 + dot(r1 x tangent_x, I1 * (r1 x tangent_x)) + dot(r2 x tangent_x, I2 * (r2 x tangent_x))
108        // K_y = m1 + m2 + dot(r1 x tangent_y, I1 * (r1 x tangent_y)) + dot(r2 x tangent_y, I2 * (r2 x tangent_y))
109
110        #[cfg(feature = "2d")]
111        {
112            let rt1 = cross(r1, tangents[0]);
113            let rt2 = cross(r2, tangents[0]);
114
115            let k_linear = tangents[0].dot(effective_inverse_mass_sum * tangents[0]);
116            let k = k_linear + i1 * rt1 * rt1 + i2 * rt2 * rt2;
117
118            part.effective_mass = k.recip_or_zero();
119        }
120
121        #[cfg(feature = "3d")]
122        {
123            // Based on Rapier's two-body constraint.
124            // https://github.com/dimforge/rapier/blob/af1ac9baa26b1199ae2728e91adf5345bcd1c693/src/dynamics/solver/contact_constraint/two_body_constraint.rs#L257-L289
125
126            let rt11 = cross(r1, tangents[0]);
127            let rt12 = cross(r2, tangents[0]);
128            let rt21 = cross(r1, tangents[1]);
129            let rt22 = cross(r2, tangents[1]);
130
131            // Multiply by the inverse inertia early to reuse the values.
132            let i1_rt11 = i1 * rt11;
133            let i2_rt12 = i2 * rt12;
134            let i1_rt21 = i1 * rt21;
135            let i2_rt22 = i2 * rt22;
136
137            let k_linear1 = tangents[0].dot(effective_inverse_mass_sum * tangents[0]);
138            let k_linear2 = tangents[1].dot(effective_inverse_mass_sum * tangents[1]);
139            let k1 = k_linear1 + rt11.dot(i1_rt11) + rt12.dot(i2_rt12);
140            let k2 = k_linear2 + rt21.dot(i1_rt21) + rt22.dot(i2_rt22);
141
142            // Note: The invertion is done in `solve_impulse`, unlike in 2D.
143            part.effective_inverse_mass[0] = k1;
144            part.effective_inverse_mass[1] = k2;
145
146            // This is needed for solving the two tangent directions simultaneously.
147            // TODO. Derive and explain the math for this, or consider an alternative approach,
148            //       like using the Jacobians to compute the actual effective mass matrix.
149            part.effective_inverse_mass[2] = 2.0 * (rt11.dot(i1_rt21) + rt12.dot(i2_rt22));
150        }
151
152        part
153    }
154
155    /// Solves the friction constraint, updating the total impulse in `self` and returning
156    /// the incremental impulse to apply to each body.
157    pub fn solve_impulse(
158        &mut self,
159        tangent_directions: [Vector; DIM - 1],
160        relative_velocity: Vector,
161        // The desired relative velocity along the contact surface, used to simulate things like conveyor belts.
162        #[cfg(feature = "2d")] surface_speed: f32,
163        #[cfg(feature = "3d")] surface_velocity: Vector,
164        friction: f32,
165        normal_impulse: f32,
166    ) -> Vector {
167        // Compute the maximum bound for the friction impulse.
168        //
169        // According to the Coulomb friction law:
170        //
171        // length(friction_force) <= coefficient * length(normal_force)
172        //
173        // Now, we need to find the Lagrange multiplier, which corresponds
174        // to the constraint force magnitude.
175        //
176        // F_c = J^T * lagrange, where J is the Jacobian, which in this case is of unit length.
177        //
178        // We get the following:
179        //
180        // length(J^T * force_magnitude) <= coefficient * length(normal_force)
181        // <=> abs(force_magnitude) <= coefficient * length(normal_force)
182        // <=> -coefficient * length(normal_force) <= force_magnitude <= coefficient * length(normal_force)
183        //
184        // We are dealing with impulses instead of forces. Multiplying by delta time,
185        // we get the minimum and maximum bound for the friction impulse:
186        //
187        // -coefficient * length(normal_impulse) <= impulse_magnitude <= coefficient * length(normal_impulse)
188
189        let impulse_limit = friction * normal_impulse;
190
191        #[cfg(feature = "2d")]
192        {
193            // Compute the relative velocity along the tangent.
194            // Add the relative speed along the surface to the total tangent speed.
195            let tangent = tangent_directions[0];
196            let tangent_speed = relative_velocity.dot(tangent) + surface_speed;
197
198            // Compute the incremental tangent impoulse magnitude.
199            let mut impulse = self.effective_mass * (-tangent_speed);
200            // Clamp the accumulated impulse.
201            let new_impulse = (self.impulse + impulse).clamp(-impulse_limit, impulse_limit);
202            impulse = new_impulse - self.impulse;
203            self.impulse = new_impulse;
204            // Return the incremental friction impulse.
205            impulse * tangent
206        }
207        #[cfg(feature = "3d")]
208        {
209            // Compute the relative velocity along the tangents.
210            // Add the relative velocity along the surface to the total tangent speed.
211            let relative_velocity = relative_velocity + surface_velocity;
212            let tangent_speed1 = relative_velocity.dot(tangent_directions[0]);
213            let tangent_speed2 = relative_velocity.dot(tangent_directions[1]);
214
215            // Solve the two tangent directions simultaneously.
216            // Based on Rapier's two-body constraint.
217            // https://github.com/dimforge/rapier/blob/af1ac9baa26b1199ae2728e91adf5345bcd1c693/src/dynamics/solver/contact_constraint/two_body_constraint_element.rs#L127-L133
218            let t11 = tangent_speed1.powi(2);
219            let t22 = tangent_speed2.powi(2);
220            let t12 = tangent_speed1 * tangent_speed2;
221            let inv = t11 * self.effective_inverse_mass[0]
222                + t22 * self.effective_inverse_mass[1]
223                + t12 * self.effective_inverse_mass[2];
224
225            // Compute the effective mass "seen" by the constraint along the tangent.
226            // Note the guard against division by zero.
227            let effective_mass = (t11 + t22) * inv.recip();
228
229            // TODO: Could this be handled earlier reliably?
230            if !effective_mass.is_finite() {
231                return Vector::ZERO;
232            }
233
234            // Compute the incremental tangent impoulse.
235            let delta_impulse = effective_mass * Vec2::new(tangent_speed1, tangent_speed2);
236
237            // Clamp the accumulated impulse.
238            let new_impulse = (self.impulse - delta_impulse).clamp_length_max(impulse_limit);
239            let impulse = new_impulse - self.impulse;
240
241            self.impulse = new_impulse;
242
243            // Return the clamped incremental friction impulse.
244            impulse.x * tangent_directions[0] + impulse.y * tangent_directions[1]
245        }
246    }
247}