Skip to content

Commit 6bfa89e

Browse files
authored
Mass Property Rework (#574)
# Objective Closes #443. Implements the rest of #499, continuing the work done in #500 and #532. The current mass property functionality is very limited, confusing, and footgunny. - `Mass`, `AngularInertia`, and `CenterOfMass` can be added to a rigid body to specify its initial mass properties. However, the mass properties of colliders and descendants are *always* added on top, to the same components. From the user's perspective, the initial mass information is lost, and *there is no way to fully override mass properties on spawn*. - If you manually set `Mass` to a lower value at runtime, and then remove a collider, you could end up with *negative* mass. - Angular inertia is not scaled when mass is changed at runtime. - Specifying the center of mass at spawn does nothing, *except* if an initial mass is specified. This is because when mass properties from colliders are added/removed, the new center of mass is computed as the weighted average, and the initial mass for the rigid body is zero. - `Mass`, `AngularInertia`, and `CenterOfMass` do *nothing* on child colliders. - After #500, mass and angular inertia store inverses, and 3D angular inertia in particular stores a 3x3 matrix. These representations aren't very user-friendly, and may not be ideal for scene and editor workflows. - The internal implementation is confusing and has a decent amount of overhead, relying on observers and storing previous collider transforms for mass property updates. We need a mass property system that is simple, understandable, and lightweight, while being flexible enough to support more advanced use cases. Some high-level goals here are: - Unless overridden, colliders and child colliders should contribute to the total mass properties. - It must be straightforward to override the total mass properties and disable automatic mass computation. - It should also be possible to override mass properties for indidual child colliders, using the same components as for rigid bodies. - User-specified mass properties should not be lost, and it should not be possible to get negative mass unless explicitly overridden. - Behavior should be intuitive. ## Solution ### `Mass` and `ComputedMass` Using the same component for the *user-specified* mass and the *total* mass (that takes all attached colliders into account) was problematic. The *total mass properties* of rigid bodies are now stored in the new `ComputedMass`, `ComputedAngularInertia`, and `ComputedCenterOfMass` components. By default, these are updated automatically when mass properties are changed, or when colliders are added or removed. Computed mass properties are required components for `RigidBody`. `Mass`, `AngularInertia`, and `CenterOfMass` now instead represent the mass properties associated with a *specific* entity. These are optional and never modified by Avian directly. If a rigid body entity has `Mass(10.0)`, and its child collider has `Mass(5.0)`, their mass properties will be combined as `ComputedMass(15.0)`. ```rust // Total mass for rigid body: 10 + 5 = 15 commands.spawn((     RigidBody::Dynamic,     Collider::capsule(0.5, 1.5),     Mass(10.0), )) .with_child((Collider::circle(1.0), Mass(5.0))); ``` If `Mass`, `AngularInertia`, or `CenterOfMass` are not set for an entity, the mass properties of its collider will be used instead, if present. Overridding mass with `Mass` also scales angular inertia accordingly, unless it is also overriden with `AngularInertia`. Sometimes, you might not want child entities or colliders to contribute to the total mass properties. This can be done by adding the `NoAutoMass`, `NoAutoAngularInertia`, and `NoAutoCenterOfMass` marker components, giving you full manual control. ```rust // Total mass: 10.0 // Total center of mass: [0.0, -0.5, 0.0] commands.spawn(( RigidBody::Dynamic, Collider::capsule(0.5, 1.5), Mass(10.0), CenterOfMass::new(0.0, -0.5, 0.0), NoAutoMass, NoAutoCenterOfMass, Transform::default(), )) .with_child(( Collider::circle(1.0), Mass(5.0), Transform::from_translation(Vec3::new(0.0, 4.0, 0.0)), )); ``` That's pretty much it! To recap, the core API has been distilled into: 1. By default, mass properties are computed from attached colliders and `ColliderDensity`. 2. Mass properties can be overridden for individual entities with `Mass`, `AngularInertia`, and `CenterOfMass`. 3. If the rigid body has descendants (child colliders), their mass properties will be combined for the total `ComputedMass`, `ComputedAngularInertia`, and `ComputedCenterOfMass`. 4. To prevent child entities from contributing to the total mass properties, use the `NoAutoMass`, `NoAutoAngularInertia`, and `NoAutoCenterOfMass` marker components. This is *much* more predictable and flexible than the old system. This isn't all that has changed though. I have implemented *many* more improvements here. ## API Improvements ### Representation Unlike the computed mass property components, `Mass`, `AngularInertia`, and `CenterOfMass` have user-friendly representations with public fields. 3D `AngularInertia` differs the most, as it now stores a principal angular inertia (`Vec3`) and the orientation of the local inertial frame (`Quat`) instead of an inertia tensor (`Mat3`). This is more memory efficient and more intuitive to tune by hand. ```rust // Irrelevant derives and docs stripped for readability. #[derive(Component, Default, Deref, DerefMut)] pub struct Mass(pub f32); #[cfg(feature = "2d")] #[derive(Component, Default, Deref, DerefMut)] pub struct AngularInertia(pub f32); #[cfg(feature = "3d")] #[derive(Component)] pub struct AngularInertia { /// The principal angular inertia, representing resistance to angular acceleration /// about the local coordinate axes defined by the `local_frame`. pub principal: Vec3, /// The orientation of the local inertial frame. pub local_frame: Quat, } #[cfg(feature = "2d")] #[derive(Component, Default, Deref, DerefMut)] pub struct CenterOfMass(pub Vec2); #[cfg(feature = "3d")] #[derive(Component, Default, Deref, DerefMut)] pub struct CenterOfMass(pub Vec3); ``` ### Helpers and Constructors There are now a *ton* more helpers and constructors, especially for 3D `AngularInertia`. It has methods like: - `new`, `try_new` - `new_with_local_frame`, `try_with_local_frame` - `from_tensor` - `tensor` - This returns an `AngularInertiaTensor`, which has further methods and operations. More on that in the next section! `ComputedMass`, `ComputedAngularInertia`, and `ComputedCenterOfMass` have even more methods in order to help work with the inverse representation efficiently. ### `bevy_heavy` Integration [`bevy_heavy`](https://github.com/Jondolf/bevy_heavy) is my new mass property crate for Bevy. It provides `MassProperty2d` and `MassProperty3d` types, and traits for computing mass properties for *all* of Bevy's primitive shapes. Avian now takes advantage of this in a few ways. `Collider` now implements the `ComputeMassProperties2d`/`ComputeMassProperties3d` trait for mass property computation. The `mass_properties` method returns `MassProperty2d`/`MassProperty3d` instead of `ColliderMassProperties`, and you can also compute mass, angular inertia, and the center of mass individually: ```rust // Compute all mass properties for a capsule collider with a density of `2.0`. let capsule = Collider::capsule(0.5, 1.5); let mass_properties = capsule.mass_properties(2.0); // Compute individual mass properties (2D here) let mass = capsule.mass(2.0); let angular_inertia = capsule.angular_inertia(mass); let center_of_mass = capsule.center_of_mass(); ``` `Mass`, `AngularInertia`, `CenterOfMass`, and `MassPropertiesBundle` now also have a `from_shape` method that takes a type implementing `ComputeMassProperties2d`/`ComputeMassProperties3d` and a density. The nice part here is that you can also use Bevy's primitive shapes: ```rust // Construct individual mass properties from a collider. let shape = Collider::sphere(0.5); commands.spawn(( RigidBody::Dynamic, Mass::from_shape(&shape, 2.0), AngularInertia::from_shape(&shape, 1.5), CenterOfMass::from_shape(&shape), )); // Construct a `MassPropertiesBundle` from a primitive shape. let shape = Sphere::new(0.5); commands.spawn((RigidBody::Dynamic, MassPropertiesBundle::from_shape(&shape, 2.0))); ``` > [!NOTE] > For now, mass properties for actual colliders still use Parry's mass computation methods, which are less flexible. If we eventually manage to replace Parry with an alternative using Bevy's geometric primitives though, we could transition to only using `bevy_heavy` here. Working with 3D angular inertia and converting between different representations can be somewhat complex. `bevy_heavy` has an eigensolver for diagonalizing angular inertia tensors, and provides an `AngularInertiaTensor` type to wrap this in a nice API. This is used a bit internally, and also returned by methods like `AngularInertia::tensor`. As you might have noticed earlier, `Mass`, `AngularInertia`, `CenterOfMass`, and `ColliderDensity` now only use `f32` types. This is partially to integrate better with `bevy_heavy`, but also because I believe `f64` precision just isn't needed for these user-facing mass property types. The total computed mass properties still support `f64` however. ### `MassPropertyHelper` Sometimes, it might be useful to compute or update mass properties for individual entities or hierarchies manually. There is now a new `MassPropertyHelper` system parameter for this, with the following methods: - `update_mass_properties` - `total_mass_properties` (descendants + local) - `descendants_mass_properties` - `local_mass_properties` The old internal logic for mass property updates relied on storing previous and current collider transforms, subtracting old mass properties if present, and adding the new mass properties. This was very error-prone, probably buggy, had bookkeeping overhead, and was somewhat expensive, since it used observers to trigger recomputation. Now, mass properties are always just recomputed "from scratch" with `update_mass_properties`, which recomputes the total mass properties, taking into account descendants, colliders, and the `NoAutoMass`, `NoAutoAngularInertia`, and `NoAutoCenterOfMass` components. Mass properties are combined using `Iterator::sum`, which is more efficient than the old approach of adding every collider's mass properties individually. Updates are triggered by adding the `RecomputeMassProperties` sparse-set component when mass properties are detected to have changed, avoiding duplicate computation and using standard query iteration instead of observer triggers. I expect this to have much less overhead, and it at least reduces a lot of internal complexity. I expect the `MassPropertyHelper` to get more user-facing utilities in the future as we identify usage patterns and common tasks users need to perform. ### Other Changes - Zero mass and angular inertia is now treated as valid, and interpreted as infinite mass (like in most engines). It no longer emits warnings, and collider density is not clamped in any way. - `ColliderMassProperties` stores `MassProperties2d`/`MassProperties3d` instead of separate properties. This simplifies a lot of internals and provides a richer API. - `ColliderMassProperties` is now properly read-only, excluding setting the component directly or reinserting it. - Added a *ton* of documentation and polish for mass properties. - Added lots of tests to verify behavior is as expected. - Added `MassPropertiesSystems` system sets for mass properties, and decoupled the `ColliderBackendPlugin` further from `MassPropertyPlugin`. - Fixed some scheduling issues. - Reworked the module structure a bit to organize things better. ## Future Work - Make computed mass properties only required for dynamic bodies. We could distinguish between different types of rigid bodies at the component-level, e.g. `DynamicBody`, `KinematicBody`, and `StaticBody` (there are many approaches we could take here). - Only compute `ColliderMassProperties` automatically for colliders that are attached to a (dynamic) rigid body. --- ## Migration Guide ### Behavior Changes - `Mass`, `AngularInertia`, and `CenterOfMass` are now optional, and can be used to override the mass properties of an entity if present, ignoring the entity's collider. Mass properties that are not set are still computed from the entity's `Collider` and `ColliderDensity`. - Mass properties of child entities still contribute to the total mass properties of rigid bodies by default, but the total values are stored in `ComputedMass`, `ComputedAngularInertia`, and `ComputedCenterOfMass` instead of `Mass`, `AngularInertia`, and `CenterOfMass`. The latter components are now never modified by Avian directly. - To prevent colliders or descendants from contributing to the total mass properties, add the `NoAutoMass`, `NoAutoAngularInertia`, and `NoAutoCenterOfMass` marker components to the rigid body, giving you full manual control. - Previously, changing `Mass` at runtime did not affect angular inertia. Now, it is scaled accordingly, unless `NoAutoAngularInertia` is present. - Previously, specifying the `CenterOfMass` at spawn did nothing *unless* an initial `Mass` was specified, even if the entity had a collider that would give it mass. This has been fixed. - Previously, `Mass`, `AngularInertia`, and `CenterOfMass` did *nothing* on child colliders. Now, they effectively override `ColliderMassProperties` when computing the total mass properties for the rigid body. - Previously, zero mass and angular inertia were treated as invalid. It emitted warnings, which was especially problematic and spammy for runtime collider constructors. Now, they are treated as acceptable values, and interpreted as infinite mass, like in most other engines. ### API Changes - `Mass`, `AngularInertia`, `CenterOfMass`, `ColliderDensity`, and `ColliderMassProperties` now always use `f32` types, even with the `f64` feature. Total mass properties stored in `ComputedMass`, `ComputedAngularInertia`, and `ComputedCenterOfMass` still support `f64`. - In 3D, `AngularInertia` now stores a principal angular inertia (`Vec3`) and the orientation of the local inertial frame (`Quat`) instead of an inertia tensor (`Mat3`). However, several different constructors are provided, including `from_tensor`. - `MassPropertiesBundle::new_computed` and `ColliderMassProperties::from_collider` have been renamed to `from_shape`. - `ColliderMassProperties` now stores a `MassProperties2d`/`MassProperties3d` instead of separate properties. - Types implementing `AnyCollider` must now also implement the `ComputeMassProperties2d`/`ComputeMassProperties3d` trait instead of the `mass_properties` method.
1 parent 07831d5 commit 6bfa89e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2961
-1009
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ fn setup(
121121
Collider::cuboid(1.0, 1.0, 1.0),
122122
AngularVelocity(Vec3::new(2.5, 3.5, 1.5)),
123123
PbrBundle {
124-
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
124+
mesh: meshes.add(Cuboid::from_length(1.0)),
125125
material: materials.add(Color::srgb_u8(124, 144, 255)),
126126
transform: Transform::from_xyz(0.0, 4.0, 0.0),
127127
..default()

crates/avian2d/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ enhanced-determinism = [
3333
"parry2d?/enhanced-determinism",
3434
"parry2d-f64?/enhanced-determinism",
3535
"bevy_math/libm",
36+
"bevy_heavy/libm",
3637
]
3738

3839
default-collider = ["dep:nalgebra"]
@@ -61,6 +62,7 @@ bench = false
6162
avian_derive = { path = "../avian_derive", version = "0.1" }
6263
bevy = { version = "0.15", default-features = false }
6364
bevy_math = { version = "0.15" }
65+
bevy_heavy = { version = "0.1" }
6466
libm = { version = "0.2", optional = true }
6567
parry2d = { version = "0.17", optional = true }
6668
parry2d-f64 = { version = "0.17", optional = true }
@@ -76,6 +78,7 @@ bitflags = "2.5.0"
7678
examples_common_2d = { path = "../examples_common_2d" }
7779
benches_common_2d = { path = "../benches_common_2d" }
7880
bevy_math = { version = "0.15", features = ["approx"] }
81+
bevy_heavy = { version = "0.1", features = ["approx"] }
7982
glam = { version = "0.29", features = ["bytemuck"] }
8083
approx = "0.5"
8184
bytemuck = "1.19"

crates/avian2d/examples/chain_2d.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ fn setup(
4949
let current_particle = commands
5050
.spawn((
5151
RigidBody::Dynamic,
52-
MassPropertiesBundle::new_computed(&Collider::circle(particle_radius), 1.0),
52+
MassPropertiesBundle::from_shape(&Circle::new(particle_radius as f32), 1.0),
5353
Mesh2d(particle_mesh.clone()),
5454
MeshMaterial2d(particle_material.clone()),
5555
Transform::from_xyz(0.0, -i as f32 * (particle_radius as f32 * 2.0 + 1.0), 0.0),

crates/avian2d/examples/custom_collider.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! An example demonstrating how to make a custom collider and use it for collision detection.
22
3+
#![allow(clippy::unnecessary_cast)]
4+
35
use avian2d::{math::*, prelude::*};
46
use bevy::prelude::*;
57
use examples_common_2d::ExampleCommonPlugin;
@@ -55,21 +57,8 @@ impl AnyCollider for CircleCollider {
5557
ColliderAabb::new(position, Vector::splat(self.radius))
5658
}
5759

58-
fn mass_properties(&self, density: Scalar) -> ColliderMassProperties {
59-
// In 2D, the Z length is assumed to be 1.0, so volume = area
60-
let volume = PI * self.radius.powi(2);
61-
let mass = density * volume;
62-
let angular_inertia = mass * self.radius.powi(2) / 2.0;
63-
64-
ColliderMassProperties {
65-
mass,
66-
angular_inertia,
67-
center_of_mass: default(),
68-
}
69-
}
70-
7160
// This is the actual collision detection part.
72-
// It compute all contacts between two colliders at the given positions.
61+
// It computes all contacts between two colliders at the given positions.
7362
fn contact_manifolds(
7463
&self,
7564
other: &Self,
@@ -122,6 +111,25 @@ impl AnyCollider for CircleCollider {
122111
}
123112
}
124113

114+
// Implement mass computation for the collider shape.
115+
// This is needed for physics to behave correctly.
116+
impl ComputeMassProperties2d for CircleCollider {
117+
fn mass(&self, density: f32) -> f32 {
118+
// In 2D, the Z length is assumed to be `1.0`, so volume == area.
119+
let volume = std::f32::consts::PI * self.radius.powi(2) as f32;
120+
density * volume
121+
}
122+
123+
fn unit_angular_inertia(&self) -> f32 {
124+
// Angular inertia for a circle, assuming a mass of `1.0`.
125+
self.radius.powi(2) as f32 / 2.0
126+
}
127+
128+
fn center_of_mass(&self) -> Vec2 {
129+
Vec2::ZERO
130+
}
131+
}
132+
125133
// Note: This circle collider only supports uniform scaling.
126134
impl ScalableCollider for CircleCollider {
127135
fn scale(&self) -> Vector {

crates/avian2d/examples/distance_joint_2d.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn setup(mut commands: Commands) {
3434
square_sprite,
3535
Transform::from_xyz(100.0, 0.0, 0.0),
3636
RigidBody::Dynamic,
37-
MassPropertiesBundle::new_computed(&Collider::rectangle(50.0, 50.0), 1.0),
37+
MassPropertiesBundle::from_shape(&Rectangle::from_length(50.0), 1.0),
3838
))
3939
.id();
4040

crates/avian2d/examples/fixed_joint_2d.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn setup(mut commands: Commands) {
3838
square_sprite,
3939
Transform::from_xyz(100.0, 0.0, 0.0),
4040
RigidBody::Dynamic,
41-
MassPropertiesBundle::new_computed(&Collider::rectangle(50.0, 50.0), 1.0),
41+
MassPropertiesBundle::from_shape(&Rectangle::from_length(50.0), 1.0),
4242
))
4343
.id();
4444

crates/avian2d/examples/prismatic_joint_2d.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn setup(mut commands: Commands) {
3838
square_sprite,
3939
Transform::from_xyz(100.0, 0.0, 0.0),
4040
RigidBody::Dynamic,
41-
MassPropertiesBundle::new_computed(&Collider::rectangle(50.0, 50.0), 1.0),
41+
MassPropertiesBundle::from_shape(&Rectangle::from_length(50.0), 1.0),
4242
))
4343
.id();
4444

crates/avian2d/examples/revolute_joint_2d.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn setup(mut commands: Commands) {
3838
square_sprite,
3939
Transform::from_xyz(0.0, -100.0, 0.0),
4040
RigidBody::Dynamic,
41-
MassPropertiesBundle::new_computed(&Collider::rectangle(50.0, 50.0), 1.0),
41+
MassPropertiesBundle::from_shape(&Rectangle::from_length(50.0), 1.0),
4242
))
4343
.id();
4444

crates/avian3d/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ enhanced-determinism = [
3434
"parry3d?/enhanced-determinism",
3535
"parry3d-f64?/enhanced-determinism",
3636
"bevy_math/libm",
37+
"bevy_heavy/libm",
3738
]
3839

3940
default-collider = ["dep:nalgebra"]
@@ -48,6 +49,7 @@ bevy_picking = ["bevy/bevy_picking"]
4849
serialize = [
4950
"dep:serde",
5051
"bevy/serialize",
52+
"bevy_heavy/serialize",
5153
"parry3d?/serde-serialize",
5254
"parry3d-f64?/serde-serialize",
5355
"bitflags/serde",
@@ -63,6 +65,7 @@ bench = false
6365
avian_derive = { path = "../avian_derive", version = "0.1" }
6466
bevy = { version = "0.15", default-features = false }
6567
bevy_math = { version = "0.15" }
68+
bevy_heavy = { version = "0.1" }
6669
libm = { version = "0.2", optional = true }
6770
parry3d = { version = "0.17", optional = true }
6871
parry3d-f64 = { version = "0.17", optional = true }
@@ -75,13 +78,14 @@ itertools = "0.13"
7578
bitflags = "2.5.0"
7679

7780
[dev-dependencies]
81+
examples_common_3d = { path = "../examples_common_3d" }
82+
benches_common_3d = { path = "../benches_common_3d" }
7883
bevy = { version = "0.15", default-features = false, features = [
7984
"bevy_gltf",
8085
"animation",
8186
] }
82-
examples_common_3d = { path = "../examples_common_3d" }
83-
benches_common_3d = { path = "../benches_common_3d" }
8487
bevy_math = { version = "0.15", features = ["approx"] }
88+
bevy_heavy = { version = "0.1", features = ["approx"] }
8589
approx = "0.5"
8690
criterion = { version = "0.5", features = ["html_reports"] }
8791
bevy_mod_debugdump = { git = "https://github.com/jakobhellermann/bevy_mod_debugdump" }

crates/avian3d/examples/chain_3d.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn setup(
5656
let current_particle = commands
5757
.spawn((
5858
RigidBody::Dynamic,
59-
MassPropertiesBundle::new_computed(&Collider::sphere(particle_radius), 1.0),
59+
MassPropertiesBundle::from_shape(&Sphere::new(particle_radius as f32), 1.0),
6060
Mesh3d(particle_mesh.clone()),
6161
MeshMaterial3d(particle_material.clone()),
6262
Transform::from_xyz(0.0, -i as f32 * particle_radius as f32 * 2.2, 0.0),

0 commit comments

Comments
 (0)