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}