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::{prelude::*, prepare::PrepareSet};
200#[cfg(feature = "3d")]
201use bevy::ecs::query::QueryFilter;
202use bevy::{
203    ecs::{intern::Interned, schedule::ScheduleLabel},
204    prelude::*,
205};
206
207pub mod components;
208use components::RecomputeMassProperties;
209
210mod system_param;
211pub use system_param::MassPropertyHelper;
212
213/// Mass property computation with `bevy_heavy`, re-exported for your convenience.
214pub use bevy_heavy;
215
216#[cfg(feature = "2d")]
217pub(crate) use bevy_heavy::{
218    ComputeMassProperties2d as ComputeMassProperties, MassProperties2d as MassProperties,
219};
220#[cfg(feature = "3d")]
221pub(crate) use bevy_heavy::{
222    ComputeMassProperties3d as ComputeMassProperties, MassProperties3d as MassProperties,
223};
224
225/// An extension trait for [`MassProperties`].
226pub trait MassPropertiesExt {
227    /// Converts the [`MassProperties`] to a [`MassPropertiesBundle`]
228    /// containing the [`Mass`], [`AngularInertia`], and [`CenterOfMass`] components.
229    fn to_bundle(&self) -> MassPropertiesBundle;
230}
231
232impl MassPropertiesExt for MassProperties {
233    fn to_bundle(&self) -> MassPropertiesBundle {
234        #[cfg(feature = "2d")]
235        let angular_inertia = AngularInertia(self.angular_inertia);
236        #[cfg(feature = "3d")]
237        let angular_inertia = AngularInertia::new_with_local_frame(
238            self.principal_angular_inertia.f32(),
239            self.local_inertial_frame.f32(),
240        );
241
242        MassPropertiesBundle {
243            mass: Mass(self.mass),
244            angular_inertia,
245            center_of_mass: CenterOfMass(self.center_of_mass),
246        }
247    }
248}
249
250/// A plugin for managing [mass properties] of rigid bodies.
251///
252/// - Updates the [`ComputedMass`], [`ComputedAngularInertia`], and [`ComputedCenterOfMass`] components
253///   for rigid bodies when their mass properties are changed, or when colliders are added or removed.
254/// - Logs warnings when dynamic bodies have invalid [`Mass`] or [`AngularInertia`].
255///
256/// [mass properties]: crate::dynamics::rigid_body::mass_properties
257pub struct MassPropertyPlugin {
258    schedule: Interned<dyn ScheduleLabel>,
259}
260
261impl MassPropertyPlugin {
262    /// Creates a [`MassPropertyPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
263    ///
264    /// The default schedule is `FixedPostUpdate`.
265    pub fn new(schedule: impl ScheduleLabel) -> Self {
266        Self {
267            schedule: schedule.intern(),
268        }
269    }
270}
271
272impl Default for MassPropertyPlugin {
273    fn default() -> Self {
274        Self::new(FixedPostUpdate)
275    }
276}
277
278impl Plugin for MassPropertyPlugin {
279    fn build(&self, app: &mut App) {
280        app.register_type::<(
281            Mass,
282            AngularInertia,
283            CenterOfMass,
284            ComputedMass,
285            ComputedAngularInertia,
286            ComputedCenterOfMass,
287            ColliderDensity,
288            ColliderMassProperties,
289            NoAutoMass,
290            NoAutoAngularInertia,
291            NoAutoCenterOfMass,
292        )>();
293
294        // Force mass property computation for new rigid bodies.
295        app.register_required_components::<RigidBody, RecomputeMassProperties>();
296
297        // Configure system sets for mass property computation.
298        app.configure_sets(
299            self.schedule,
300            (
301                MassPropertySystems::UpdateColliderMassProperties,
302                MassPropertySystems::QueueRecomputation,
303                MassPropertySystems::UpdateComputedMassProperties,
304            )
305                .chain()
306                .in_set(PrepareSet::Finalize),
307        );
308
309        // Queue mass property recomputation when mass properties are changed.
310        app.add_systems(
311            self.schedule,
312            (
313                queue_mass_recomputation_on_mass_change,
314                queue_mass_recomputation_on_collider_mass_change,
315            )
316                .in_set(MassPropertySystems::QueueRecomputation),
317        );
318
319        // Update mass properties for entities with the `RecomputeMassProperties` component.
320        app.add_systems(
321            self.schedule,
322            (
323                update_mass_properties,
324                #[cfg(feature = "3d")]
325                update_global_angular_inertia::<Added<RigidBody>>,
326                warn_missing_mass,
327            )
328                .chain()
329                .in_set(MassPropertySystems::UpdateComputedMassProperties),
330        );
331    }
332}
333
334/// A system set for logic related to updating mass properties.
335#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
336pub enum MassPropertySystems {
337    /// Update [`ColliderMassProperties`] for colliders.
338    UpdateColliderMassProperties,
339    /// Adds the [`RecomputeMassProperties`] component to entities with changed mass properties.
340    QueueRecomputation,
341    /// Update [`ComputedMass`], [`ComputedAngularInertia`], and [`ComputedCenterOfMass`]
342    /// for entities with the [`RecomputeMassProperties`] component. The component is removed after updating.
343    UpdateComputedMassProperties,
344}
345
346/// A query filter for entities with [`ComputedMass`], [`ComputedAngularInertia`], or [`ComputedCenterOfMass`].
347pub type WithComputedMassProperty = Or<(
348    With<ComputedMass>,
349    With<ComputedAngularInertia>,
350    With<ComputedCenterOfMass>,
351)>;
352
353/// A query filter for entities with changed [`Mass`], [`AngularInertia`], or [`CenterOfMass`].
354pub type MassPropertyChanged = Or<(
355    Changed<Mass>,
356    Changed<AngularInertia>,
357    Changed<CenterOfMass>,
358)>;
359
360/// Queues mass property recomputation for rigid bodies when their [`Mass`], [`AngularInertia`],
361/// or [`CenterOfMass`] components are changed.
362///
363/// Colliders attached to rigid bodies are excluded, as they are handled by
364/// [`queue_mass_recomputation_on_collider_mass_change`].
365fn queue_mass_recomputation_on_mass_change(
366    mut commands: Commands,
367    mut query: Query<
368        Entity,
369        (
370            WithComputedMassProperty,
371            Without<ColliderOf>,
372            MassPropertyChanged,
373        ),
374    >,
375) {
376    for entity in &mut query {
377        commands.entity(entity).insert(RecomputeMassProperties);
378    }
379}
380
381/// Queues mass property recomputation for rigid bodies when the [`ColliderMassProperties`],
382/// [`Mass`], [`AngularInertia`], or [`CenterOfMass`] components of their colliders are changed.
383fn queue_mass_recomputation_on_collider_mass_change(
384    mut commands: Commands,
385    mut query: Query<
386        &ColliderOf,
387        Or<(
388            Changed<ColliderMassProperties>,
389            Changed<ColliderTransform>,
390            MassPropertyChanged,
391        )>,
392    >,
393) {
394    for &ColliderOf { body } in &mut query {
395        if let Ok(mut entity_commands) = commands.get_entity(body) {
396            entity_commands.insert(RecomputeMassProperties);
397        }
398    }
399}
400
401fn update_mass_properties(
402    mut commands: Commands,
403    query: Query<Entity, With<RecomputeMassProperties>>,
404    mut mass_helper: MassPropertyHelper,
405) {
406    // TODO: Parallelize mass property updates.
407    for entity in query.iter() {
408        mass_helper.update_mass_properties(entity);
409        commands.entity(entity).remove::<RecomputeMassProperties>();
410    }
411}
412
413/// Updates [`GlobalAngularInertia`] for entities that match the given query filter `F`.
414#[cfg(feature = "3d")]
415pub(crate) fn update_global_angular_inertia<F: QueryFilter>(
416    mut query: Populated<
417        (
418            &Rotation,
419            &ComputedAngularInertia,
420            &mut GlobalAngularInertia,
421        ),
422        (Or<(Changed<ComputedAngularInertia>, Changed<Rotation>)>, F),
423    >,
424) {
425    query
426        .par_iter_mut()
427        .for_each(|(rotation, angular_inertia, mut global_angular_inertia)| {
428            global_angular_inertia.update(*angular_inertia, rotation.0);
429        });
430}
431
432/// Logs warnings when dynamic bodies have invalid [`Mass`] or [`AngularInertia`].
433fn warn_missing_mass(
434    mut bodies: Query<
435        (
436            Entity,
437            &RigidBody,
438            Ref<ComputedMass>,
439            Ref<ComputedAngularInertia>,
440        ),
441        Or<(Changed<ComputedMass>, Changed<ComputedAngularInertia>)>,
442    >,
443) {
444    for (entity, rb, mass, inertia) in &mut bodies {
445        let is_mass_valid = mass.value().is_finite();
446        #[cfg(feature = "2d")]
447        let is_inertia_valid = inertia.value().is_finite();
448        #[cfg(feature = "3d")]
449        let is_inertia_valid = inertia.value().is_finite();
450
451        // Warn about dynamic bodies with no mass or inertia
452        if rb.is_dynamic() && !(is_mass_valid && is_inertia_valid) {
453            warn!(
454                "Dynamic rigid body {:?} has no mass or inertia. This can cause NaN values. Consider adding a `MassPropertiesBundle` or a `Collider` with mass.",
455                entity
456            );
457        }
458    }
459}
460
461#[cfg(test)]
462#[cfg(feature = "2d")]
463#[expect(clippy::unnecessary_cast)]
464mod tests {
465    use approx::assert_relative_eq;
466
467    use super::*;
468
469    fn create_app() -> App {
470        let mut app = App::new();
471        app.add_plugins((MinimalPlugins, PhysicsPlugins::default(), TransformPlugin));
472        app
473    }
474
475    fn get_computed_mass_properties(
476        world: &mut World,
477        entity: Entity,
478    ) -> (
479        &ComputedMass,
480        &ComputedAngularInertia,
481        &ComputedCenterOfMass,
482    ) {
483        let mut query = world.query::<(
484            &ComputedMass,
485            &ComputedAngularInertia,
486            &ComputedCenterOfMass,
487        )>();
488        query.get(world, entity).unwrap()
489    }
490
491    #[test]
492    fn mass_properties_zero_by_default() {
493        // `RigidBody`
494
495        let mut app = create_app();
496
497        let body_entity = app.world_mut().spawn(RigidBody::Dynamic).id();
498
499        app.world_mut().run_schedule(FixedPostUpdate);
500
501        let (mass, angular_inertia, center_of_mass) =
502            get_computed_mass_properties(app.world_mut(), body_entity);
503
504        assert_eq!(*mass, ComputedMass::default());
505        assert_eq!(*angular_inertia, ComputedAngularInertia::default());
506        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
507    }
508
509    #[test]
510    fn mass_properties_rb_collider() {
511        // `RigidBody`, `Collider`
512
513        let mut app = create_app();
514
515        let collider = Collider::circle(1.0);
516        let collider_mass_props = collider.mass_properties(1.0);
517
518        let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
519
520        app.world_mut().run_schedule(FixedPostUpdate);
521
522        let (mass, angular_inertia, center_of_mass) =
523            get_computed_mass_properties(app.world_mut(), body_entity);
524
525        assert_eq!(mass.value() as f32, collider_mass_props.mass);
526        assert_eq!(
527            angular_inertia.value() as f32,
528            collider_mass_props.angular_inertia
529        );
530        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
531    }
532
533    #[test]
534    fn mass_properties_rb_collider_with_set_mass() {
535        // `RigidBody`, `Collider`, `Mass(5.0)`
536
537        let mut app = create_app();
538
539        let collider = Collider::circle(1.0);
540        let collider_mass_props = collider.mass_properties(1.0);
541
542        let body_entity = app
543            .world_mut()
544            .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
545            .id();
546
547        app.world_mut().run_schedule(FixedPostUpdate);
548
549        let (mass, angular_inertia, center_of_mass) =
550            get_computed_mass_properties(app.world_mut(), body_entity);
551
552        assert_eq!(mass.value() as f32, 5.0);
553        assert_eq!(
554            angular_inertia.value() as f32,
555            mass.value() as f32 * collider_mass_props.unit_angular_inertia()
556        );
557        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
558    }
559
560    #[test]
561    fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
562        // `RigidBody`, `Collider`, `Mass(5.0)`, `AngularInertia(10.0)`
563
564        let mut app = create_app();
565
566        let collider = Collider::circle(1.0);
567
568        let body_entity = app
569            .world_mut()
570            .spawn((
571                RigidBody::Dynamic,
572                collider,
573                Mass(5.0),
574                AngularInertia(10.0),
575            ))
576            .id();
577
578        app.world_mut().run_schedule(FixedPostUpdate);
579
580        let (mass, angular_inertia, center_of_mass) =
581            get_computed_mass_properties(app.world_mut(), body_entity);
582
583        assert_eq!(mass.value() as f32, 5.0);
584        assert_eq!(angular_inertia.value() as f32, 10.0);
585        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
586    }
587
588    #[test]
589    fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
590        // `RigidBody`, `Collider`, `Mass(5.0)`
591        // - `Collider`, `ColliderDensity(2.0)`
592
593        let mut app = create_app();
594
595        let collider = Collider::circle(1.0);
596        let collider_mass_props = collider.mass_properties(2.0);
597
598        let body_entity = app
599            .world_mut()
600            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
601            .with_child((collider, ColliderDensity(2.0)))
602            .id();
603
604        app.world_mut().run_schedule(FixedPostUpdate);
605
606        let (mass, angular_inertia, center_of_mass) =
607            get_computed_mass_properties(app.world_mut(), body_entity);
608
609        assert_eq!(mass.value() as f32, 5.0 + collider_mass_props.mass);
610        assert_eq!(
611            angular_inertia.value() as f32,
612            5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
613        );
614        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
615    }
616
617    #[test]
618    fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
619        // `RigidBody`, `Collider`, `Mass(5.0)`
620        // - `Collider`, `ColliderDensity(2.0)`, `Mass(10.0)`
621
622        let mut app = create_app();
623
624        let collider = Collider::circle(1.0);
625        let collider_mass_props = collider.mass_properties(1.0);
626
627        let body_entity = app
628            .world_mut()
629            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
630            .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
631            .id();
632
633        app.world_mut().run_schedule(FixedPostUpdate);
634
635        let (mass, angular_inertia, center_of_mass) =
636            get_computed_mass_properties(app.world_mut(), body_entity);
637
638        assert_eq!(mass.value() as f32, 5.0 + 10.0);
639        assert_eq!(
640            angular_inertia.value() as f32,
641            5.0 * collider_mass_props.unit_angular_inertia()
642                + 10.0 * collider_mass_props.unit_angular_inertia()
643        );
644        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
645    }
646
647    #[test]
648    fn mass_properties_no_auto_mass_collider_no_set_mass() {
649        // `RigidBody`, `Collider`, `NoAutoMass`
650        //
651        // Mass properties should be zero.
652
653        let mut app = create_app();
654
655        let body_entity = app
656            .world_mut()
657            .spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
658            .id();
659
660        app.world_mut().run_schedule(FixedPostUpdate);
661
662        let (mass, angular_inertia, center_of_mass) =
663            get_computed_mass_properties(app.world_mut(), body_entity);
664
665        assert_eq!(*mass, ComputedMass::default());
666        assert_eq!(*angular_inertia, ComputedAngularInertia::default());
667        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
668    }
669
670    #[test]
671    fn mass_properties_no_auto_mass_hierarchy() {
672        // `RigidBody`, `Collider`, `Mass(5.0)`, `NoAutoMass`
673        // - `Collider`, `ColliderDensity(2.0)`, `Mass(10.0)`
674
675        let mut app = create_app();
676
677        let collider = Collider::circle(1.0);
678        let collider_mass_props = collider.mass_properties(1.0);
679
680        let body_entity = app
681            .world_mut()
682            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
683            .with_child((collider, ColliderDensity(2.0), Mass(10.0)))
684            .id();
685
686        app.world_mut().run_schedule(FixedPostUpdate);
687
688        let (mass, angular_inertia, center_of_mass) =
689            get_computed_mass_properties(app.world_mut(), body_entity);
690
691        assert_eq!(mass.value() as f32, 5.0);
692        assert_eq!(
693            angular_inertia.value() as f32,
694            mass.value() as f32
695                * (5.0 * collider_mass_props.unit_angular_inertia()
696                    + 10.0 * collider_mass_props.unit_angular_inertia())
697                / 15.0
698        );
699        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
700    }
701
702    #[test]
703    fn mass_properties_add_remove_collider() {
704        // `RigidBody`, `Collider`
705        //
706        // - Check mass properties
707        // - Add child `Collider`
708        // - Check mass properties
709        // - Remove child `Collider`
710        // - Check mass properties
711
712        let mut app = create_app();
713
714        let collider = Collider::circle(1.0);
715        let collider_mass_props = collider.mass_properties(1.0);
716
717        let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
718
719        app.world_mut().run_schedule(FixedPostUpdate);
720
721        let (mass, angular_inertia, center_of_mass) =
722            get_computed_mass_properties(app.world_mut(), body_entity);
723
724        assert_eq!(mass.value() as f32, collider_mass_props.mass);
725        assert_eq!(
726            angular_inertia.value() as f32,
727            collider_mass_props.angular_inertia
728        );
729        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
730
731        // Add a child collider
732        let child_collider = Collider::circle(2.0);
733        let child_collider_mass_props = child_collider.mass_properties(1.0);
734
735        let child_entity = app
736            .world_mut()
737            .spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
738            .id();
739
740        app.world_mut().run_schedule(FixedPostUpdate);
741
742        let (mass, angular_inertia, center_of_mass) =
743            get_computed_mass_properties(app.world_mut(), body_entity);
744
745        assert_eq!(
746            mass.value() as f32,
747            collider_mass_props.mass + child_collider_mass_props.mass
748        );
749        assert_eq!(
750            angular_inertia.value() as f32,
751            collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
752        );
753        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
754
755        // Remove the child collider
756        app.world_mut().entity_mut(child_entity).despawn();
757
758        app.world_mut().run_schedule(FixedPostUpdate);
759
760        let (mass, angular_inertia, center_of_mass) =
761            get_computed_mass_properties(app.world_mut(), body_entity);
762
763        assert_eq!(mass.value() as f32, collider_mass_props.mass);
764        assert_eq!(
765            angular_inertia.value() as f32,
766            collider_mass_props.angular_inertia
767        );
768        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
769    }
770
771    #[test]
772    fn mass_properties_change_mass() {
773        // `RigidBody`, `Collider`, `Mass(5.0)`
774        //
775        // - Check mass properties
776        // - Change mass
777        // - Check mass properties
778
779        let mut app = create_app();
780
781        let collider = Collider::circle(1.0);
782        let collider_mass_props = collider.mass_properties(1.0);
783
784        let body_entity = app
785            .world_mut()
786            .spawn((RigidBody::Dynamic, collider, Mass(5.0)))
787            .id();
788
789        app.world_mut().run_schedule(FixedPostUpdate);
790
791        let (mass, angular_inertia, center_of_mass) =
792            get_computed_mass_properties(app.world_mut(), body_entity);
793
794        assert_eq!(mass.value() as f32, 5.0);
795        assert_eq!(
796            angular_inertia.value() as f32,
797            mass.value() as f32 * collider_mass_props.unit_angular_inertia()
798        );
799        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
800
801        // Change mass
802        app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
803
804        app.world_mut().run_schedule(FixedPostUpdate);
805
806        let (mass, angular_inertia, center_of_mass) =
807            get_computed_mass_properties(app.world_mut(), body_entity);
808
809        assert_eq!(mass.value() as f32, 10.0);
810        assert_eq!(
811            angular_inertia.value() as f32,
812            mass.value() as f32 * collider_mass_props.unit_angular_inertia()
813        );
814        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
815    }
816
817    #[test]
818    fn mass_properties_move_child_collider() {
819        // `RigidBody`, `Mass(10.0)`, `CenterOfMass(0.0, -0.5)`, `Transform`
820        // - `Collider`, `Mass(5.0)`, `Transform`
821        //
822        // - Check mass properties
823        // - Move child collider
824        // - Check mass properties
825
826        let mut app = create_app();
827
828        let body_entity = app
829            .world_mut()
830            .spawn((
831                RigidBody::Dynamic,
832                Mass(10.0),
833                CenterOfMass::new(0.0, -0.5),
834                Transform::default(),
835            ))
836            .id();
837
838        let child_entity = app
839            .world_mut()
840            .spawn((
841                ChildOf(body_entity),
842                Collider::circle(1.0),
843                Mass(5.0),
844                Transform::default(),
845            ))
846            .id();
847
848        app.world_mut().run_schedule(FixedPostUpdate);
849
850        let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
851
852        assert_eq!(mass.value() as f32, 10.0 + 5.0);
853        assert_relative_eq!(
854            center_of_mass.0,
855            Vector::new(0.0, -1.0 / 3.0),
856            epsilon = 1.0e-6
857        );
858
859        // Move child collider
860        app.world_mut()
861            .entity_mut(child_entity)
862            .insert(Transform::from_xyz(0.0, 4.0, 0.0));
863
864        app.world_mut().run_schedule(FixedPostUpdate);
865
866        let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
867
868        assert_eq!(mass.value(), 10.0 + 5.0);
869        assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
870    }
871
872    #[test]
873    fn mass_properties_no_auto_mass_add_remove() {
874        // `RigidBody`, `Collider`, `Mass(5.0)`
875        // - `Collider`, `Mass(10.0)`
876        //
877        // - Check mass properties
878        // - Add `NoAutoMass`
879        // - Check mass properties
880        // - Remove `NoAutoMass`
881        // - Check mass properties
882
883        let mut app = create_app();
884
885        let collider = Collider::circle(1.0);
886        let collider_mass_props = collider.mass_properties(1.0);
887
888        let body_entity = app
889            .world_mut()
890            .spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
891            .with_child((collider, Mass(10.0)))
892            .id();
893
894        app.world_mut().run_schedule(FixedPostUpdate);
895
896        let (mass, angular_inertia, center_of_mass) =
897            get_computed_mass_properties(app.world_mut(), body_entity);
898
899        assert_eq!(mass.value() as f32, 5.0 + 10.0);
900        assert_eq!(
901            angular_inertia.value() as f32,
902            5.0 * collider_mass_props.unit_angular_inertia()
903                + 10.0 * collider_mass_props.unit_angular_inertia()
904        );
905        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
906
907        // Add `NoAutoMass`
908        app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
909
910        app.world_mut().run_schedule(FixedPostUpdate);
911
912        let (mass, angular_inertia, center_of_mass) =
913            get_computed_mass_properties(app.world_mut(), body_entity);
914
915        assert_eq!(mass.value() as f32, 5.0);
916        assert_eq!(
917            angular_inertia.value() as f32,
918            5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
919                + 10.0 * collider_mass_props.unit_angular_inertia())
920                / 15.0
921        );
922        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
923
924        // Remove `NoAutoMass`
925        app.world_mut()
926            .entity_mut(body_entity)
927            .remove::<NoAutoMass>();
928
929        app.world_mut().run_schedule(FixedPostUpdate);
930
931        let (mass, angular_inertia, center_of_mass) =
932            get_computed_mass_properties(app.world_mut(), body_entity);
933
934        assert_eq!(mass.value() as f32, 5.0 + 10.0);
935        assert_eq!(
936            angular_inertia.value() as f32,
937            5.0 * collider_mass_props.unit_angular_inertia()
938                + 10.0 * collider_mass_props.unit_angular_inertia()
939        );
940        assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
941    }
942}