bevy_xpbd_3d/plugins/
integrator.rs

1//! Integrates Newton's 2nd law of motion, applying forces and moving entities according to their velocities.
2//!
3//! See [`IntegratorPlugin`].
4
5use crate::prelude::*;
6use bevy::prelude::*;
7
8/// Integrates Newton's 2nd law of motion, applying forces and moving entities according to their velocities.
9///
10/// This acts as a prediction for the next positions and orientations of the bodies. The [solver] corrects these predicted
11/// positions to follow the rules set by the [constraints].
12///
13/// The integration scheme used is very closely related to implicit Euler integration.
14///
15/// The integration systems run in [`SubstepSet::Integrate`].
16pub struct IntegratorPlugin;
17
18impl Plugin for IntegratorPlugin {
19    fn build(&self, app: &mut App) {
20        app.get_schedule_mut(SubstepSchedule)
21            .expect("add SubstepSchedule first")
22            .add_systems((integrate_pos, integrate_rot).in_set(SubstepSet::Integrate));
23        app.get_schedule_mut(PhysicsSchedule)
24            .expect("add PhysicsSchedule first")
25            .add_systems(
26                apply_impulses
27                    .after(PhysicsStepSet::BroadPhase)
28                    .before(PhysicsStepSet::Substeps),
29            )
30            .add_systems(clear_forces_and_impulses.after(PhysicsStepSet::SpatialQuery));
31    }
32}
33
34type PosIntegrationComponents = (
35    &'static RigidBody,
36    &'static Position,
37    &'static mut PreviousPosition,
38    &'static mut AccumulatedTranslation,
39    &'static mut LinearVelocity,
40    Option<&'static LinearDamping>,
41    Option<&'static GravityScale>,
42    &'static ExternalForce,
43    &'static Mass,
44    &'static InverseMass,
45    Option<&'static LockedAxes>,
46);
47
48/// Explicitly integrates the positions and linear velocities of bodies taking only external forces
49/// like gravity into account. This acts as a prediction for the next positions of the bodies.
50fn integrate_pos(
51    mut bodies: Query<PosIntegrationComponents, Without<Sleeping>>,
52    gravity: Res<Gravity>,
53    time: Res<Time>,
54) {
55    let delta_secs = time.delta_seconds_adjusted();
56
57    for (
58        rb,
59        pos,
60        mut prev_pos,
61        mut translation,
62        mut lin_vel,
63        lin_damping,
64        gravity_scale,
65        external_force,
66        mass,
67        inv_mass,
68        locked_axes,
69    ) in &mut bodies
70    {
71        prev_pos.0 = pos.0;
72
73        if rb.is_static() {
74            continue;
75        }
76
77        let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
78
79        // Apply damping, gravity and other external forces
80        if rb.is_dynamic() {
81            // Apply damping
82            if let Some(damping) = lin_damping {
83                lin_vel.0 *= 1.0 / (1.0 + delta_secs * damping.0);
84            }
85
86            let effective_mass = locked_axes.apply_to_vec(Vector::splat(mass.0));
87            let effective_inv_mass = locked_axes.apply_to_vec(Vector::splat(inv_mass.0));
88
89            // Apply forces
90            let gravitation_force =
91                effective_mass * gravity.0 * gravity_scale.map_or(1.0, |scale| scale.0);
92            let external_forces = gravitation_force + external_force.force();
93            let delta_lin_vel = delta_secs * external_forces * effective_inv_mass;
94            // avoid triggering bevy's change detection unnecessarily
95            if delta_lin_vel != Vector::ZERO {
96                lin_vel.0 += delta_lin_vel;
97            }
98        }
99        if lin_vel.0 != Vector::ZERO {
100            translation.0 += locked_axes.apply_to_vec(delta_secs * lin_vel.0);
101        }
102    }
103}
104
105type RotIntegrationComponents = (
106    &'static RigidBody,
107    &'static mut Rotation,
108    &'static mut PreviousRotation,
109    &'static mut AngularVelocity,
110    Option<&'static AngularDamping>,
111    &'static ExternalForce,
112    &'static ExternalTorque,
113    &'static Inertia,
114    &'static InverseInertia,
115    Option<&'static LockedAxes>,
116);
117
118/// Explicitly integrates the rotations and angular velocities of bodies taking only external torque into account.
119/// This acts as a prediction for the next rotations of the bodies.
120#[cfg(feature = "2d")]
121fn integrate_rot(mut bodies: Query<RotIntegrationComponents, Without<Sleeping>>, time: Res<Time>) {
122    let delta_secs = time.delta_seconds_adjusted();
123
124    for (
125        rb,
126        mut rot,
127        mut prev_rot,
128        mut ang_vel,
129        ang_damping,
130        external_force,
131        external_torque,
132        _inertia,
133        inv_inertia,
134        locked_axes,
135    ) in &mut bodies
136    {
137        prev_rot.0 = *rot;
138
139        if rb.is_static() {
140            continue;
141        }
142
143        let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
144
145        // Apply damping and external torque
146        if rb.is_dynamic() {
147            // Apply damping
148            if let Some(damping) = ang_damping {
149                // avoid triggering bevy's change detection unnecessarily
150                if ang_vel.0 != 0.0 && damping.0 != 0.0 {
151                    ang_vel.0 *= 1.0 / (1.0 + delta_secs * damping.0);
152                }
153            }
154
155            let effective_inv_inertia = locked_axes.apply_to_rotation(inv_inertia.0);
156
157            // Apply external torque
158            let delta_ang_vel = delta_secs
159                * effective_inv_inertia
160                * (external_torque.torque() + external_force.torque());
161            // avoid triggering bevy's change detection unnecessarily
162            if delta_ang_vel != 0.0 {
163                ang_vel.0 += delta_ang_vel;
164            }
165        }
166        // avoid triggering bevy's change detection unnecessarily
167        let delta = locked_axes.apply_to_angular_velocity(delta_secs * ang_vel.0);
168        if delta != 0.0 {
169            *rot += Rotation::from_radians(delta);
170        }
171    }
172}
173
174/// Explicitly integrates the rotations and angular velocities of bodies taking only external torque into account.
175/// This acts as a prediction for the next rotations of the bodies.
176#[cfg(feature = "3d")]
177fn integrate_rot(mut bodies: Query<RotIntegrationComponents, Without<Sleeping>>, time: Res<Time>) {
178    let delta_secs = time.delta_seconds_adjusted();
179
180    for (
181        rb,
182        mut rot,
183        mut prev_rot,
184        mut ang_vel,
185        ang_damping,
186        external_force,
187        external_torque,
188        inertia,
189        inv_inertia,
190        locked_axes,
191    ) in &mut bodies
192    {
193        prev_rot.0 = *rot;
194
195        if rb.is_static() {
196            continue;
197        }
198
199        let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
200
201        // Apply damping and external torque
202        if rb.is_dynamic() {
203            // Apply damping
204            if let Some(damping) = ang_damping {
205                // avoid triggering bevy's change detection unnecessarily
206                if ang_vel.0 != Vector::ZERO && damping.0 != 0.0 {
207                    ang_vel.0 *= 1.0 / (1.0 + delta_secs * damping.0);
208                }
209            }
210
211            let effective_inertia = locked_axes.apply_to_rotation(inertia.rotated(&rot).0);
212            let effective_inv_inertia = locked_axes.apply_to_rotation(inv_inertia.rotated(&rot).0);
213
214            // Apply external torque
215            let delta_ang_vel = delta_secs
216                * effective_inv_inertia
217                * ((external_torque.torque() + external_force.torque())
218                    - ang_vel.0.cross(effective_inertia * ang_vel.0));
219            // avoid triggering bevy's change detection unnecessarily
220            if delta_ang_vel != Vector::ZERO {
221                ang_vel.0 += delta_ang_vel;
222            }
223        }
224
225        let q = Quaternion::from_vec4(ang_vel.0.extend(0.0)) * rot.0;
226        let effective_dq = locked_axes
227            .apply_to_angular_velocity(delta_secs * 0.5 * q.xyz())
228            .extend(delta_secs * 0.5 * q.w);
229        // avoid triggering bevy's change detection unnecessarily
230        let delta = Quaternion::from_vec4(effective_dq);
231        if delta != Quaternion::from_xyzw(0.0, 0.0, 0.0, 0.0) {
232            rot.0 = (rot.0 + delta).normalize();
233        }
234    }
235}
236
237type ImpulseQueryComponents = (
238    &'static RigidBody,
239    &'static mut ExternalImpulse,
240    &'static mut ExternalAngularImpulse,
241    &'static mut LinearVelocity,
242    &'static mut AngularVelocity,
243    &'static Rotation,
244    &'static InverseMass,
245    &'static InverseInertia,
246    Option<&'static LockedAxes>,
247);
248
249fn apply_impulses(mut bodies: Query<ImpulseQueryComponents, Without<Sleeping>>) {
250    for (
251        rb,
252        impulse,
253        ang_impulse,
254        mut lin_vel,
255        mut ang_vel,
256        rotation,
257        inv_mass,
258        inv_inertia,
259        locked_axes,
260    ) in &mut bodies
261    {
262        if !rb.is_dynamic() {
263            continue;
264        }
265
266        let locked_axes = locked_axes.map_or(LockedAxes::default(), |locked_axes| *locked_axes);
267
268        let effective_inv_mass = locked_axes.apply_to_vec(Vector::splat(inv_mass.0));
269        let effective_inv_inertia = locked_axes.apply_to_rotation(inv_inertia.rotated(rotation).0);
270
271        // avoid triggering bevy's change detection unnecessarily
272        let delta_lin_vel = impulse.impulse() * effective_inv_mass;
273        let delta_ang_vel =
274            effective_inv_inertia * (ang_impulse.impulse() + impulse.angular_impulse());
275
276        if delta_lin_vel != Vector::ZERO {
277            lin_vel.0 += delta_lin_vel;
278        }
279        if delta_ang_vel != AngularVelocity::ZERO.0 {
280            ang_vel.0 += delta_ang_vel;
281        }
282    }
283}
284
285type ForceComponents = (
286    &'static mut ExternalForce,
287    &'static mut ExternalTorque,
288    &'static mut ExternalImpulse,
289    &'static mut ExternalAngularImpulse,
290);
291type ForceComponentsChanged = Or<(
292    Changed<ExternalForce>,
293    Changed<ExternalTorque>,
294    Changed<ExternalImpulse>,
295    Changed<ExternalAngularImpulse>,
296)>;
297
298/// Responsible for clearing forces and impulses on bodies.
299///
300/// Runs in [`PhysicsSchedule`], after [`PhysicsStepSet::SpatialQuery`].
301pub fn clear_forces_and_impulses(mut forces: Query<ForceComponents, ForceComponentsChanged>) {
302    for (mut force, mut torque, mut impulse, mut angular_ímpulse) in &mut forces {
303        if !force.persistent {
304            force.clear();
305        }
306        if !torque.persistent {
307            torque.clear();
308        }
309        if !impulse.persistent {
310            impulse.clear();
311        }
312        if !angular_ímpulse.persistent {
313            angular_ímpulse.clear();
314        }
315    }
316}