#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
#![cfg_attr(feature = "2d", doc = " CenterOfMass::new(0.0, -0.5),")]
#![cfg_attr(feature = "3d", doc = " CenterOfMass::new(0.0, -0.5, 0.0),")]
#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
#![cfg_attr(
feature = "2d",
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]"
)]
#![cfg_attr(
feature = "3d",
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]"
)]
#![cfg_attr(feature = "2d", doc = " CenterOfMass::new(0.0, -0.5),")]
#![cfg_attr(feature = "3d", doc = " CenterOfMass::new(0.0, -0.5, 0.0),")]
#![cfg_attr(feature = "2d", doc = " Collider::circle(1.0),")]
#![cfg_attr(feature = "3d", doc = " Collider::sphere(1.0),")]
#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
#![cfg_attr(feature = "2d", doc = "// Total center of mass: [0.0, -0.5]")]
#![cfg_attr(feature = "3d", doc = "// Total center of mass: [0.0, -0.5, 0.0]")]
#![cfg_attr(feature = "2d", doc = " CenterOfMass::new(0.0, -0.5),")]
#![cfg_attr(feature = "3d", doc = " CenterOfMass::new(0.0, -0.5, 0.0),")]
#![cfg_attr(feature = "2d", doc = " Collider::circle(1.0),")]
#![cfg_attr(feature = "3d", doc = " Collider::sphere(1.0),")]
#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
#![cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#![cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
#![cfg_attr(feature = "2d", doc = "let shape = Collider::circle(0.5);")]
#![cfg_attr(feature = "3d", doc = "let shape = Collider::sphere(0.5);")]
#![cfg_attr(feature = "2d", doc = "let shape = Circle::new(0.5);")]
#![cfg_attr(feature = "3d", doc = "let shape = Sphere::new(0.5);")]
use crate::physics_transform::PhysicsTransformSystems;
use crate::prelude::*;
use bevy::{
ecs::{intern::Interned, schedule::ScheduleLabel},
prelude::*,
};
pub mod components;
use components::RecomputeMassProperties;
mod system_param;
pub use system_param::MassPropertyHelper;
pub use bevy_heavy;
#[cfg(feature = "2d")]
pub(crate) use bevy_heavy::{
ComputeMassProperties2d as ComputeMassProperties, MassProperties2d as MassProperties,
};
#[cfg(feature = "3d")]
pub(crate) use bevy_heavy::{
ComputeMassProperties3d as ComputeMassProperties, MassProperties3d as MassProperties,
};
pub trait MassPropertiesExt {
fn to_bundle(&self) -> MassPropertiesBundle;
}
impl MassPropertiesExt for MassProperties {
fn to_bundle(&self) -> MassPropertiesBundle {
#[cfg(feature = "2d")]
let angular_inertia = AngularInertia(self.angular_inertia);
#[cfg(feature = "3d")]
let angular_inertia = AngularInertia::new_with_local_frame(
self.principal_angular_inertia.f32(),
self.local_inertial_frame.f32(),
);
MassPropertiesBundle {
mass: Mass(self.mass),
angular_inertia,
center_of_mass: CenterOfMass(self.center_of_mass),
}
}
}
pub struct MassPropertyPlugin {
schedule: Interned<dyn ScheduleLabel>,
}
impl MassPropertyPlugin {
pub fn new(schedule: impl ScheduleLabel) -> Self {
Self {
schedule: schedule.intern(),
}
}
}
impl Default for MassPropertyPlugin {
fn default() -> Self {
Self::new(FixedPostUpdate)
}
}
impl Plugin for MassPropertyPlugin {
fn build(&self, app: &mut App) {
app.register_required_components::<RigidBody, RecomputeMassProperties>();
app.add_observer(
|trigger: On<Add, RigidBody>, mut mass_helper: MassPropertyHelper| {
mass_helper.update_mass_properties(trigger.entity);
},
);
app.add_observer(
|trigger: On<Insert, RigidBodyColliders>, mut mass_helper: MassPropertyHelper| {
mass_helper.update_mass_properties(trigger.entity);
},
);
app.configure_sets(
self.schedule,
(
MassPropertySystems::UpdateColliderMassProperties,
MassPropertySystems::QueueRecomputation,
MassPropertySystems::UpdateComputedMassProperties,
)
.chain()
.in_set(PhysicsSystems::Prepare)
.after(PhysicsTransformSystems::TransformToPosition),
);
app.add_systems(
self.schedule,
(
queue_mass_recomputation_on_mass_change,
queue_mass_recomputation_on_collider_mass_change,
)
.in_set(MassPropertySystems::QueueRecomputation),
);
app.add_systems(
self.schedule,
(update_mass_properties, warn_invalid_mass)
.chain()
.in_set(MassPropertySystems::UpdateComputedMassProperties),
);
}
}
#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MassPropertySystems {
UpdateColliderMassProperties,
QueueRecomputation,
UpdateComputedMassProperties,
}
pub type WithComputedMassProperty = Or<(
With<ComputedMass>,
With<ComputedAngularInertia>,
With<ComputedCenterOfMass>,
)>;
pub type MassPropertyChanged = Or<(
Changed<Mass>,
Changed<AngularInertia>,
Changed<CenterOfMass>,
)>;
fn queue_mass_recomputation_on_mass_change(
mut commands: Commands,
mut query: Query<
Entity,
(
WithComputedMassProperty,
Without<ColliderOf>,
MassPropertyChanged,
),
>,
) {
for entity in &mut query {
commands.entity(entity).insert(RecomputeMassProperties);
}
}
fn queue_mass_recomputation_on_collider_mass_change(
mut commands: Commands,
mut query: Query<
&ColliderOf,
Or<(
Changed<ColliderMassProperties>,
Changed<ColliderTransform>,
MassPropertyChanged,
)>,
>,
) {
for &ColliderOf { body } in &mut query {
if let Ok(mut entity_commands) = commands.get_entity(body) {
entity_commands.insert(RecomputeMassProperties);
}
}
}
fn update_mass_properties(
mut commands: Commands,
query: Query<Entity, With<RecomputeMassProperties>>,
mut mass_helper: MassPropertyHelper,
) {
for entity in query.iter() {
mass_helper.update_mass_properties(entity);
commands.entity(entity).remove::<RecomputeMassProperties>();
}
}
#[cfg(feature = "default-collider")]
type ShouldWarn = (
Without<ColliderConstructor>,
Without<ColliderConstructorHierarchy>,
);
#[cfg(not(feature = "default-collider"))]
type ShouldWarn = ();
fn warn_invalid_mass(
mut bodies: Query<
(
Entity,
&RigidBody,
Ref<ComputedMass>,
Ref<ComputedAngularInertia>,
),
(
Or<(Changed<ComputedMass>, Changed<ComputedAngularInertia>)>,
ShouldWarn,
),
>,
) {
for (entity, rb, mass, inertia) in &mut bodies {
let is_mass_valid = mass.is_finite();
#[cfg(feature = "2d")]
let is_inertia_valid = inertia.is_finite();
#[cfg(feature = "3d")]
let is_inertia_valid = inertia.is_finite();
if rb.is_dynamic() && !(is_mass_valid && is_inertia_valid) {
warn!(
"Dynamic rigid body {:?} has no mass or inertia. This can cause NaN values. Consider adding a `MassPropertiesBundle` or a `Collider` with mass.",
entity
);
}
}
}
#[cfg(test)]
#[cfg(all(feature = "2d", feature = "default-collider"))]
#[allow(clippy::unnecessary_cast)]
mod tests {
use approx::assert_relative_eq;
use super::*;
fn create_app() -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, PhysicsPlugins::default(), TransformPlugin));
app
}
fn get_computed_mass_properties(
world: &mut World,
entity: Entity,
) -> (
&ComputedMass,
&ComputedAngularInertia,
&ComputedCenterOfMass,
) {
let mut query = world.query::<(
&ComputedMass,
&ComputedAngularInertia,
&ComputedCenterOfMass,
)>();
query.get(world, entity).unwrap()
}
#[test]
fn mass_properties_zero_by_default() {
let mut app = create_app();
let body_entity = app.world_mut().spawn(RigidBody::Dynamic).id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(*mass, ComputedMass::default());
assert_eq!(*angular_inertia, ComputedAngularInertia::default());
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_rb_collider() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, collider_mass_props.mass);
assert_eq!(
angular_inertia.value() as f32,
collider_mass_props.angular_inertia
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_rb_collider_with_set_mass() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, collider, Mass(5.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0);
assert_eq!(
angular_inertia.value() as f32,
mass.value() as f32 * collider_mass_props.unit_angular_inertia()
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_rb_collider_with_set_mass_and_angular_inertia() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let body_entity = app
.world_mut()
.spawn((
RigidBody::Dynamic,
collider,
Mass(5.0),
AngularInertia(10.0),
))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0);
assert_eq!(angular_inertia.value() as f32, 10.0);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_rb_collider_with_set_mass_and_child_collider() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(2.0);
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
.with_child((collider, ColliderDensity(2.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0 + collider_mass_props.mass);
assert_eq!(
angular_inertia.value() as f32,
5.0 * collider_mass_props.unit_angular_inertia() + collider_mass_props.angular_inertia
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_rb_collider_with_set_mass_and_child_collider_with_set_mass() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
.with_child((collider, ColliderDensity(2.0), Mass(10.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0 + 10.0);
assert_eq!(
angular_inertia.value() as f32,
5.0 * collider_mass_props.unit_angular_inertia()
+ 10.0 * collider_mass_props.unit_angular_inertia()
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_no_auto_mass_collider_no_set_mass() {
let mut app = create_app();
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, Collider::circle(1.0), NoAutoMass))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(*mass, ComputedMass::default());
assert_eq!(*angular_inertia, ComputedAngularInertia::default());
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_no_auto_mass_hierarchy() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0), NoAutoMass))
.with_child((collider, ColliderDensity(2.0), Mass(10.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0);
assert_eq!(
angular_inertia.value() as f32,
mass.value() as f32
* (5.0 * collider_mass_props.unit_angular_inertia()
+ 10.0 * collider_mass_props.unit_angular_inertia())
/ 15.0
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_add_remove_collider() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app.world_mut().spawn((RigidBody::Dynamic, collider)).id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, collider_mass_props.mass);
assert_eq!(
angular_inertia.value() as f32,
collider_mass_props.angular_inertia
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
let child_collider = Collider::circle(2.0);
let child_collider_mass_props = child_collider.mass_properties(1.0);
let child_entity = app
.world_mut()
.spawn((ChildOf(body_entity), child_collider, ColliderDensity(1.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(
mass.value() as f32,
collider_mass_props.mass + child_collider_mass_props.mass
);
assert_eq!(
angular_inertia.value() as f32,
collider_mass_props.angular_inertia + child_collider_mass_props.angular_inertia
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
app.world_mut().entity_mut(child_entity).despawn();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, collider_mass_props.mass);
assert_eq!(
angular_inertia.value() as f32,
collider_mass_props.angular_inertia
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_change_mass() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, collider, Mass(5.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0);
assert_eq!(
angular_inertia.value() as f32,
mass.value() as f32 * collider_mass_props.unit_angular_inertia()
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
app.world_mut().entity_mut(body_entity).insert(Mass(10.0));
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 10.0);
assert_eq!(
angular_inertia.value() as f32,
mass.value() as f32 * collider_mass_props.unit_angular_inertia()
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
#[test]
fn mass_properties_move_child_collider() {
let mut app = create_app();
let body_entity = app
.world_mut()
.spawn((
RigidBody::Dynamic,
Mass(10.0),
CenterOfMass::new(0.0, -0.5),
Transform::default(),
))
.id();
let child_entity = app
.world_mut()
.spawn((
ChildOf(body_entity),
Collider::circle(1.0),
Mass(5.0),
Transform::default(),
))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 10.0 + 5.0);
assert_relative_eq!(
center_of_mass.0,
Vector::new(0.0, -1.0 / 3.0),
epsilon = 1.0e-6
);
app.world_mut()
.entity_mut(child_entity)
.insert(Transform::from_xyz(0.0, 4.0, 0.0));
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, _, center_of_mass) = get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value(), 10.0 + 5.0);
assert_relative_eq!(center_of_mass.0, Vector::new(0.0, 1.0));
}
#[test]
fn mass_properties_no_auto_mass_add_remove() {
let mut app = create_app();
let collider = Collider::circle(1.0);
let collider_mass_props = collider.mass_properties(1.0);
let body_entity = app
.world_mut()
.spawn((RigidBody::Dynamic, collider.clone(), Mass(5.0)))
.with_child((collider, Mass(10.0)))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0 + 10.0);
assert_eq!(
angular_inertia.value() as f32,
5.0 * collider_mass_props.unit_angular_inertia()
+ 10.0 * collider_mass_props.unit_angular_inertia()
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
app.world_mut().entity_mut(body_entity).insert(NoAutoMass);
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0);
assert_eq!(
angular_inertia.value() as f32,
5.0 * (5.0 * collider_mass_props.unit_angular_inertia()
+ 10.0 * collider_mass_props.unit_angular_inertia())
/ 15.0
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
app.world_mut()
.entity_mut(body_entity)
.remove::<NoAutoMass>();
app.world_mut().run_schedule(FixedPostUpdate);
let (mass, angular_inertia, center_of_mass) =
get_computed_mass_properties(app.world_mut(), body_entity);
assert_eq!(mass.value() as f32, 5.0 + 10.0);
assert_eq!(
angular_inertia.value() as f32,
5.0 * collider_mass_props.unit_angular_inertia()
+ 10.0 * collider_mass_props.unit_angular_inertia()
);
assert_eq!(*center_of_mass, ComputedCenterOfMass::default());
}
}