avian2d/dynamics/solver/solver_body/
plugin.rs

1use bevy::{
2    ecs::{entity_disabling::Disabled, query::QueryFilter, world::DeferredWorld},
3    prelude::*,
4};
5
6use super::{SolverBody, SolverBodyInertia};
7use crate::{
8    AngularVelocity, LinearVelocity, PhysicsSchedule, Position, RigidBody, RigidBodyActiveFilter,
9    RigidBodyDisabled, Rotation, Sleeping, SolverSystems, Vector,
10    dynamics::solver::{SolverDiagnostics, solver_body::SolverBodyFlags},
11    prelude::{
12        AppDiagnosticsExt, ComputedAngularInertia, ComputedCenterOfMass, ComputedMass, Dominance,
13        LockedAxes,
14    },
15};
16#[cfg(feature = "3d")]
17use crate::{
18    MatExt,
19    dynamics::integrator::{IntegrationSystems, integrate_positions},
20    prelude::SubstepSchedule,
21};
22
23/// A plugin for managing solver bodies.
24///
25/// A [`SolverBody`] is created for each dynamic and kinematic rigid body when:
26///
27/// 1. The rigid body is created.
28/// 2. The rigid body is enabled by removing [`Disabled`]/[`RigidBodyDisabled`].
29/// 3. The rigid body is woken up.
30/// 4. The rigid body is set to be dynamic or kinematic.
31///
32/// A [`SolverBody`] is removed when:
33///
34/// 1. The rigid body is removed.
35/// 2. The rigid body is disabled by adding [`Disabled`]/[`RigidBodyDisabled`].
36/// 3. The rigid body is put to sleep.
37/// 4. The rigid body is set to be static.
38pub struct SolverBodyPlugin;
39
40impl Plugin for SolverBodyPlugin {
41    fn build(&self, app: &mut App) {
42        // Add or remove solver bodies when `RigidBody` component is added or replaced.
43        app.add_observer(on_insert_rigid_body);
44
45        // Add a solver body for each dynamic and kinematic rigid body
46        // when the associated rigid body is enabled or woken up.
47        app.add_observer(
48            |trigger: On<Remove, RigidBodyDisabled>,
49             rb_query: Query<&RigidBody, Without<Sleeping>>,
50             commands: Commands| {
51                add_solver_body::<Without<Sleeping>>(In(trigger.entity), rb_query, commands);
52            },
53        );
54        app.add_observer(
55            |trigger: On<Remove, Disabled>,
56             rb_query: Query<
57                &RigidBody,
58                (
59                    // The body still has `Disabled` at this point,
60                    // and we need to include in the query to match against the entity.
61                    With<Disabled>,
62                    Without<RigidBodyDisabled>,
63                    Without<Sleeping>,
64                ),
65            >,
66             commands: Commands| {
67                add_solver_body::<(
68                    With<Disabled>,
69                    Without<RigidBodyDisabled>,
70                    Without<Sleeping>,
71                )>(In(trigger.entity), rb_query, commands);
72            },
73        );
74        app.add_observer(
75            |trigger: On<Remove, Sleeping>,
76             rb_query: Query<&RigidBody, Without<RigidBodyDisabled>>,
77             commands: Commands| {
78                add_solver_body::<Without<RigidBodyDisabled>>(
79                    In(trigger.entity),
80                    rb_query,
81                    commands,
82                );
83            },
84        );
85
86        // Remove solver bodies when their associated rigid body is removed.
87        app.add_observer(
88            |trigger: On<Remove, RigidBody>, deferred_world: DeferredWorld| {
89                remove_solver_body(In(trigger.entity), deferred_world);
90            },
91        );
92
93        // Remove solver bodies when their associated rigid body is disabled or put to sleep.
94        app.add_observer(
95            |trigger: On<Add, (Disabled, RigidBodyDisabled, Sleeping)>,
96             deferred_world: DeferredWorld| {
97                remove_solver_body(In(trigger.entity), deferred_world);
98            },
99        );
100
101        // Prepare solver bodies before the substepping loop.
102        app.add_systems(
103            PhysicsSchedule,
104            prepare_solver_bodies
105                .chain()
106                .in_set(SolverSystems::PrepareSolverBodies),
107        );
108
109        // Write back solver body data to rigid bodies after the substepping loop.
110        app.add_systems(
111            PhysicsSchedule,
112            writeback_solver_bodies.in_set(SolverSystems::Finalize),
113        );
114
115        // Update the world-space angular inertia of solver bodies right after position integration
116        // in the substepping loop.
117        #[cfg(feature = "3d")]
118        app.add_systems(
119            SubstepSchedule,
120            update_solver_body_angular_inertia
121                .in_set(IntegrationSystems::Position)
122                .after(integrate_positions),
123        );
124    }
125
126    fn finish(&self, app: &mut App) {
127        // Register timer and counter diagnostics for the solver.
128        app.register_physics_diagnostics::<SolverDiagnostics>();
129    }
130}
131
132fn on_insert_rigid_body(
133    trigger: On<Insert, RigidBody>,
134    bodies: Query<(&RigidBody, Has<SolverBody>), RigidBodyActiveFilter>,
135    mut commands: Commands,
136) {
137    let Ok((rb, has_solver_body)) = bodies.get(trigger.entity) else {
138        return;
139    };
140
141    if rb.is_static() && has_solver_body {
142        // Remove the solver body if the rigid body is static.
143        commands
144            .entity(trigger.entity)
145            .try_remove::<(SolverBody, SolverBodyInertia)>();
146    } else if !rb.is_static() && !has_solver_body {
147        // Create a new solver body if the rigid body is dynamic or kinematic.
148        commands
149            .entity(trigger.entity)
150            .try_insert((SolverBody::default(), SolverBodyInertia::default()));
151    }
152}
153
154fn add_solver_body<F: QueryFilter>(
155    In(entity): In<Entity>,
156    mut rb_query: Query<&RigidBody, F>,
157    mut commands: Commands,
158) {
159    if let Ok(rb) = rb_query.get_mut(entity) {
160        if rb.is_static() {
161            return;
162        }
163
164        // Create a new solver body if the rigid body is dynamic or kinematic.
165        commands
166            .entity(entity)
167            .try_insert((SolverBody::default(), SolverBodyInertia::default()));
168    }
169}
170
171fn remove_solver_body(In(entity): In<Entity>, mut deferred_world: DeferredWorld) {
172    let entity_ref = deferred_world.entity(entity);
173    if entity_ref.contains::<SolverBody>() {
174        deferred_world
175            .commands()
176            .entity(entity)
177            .try_remove::<(SolverBody, SolverBodyInertia)>();
178    }
179}
180
181fn prepare_solver_bodies(
182    mut query: Query<(
183        &RigidBody,
184        &mut SolverBody,
185        &mut SolverBodyInertia,
186        &LinearVelocity,
187        &AngularVelocity,
188        &Rotation,
189        &ComputedMass,
190        &ComputedAngularInertia,
191        Option<&LockedAxes>,
192        Option<&Dominance>,
193    )>,
194) {
195    #[allow(unused_variables)]
196    query.par_iter_mut().for_each(
197        |(
198            rb,
199            mut solver_body,
200            mut inertial_properties,
201            linear_velocity,
202            angular_velocity,
203            rotation,
204            mass,
205            angular_inertia,
206            locked_axes,
207            dominance,
208        )| {
209            solver_body.linear_velocity = linear_velocity.0;
210            solver_body.angular_velocity = angular_velocity.0;
211            solver_body.delta_position = Vector::ZERO;
212            solver_body.delta_rotation = Rotation::IDENTITY;
213
214            let locked_axes = locked_axes.copied().unwrap_or_default();
215            *inertial_properties = SolverBodyInertia::new(
216                mass.inverse(),
217                #[cfg(feature = "2d")]
218                angular_inertia.inverse(),
219                #[cfg(feature = "3d")]
220                angular_inertia.rotated(rotation.0).inverse(),
221                locked_axes,
222                dominance.map_or(0, |dominance| dominance.0),
223                rb.is_dynamic(),
224            );
225            solver_body.flags = SolverBodyFlags(locked_axes.to_bits() as u32);
226            solver_body
227                .flags
228                .set(SolverBodyFlags::IS_KINEMATIC, rb.is_kinematic());
229
230            #[cfg(feature = "3d")]
231            {
232                // Only compute gyroscopic motion if the following conditions are met:
233                //
234                // 1. Rotation is unlocked on at least one axis.
235                // 2. The inertia tensor is not isotropic.
236                //
237                // If the inertia tensor is isotropic, it is invariant under all rotations,
238                // and the same around any axis passing through the object's center of mass.
239                // This is the case for shapes like spheres, as well as cubes and other regular solids.
240                //
241                // From Moments of inertia of Archimedean solids by Frédéric Perrier:
242                // "The condition of two axes of threefold or higher rotational symmetry
243                // crossing at the center of gravity is sufficient to produce an isotropic tensor of inertia.
244                // The MI around any axis passing by the center of gravity are then identical."
245
246                // TODO: Kinematic bodies should not have gyroscopic motion.
247                // TODO: Should we scale the epsilon based on the `PhysicsLengthUnit`?
248                // TODO: We should only do this when the body is added or the local inertia tensor is changed.
249                let epsilon = 1e-6;
250                let is_gyroscopic = !locked_axes.is_rotation_locked()
251                    && !angular_inertia.inverse().is_isotropic(epsilon);
252
253                solver_body
254                    .flags
255                    .set(SolverBodyFlags::GYROSCOPIC_MOTION, is_gyroscopic);
256            }
257        },
258    );
259}
260
261/// Writes back solver body data to rigid bodies.
262#[allow(clippy::type_complexity)]
263fn writeback_solver_bodies(
264    mut query: Query<(
265        &SolverBody,
266        &mut Position,
267        &mut Rotation,
268        &ComputedCenterOfMass,
269        &mut LinearVelocity,
270        &mut AngularVelocity,
271    )>,
272    mut diagnostics: ResMut<SolverDiagnostics>,
273) {
274    let start = bevy::platform::time::Instant::now();
275
276    query.par_iter_mut().for_each(
277        |(solver_body, mut pos, mut rot, com, mut lin_vel, mut ang_vel)| {
278            // Write back the position and rotation deltas,
279            // rotating the body around its center of mass.
280            let old_world_com = *rot * com.0;
281            *rot = (solver_body.delta_rotation * *rot).fast_renormalize();
282            let new_world_com = *rot * com.0;
283            pos.0 += solver_body.delta_position + old_world_com - new_world_com;
284
285            // Write back velocities.
286            lin_vel.0 = solver_body.linear_velocity;
287            ang_vel.0 = solver_body.angular_velocity;
288        },
289    );
290
291    diagnostics.finalize += start.elapsed();
292}
293
294#[cfg(feature = "3d")]
295pub(crate) fn update_solver_body_angular_inertia(
296    mut query: Query<(&mut SolverBodyInertia, &ComputedAngularInertia, &Rotation)>,
297) {
298    query
299        .par_iter_mut()
300        .for_each(|(mut inertia, angular_inertia, rotation)| {
301            inertia.update_effective_inv_angular_inertia(angular_inertia, rotation.0);
302        });
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::{PhysicsSchedulePlugin, SolverSchedulePlugin};
309
310    fn create_app() -> App {
311        let mut app = App::new();
312
313        app.add_plugins((
314            PhysicsSchedulePlugin::default(),
315            SolverSchedulePlugin,
316            SolverBodyPlugin,
317        ));
318
319        app
320    }
321
322    fn has_solver_body(app: &App, entity: Entity) -> bool {
323        app.world().get::<SolverBody>(entity).is_some()
324    }
325
326    #[test]
327    fn add_remove_solver_bodies() {
328        let mut app = create_app();
329
330        // Create a dynamic, kinematic, and static rigid body.
331        let entity1 = app.world_mut().spawn(RigidBody::Dynamic).id();
332        let entity2 = app.world_mut().spawn(RigidBody::Kinematic).id();
333        let entity3 = app.world_mut().spawn(RigidBody::Static).id();
334
335        app.update();
336
337        // The dynamic and kinematic rigid bodies should have solver bodies.
338        assert!(has_solver_body(&app, entity1));
339        assert!(has_solver_body(&app, entity2));
340        assert!(!has_solver_body(&app, entity3));
341
342        // Disable the dynamic rigid body.
343        app.world_mut()
344            .entity_mut(entity1)
345            .insert(RigidBodyDisabled);
346
347        app.update();
348
349        // The entity should no longer have a solver body.
350        assert!(!has_solver_body(&app, entity1));
351
352        // Enable the dynamic rigid body.
353        app.world_mut()
354            .entity_mut(entity1)
355            .remove::<RigidBodyDisabled>();
356
357        app.update();
358
359        // The dynamic rigid body should have a solver body again.
360        assert!(has_solver_body(&app, entity1));
361    }
362}