Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mass Property Rework #574

Merged
merged 37 commits into from
Dec 6, 2024
Merged

Mass Property Rework #574

merged 37 commits into from
Dec 6, 2024

Conversation

Jondolf
Copy link
Owner

@Jondolf Jondolf commented Dec 4, 2024

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 Rework Mass and Inertia and add GlobalAngularInertia in 3D #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).

// 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.

// 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.

// 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 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:

// 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:

// 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.

@Jondolf Jondolf added C-Enhancement New feature or request A-Dynamics Relates to rigid body dynamics: motion, mass, constraint solving, joints, CCD, and so on C-Usability A quality-of-life improvement that makes Avian easier to use labels Dec 4, 2024
@Jondolf Jondolf added this to the 0.2 milestone Dec 4, 2024
@Jondolf Jondolf enabled auto-merge (squash) December 6, 2024 21:27
@Jondolf Jondolf merged commit 6bfa89e into main Dec 6, 2024
4 checks passed
@Jondolf Jondolf deleted the mass-property-rework-2 branch December 6, 2024 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Dynamics Relates to rigid body dynamics: motion, mass, constraint solving, joints, CCD, and so on C-Enhancement New feature or request C-Usability A quality-of-life improvement that makes Avian easier to use
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rework mass properties
1 participant