-
-
Notifications
You must be signed in to change notification settings - Fork 186
Add MassPropertyPlugin
#532
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
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Lubba-64
added a commit
to Lubba-64/avian
that referenced
this pull request
Oct 23, 2024
This reverts commit 55d6815.
Merged
Jondolf
added a commit
that referenced
this pull request
Dec 6, 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 #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.
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-Breaking-Change
This change removes or changes behavior or APIs, requiring users to adapt
C-Code-Quality
Improvements to code readability, maintainability, or best practices
C-Enhancement
New feature or request
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Objective
Logic related to mass properties is currently a bit scattered around (
PreparePlugin
,ColliderBackendPlugin
...), and there is also no way to opt out of automatic updating of mass properties. It would be nice to encapsulate this logic inside a single plugin, especially as we rework mass properties (#499) and add more APIs and functionality for working with mass.This also contributes to the goal of removing
PreparePlugin
, which is currently full of various miscallaneous and unrelated logic.Solution
Add a
MassPropertyPlugin
. It updates mass properties of rigid bodies when colliders are added or removed, or when theirColliderMassProperties
are updated. It also logs warnings for invalid mass properties, and updatesGlobalAngularInertia
.Previously, the logic for updating mass properties based on
ColliderMassProperties
changes was heavily tied to theColliderMassProperties
update logic inColliderBackendPlugin
, because it required knowing both the previous and new mass properties of the collider. Same for mass property updates caused by adding or removing colliders.Now, it doesn't directly change the body's mass properties, but instead triggers an
OnColliderMassPropertiesChanged
event that theMassPropertyPlugin
(and users) can react to. This makes the collider logic only responsible for managingColliderMassProperties
, and the logic for rigid bodies is self-contained.Migration Guide
The
MassPropertyPlugin
is now needed to update mass properties automatically based on attached colliders. Most apps won't need to add it manually, as it is included in thePhysicsPlugins
plugin group by default.