Skip to main content

avian3d/dynamics/rigid_body/mass_properties/
mod.rs

1//! Mass property functionality for [rigid bodies] and [colliders].
2//!
3//! # Overview
4//!
5//! Every dynamic rigid body has [mass], [angular inertia], and a [center of mass].
6//! These mass properties determine how the rigid body responds to forces and torques.
7//!
8//! - **Mass**: Represents resistance to linear acceleration. A higher mass requires more force for the same acceleration.
9//! - **Angular Inertia**: Represents resistance to angular acceleration. A higher angular inertia requires more torque for the same angular acceleration.
10//! - **Center of Mass**: The average position of mass in the body. Applying forces at this point produces no torque.
11//!
12//! Static and kinematic rigid bodies have infinite mass and angular inertia,
13//! and do not respond to forces or torques. Zero mass for a dynamic body is also
14//! treated as a special case, and corresponds to infinite mass.
15//!
16//! Mass properties can be set for individual entities using the [`Mass`], [`AngularInertia`],
17//! and [`CenterOfMass`] components. If they are not present, mass properties are instead computed
18//! automatically from attached colliders based on their shape and [`ColliderDensity`].
19//!
20//! If a rigid body has child entities, their mass properties are combined to compute
21//! the total mass properties of the rigid body. These are stored in the [`ComputedMass`],
22//! [`ComputedAngularInertia`], and [`ComputedCenterOfMass`] components, which are updated
23//! automatically when mass properties are changed, or when colliders are added or removed.
24//!
25//! To prevent mass properties of child entities from contributing to the total mass properties,
26//! you can use the [`NoAutoMass`], [`NoAutoAngularInertia`], and [`NoAutoCenterOfMass`] marker components.
27//! This can be useful when full control over mass properties is desired.
28//!
29//! [rigid bodies]: crate::dynamics::rigid_body::RigidBody
30//! [colliders]: crate::collision::collider::Collider
31//! [mass]: components::Mass
32//! [angular inertia]: components::AngularInertia
33//! [center of mass]: components::CenterOfMass
34//!
35//! ## Example
36//!
37//! If no mass properties are set, they are computed automatically from attached colliders
38//! based on their shape and density.
39//!
40//! ```
41#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
42#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
43//! # use bevy::prelude::*;
44//! #
45//! # fn setup(mut commands: Commands) {
46//! // Note: `ColliderDensity` is optional, and defaults to `1.0` if not present.
47//! commands.spawn((
48//!     RigidBody::Dynamic,
49//!     Collider::capsule(0.5, 1.5),
50//!     ColliderDensity(2.0),
51//! ));
52//! # }
53//! ```
54//!
55//! If mass properties are set with the [`Mass`], [`AngularInertia`], and [`CenterOfMass`] components,
56//! they override the values computed from colliders.
57//!
58//! ```
59#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
60#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
61//! # use bevy::prelude::*;
62//! #
63//! # fn setup(mut commands: Commands) {
64//! // Override mass and the center of mass, but use the collider's angular inertia.
65//! commands.spawn((
66//!     RigidBody::Dynamic,
67//!     Collider::capsule(0.5, 1.5),
68//!     Mass(5.0),
69#![cfg_attr(feature = "2d", doc = "    CenterOfMass::new(0.0, -0.5),")]
70#![cfg_attr(feature = "3d", doc = "    CenterOfMass::new(0.0, -0.5, 0.0),")]
71//! ));
72//! # }
73//! ```
74//!
75//! If the rigid body has child colliders, their mass properties will be combined for
76//! the total [`ComputedMass`], [`ComputedAngularInertia`], and [`ComputedCenterOfMass`].
77//!
78//! ```
79#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
80#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
81//! # use bevy::prelude::*;
82//! #
83//! # fn setup(mut commands: Commands) {
84//! // Total mass: 10.0 + 5.0 = 15.0
85#![cfg_attr(
86    feature = "2d",
87    doc = "// Total center of mass: (10.0 * [0.0, -0.5] + 5.0 * [0.0, 4.0]) / (10.0 + 5.0) = [0.0, 1.0]"
88)]
89#![cfg_attr(
90    feature = "3d",
91    doc = "// Total center of mass: (10.0 * [0.0, -0.5, 0.0] + 5.0 * [0.0, 4.0, 0.0]) / (10.0 + 5.0) = [0.0, 1.0, 0.0]"
92)]
93//! commands.spawn((
94//!     RigidBody::Dynamic,
95//!     Collider::capsule(0.5, 1.5),
96//!     Mass(10.0),
97#![cfg_attr(feature = "2d", doc = "    CenterOfMass::new(0.0, -0.5),")]
98#![cfg_attr(feature = "3d", doc = "    CenterOfMass::new(0.0, -0.5, 0.0),")]
99//!     Transform::default(),
100//! ))
101//! .with_child((
102#![cfg_attr(feature = "2d", doc = "    Collider::circle(1.0),")]
103#![cfg_attr(feature = "3d", doc = "    Collider::sphere(1.0),")]
104//!     Mass(5.0),
105//!     Transform::from_xyz(0.0, 4.0, 0.0),
106//! ));
107//! # }
108//! ```
109//!
110//! To prevent child entities from contributing to the total mass properties, use the [`NoAutoMass`],
111//! [`NoAutoAngularInertia`], and [`NoAutoCenterOfMass`] marker components.
112//!
113//! ```
114#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
115#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
116//! # use bevy::prelude::*;
117//! #
118//! # fn setup(mut commands: Commands) {
119//! // Total mass: 10.0
120#![cfg_attr(feature = "2d", doc = "// Total center of mass: [0.0, -0.5]")]
121#![cfg_attr(feature = "3d", doc = "// Total center of mass: [0.0, -0.5, 0.0]")]
122//! commands.spawn((
123//!     RigidBody::Dynamic,
124//!     Collider::capsule(0.5, 1.5),
125//!     Mass(10.0),
126#![cfg_attr(feature = "2d", doc = "    CenterOfMass::new(0.0, -0.5),")]
127#![cfg_attr(feature = "3d", doc = "    CenterOfMass::new(0.0, -0.5, 0.0),")]
128//!     NoAutoMass,
129//!     NoAutoCenterOfMass,
130//!     Transform::default(),
131//! ))
132//! .with_child((
133#![cfg_attr(feature = "2d", doc = "    Collider::circle(1.0),")]
134#![cfg_attr(feature = "3d", doc = "    Collider::sphere(1.0),")]
135//!     Mass(5.0),
136//!     Transform::from_xyz(0.0, 4.0, 0.0),
137//! ));
138//! # }
139//! ```
140//!
141//! # Computing Mass Properties for Shapes
142//!
143//! Mass properties of colliders and Bevy's primitive shapes can be computed using methods
144//! provided by the [`ComputeMassProperties2d`] and [`ComputeMassProperties3d`] traits.
145//!
146//! ```
147#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
148#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
149//! # use bevy::prelude::*;
150//! #
151//! #
152//! // Compute mass properties for a capsule collider with a density of `2.0`.
153//! let capsule = Collider::capsule(0.5, 1.5);
154//! let mass_properties = capsule.mass_properties(2.0);
155//!
156//! // Compute individual mass properties for a `Circle`.
157//! let circle = Circle::new(1.0);
158//! let mass = circle.mass(2.0);
159//! let angular_inertia = circle.angular_inertia(mass);
160//! let center_of_mass = circle.center_of_mass();
161//! ```
162//!
163//! Similarly, shapes can be used to construct the [`Mass`], [`AngularInertia`],
164//! and [`CenterOfMass`] components, or the [`MassPropertiesBundle`].
165//!
166//! ```
167#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
168#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
169//! # use bevy::prelude::*;
170//! #
171//! # fn setup(mut commands: Commands) {
172//! // Construct individual mass properties from a collider.
173#![cfg_attr(feature = "2d", doc = "let shape = Collider::circle(0.5);")]
174#![cfg_attr(feature = "3d", doc = "let shape = Collider::sphere(0.5);")]
175//! commands.spawn((
176//!     RigidBody::Dynamic,
177//!     Mass::from_shape(&shape, 2.0),
178//!     AngularInertia::from_shape(&shape, 1.5),
179//!     CenterOfMass::from_shape(&shape),
180//! ));
181//!
182//! // Construct a `MassPropertiesBundle` from a primitive shape.
183#![cfg_attr(feature = "2d", doc = "let shape = Circle::new(0.5);")]
184#![cfg_attr(feature = "3d", doc = "let shape = Sphere::new(0.5);")]
185//! commands.spawn((RigidBody::Dynamic, MassPropertiesBundle::from_shape(&shape, 2.0)));
186//! # }
187//! ```
188//!
189//! This mass property computation functionality is provided by the [`bevy_heavy`] crate.
190//!
191//! # Mass Property Helper
192//!
193//! [`MassPropertyHelper`] is a [`SystemParam`](bevy::ecs::system::SystemParam) that provides convenient helper methods
194//! that can be used to modify or compute mass properties for individual entities and hierarchies at runtime.
195//!
196//! For example, [`MassPropertyHelper::total_mass_properties`] computes the total mass properties of an entity,
197//! taking into account the mass properties of descendants and colliders.
198
199use crate::physics_transform::PhysicsTransformSystems;
200use crate::prelude::*;
201use bevy::{
202    ecs::{intern::Interned, schedule::ScheduleLabel},
203    prelude::*,
204};
205
206pub mod components;
207use components::RecomputeMassProperties;
208
209mod system_param;
210pub use system_param::MassPropertyHelper;
211
212/// Mass property computation with `bevy_heavy`, re-exported for your convenience.
213pub use bevy_heavy;
214
215#[cfg(feature = "2d")]
216pub(crate) use bevy_heavy::{
217    ComputeMassProperties2d as ComputeMassProperties, MassProperties2d as MassProperties,
218};
219#[cfg(feature = "3d")]
220pub(crate) use bevy_heavy::{
221    ComputeMassProperties3d as ComputeMassProperties, MassProperties3d as MassProperties,
222};
223
224/// An extension trait for [`MassProperties`].
225pub trait MassPropertiesExt {
226    /// Converts the [`MassProperties`] to a [`MassPropertiesBundle`]
227    /// containing the [`Mass`], [`AngularInertia`], and [`CenterOfMass`] components.
228    fn to_bundle(&self) -> MassPropertiesBundle;
229}
230
231impl MassPropertiesExt for MassProperties {
232    fn to_bundle(&self) -> MassPropertiesBundle {
233        #[cfg(feature = "2d")]
234        let angular_inertia = AngularInertia(self.angular_inertia);
235        #[cfg(feature = "3d")]
236        let angular_inertia = AngularInertia::new_with_local_frame(
237            self.principal_angular_inertia.f32(),
238            self.local_inertial_frame.f32(),
239        );
240
241        MassPropertiesBundle {
242            mass: Mass(self.mass),
243            angular_inertia,
244            center_of_mass: CenterOfMass(self.center_of_mass),
245        }
246    }
247}
248
249/// A plugin for managing [mass properties] of rigid bodies.
250///
251/// - Updates the [`ComputedMass`], [`ComputedAngularInertia`], and [`ComputedCenterOfMass`] components
252///   for rigid bodies when their mass properties are changed, or when colliders are added or removed.
253/// - Logs warnings when dynamic bodies have invalid [`Mass`] or [`AngularInertia`].
254///
255/// [mass properties]: crate::dynamics::rigid_body::mass_properties
256pub struct MassPropertyPlugin {
257    schedule: Interned<dyn ScheduleLabel>,
258}
259
260impl MassPropertyPlugin {
261    /// Creates a [`MassPropertyPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
262    ///
263    /// The default schedule is `FixedPostUpdate`.
264    pub fn new(schedule: impl ScheduleLabel) -> Self {
265        Self {
266            schedule: schedule.intern(),
267        }
268    }
269}
270
271impl Default for MassPropertyPlugin {
272    fn default() -> Self {
273        Self::new(FixedPostUpdate)
274    }
275}
276
277impl Plugin for MassPropertyPlugin {
278    fn build(&self, app: &mut App) {
279        // TODO: We probably don't need this since we have the observer.
280        // Force mass property computation for new rigid bodies.
281        app.register_required_components::<RigidBody, RecomputeMassProperties>();
282
283        // Compute mass properties for new rigid bodies at spawn.
284        app.add_observer(
285            |trigger: On<Add, RigidBody>, mut mass_helper: MassPropertyHelper| {
286                mass_helper.update_mass_properties(trigger.entity);
287            },
288        );
289
290        // Update the mass properties of rigid bodies when colliders added or removed.
291        // TODO: Avoid duplicating work with the above observer.
292        app.add_observer(
293            |trigger: On<Insert, RigidBodyColliders>, mut mass_helper: MassPropertyHelper| {
294                mass_helper.update_mass_properties(trigger.entity);
295            },
296        );
297
298        // Configure system sets for mass property computation.
299        app.configure_sets(
300            self.schedule,
301            (
302                MassPropertySystems::UpdateColliderMassProperties,
303                MassPropertySystems::QueueRecomputation,
304                MassPropertySystems::UpdateComputedMassProperties,
305            )
306                .chain()
307                .in_set(PhysicsSystems::Prepare)
308                .after(PhysicsTransformSystems::TransformToPosition),
309        );
310
311        // Queue mass property recomputation when mass properties are changed.
312        app.add_systems(
313            self.schedule,
314            (
315                queue_mass_recomputation_on_mass_change,
316                queue_mass_recomputation_on_collider_mass_change,
317            )
318                .in_set(MassPropertySystems::QueueRecomputation),
319        );
320
321        // Update mass properties for entities with the `RecomputeMassProperties` component.
322        app.add_systems(
323            self.schedule,
324            (update_mass_properties, warn_invalid_mass)
325                .chain()
326                .in_set(MassPropertySystems::UpdateComputedMassProperties),
327        );
328    }
329}
330
331/// A system set for logic related to updating mass properties.
332#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
333pub enum MassPropertySystems {
334    /// Update [`ColliderMassProperties`] for colliders.
335    UpdateColliderMassProperties,
336    /// Adds the [`RecomputeMassProperties`] component to entities with changed mass properties.
337    QueueRecomputation,
338    /// Update [`ComputedMass`], [`ComputedAngularInertia`], and [`ComputedCenterOfMass`]
339    /// for entities with the [`RecomputeMassProperties`] component. The component is removed after updating.
340    UpdateComputedMassProperties,
341}
342
343/// A query filter for entities with [`ComputedMass`], [`ComputedAngularInertia`], or [`ComputedCenterOfMass`].
344pub type WithComputedMassProperty = Or<(
345    With<ComputedMass>,
346    With<ComputedAngularInertia>,
347    With<ComputedCenterOfMass>,
348)>;
349
350/// A query filter for entities with changed [`Mass`], [`AngularInertia`], or [`CenterOfMass`].
351pub type MassPropertyChanged = Or<(
352    Changed<Mass>,
353    Changed<AngularInertia>,
354    Changed<CenterOfMass>,
355)>;
356
357/// Queues mass property recomputation for rigid bodies when their [`Mass`], [`AngularInertia`],
358/// or [`CenterOfMass`] components are changed.
359///
360/// Colliders attached to rigid bodies are excluded, as they are handled by
361/// [`queue_mass_recomputation_on_collider_mass_change`].
362fn queue_mass_recomputation_on_mass_change(
363    mut commands: Commands,
364    mut query: Query<
365        Entity,
366        (
367            WithComputedMassProperty,
368            Without<ColliderOf>,
369            MassPropertyChanged,
370        ),
371    >,
372) {
373    for entity in &mut query {
374        commands.entity(entity).insert(RecomputeMassProperties);
375    }
376}
377
378/// Queues mass property recomputation for rigid bodies when the [`ColliderMassProperties`],
379/// [`Mass`], [`AngularInertia`], or [`CenterOfMass`] components of their colliders are changed.
380fn queue_mass_recomputation_on_collider_mass_change(
381    mut commands: Commands,
382    mut query: Query<
383        &ColliderOf,
384        Or<(
385            Changed<ColliderMassProperties>,
386            Changed<ColliderTransform>,
387            MassPropertyChanged,
388        )>,
389    >,
390) {
391    for &ColliderOf { body } in &mut query {
392        if let Ok(mut entity_commands) = commands.get_entity(body) {
393            entity_commands.insert(RecomputeMassProperties);
394        }
395    }
396}
397
398fn update_mass_properties(
399    mut commands: Commands,
400    query: Query<Entity, With<RecomputeMassProperties>>,
401    mut mass_helper: MassPropertyHelper,
402) {
403    // TODO: Parallelize mass property updates.
404    for entity in query.iter() {
405        mass_helper.update_mass_properties(entity);
406        commands.entity(entity).remove::<RecomputeMassProperties>();
407    }
408}
409
410#[cfg(feature = "default-collider")]
411type ShouldWarn = (
412    Without<ColliderConstructor>,
413    Without<ColliderConstructorHierarchy>,
414);
415
416#[cfg(not(feature = "default-collider"))]
417type ShouldWarn = ();
418
419/// Logs warnings when dynamic bodies have invalid [`Mass`] or [`AngularInertia`].
420fn warn_invalid_mass(
421    mut bodies: Query<
422        (
423            Entity,
424            &RigidBody,
425            Ref<ComputedMass>,
426            Ref<ComputedAngularInertia>,
427        ),
428        (
429            Or<(Changed<ComputedMass>, Changed<ComputedAngularInertia>)>,
430            ShouldWarn,
431        ),
432    >,
433) {
434    for (entity, rb, mass, inertia) in &mut bodies {
435        let is_mass_valid = mass.is_finite();
436        #[cfg(feature = "2d")]
437        let is_inertia_valid = inertia.is_finite();
438        #[cfg(feature = "3d")]
439        let is_inertia_valid = inertia.is_finite();
440
441        // Warn about dynamic bodies with no mass or inertia
442        if rb.is_dynamic() && !(is_mass_valid && is_inertia_valid) {
443            warn!(
444                "Dynamic rigid body {:?} has no mass or inertia. This can cause NaN values. Consider adding a `MassPropertiesBundle` or a `Collider` with mass.",
445                entity
446            );
447        }
448    }
449}
450
451#[cfg(test)]
452#[cfg(all(feature = "2d", feature = "default-collider"))]
453#[allow(clippy::unnecessary_cast)]
454mod tests {
455    use approx::assert_relative_eq;
456
457    use super::*;
458
459    fn create_app() -> App {
460        let mut app = App::new();
461        app.add_plugins((MinimalPlugins, PhysicsPlugins::default(), TransformPlugin));
462        app.finish();
463        app
464    }
465
466    fn get_computed_mass_properties(
467        world: &mut World,
468        entity: Entity,
469    ) -> (
470        &ComputedMass,
471        &ComputedAngularInertia,
472        &ComputedCenterOfMass,
473    ) {
474        let mut query = world.query::<(
475            &ComputedMass,
476            &ComputedAngularInertia,
477            &ComputedCenterOfMass,
478        )>();
479        query.get(world, entity).unwrap()
480    }
481
482    #[test]
483    fn mass_properties_zero_by_default() {
484        // `RigidBody`
485
486        let mut app = create_app();
487
488        let body_entity = app.world_mut().spawn(RigidBody::Dynamic).id();
489
490        app.world_mut().run_schedule(FixedPostUpdate);
491
492        let (mass, angular_inertia, center_of_mass) =
493            get_computed_mass_properties(app.world_mut(), body_entity);
494
495        assert_eq!(*mass, ComputedMass::default());
496        assert_eq!(*angular_inertia, ComputedAngularInertia::default());
497        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
498    }
499
500    #[test]
501    fn mass_properties_rb_collider() {
502        // `RigidBody`, `Collider`
503
504        let mut app = create_app();
505
506        let collider = Collider::circle(1.0);
507        let collider_mass_props = collider.mass_properties(1.0);
508
509        let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
510
511        app.world_mut().run_schedule(FixedPostUpdate);
512
513        let (mass, angular_inertia, center_of_mass) =
514            get_computed_mass_properties(app.world_mut(), body_entity);
515
516        assert_eq!(mass.value() as f32, collider_mass_props.mass);
517        assert_eq!(
518            angular_inertia.value() as f32,
519            collider_mass_props.angular_inertia
520        );
521        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
522    }
523
524    #[test]
525    fn mass_properties_rb_collider_with_set_mass() {
526        // `RigidBody`, `Collider`, `Mass(5.0)`
527
528        let mut app = create_app();
529
530        let collider = Collider::circle(1.0);
531        let collider_mass_props = collider.mass_properties(1.0);
532
533        let body_entity = app
534            .world_mut()
535            .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
536            .id();
537
538        app.world_mut().run_schedule(FixedPostUpdate);
539
540        let (mass, angular_inertia, center_of_mass) =
541            get_computed_mass_properties(app.world_mut(), body_entity);
542
543        assert_eq!(mass.value() as f32, 5.0);
544        assert_eq!(
545            angular_inertia.value() as f32,
546            mass.value() as f32 * collider_mass_props.unit_angular_inertia()
547        );
548        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
549    }
550
551    #[test]
552    fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
553        // `RigidBody`, `Collider`, `Mass(5.0)`, `AngularInertia(10.0)`
554
555        let mut app = create_app();
556
557        let collider = Collider::circle(1.0);
558
559        let body_entity = app
560            .world_mut()
561            .spawn((
562                RigidBody::Dynamic,
563                collider,
564                Mass(5.0),
565                AngularInertia(10.0),
566            ))
567            .id();
568
569        app.world_mut().run_schedule(FixedPostUpdate);
570
571        let (mass, angular_inertia, center_of_mass) =
572            get_computed_mass_properties(app.world_mut(), body_entity);
573
574        assert_eq!(mass.value() as f32, 5.0);
575        assert_eq!(angular_inertia.value() as f32, 10.0);
576        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
577    }
578
579    #[test]
580    fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
581        // `RigidBody`, `Collider`, `Mass(5.0)`
582        // - `Collider`, `ColliderDensity(2.0)`
583
584        let mut app = create_app();
585
586        let collider = Collider::circle(1.0);
587        let collider_mass_props = collider.mass_properties(2.0);
588
589        let body_entity = app
590            .world_mut()
591            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
592            .with_child((collider, ColliderDensity(2.0)))
593            .id();
594
595        app.world_mut().run_schedule(FixedPostUpdate);
596
597        let (mass, angular_inertia, center_of_mass) =
598            get_computed_mass_properties(app.world_mut(), body_entity);
599
600        assert_eq!(mass.value() as f32, 5.0 + collider_mass_props.mass);
601        assert_eq!(
602            angular_inertia.value() as f32,
603            5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
604        );
605        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
606    }
607
608    #[test]
609    fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
610        // `RigidBody`, `Collider`, `Mass(5.0)`
611        // - `Collider`, `ColliderDensity(2.0)`, `Mass(10.0)`
612
613        let mut app = create_app();
614
615        let collider = Collider::circle(1.0);
616        let collider_mass_props = collider.mass_properties(1.0);
617
618        let body_entity = app
619            .world_mut()
620            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
621            .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
622            .id();
623
624        app.world_mut().run_schedule(FixedPostUpdate);
625
626        let (mass, angular_inertia, center_of_mass) =
627            get_computed_mass_properties(app.world_mut(), body_entity);
628
629        assert_eq!(mass.value() as f32, 5.0 + 10.0);
630        assert_eq!(
631            angular_inertia.value() as f32,
632            5.0 * collider_mass_props.unit_angular_inertia()
633                + 10.0 * collider_mass_props.unit_angular_inertia()
634        );
635        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
636    }
637
638    #[test]
639    fn mass_properties_no_auto_mass_collider_no_set_mass() {
640        // `RigidBody`, `Collider`, `NoAutoMass`
641        //
642        // Mass properties should be zero.
643
644        let mut app = create_app();
645
646        let body_entity = app
647            .world_mut()
648            .spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
649            .id();
650
651        app.world_mut().run_schedule(FixedPostUpdate);
652
653        let (mass, angular_inertia, center_of_mass) =
654            get_computed_mass_properties(app.world_mut(), body_entity);
655
656        assert_eq!(*mass, ComputedMass::default());
657        assert_eq!(*angular_inertia, ComputedAngularInertia::default());
658        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
659    }
660
661    #[test]
662    fn mass_properties_no_auto_mass_hierarchy() {
663        // `RigidBody`, `Collider`, `Mass(5.0)`, `NoAutoMass`
664        // - `Collider`, `ColliderDensity(2.0)`, `Mass(10.0)`
665
666        let mut app = create_app();
667
668        let collider = Collider::circle(1.0);
669        let collider_mass_props = collider.mass_properties(1.0);
670
671        let body_entity = app
672            .world_mut()
673            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
674            .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
675            .id();
676
677        app.world_mut().run_schedule(FixedPostUpdate);
678
679        let (mass, angular_inertia, center_of_mass) =
680            get_computed_mass_properties(app.world_mut(), body_entity);
681
682        assert_eq!(mass.value() as f32, 5.0);
683        assert_eq!(
684            angular_inertia.value() as f32,
685            mass.value() as f32
686                * (5.0 * collider_mass_props.unit_angular_inertia()
687                    + 10.0 * collider_mass_props.unit_angular_inertia())
688                / 15.0
689        );
690        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
691    }
692
693    #[test]
694    fn mass_properties_add_remove_collider() {
695        // `RigidBody`, `Collider`
696        //
697        // - Check mass properties
698        // - Add child `Collider`
699        // - Check mass properties
700        // - Remove child `Collider`
701        // - Check mass properties
702
703        let mut app = create_app();
704
705        let collider = Collider::circle(1.0);
706        let collider_mass_props = collider.mass_properties(1.0);
707
708        let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
709
710        app.world_mut().run_schedule(FixedPostUpdate);
711
712        let (mass, angular_inertia, center_of_mass) =
713            get_computed_mass_properties(app.world_mut(), body_entity);
714
715        assert_eq!(mass.value() as f32, collider_mass_props.mass);
716        assert_eq!(
717            angular_inertia.value() as f32,
718            collider_mass_props.angular_inertia
719        );
720        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
721
722        // Add a child collider
723        let child_collider = Collider::circle(2.0);
724        let child_collider_mass_props = child_collider.mass_properties(1.0);
725
726        let child_entity = app
727            .world_mut()
728            .spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
729            .id();
730
731        app.world_mut().run_schedule(FixedPostUpdate);
732
733        let (mass, angular_inertia, center_of_mass) =
734            get_computed_mass_properties(app.world_mut(), body_entity);
735
736        assert_eq!(
737            mass.value() as f32,
738            collider_mass_props.mass + child_collider_mass_props.mass
739        );
740        assert_eq!(
741            angular_inertia.value() as f32,
742            collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
743        );
744        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
745
746        // Remove the child collider
747        app.world_mut().entity_mut(child_entity).despawn();
748
749        app.world_mut().run_schedule(FixedPostUpdate);
750
751        let (mass, angular_inertia, center_of_mass) =
752            get_computed_mass_properties(app.world_mut(), body_entity);
753
754        assert_eq!(mass.value() as f32, collider_mass_props.mass);
755        assert_eq!(
756            angular_inertia.value() as f32,
757            collider_mass_props.angular_inertia
758        );
759        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
760    }
761
762    #[test]
763    fn mass_properties_change_mass() {
764        // `RigidBody`, `Collider`, `Mass(5.0)`
765        //
766        // - Check mass properties
767        // - Change mass
768        // - Check mass properties
769
770        let mut app = create_app();
771
772        let collider = Collider::circle(1.0);
773        let collider_mass_props = collider.mass_properties(1.0);
774
775        let body_entity = app
776            .world_mut()
777            .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
778            .id();
779
780        app.world_mut().run_schedule(FixedPostUpdate);
781
782        let (mass, angular_inertia, center_of_mass) =
783            get_computed_mass_properties(app.world_mut(), body_entity);
784
785        assert_eq!(mass.value() as f32, 5.0);
786        assert_eq!(
787            angular_inertia.value() as f32,
788            mass.value() as f32 * collider_mass_props.unit_angular_inertia()
789        );
790        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
791
792        // Change mass
793        app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
794
795        app.world_mut().run_schedule(FixedPostUpdate);
796
797        let (mass, angular_inertia, center_of_mass) =
798            get_computed_mass_properties(app.world_mut(), body_entity);
799
800        assert_eq!(mass.value() as f32, 10.0);
801        assert_eq!(
802            angular_inertia.value() as f32,
803            mass.value() as f32 * collider_mass_props.unit_angular_inertia()
804        );
805        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
806    }
807
808    #[test]
809    fn mass_properties_move_child_collider() {
810        // `RigidBody`, `Mass(10.0)`, `CenterOfMass(0.0, -0.5)`, `Transform`
811        // - `Collider`, `Mass(5.0)`, `Transform`
812        //
813        // - Check mass properties
814        // - Move child collider
815        // - Check mass properties
816
817        let mut app = create_app();
818
819        let body_entity = app
820            .world_mut()
821            .spawn((
822                RigidBody::Dynamic,
823                Mass(10.0),
824                CenterOfMass::new(0.0, -0.5),
825                Transform::default(),
826            ))
827            .id();
828
829        let child_entity = app
830            .world_mut()
831            .spawn((
832                ChildOf(body_entity),
833                Collider::circle(1.0),
834                Mass(5.0),
835                Transform::default(),
836            ))
837            .id();
838
839        app.world_mut().run_schedule(FixedPostUpdate);
840
841        let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
842
843        assert_eq!(mass.value() as f32, 10.0 + 5.0);
844        assert_relative_eq!(
845            center_of_mass.0,
846            Vector::new(0.0, -1.0 / 3.0),
847            epsilon = 1.0e-6
848        );
849
850        // Move child collider
851        app.world_mut()
852            .entity_mut(child_entity)
853            .insert(Transform::from_xyz(0.0, 4.0, 0.0));
854
855        app.world_mut().run_schedule(FixedPostUpdate);
856
857        let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
858
859        assert_eq!(mass.value(), 10.0 + 5.0);
860        assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
861    }
862
863    #[test]
864    fn mass_properties_no_auto_mass_add_remove() {
865        // `RigidBody`, `Collider`, `Mass(5.0)`
866        // - `Collider`, `Mass(10.0)`
867        //
868        // - Check mass properties
869        // - Add `NoAutoMass`
870        // - Check mass properties
871        // - Remove `NoAutoMass`
872        // - Check mass properties
873
874        let mut app = create_app();
875
876        let collider = Collider::circle(1.0);
877        let collider_mass_props = collider.mass_properties(1.0);
878
879        let body_entity = app
880            .world_mut()
881            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
882            .with_child((collider, Mass(10.0)))
883            .id();
884
885        app.world_mut().run_schedule(FixedPostUpdate);
886
887        let (mass, angular_inertia, center_of_mass) =
888            get_computed_mass_properties(app.world_mut(), body_entity);
889
890        assert_eq!(mass.value() as f32, 5.0 + 10.0);
891        assert_eq!(
892            angular_inertia.value() as f32,
893            5.0 * collider_mass_props.unit_angular_inertia()
894                + 10.0 * collider_mass_props.unit_angular_inertia()
895        );
896        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
897
898        // Add `NoAutoMass`
899        app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
900
901        app.world_mut().run_schedule(FixedPostUpdate);
902
903        let (mass, angular_inertia, center_of_mass) =
904            get_computed_mass_properties(app.world_mut(), body_entity);
905
906        assert_eq!(mass.value() as f32, 5.0);
907        assert_eq!(
908            angular_inertia.value() as f32,
909            5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
910                + 10.0 * collider_mass_props.unit_angular_inertia())
911                / 15.0
912        );
913        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
914
915        // Remove `NoAutoMass`
916        app.world_mut()
917            .entity_mut(body_entity)
918            .remove::<NoAutoMass>();
919
920        app.world_mut().run_schedule(FixedPostUpdate);
921
922        let (mass, angular_inertia, center_of_mass) =
923            get_computed_mass_properties(app.world_mut(), body_entity);
924
925        assert_eq!(mass.value() as f32, 5.0 + 10.0);
926        assert_eq!(
927            angular_inertia.value() as f32,
928            5.0 * collider_mass_props.unit_angular_inertia()
929                + 10.0 * collider_mass_props.unit_angular_inertia()
930        );
931        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
932    }
933}