avian3d/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
6#[doc(alias = "symplectic_euler")]
7pub mod semi_implicit_euler;
8
9use crate::prelude::*;
10use bevy::{
11    ecs::{intern::Interned, query::QueryData, schedule::ScheduleLabel},
12    prelude::*,
13};
14use dynamics::solver::SolverDiagnostics;
15
16/// Integrates Newton's 2nd law of motion, applying forces and moving entities according to their velocities.
17///
18/// This acts as a prediction for the next positions and orientations of the bodies. The [solver](dynamics::solver)
19/// corrects these predicted positions to take constraints like contacts and joints into account.
20///
21/// Currently, only the [semi-implicit (symplectic) Euler](semi_implicit_euler) integration scheme
22/// is supported. It is the standard for game physics, being simple, efficient, and sufficiently accurate.
23///
24/// The plugin adds systems in the [`IntegrationSet::Velocity`] and [`IntegrationSet::Position`] system sets.
25pub struct IntegratorPlugin {
26    schedule: Interned<dyn ScheduleLabel>,
27}
28
29impl IntegratorPlugin {
30    /// Creates an [`IntegratorPlugin`] with the schedule that the integration systems should run in.
31    ///
32    /// The default schedule is [`SubstepSchedule`].
33    pub fn new(schedule: impl ScheduleLabel) -> Self {
34        Self {
35            schedule: schedule.intern(),
36        }
37    }
38}
39
40impl Default for IntegratorPlugin {
41    fn default() -> Self {
42        Self::new(SubstepSchedule)
43    }
44}
45
46impl Plugin for IntegratorPlugin {
47    fn build(&self, app: &mut App) {
48        app.init_resource::<Gravity>();
49
50        app.configure_sets(
51            self.schedule.intern(),
52            (IntegrationSet::Velocity, IntegrationSet::Position).chain(),
53        );
54
55        app.add_systems(
56            self.schedule.intern(),
57            (
58                integrate_velocities.in_set(IntegrationSet::Velocity),
59                integrate_positions.in_set(IntegrationSet::Position),
60            ),
61        );
62
63        #[cfg(feature = "3d")]
64        app.add_systems(
65            self.schedule.intern(),
66            dynamics::rigid_body::mass_properties::update_global_angular_inertia::<()>
67                .in_set(IntegrationSet::Position)
68                .after(integrate_positions),
69        );
70
71        app.get_schedule_mut(PhysicsSchedule)
72            .expect("add PhysicsSchedule first")
73            .add_systems(
74                (
75                    apply_impulses.before(SolverSet::Substep),
76                    clear_forces_and_impulses.after(SolverSet::Substep),
77                )
78                    .in_set(PhysicsStepSet::Solver),
79            );
80    }
81}
82
83/// System sets for position and velocity integration,
84/// applying forces and moving bodies based on velocity.
85#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
86pub enum IntegrationSet {
87    /// Applies gravity and external forces to bodies, updating their velocities.
88    Velocity,
89    /// Moves bodies based on their current velocities and the physics time step.
90    Position,
91}
92
93/// A resource for the global gravitational acceleration.
94///
95/// The default is an acceleration of 9.81 m/s^2 pointing down, which is approximate to the gravitational
96/// acceleration near Earth's surface. Note that if you are using pixels as length units in 2D,
97/// this gravity will be tiny. You should modify the gravity to fit your application.
98///
99/// You can also control how gravity affects a specific [rigid body](RigidBody) using the [`GravityScale`]
100/// component. The magnitude of the gravity will be multiplied by this scaling factor.
101///
102/// # Example
103///
104/// ```no_run
105#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
106#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
107/// use bevy::prelude::*;
108///
109/// # #[cfg(feature = "f32")]
110/// fn main() {
111///     App::new()
112///         .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
113#[cfg_attr(
114    feature = "2d",
115    doc = "         .insert_resource(Gravity(Vec2::NEG_Y * 100.0))"
116)]
117#[cfg_attr(
118    feature = "3d",
119    doc = "         .insert_resource(Gravity(Vec3::NEG_Y * 19.6))"
120)]
121///         .run();
122/// }
123/// # #[cfg(not(feature = "f32"))]
124/// # fn main() {} // Doc test needs main
125/// ```
126///
127/// You can also modify gravity while the app is running.
128#[derive(Reflect, Resource, Debug)]
129#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
130#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
131#[reflect(Debug, Resource)]
132pub struct Gravity(pub Vector);
133
134impl Default for Gravity {
135    fn default() -> Self {
136        Self(Vector::Y * -9.81)
137    }
138}
139
140impl Gravity {
141    /// Zero gravity.
142    pub const ZERO: Gravity = Gravity(Vector::ZERO);
143}
144
145#[derive(QueryData)]
146#[query_data(mutable)]
147struct VelocityIntegrationQuery {
148    rb: &'static RigidBody,
149    pos: &'static Position,
150    prev_pos: Option<&'static mut PreSolveAccumulatedTranslation>,
151    #[cfg(feature = "3d")]
152    rot: &'static Rotation,
153    lin_vel: &'static mut LinearVelocity,
154    ang_vel: &'static mut AngularVelocity,
155    force: &'static ExternalForce,
156    torque: &'static ExternalTorque,
157    mass: &'static ComputedMass,
158    angular_inertia: &'static ComputedAngularInertia,
159    #[cfg(feature = "3d")]
160    global_angular_inertia: &'static GlobalAngularInertia,
161    lin_damping: Option<&'static LinearDamping>,
162    ang_damping: Option<&'static AngularDamping>,
163    max_linear_speed: Option<&'static MaxLinearSpeed>,
164    max_angular_speed: Option<&'static MaxAngularSpeed>,
165    gravity_scale: Option<&'static GravityScale>,
166    locked_axes: Option<&'static LockedAxes>,
167}
168
169#[allow(clippy::type_complexity)]
170fn integrate_velocities(
171    mut bodies: Query<VelocityIntegrationQuery, RigidBodyActiveFilter>,
172    gravity: Res<Gravity>,
173    time: Res<Time>,
174    mut diagnostics: ResMut<SolverDiagnostics>,
175) {
176    let start = crate::utils::Instant::now();
177
178    let delta_secs = time.delta_seconds_adjusted();
179
180    bodies.par_iter_mut().for_each(|mut body| {
181        if let Some(mut previous_position) = body.prev_pos {
182            previous_position.0 = body.pos.0;
183        }
184
185        if body.rb.is_static() {
186            if *body.lin_vel != LinearVelocity::ZERO {
187                *body.lin_vel = LinearVelocity::ZERO;
188            }
189            if *body.ang_vel != AngularVelocity::ZERO {
190                *body.ang_vel = AngularVelocity::ZERO;
191            }
192            return;
193        }
194
195        if body.rb.is_dynamic() {
196            let locked_axes = body
197                .locked_axes
198                .map_or(LockedAxes::default(), |locked_axes| *locked_axes);
199
200            // Apply damping
201            if let Some(lin_damping) = body.lin_damping {
202                if body.lin_vel.0 != Vector::ZERO && lin_damping.0 != 0.0 {
203                    body.lin_vel.0 *= 1.0 / (1.0 + delta_secs * lin_damping.0);
204                }
205            }
206            if let Some(ang_damping) = body.ang_damping {
207                if body.ang_vel.0 != AngularVelocity::ZERO.0 && ang_damping.0 != 0.0 {
208                    body.ang_vel.0 *= 1.0 / (1.0 + delta_secs * ang_damping.0);
209                }
210            }
211
212            let external_force = body.force.force();
213            let external_torque = body.torque.torque() + body.force.torque();
214            let gravity = gravity.0 * body.gravity_scale.map_or(1.0, |scale| scale.0);
215
216            semi_implicit_euler::integrate_velocity(
217                &mut body.lin_vel.0,
218                &mut body.ang_vel.0,
219                external_force,
220                external_torque,
221                *body.mass,
222                body.angular_inertia,
223                #[cfg(feature = "3d")]
224                body.global_angular_inertia,
225                #[cfg(feature = "3d")]
226                *body.rot,
227                locked_axes,
228                gravity,
229                delta_secs,
230            );
231        }
232
233        // Clamp velocities
234        if let Some(max_linear_speed) = body.max_linear_speed {
235            let linear_speed_squared = body.lin_vel.0.length_squared();
236            if linear_speed_squared > max_linear_speed.0.powi(2) {
237                body.lin_vel.0 *= max_linear_speed.0 / linear_speed_squared.sqrt();
238            }
239        }
240        if let Some(max_angular_speed) = body.max_angular_speed {
241            #[cfg(feature = "2d")]
242            if body.ang_vel.abs() > max_angular_speed.0 {
243                body.ang_vel.0 = max_angular_speed.copysign(body.ang_vel.0);
244            }
245            #[cfg(feature = "3d")]
246            {
247                let angular_speed_squared = body.ang_vel.0.length_squared();
248                if angular_speed_squared > max_angular_speed.0.powi(2) {
249                    body.ang_vel.0 *= max_angular_speed.0 / angular_speed_squared.sqrt();
250                }
251            }
252        }
253    });
254
255    diagnostics.integrate_velocities += start.elapsed();
256}
257
258#[allow(clippy::type_complexity)]
259fn integrate_positions(
260    mut bodies: Query<
261        (
262            &RigidBody,
263            &Position,
264            Option<&mut PreSolveAccumulatedTranslation>,
265            &mut AccumulatedTranslation,
266            &mut Rotation,
267            &LinearVelocity,
268            &AngularVelocity,
269            Option<&LockedAxes>,
270        ),
271        RigidBodyActiveFilter,
272    >,
273    time: Res<Time>,
274    mut diagnostics: ResMut<SolverDiagnostics>,
275) {
276    let start = crate::utils::Instant::now();
277
278    let delta_secs = time.delta_seconds_adjusted();
279
280    bodies.par_iter_mut().for_each(
281        |(
282            rb,
283            pos,
284            pre_solve_accumulated_translation,
285            mut accumulated_translation,
286            mut rot,
287            lin_vel,
288            ang_vel,
289            locked_axes,
290        )| {
291            if let Some(mut previous_position) = pre_solve_accumulated_translation {
292                previous_position.0 = pos.0;
293            }
294
295            if rb.is_static() || (lin_vel.0 == Vector::ZERO && *ang_vel == AngularVelocity::ZERO) {
296                return;
297            }
298
299            let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
300
301            semi_implicit_euler::integrate_position(
302                &mut accumulated_translation.0,
303                &mut rot,
304                lin_vel.0,
305                ang_vel.0,
306                locked_axes,
307                delta_secs,
308            );
309        },
310    );
311
312    diagnostics.integrate_positions += start.elapsed();
313}
314
315#[cfg(feature = "2d")]
316type AngularValue = Scalar;
317#[cfg(feature = "3d")]
318type AngularValue = Vector;
319#[cfg(feature = "2d")]
320type TorqueValue = Scalar;
321#[cfg(feature = "3d")]
322type TorqueValue = Vector;
323
324type ImpulseQueryComponents = (
325    &'static RigidBody,
326    &'static mut ExternalImpulse,
327    &'static mut ExternalAngularImpulse,
328    &'static mut LinearVelocity,
329    &'static mut AngularVelocity,
330    &'static Rotation,
331    &'static ComputedMass,
332    &'static GlobalAngularInertia,
333    Option<&'static LockedAxes>,
334);
335
336fn apply_impulses(mut bodies: Query<ImpulseQueryComponents, RigidBodyActiveFilter>) {
337    for (
338        rb,
339        impulse,
340        ang_impulse,
341        mut lin_vel,
342        mut ang_vel,
343        _rotation,
344        mass,
345        global_angular_inertia,
346        locked_axes,
347    ) in &mut bodies
348    {
349        if !rb.is_dynamic() {
350            continue;
351        }
352
353        let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
354
355        let effective_inv_mass = locked_axes.apply_to_vec(Vector::splat(mass.inverse()));
356        let effective_angular_inertia =
357            locked_axes.apply_to_angular_inertia(*global_angular_inertia);
358
359        // Avoid triggering bevy's change detection unnecessarily.
360        let delta_lin_vel = impulse.impulse() * effective_inv_mass;
361        let delta_ang_vel = effective_angular_inertia.inverse()
362            * (ang_impulse.impulse() + impulse.angular_impulse());
363
364        if delta_lin_vel != Vector::ZERO {
365            lin_vel.0 += delta_lin_vel;
366        }
367        if delta_ang_vel != AngularVelocity::ZERO.0 {
368            ang_vel.0 += delta_ang_vel;
369        }
370    }
371}
372
373type ForceComponents = (
374    &'static mut ExternalForce,
375    &'static mut ExternalTorque,
376    &'static mut ExternalImpulse,
377    &'static mut ExternalAngularImpulse,
378);
379type ForceComponentsChanged = Or<(
380    Changed<ExternalForce>,
381    Changed<ExternalTorque>,
382    Changed<ExternalImpulse>,
383    Changed<ExternalAngularImpulse>,
384)>;
385
386/// Responsible for clearing forces and impulses on bodies.
387///
388/// Runs in [`PhysicsSchedule`], after [`PhysicsStepSet::SpatialQuery`].
389pub fn clear_forces_and_impulses(mut forces: Query<ForceComponents, ForceComponentsChanged>) {
390    for (mut force, mut torque, mut impulse, mut angular_ímpulse) in &mut forces {
391        if !force.persistent {
392            force.clear();
393        }
394        if !torque.persistent {
395            torque.clear();
396        }
397        if !impulse.persistent {
398            impulse.clear();
399        }
400        if !angular_ímpulse.persistent {
401            angular_ímpulse.clear();
402        }
403    }
404}