avian2d/dynamics/integrator/
mod.rs

1//! Applies forces and velocities to bodies in order to move them according to the equations of motion
2//! using numerical integration.
3//!
4//! See [`IntegratorPlugin`].
5
6use crate::prelude::*;
7use bevy::{
8    ecs::{intern::Interned, query::QueryData, schedule::ScheduleLabel},
9    prelude::*,
10};
11use dynamics::solver::SolverDiagnostics;
12
13use super::solver::solver_body::SolverBody;
14
15/// Integrates Newton's 2nd law of motion, applying forces and moving entities according to their velocities.
16///
17/// This acts as a prediction for the next positions and orientations of the bodies. The [solver](dynamics::solver)
18/// corrects these predicted positions to take constraints like contacts and joints into account.
19///
20/// Currently, only the [semi-implicit (symplectic) Euler](https://en.wikipedia.org/wiki/Semi-implicit_Euler_method)
21/// integration scheme is supported. It is the standard for game physics, being stable, efficient, and sufficiently accurate.
22///
23/// See [`IntegrationSystems`] for the system sets used by this plugin.
24pub struct IntegratorPlugin {
25    schedule: Interned<dyn ScheduleLabel>,
26}
27
28impl IntegratorPlugin {
29    /// Creates an [`IntegratorPlugin`] with the schedule that the integration systems should run in.
30    ///
31    /// The default schedule is [`SubstepSchedule`].
32    pub fn new(schedule: impl ScheduleLabel) -> Self {
33        Self {
34            schedule: schedule.intern(),
35        }
36    }
37}
38
39impl Default for IntegratorPlugin {
40    fn default() -> Self {
41        Self::new(SubstepSchedule)
42    }
43}
44
45impl Plugin for IntegratorPlugin {
46    fn build(&self, app: &mut App) {
47        // Add `VelocityIntegrationData` to all `SolverBody`s.
48        app.register_required_components::<SolverBody, VelocityIntegrationData>();
49
50        app.init_resource::<Gravity>();
51
52        app.configure_sets(
53            PhysicsSchedule,
54            (
55                IntegrationSystems::UpdateVelocityIncrements
56                    .in_set(SolverSystems::PreSubstep)
57                    .before(IntegrationSystems::Velocity),
58                IntegrationSystems::ClearVelocityIncrements
59                    .in_set(SolverSystems::PostSubstep)
60                    .after(IntegrationSystems::Velocity),
61            ),
62        );
63
64        app.add_systems(
65            PhysicsSchedule,
66            (
67                pre_process_velocity_increments
68                    .in_set(IntegrationSystems::UpdateVelocityIncrements),
69                clear_velocity_increments.in_set(IntegrationSystems::ClearVelocityIncrements),
70            ),
71        );
72
73        app.configure_sets(
74            self.schedule.intern(),
75            (IntegrationSystems::Velocity, IntegrationSystems::Position).chain(),
76        );
77
78        app.add_systems(
79            self.schedule.intern(),
80            (
81                (integrate_velocities, clamp_velocities)
82                    .chain()
83                    .in_set(IntegrationSystems::Velocity),
84                integrate_positions.in_set(IntegrationSystems::Position),
85            ),
86        );
87    }
88}
89
90/// System sets for position and velocity integration,
91/// applying forces and moving bodies based on velocity.
92#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub enum IntegrationSystems {
94    /// Applies gravity and locked axes to the linear and angular velocity increments of bodies,
95    /// and multiplies them by the substep delta time to get the final per-substep increments.
96    ///
97    /// Runs in the [`PhysicsSchedule`], in [`SolverSystems::PreSubstep`].
98    UpdateVelocityIncrements,
99    /// Applies velocity increments to the linear and angular velocities of bodies.
100    ///
101    /// Typically runs in the [`SubstepSchedule`], in [`IntegrationSystems::Velocity`].
102    Velocity,
103    /// Moves bodies based on their current velocities and the physics time step.
104    ///
105    /// Typically runs in the [`SubstepSchedule`], in [`IntegrationSystems::Position`].
106    Position,
107    /// Clears the velocity increments of bodies after the substepping loop.
108    ///
109    /// Runs in the [`PhysicsSchedule`], in [`SolverSystems::PostSubstep`].
110    ClearVelocityIncrements,
111}
112
113/// A deprecated alias for [`IntegrationSystems`].
114#[deprecated(since = "0.4.0", note = "Renamed to `IntegrationSystems`")]
115pub type IntegrationSet = IntegrationSystems;
116
117/// A resource for the global gravitational acceleration.
118///
119/// The default is an acceleration of 9.81 m/s^2 pointing down, which is approximate to the gravitational
120/// acceleration near Earth's surface. Note that if you are using pixels as length units in 2D,
121/// this gravity will be tiny. You should modify the gravity to fit your application.
122///
123/// You can also control how gravity affects a specific [rigid body](RigidBody) using the [`GravityScale`]
124/// component. The magnitude of the gravity will be multiplied by this scaling factor.
125///
126/// # Example
127///
128/// ```no_run
129#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
130#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
131/// use bevy::prelude::*;
132///
133/// # #[cfg(feature = "f32")]
134/// fn main() {
135///     App::new()
136///         .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
137#[cfg_attr(
138    feature = "2d",
139    doc = "         .insert_resource(Gravity(Vec2::NEG_Y * 100.0))"
140)]
141#[cfg_attr(
142    feature = "3d",
143    doc = "         .insert_resource(Gravity(Vec3::NEG_Y * 19.6))"
144)]
145///         .run();
146/// }
147/// # #[cfg(not(feature = "f32"))]
148/// # fn main() {} // Doc test needs main
149/// ```
150///
151/// You can also modify gravity while the app is running.
152#[derive(Reflect, Resource, Debug)]
153#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
154#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
155#[reflect(Debug, Resource)]
156pub struct Gravity(pub Vector);
157
158impl Default for Gravity {
159    fn default() -> Self {
160        Self(Vector::Y * -9.81)
161    }
162}
163
164impl Gravity {
165    /// Zero gravity.
166    pub const ZERO: Gravity = Gravity(Vector::ZERO);
167}
168
169/// Pre-computed data for speeding up velocity integration.
170///
171/// This includes:
172///
173/// - Velocity increments for [`Gravity`].
174/// - Velocity increments for [`ConstantForce`], [`ConstantTorque`], [`ConstantLinearAcceleration`], and [`ConstantAngularAcceleration`].
175/// - Velocity increments for forces, torques, and accelerations applied using [`Forces`].
176/// - Cached operands for applying linear and angular velocity damping.
177///
178/// The values are computed once per time step, and applied to the body at each substep
179/// with basic addition and multiplication. This moves the more expensive operations
180/// and branching out of the substepping loop.
181// -----------------------
182// 20 bytes in 2D with f32
183// 32 bytes in 3D with f32
184#[derive(Component, Debug, Default, PartialEq, Reflect)]
185#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
186#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
187#[reflect(Component, Debug, Default, PartialEq)]
188pub struct VelocityIntegrationData {
189    /// The linear velocity increment to be applied to the body at each substep.
190    ///
191    /// **Note:** This is treated as linear acceleration until [`IntegrationSystems::UpdateVelocityIncrements`].
192    /// where it is multiplied by the time step to get the corresponding velocity increment.
193    pub linear_increment: Vector,
194    /// The angular velocity increment to be applied to the body at each substep.
195    ///
196    /// **Note:** This is treated as angular acceleration until [`IntegrationSystems::UpdateVelocityIncrements`].
197    /// where it is multiplied by the time step to get the corresponding velocity increment.
198    pub angular_increment: AngularVector,
199    /// The right-hand side of the linear damping equation,
200    /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
201    pub linear_damping_rhs: Scalar,
202    /// The right-hand side of the angular damping equation,
203    /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
204    pub angular_damping_rhs: Scalar,
205}
206
207impl VelocityIntegrationData {
208    /// Applies a given linear acceleration to the body.
209    pub fn apply_linear_acceleration(&mut self, acceleration: Vector) {
210        self.linear_increment += acceleration;
211    }
212
213    /// Applies a given angular acceleration to the body.
214    pub fn apply_angular_acceleration(&mut self, acceleration: AngularVector) {
215        self.angular_increment += acceleration;
216    }
217
218    /// Updates the cached right-hand side of the linear damping equation,
219    /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
220    pub fn update_linear_damping_rhs(&mut self, damping_coefficient: Scalar, delta_secs: Scalar) {
221        self.linear_damping_rhs = 1.0 / (1.0 + delta_secs * damping_coefficient);
222    }
223
224    /// Updates the cached right-hand side of the angular damping equation,
225    /// `1 / (1 + dt * c)`, where `c` is the damping coefficient.
226    pub fn update_angular_damping_rhs(&mut self, damping_coefficient: Scalar, delta_secs: Scalar) {
227        self.angular_damping_rhs = 1.0 / (1.0 + delta_secs * damping_coefficient);
228    }
229}
230
231/// Applies gravity and locked axes to the linear and angular velocity increments of bodies.
232pub fn pre_process_velocity_increments(
233    mut bodies: Query<(
234        &RigidBody,
235        &mut VelocityIntegrationData,
236        Option<&LinearDamping>,
237        Option<&AngularDamping>,
238        Option<&GravityScale>,
239        Option<&LockedAxes>,
240    )>,
241    gravity: Res<Gravity>,
242    time: Res<Time<Substeps>>,
243    mut diagnostics: ResMut<SolverDiagnostics>,
244) {
245    let start = crate::utils::Instant::now();
246
247    let delta_secs = time.delta_secs_f64() as Scalar;
248
249    // TODO: Do we want to skip kinematic bodies here?
250    bodies.par_iter_mut().for_each(
251        |(rb, mut integration, lin_damping, ang_damping, gravity_scale, locked_axes)| {
252            if !rb.is_dynamic() {
253                // Skip non-dynamic bodies.
254                return;
255            }
256
257            let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
258
259            // Update the cached right-hand side of the velocity damping equation,
260            // `1 / (1 + dt * c)`, where `c` is the damping coefficient.
261            let lin_damping = lin_damping.map_or(0.0, |damping| damping.0);
262            let ang_damping = ang_damping.map_or(0.0, |damping| damping.0);
263            integration.update_linear_damping_rhs(lin_damping, delta_secs);
264            integration.update_angular_damping_rhs(ang_damping, delta_secs);
265
266            // NOTE: The `ForcePlugin` handles the application of external forces and torques.
267            // NOTE: The velocity increments are treated as accelerations at this point.
268
269            // Apply gravity.
270            integration.linear_increment += gravity.0 * gravity_scale.map_or(1.0, |scale| scale.0);
271
272            // Apply locked axes.
273            integration.linear_increment = locked_axes.apply_to_vec(integration.linear_increment);
274            integration.angular_increment =
275                locked_axes.apply_to_angular_velocity(integration.angular_increment);
276
277            // The velocity increments are treated as accelerations until this point.
278            // Multiply by the time step to get the final velocity increments.
279            integration.linear_increment *= delta_secs;
280            integration.angular_increment *= delta_secs;
281        },
282    );
283
284    diagnostics.update_velocity_increments += start.elapsed();
285}
286
287/// Clears the velocity increments of bodies after the substepping loop.
288fn clear_velocity_increments(
289    mut bodies: Query<&mut VelocityIntegrationData, With<SolverBody>>,
290    mut diagnostics: ResMut<SolverDiagnostics>,
291) {
292    let start = crate::utils::Instant::now();
293
294    bodies.par_iter_mut().for_each(|mut integration| {
295        integration.linear_increment = Vector::ZERO;
296        integration.angular_increment = AngularVector::ZERO;
297    });
298
299    diagnostics.update_velocity_increments += start.elapsed();
300}
301
302#[derive(QueryData)]
303#[query_data(mutable)]
304#[doc(hidden)]
305pub struct VelocityIntegrationQuery {
306    solver_body: &'static mut SolverBody,
307    integration: &'static mut VelocityIntegrationData,
308    #[cfg(feature = "3d")]
309    angular_inertia: &'static ComputedAngularInertia,
310    #[cfg(feature = "3d")]
311    rotation: &'static Rotation,
312}
313
314/// Integrates the velocities of bodies by applying velocity increments and damping.
315pub fn integrate_velocities(
316    mut bodies: Query<VelocityIntegrationQuery, RigidBodyActiveFilter>,
317    mut diagnostics: ResMut<SolverDiagnostics>,
318    #[cfg(feature = "3d")] time: Res<Time>,
319) {
320    let start = crate::utils::Instant::now();
321
322    #[cfg(feature = "3d")]
323    let delta_secs = time.delta_secs_f64() as Scalar;
324
325    bodies.par_iter_mut().for_each(|mut body| {
326        if body.solver_body.flags.is_kinematic() {
327            // Skip kinematic bodies.
328            return;
329        }
330
331        // Apply velocity damping.
332        body.solver_body.linear_velocity *= body.integration.linear_damping_rhs;
333        body.solver_body.angular_velocity *= body.integration.angular_damping_rhs;
334
335        // Apply velocity increments.
336        body.solver_body.linear_velocity += body.integration.linear_increment;
337        body.solver_body.angular_velocity += body.integration.angular_increment;
338
339        #[cfg(feature = "3d")]
340        {
341            if body.solver_body.is_gyroscopic() {
342                // TODO: Should this be opt-in with a `GyroscopicMotion` component?
343                // TODO: It's a bit unfortunate that this has to run in the substepping loop
344                //       rather than pre-computing the velocity increments once per time step.
345                //       This needs to be done because the gyroscopic torque relies on up-to-date rotations
346                //       and world-space angular inertia tensors. Omitting the change in orientation would
347                //       lead to worse accuracy and angular momentum not being conserved.
348                let rotation = body.solver_body.delta_rotation.0 * body.rotation.0;
349                solve_gyroscopic_torque(
350                    &mut body.solver_body.angular_velocity,
351                    rotation,
352                    body.angular_inertia,
353                    delta_secs,
354                );
355            }
356        }
357    });
358
359    diagnostics.integrate_velocities += start.elapsed();
360}
361
362/// Applies the effects of gyroscopic motion to the given angular velocity.
363///
364/// Gyroscopic motion is the tendency of a rotating object to maintain its axis of rotation
365/// unless acted upon by an external torque. It manifests as objects with non-uniform angular
366/// inertia tensors seemingly wobbling as they spin in the air or on the ground.
367///
368/// Gyroscopic motion is important for realistic spinning behavior, and for simulating
369/// gyroscopic phenomena such as the Dzhanibekov effect.
370#[cfg(feature = "3d")]
371#[inline]
372pub fn solve_gyroscopic_torque(
373    ang_vel: &mut Vector,
374    rotation: Quaternion,
375    local_inertia: &ComputedAngularInertia,
376    delta_secs: Scalar,
377) {
378    // References:
379    // - The "Gyroscopic Motion" section of Erin Catto's GDC 2015 slides on Numerical Methods.
380    //   https://box2d.org/files/ErinCatto_NumericalMethods_GDC2015.pdf
381    // - Jolt Physics - MotionProperties::ApplyGyroscopicForceInternal
382    //   https://github.com/jrouwe/JoltPhysics/blob/d497df2b9b0fa9aaf41295e1406079c23148232d/Jolt/Physics/Body/MotionProperties.inl#L102
383    //
384    // Erin Catto's GDC presentation suggests using implicit Euler for gyroscopic torque,
385    // as semi-implicit Euler can easily blow up with larger time steps due to extrapolating velocity.
386    // The extrapolation diverges quickly because gyroscopic torque is quadratic in the angular velocity.
387    //
388    // However, implicit Euler is measurably more expensive than semi-implicit Euler.
389    // We instead take inspiration from Jolt, and use semi-implicit Euler integration,
390    // clamping the magnitude of the angular momentum to remain the same.
391    // This is efficient, prevents energy from being introduced into the system,
392    // and produces reasonably accurate results for game purposes.
393
394    // Convert angular velocity to body space so that we can use the local angular inertia.
395    let local_ang_vel = rotation.inverse() * *ang_vel;
396
397    // Compute body-space angular momentum.
398    let local_momentum = local_inertia.tensor() * local_ang_vel;
399
400    // The gyroscopic torque is given by:
401    //
402    // T = -ω x I ω = -ω x L
403    //
404    // where ω is the angular velocity, I is the angular inertia tensor,
405    // and L is the angular momentum.
406    //
407    // The change in angular momentum is given by:
408    //
409    // ΔL = T Δt = -ω x L Δt
410    //
411    // Thus, we can compute the new angular momentum as:
412    //
413    // L' = L + ΔL = L - Δt (ω x L)
414    let mut new_local_momentum = local_momentum - delta_secs * local_ang_vel.cross(local_momentum);
415
416    // Make sure the magnitude of the angular momentum remains the same to avoid introducing
417    // energy into the system due to the extrapolation done by semi-implicit Euler integration.
418    let new_local_momentum_length_squared = new_local_momentum.length_squared();
419    if new_local_momentum_length_squared == 0.0 {
420        *ang_vel = Vector::ZERO;
421        return;
422    }
423    new_local_momentum *=
424        (local_momentum.length_squared() / new_local_momentum_length_squared).sqrt();
425
426    // Convert back to world-space angular velocity.
427    let local_inverse_inertia = local_inertia.inverse();
428    *ang_vel = rotation * (local_inverse_inertia * new_local_momentum);
429}
430
431// NOTE: If the majority of bodies have clamped velocities, it would be more efficient
432//       to do this in `integrate_velocities` rather than in a separate system.
433//       By doing this in a separate system, we're optimizing for the assumption
434//       that only some bodies have clamped velocities.
435/// Clamps the velocities of bodies to [`MaxLinearSpeed`] and [`MaxAngularSpeed`].
436fn clamp_velocities(
437    mut bodies: ParamSet<(
438        Query<(&mut SolverBody, &MaxLinearSpeed)>,
439        Query<(&mut SolverBody, &MaxAngularSpeed)>,
440    )>,
441    mut diagnostics: ResMut<SolverDiagnostics>,
442) {
443    let start = crate::utils::Instant::now();
444
445    // Clamp linear velocity.
446    bodies.p0().iter_mut().for_each(|(mut body, max_speed)| {
447        let linear_speed_squared = body.linear_velocity.length_squared();
448        if linear_speed_squared > max_speed.0 * max_speed.0 {
449            body.linear_velocity *= max_speed.0 / linear_speed_squared.sqrt();
450        }
451    });
452
453    // Clamp angular velocity.
454    bodies.p1().iter_mut().for_each(|(mut body, max_speed)| {
455        #[cfg(feature = "2d")]
456        if body.angular_velocity.abs() > max_speed.0 {
457            body.angular_velocity = max_speed.copysign(body.angular_velocity);
458        }
459        #[cfg(feature = "3d")]
460        {
461            let angular_speed_squared = body.angular_velocity.length_squared();
462            if angular_speed_squared > max_speed.0 * max_speed.0 {
463                body.angular_velocity *= max_speed.0 / angular_speed_squared.sqrt();
464            }
465        }
466    });
467
468    diagnostics.integrate_velocities += start.elapsed();
469}
470
471/// Integrates the positions of bodies based on their velocities and the time step.
472pub fn integrate_positions(
473    mut solver_bodies: Query<&mut SolverBody>,
474    time: Res<Time>,
475    mut diagnostics: ResMut<SolverDiagnostics>,
476) {
477    let start = crate::utils::Instant::now();
478
479    let delta_secs = time.delta_seconds_adjusted();
480
481    solver_bodies.par_iter_mut().for_each(|body| {
482        let SolverBody {
483            linear_velocity,
484            angular_velocity,
485            delta_position,
486            delta_rotation,
487            ..
488        } = body.into_inner();
489
490        *delta_position += *linear_velocity * delta_secs;
491        #[cfg(feature = "2d")]
492        {
493            // Note: We should probably use `add_angle_fast` here
494            *delta_rotation = Rotation::radians(*angular_velocity * delta_secs) * *delta_rotation;
495        }
496        #[cfg(feature = "3d")]
497        {
498            delta_rotation.0 =
499                Quaternion::from_scaled_axis(*angular_velocity * delta_secs) * delta_rotation.0;
500        }
501    });
502
503    diagnostics.integrate_positions += start.elapsed();
504}
505
506#[cfg(test)]
507mod tests {
508    use core::time::Duration;
509
510    use approx::assert_relative_eq;
511
512    use crate::prelude::*;
513    use bevy::{mesh::MeshPlugin, prelude::*, time::TimeUpdateStrategy};
514
515    fn create_app() -> App {
516        let mut app = App::new();
517        app.add_plugins((
518            MinimalPlugins,
519            PhysicsPlugins::default(),
520            TransformPlugin,
521            #[cfg(feature = "bevy_scene")]
522            AssetPlugin::default(),
523            #[cfg(feature = "bevy_scene")]
524            bevy::scene::ScenePlugin,
525            MeshPlugin,
526        ));
527        app
528    }
529
530    #[test]
531    fn semi_implicit_euler() {
532        let mut app = create_app();
533        app.insert_resource(SubstepCount(1));
534        app.finish();
535
536        let body_entity = app
537            .world_mut()
538            .spawn((
539                RigidBody::Dynamic,
540                #[cfg(feature = "2d")]
541                {
542                    (
543                        MassPropertiesBundle::from_shape(&Rectangle::from_length(1.0), 1.0),
544                        AngularVelocity(2.0),
545                    )
546                },
547                #[cfg(feature = "3d")]
548                {
549                    (
550                        MassPropertiesBundle::from_shape(&Cuboid::from_length(1.0), 1.0),
551                        AngularVelocity(Vector::Z * 2.0),
552                    )
553                },
554            ))
555            .id();
556
557        // Step by 100 steps of 0.1 seconds.
558        app.insert_resource(Time::from_hz(10.0));
559        app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f64(
560            1.0 / 10.0,
561        )));
562
563        // Initialize the app.
564        app.update();
565
566        for _ in 0..100 {
567            app.update();
568        }
569
570        // Get the body after the simulation.
571        let entity_ref = app.world_mut().entity(body_entity);
572        let position = entity_ref.get::<Position>().unwrap().0;
573        let rotation = *entity_ref.get::<Rotation>().unwrap();
574        let linear_velocity = entity_ref.get::<LinearVelocity>().unwrap().0;
575        let angular_velocity = entity_ref.get::<AngularVelocity>().unwrap().0;
576
577        // Euler methods have some precision issues, but this seems weirdly inaccurate.
578        assert_relative_eq!(position, Vector::NEG_Y * 490.5, epsilon = 10.0);
579
580        #[cfg(feature = "2d")]
581        assert_relative_eq!(
582            rotation.as_radians(),
583            Rotation::radians(20.0).as_radians(),
584            epsilon = 0.00001
585        );
586        #[cfg(feature = "3d")]
587        assert_relative_eq!(
588            rotation.0,
589            Quaternion::from_rotation_z(20.0),
590            epsilon = 0.01
591        );
592
593        assert_relative_eq!(linear_velocity, Vector::NEG_Y * 98.1, epsilon = 0.0001);
594        #[cfg(feature = "2d")]
595        assert_relative_eq!(angular_velocity, 2.0, epsilon = 0.00001);
596        #[cfg(feature = "3d")]
597        assert_relative_eq!(angular_velocity, Vector::Z * 2.0, epsilon = 0.00001);
598    }
599}