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