Skip to main content

avian3d/dynamics/solver/solver_body/
plugin.rs

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