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,
238            self.local_inertial_frame,
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_relative_eq!(mass.value(), 0.0);
496        assert_relative_eq!(angular_inertia.value(), SymmetricTensor::default());
497        assert_relative_eq!(center_of_mass.0, 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_relative_eq!(mass.value(), collider_mass_props.mass);
517        assert_relative_eq!(angular_inertia.value(), collider_mass_props.angular_inertia);
518        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
519    }
520
521    #[test]
522    fn mass_properties_rb_collider_with_set_mass() {
523        // `RigidBody`, `Collider`, `Mass(5.0)`
524
525        let mut app = create_app();
526
527        let collider = Collider::circle(1.0);
528        let collider_mass_props = collider.mass_properties(1.0);
529
530        let body_entity = app
531            .world_mut()
532            .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
533            .id();
534
535        app.world_mut().run_schedule(FixedPostUpdate);
536
537        let (mass, angular_inertia, center_of_mass) =
538            get_computed_mass_properties(app.world_mut(), body_entity);
539
540        assert_relative_eq!(mass.value(), 5.0);
541        assert_relative_eq!(
542            angular_inertia.value(),
543            mass.value() * collider_mass_props.unit_angular_inertia()
544        );
545        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
546    }
547
548    #[test]
549    fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
550        // `RigidBody`, `Collider`, `Mass(5.0)`, `AngularInertia(10.0)`
551
552        let mut app = create_app();
553
554        let collider = Collider::circle(1.0);
555
556        let body_entity = app
557            .world_mut()
558            .spawn((
559                RigidBody::Dynamic,
560                collider,
561                Mass(5.0),
562                AngularInertia(10.0),
563            ))
564            .id();
565
566        app.world_mut().run_schedule(FixedPostUpdate);
567
568        let (mass, angular_inertia, center_of_mass) =
569            get_computed_mass_properties(app.world_mut(), body_entity);
570
571        assert_relative_eq!(mass.value(), 5.0);
572        assert_relative_eq!(angular_inertia.value(), 10.0);
573        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
574    }
575
576    #[test]
577    fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
578        // `RigidBody`, `Collider`, `Mass(5.0)`
579        // - `Collider`, `ColliderDensity(2.0)`
580
581        let mut app = create_app();
582
583        let collider = Collider::circle(1.0);
584        let collider_mass_props = collider.mass_properties(2.0);
585
586        let body_entity = app
587            .world_mut()
588            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
589            .with_child((collider, ColliderDensity(2.0)))
590            .id();
591
592        app.world_mut().run_schedule(FixedPostUpdate);
593
594        let (mass, angular_inertia, center_of_mass) =
595            get_computed_mass_properties(app.world_mut(), body_entity);
596
597        assert_relative_eq!(mass.value(), 5.0 + collider_mass_props.mass);
598        assert_relative_eq!(
599            angular_inertia.value(),
600            5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
601        );
602        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
603    }
604
605    #[test]
606    fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
607        // `RigidBody`, `Collider`, `Mass(5.0)`
608        // - `Collider`, `ColliderDensity(2.0)`, `Mass(10.0)`
609
610        let mut app = create_app();
611
612        let collider = Collider::circle(1.0);
613        let collider_mass_props = collider.mass_properties(1.0);
614
615        let body_entity = app
616            .world_mut()
617            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
618            .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
619            .id();
620
621        app.world_mut().run_schedule(FixedPostUpdate);
622
623        let (mass, angular_inertia, center_of_mass) =
624            get_computed_mass_properties(app.world_mut(), body_entity);
625
626        assert_relative_eq!(mass.value(), 5.0 + 10.0);
627        assert_relative_eq!(
628            angular_inertia.value(),
629            5.0 * collider_mass_props.unit_angular_inertia()
630                + 10.0 * collider_mass_props.unit_angular_inertia()
631        );
632        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
633    }
634
635    #[test]
636    fn mass_properties_no_auto_mass_collider_no_set_mass() {
637        // `RigidBody`, `Collider`, `NoAutoMass`
638        //
639        // Mass properties should be zero.
640
641        let mut app = create_app();
642
643        let body_entity = app
644            .world_mut()
645            .spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
646            .id();
647
648        app.world_mut().run_schedule(FixedPostUpdate);
649
650        let (mass, angular_inertia, center_of_mass) =
651            get_computed_mass_properties(app.world_mut(), body_entity);
652
653        assert_relative_eq!(mass.value(), 0.0);
654        assert_relative_eq!(angular_inertia.value(), SymmetricTensor::default());
655        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
656    }
657
658    #[test]
659    fn mass_properties_no_auto_mass_hierarchy() {
660        // `RigidBody`, `Collider`, `Mass(5.0)`, `NoAutoMass`
661        // - `Collider`, `ColliderDensity(2.0)`, `Mass(10.0)`
662
663        let mut app = create_app();
664
665        let collider = Collider::circle(1.0);
666        let collider_mass_props = collider.mass_properties(1.0);
667
668        let body_entity = app
669            .world_mut()
670            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
671            .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
672            .id();
673
674        app.world_mut().run_schedule(FixedPostUpdate);
675
676        let (mass, angular_inertia, center_of_mass) =
677            get_computed_mass_properties(app.world_mut(), body_entity);
678
679        assert_relative_eq!(mass.value(), 5.0);
680        assert_relative_eq!(
681            angular_inertia.value(),
682            mass.value()
683                * (5.0 * collider_mass_props.unit_angular_inertia()
684                    + 10.0 * collider_mass_props.unit_angular_inertia())
685                / 15.0
686        );
687        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
688    }
689
690    #[test]
691    fn mass_properties_add_remove_collider() {
692        // `RigidBody`, `Collider`
693        //
694        // - Check mass properties
695        // - Add child `Collider`
696        // - Check mass properties
697        // - Remove child `Collider`
698        // - Check mass properties
699
700        let mut app = create_app();
701
702        let collider = Collider::circle(1.0);
703        let collider_mass_props = collider.mass_properties(1.0);
704
705        let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
706
707        app.world_mut().run_schedule(FixedPostUpdate);
708
709        let (mass, angular_inertia, center_of_mass) =
710            get_computed_mass_properties(app.world_mut(), body_entity);
711
712        assert_relative_eq!(mass.value(), collider_mass_props.mass);
713        assert_relative_eq!(angular_inertia.value(), collider_mass_props.angular_inertia);
714        assert_relative_eq!(center_of_mass.0, Vector::ZERO);
715
716        // Add a child collider
717        let child_collider = Collider::circle(2.0);
718        let child_collider_mass_props = child_collider.mass_properties(1.0);
719
720        let child_entity = app
721            .world_mut()
722            .spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
723            .id();
724
725        app.world_mut().run_schedule(FixedPostUpdate);
726
727        let (mass, angular_inertia, center_of_mass) =
728            get_computed_mass_properties(app.world_mut(), body_entity);
729
730        assert_relative_eq!(
731            mass.value(),
732            collider_mass_props.mass + child_collider_mass_props.mass
733        );
734        assert_relative_eq!(
735            angular_inertia.value(),
736            collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
737        );
738        assert_relative_eq!(center_of_mass.0, Vector::ZERO);
739
740        // Remove the child collider
741        app.world_mut().entity_mut(child_entity).despawn();
742
743        app.world_mut().run_schedule(FixedPostUpdate);
744
745        let (mass, angular_inertia, center_of_mass) =
746            get_computed_mass_properties(app.world_mut(), body_entity);
747
748        assert_relative_eq!(mass.value(), collider_mass_props.mass);
749        assert_relative_eq!(angular_inertia.value(), collider_mass_props.angular_inertia);
750        assert_relative_eq!(center_of_mass.0, Vector::ZERO);
751    }
752
753    #[test]
754    fn mass_properties_change_mass() {
755        // `RigidBody`, `Collider`, `Mass(5.0)`
756        //
757        // - Check mass properties
758        // - Change mass
759        // - Check mass properties
760
761        let mut app = create_app();
762
763        let collider = Collider::circle(1.0);
764        let collider_mass_props = collider.mass_properties(1.0);
765
766        let body_entity = app
767            .world_mut()
768            .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
769            .id();
770
771        app.world_mut().run_schedule(FixedPostUpdate);
772
773        let (mass, angular_inertia, center_of_mass) =
774            get_computed_mass_properties(app.world_mut(), body_entity);
775
776        assert_relative_eq!(mass.value(), 5.0);
777        assert_relative_eq!(
778            angular_inertia.value(),
779            mass.value() * collider_mass_props.unit_angular_inertia()
780        );
781        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
782
783        // Change mass
784        app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
785
786        app.world_mut().run_schedule(FixedPostUpdate);
787
788        let (mass, angular_inertia, center_of_mass) =
789            get_computed_mass_properties(app.world_mut(), body_entity);
790
791        assert_relative_eq!(mass.value(), 10.0);
792        assert_relative_eq!(
793            angular_inertia.value(),
794            mass.value() * collider_mass_props.unit_angular_inertia()
795        );
796        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
797    }
798
799    #[test]
800    fn mass_properties_move_child_collider() {
801        // `RigidBody`, `Mass(10.0)`, `CenterOfMass(0.0, -0.5)`, `Transform`
802        // - `Collider`, `Mass(5.0)`, `Transform`
803        //
804        // - Check mass properties
805        // - Move child collider
806        // - Check mass properties
807
808        let mut app = create_app();
809
810        let body_entity = app
811            .world_mut()
812            .spawn((
813                RigidBody::Dynamic,
814                Mass(10.0),
815                CenterOfMass::new(0.0, -0.5),
816                Transform::default(),
817            ))
818            .id();
819
820        let child_entity = app
821            .world_mut()
822            .spawn((
823                ChildOf(body_entity),
824                Collider::circle(1.0),
825                Mass(5.0),
826                Transform::default(),
827            ))
828            .id();
829
830        app.world_mut().run_schedule(FixedPostUpdate);
831
832        let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
833
834        assert_relative_eq!(mass.value(), 10.0 + 5.0);
835        assert_relative_eq!(
836            center_of_mass.0,
837            Vector::new(0.0, -1.0 / 3.0),
838            epsilon = 1.0e-6
839        );
840
841        // Move child collider
842        app.world_mut()
843            .entity_mut(child_entity)
844            .insert(Transform::from_xyz(0.0, 4.0, 0.0));
845
846        app.world_mut().run_schedule(FixedPostUpdate);
847
848        let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
849
850        assert_relative_eq!(mass.value(), 10.0 + 5.0);
851        assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
852    }
853
854    #[test]
855    fn mass_properties_no_auto_mass_add_remove() {
856        // `RigidBody`, `Collider`, `Mass(5.0)`
857        // - `Collider`, `Mass(10.0)`
858        //
859        // - Check mass properties
860        // - Add `NoAutoMass`
861        // - Check mass properties
862        // - Remove `NoAutoMass`
863        // - Check mass properties
864
865        let mut app = create_app();
866
867        let collider = Collider::circle(1.0);
868        let collider_mass_props = collider.mass_properties(1.0);
869
870        let body_entity = app
871            .world_mut()
872            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
873            .with_child((collider, Mass(10.0)))
874            .id();
875
876        app.world_mut().run_schedule(FixedPostUpdate);
877
878        let (mass, angular_inertia, center_of_mass) =
879            get_computed_mass_properties(app.world_mut(), body_entity);
880
881        assert_relative_eq!(mass.value(), 5.0 + 10.0);
882        assert_relative_eq!(
883            angular_inertia.value(),
884            5.0 * collider_mass_props.unit_angular_inertia()
885                + 10.0 * collider_mass_props.unit_angular_inertia()
886        );
887        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
888
889        // Add `NoAutoMass`
890        app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
891
892        app.world_mut().run_schedule(FixedPostUpdate);
893
894        let (mass, angular_inertia, center_of_mass) =
895            get_computed_mass_properties(app.world_mut(), body_entity);
896
897        assert_relative_eq!(mass.value(), 5.0);
898        assert_relative_eq!(
899            angular_inertia.value(),
900            5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
901                + 10.0 * collider_mass_props.unit_angular_inertia())
902                / 15.0
903        );
904        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
905
906        // Remove `NoAutoMass`
907        app.world_mut()
908            .entity_mut(body_entity)
909            .remove::<NoAutoMass>();
910
911        app.world_mut().run_schedule(FixedPostUpdate);
912
913        let (mass, angular_inertia, center_of_mass) =
914            get_computed_mass_properties(app.world_mut(), body_entity);
915
916        assert_relative_eq!(mass.value(), 5.0 + 10.0);
917        assert_relative_eq!(
918            angular_inertia.value(),
919            5.0 * collider_mass_props.unit_angular_inertia()
920                + 10.0 * collider_mass_props.unit_angular_inertia()
921        );
922        assert_relative_eq!(center_of_mass.0, ComputedCenterOfMass::default());
923    }
924}