avian3d/dynamics/integrator/
semi_implicit_euler.rs

1//! The *semi-implicit* or *symplectic* Euler [integration](super) scheme.
2//!
3//! [Semi-implicit Euler](https://en.wikipedia.org/wiki/Semi-implicit_Euler_method)
4//! integration is the most common integration scheme because it is simpler and more
5//! efficient than implicit Euler integration, has great energy conservation,
6//! and provides much better accuracy than explicit Euler integration.
7//!
8//! Semi-implicit Euler integration evalutes the acceleration at
9//! the current timestep and the velocity at the next timestep:
10//!
11//! ```text
12//! v = v_0 + a * Δt (linear velocity)
13//! ω = ω_0 + α * Δt (angular velocity)
14//! ```
15//!
16//! and computes the new position:
17//!
18//! ```text
19//! x = x_0 + v * Δt (position)
20//! θ = θ_0 + ω * Δt (rotation)
21//! ```
22//!
23//! This order is opposite to explicit Euler integration, which uses the velocity
24//! at the current timestep instead of the next timestep. The explicit approach
25//! can lead to bodies gaining energy over time, which is why the semi-implicit
26//! approach is typically preferred.
27
28use super::*;
29
30/// Integrates velocity based on the given forces in order to find
31/// the linear and angular velocity after `delta_seconds` have passed.
32///
33/// This uses [semi-implicit (symplectic) Euler integration](self).
34#[allow(clippy::too_many_arguments)]
35pub fn integrate_velocity(
36    lin_vel: &mut Vector,
37    ang_vel: &mut AngularValue,
38    force: Vector,
39    torque: TorqueValue,
40    mass: ComputedMass,
41    angular_inertia: &ComputedAngularInertia,
42    #[cfg(feature = "3d")] global_angular_inertia: &GlobalAngularInertia,
43    #[cfg(feature = "3d")] rotation: Rotation,
44    locked_axes: LockedAxes,
45    gravity: Vector,
46    delta_seconds: Scalar,
47) {
48    // Compute linear acceleration.
49    let lin_acc = linear_acceleration(force, mass, locked_axes, gravity);
50
51    // Compute next linear velocity.
52    // v = v_0 + a * Δt
53    let next_lin_vel = *lin_vel + lin_acc * delta_seconds;
54    if next_lin_vel != *lin_vel {
55        *lin_vel = next_lin_vel;
56    }
57
58    // Compute angular acceleration.
59    #[cfg(feature = "2d")]
60    let ang_acc = angular_acceleration(torque, angular_inertia, locked_axes);
61    #[cfg(feature = "3d")]
62    let ang_acc = angular_acceleration(torque, global_angular_inertia, locked_axes);
63
64    // Compute angular velocity delta.
65    // Δω = α * Δt
66    #[allow(unused_mut)]
67    let mut delta_ang_vel = ang_acc * delta_seconds;
68
69    #[cfg(feature = "3d")]
70    {
71        // In 3D, we should also handle gyroscopic motion, which accounts for
72        // non-spherical shapes that may wobble as they spin in the air.
73        //
74        // Gyroscopic motion happens when the inertia tensor is not uniform, causing
75        // the angular momentum to point in a different direction than the angular velocity.
76        //
77        // The gyroscopic torque is τ = ω x Iω.
78        //
79        // However, the basic semi-implicit approach can blow up, as semi-implicit Euler
80        // extrapolates velocity and the gyroscopic torque is quadratic in the angular velocity.
81        // Thus, we use implicit Euler, which is much more accurate and stable, although slightly more expensive.
82        let delta_ang_vel_gyro = solve_gyroscopic_torque(
83            *ang_vel,
84            rotation.0,
85            angular_inertia.tensor(),
86            delta_seconds,
87        );
88        delta_ang_vel += locked_axes.apply_to_angular_velocity(delta_ang_vel_gyro);
89    }
90
91    if delta_ang_vel != AngularVelocity::ZERO.0 && delta_ang_vel.is_finite() {
92        *ang_vel += delta_ang_vel;
93    }
94}
95
96/// Integrates position and rotation based on the given velocities in order to
97/// find the position and rotation after `delta_seconds` have passed.
98///
99/// This uses [semi-implicit (symplectic) Euler integration](self).
100pub fn integrate_position(
101    pos: &mut Vector,
102    rot: &mut Rotation,
103    lin_vel: Vector,
104    ang_vel: AngularValue,
105    locked_axes: LockedAxes,
106    delta_seconds: Scalar,
107) {
108    let lin_vel = locked_axes.apply_to_vec(lin_vel);
109
110    // x = x_0 + v * Δt
111    let next_pos = *pos + lin_vel * delta_seconds;
112
113    if next_pos != *pos && next_pos.is_finite() {
114        *pos = next_pos;
115    }
116
117    // Effective inverse inertia along each rotational axis
118    let ang_vel = locked_axes.apply_to_angular_velocity(ang_vel);
119
120    // θ = θ_0 + ω * Δt
121    #[cfg(feature = "2d")]
122    {
123        let delta_rot = Rotation::radians(ang_vel * delta_seconds);
124        if delta_rot != Rotation::IDENTITY && delta_rot.is_finite() {
125            *rot *= delta_rot;
126            *rot = rot.fast_renormalize();
127        }
128    }
129    #[cfg(feature = "3d")]
130    {
131        // This is a bit more complicated because quaternions are weird.
132        // Maybe there's a simpler and more numerically stable way?
133        let scaled_axis = ang_vel * delta_seconds;
134        if scaled_axis != AngularVelocity::ZERO.0 && scaled_axis.is_finite() {
135            let delta_rot = Quaternion::from_scaled_axis(scaled_axis);
136            rot.0 = delta_rot * rot.0;
137            *rot = rot.fast_renormalize();
138        }
139    }
140}
141
142/// Computes linear acceleration based on the given forces and mass.
143pub fn linear_acceleration(
144    force: Vector,
145    mass: ComputedMass,
146    locked_axes: LockedAxes,
147    gravity: Vector,
148) -> Vector {
149    // Effective inverse mass along each axis
150    let effective_inverse_mass = locked_axes.apply_to_vec(Vector::splat(mass.inverse()));
151
152    if effective_inverse_mass != Vector::ZERO && effective_inverse_mass.is_finite() {
153        // Newton's 2nd law for translational movement:
154        //
155        // F = m * a
156        // a = F / m
157        //
158        // where a is the acceleration, F is the force, and m is the mass.
159        //
160        // `gravity` below is the gravitational acceleration,
161        // so it doesn't need to be divided by mass.
162        force * effective_inverse_mass + locked_axes.apply_to_vec(gravity)
163    } else {
164        Vector::ZERO
165    }
166}
167
168/// Computes angular acceleration based on the current angular velocity, torque, and inertia.
169#[cfg_attr(
170    feature = "3d",
171    doc = "
172Note that this does not account for gyroscopic motion. To compute the gyroscopic angular velocity
173correction, use `solve_gyroscopic_torque`."
174)]
175pub fn angular_acceleration(
176    torque: TorqueValue,
177    global_angular_inertia: &ComputedAngularInertia,
178    locked_axes: LockedAxes,
179) -> AngularValue {
180    // Effective inverse inertia along each axis
181    let effective_angular_inertia = locked_axes.apply_to_angular_inertia(*global_angular_inertia);
182
183    if effective_angular_inertia != ComputedAngularInertia::INFINITY
184        && effective_angular_inertia.is_finite()
185    {
186        // Newton's 2nd law for rotational movement:
187        //
188        // τ = I * α
189        // α = τ / I
190        //
191        // where α (alpha) is the angular acceleration,
192        // τ (tau) is the torque, and I is the moment of inertia.
193        effective_angular_inertia.inverse() * torque
194    } else {
195        AngularValue::ZERO
196    }
197}
198
199/// Computes the angular correction caused by gyroscopic motion,
200/// which may cause objects with non-uniform angular inertia to wobble
201/// while spinning.
202#[cfg(feature = "3d")]
203pub fn solve_gyroscopic_torque(
204    ang_vel: Vector,
205    rotation: Quaternion,
206    local_inertia: Matrix,
207    delta_seconds: Scalar,
208) -> Vector {
209    // Based on the "Gyroscopic Motion" section of Erin Catto's GDC 2015 slides on Numerical Methods.
210    // https://box2d.org/files/ErinCatto_NumericalMethods_GDC2015.pdf
211
212    // Convert angular velocity to body coordinates so that we can use the local angular inertia
213    let local_ang_vel = rotation.inverse() * ang_vel;
214
215    // Compute body-space angular momentum
216    let angular_momentum = local_inertia * local_ang_vel;
217
218    // Compute Jacobian
219    let jacobian = local_inertia
220        + delta_seconds
221            * (skew_symmetric_mat3(local_ang_vel) * local_inertia
222                - skew_symmetric_mat3(angular_momentum));
223
224    // Residual vector
225    let f = delta_seconds * local_ang_vel.cross(angular_momentum);
226
227    // Do one Newton-Raphson iteration
228    let delta_ang_vel = -jacobian.inverse() * f;
229
230    // Convert back to world coordinates
231    rotation * delta_ang_vel
232}
233
234#[cfg(test)]
235mod tests {
236    use approx::assert_relative_eq;
237
238    use super::*;
239
240    #[test]
241    fn semi_implicit_euler() {
242        let mut position = Vector::ZERO;
243        let mut rotation = Rotation::default();
244
245        let mut linear_velocity = Vector::ZERO;
246        #[cfg(feature = "2d")]
247        let mut angular_velocity = 2.0;
248        #[cfg(feature = "3d")]
249        let mut angular_velocity = Vector::Z * 2.0;
250
251        let mass = ComputedMass::new(1.0);
252        #[cfg(feature = "2d")]
253        let angular_inertia = ComputedAngularInertia::new(1.0);
254        #[cfg(feature = "3d")]
255        let angular_inertia = ComputedAngularInertia::new(Vector::ONE);
256
257        let gravity = Vector::NEG_Y * 9.81;
258
259        // Step by 100 steps of 0.1 seconds
260        for _ in 0..100 {
261            integrate_velocity(
262                &mut linear_velocity,
263                &mut angular_velocity,
264                default(),
265                default(),
266                mass,
267                &angular_inertia,
268                #[cfg(feature = "3d")]
269                &GlobalAngularInertia::new(angular_inertia, rotation),
270                #[cfg(feature = "3d")]
271                rotation,
272                default(),
273                gravity,
274                1.0 / 10.0,
275            );
276            integrate_position(
277                &mut position,
278                &mut rotation,
279                linear_velocity,
280                angular_velocity,
281                default(),
282                1.0 / 10.0,
283            );
284        }
285
286        // Euler methods have some precision issues, but this seems weirdly inaccurate.
287        assert_relative_eq!(position, Vector::NEG_Y * 490.5, epsilon = 10.0);
288
289        #[cfg(feature = "2d")]
290        assert_relative_eq!(
291            rotation.as_radians(),
292            Rotation::radians(20.0).as_radians(),
293            epsilon = 0.00001
294        );
295        #[cfg(feature = "3d")]
296        assert_relative_eq!(
297            rotation.0,
298            Quaternion::from_rotation_z(20.0),
299            epsilon = 0.01
300        );
301
302        assert_relative_eq!(linear_velocity, Vector::NEG_Y * 98.1, epsilon = 0.0001);
303        #[cfg(feature = "2d")]
304        assert_relative_eq!(angular_velocity, 2.0, epsilon = 0.00001);
305        #[cfg(feature = "3d")]
306        assert_relative_eq!(angular_velocity, Vector::Z * 2.0, epsilon = 0.00001);
307    }
308}