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