-
-
Notifications
You must be signed in to change notification settings - Fork 132
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
Rework Mass
and Inertia
and add GlobalAngularInertia
in 3D
#500
Merged
Conversation
This file contains 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
…tia` constructors
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-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
Part of the ongoing mass property rework proposed in #499.
Currently, the inverses of
Mass
andInertia
(poor name, since mass is also inertia) are cached in theInverseMass
andInverseInertia
components. They are updated automatically.This dependency is confusing and adds complexity. The physics engine primarily just needs the inverse mass properties, while
Mass
andInertia
are mainly a user-facing API. In addition, when theMass
orInertia
are changed, their inverses will be out of sync until the systems responsible for updating them run. The components for the inverse and non-inverse versions should be more tightly coupled or combined to reduce complexity and lessen misuse.In 3D, angular inertia also depends on the orientation of the object. The local angular inertia tensor is stored in
Inertia
, and the world-space version is currently computed multiple times per substep. This is expensive. The rotation is only changed once per substep, during position/rotation integration, so the world-space angular inertia should be cached in its own read-only component.Solution
Remove
InverseMass
andInverseInertia
, and instead store the inverses inMass
andAngularInertia
directly. This internal representation is abstracted away through constructors and getters.2D:
3D:
Notice how
Inertia
was also renamed toAngularInertia
to be more explicit.Mass
andAngularInertia
also have a lot more helpers and documentation than before.Secondly, in 3D, the world-space angular inertia is now cached in a
GlobalAngularInertia
component. It is updated after position/rotation integration and when the local angular inertia is changed.Discussion
We might still want the non-inverse versions to be stored as well, at least for angular inertia. I'm not sure if it would be worth it though, so it would need benchmarking.
We could also consider storing the world-space angular inertia in
AngularInertia
directly, and design the API such that the local and global versions are always kept in sync. This would require taking a rotation in constructors though, and make the type less general-purpose, so I don't think it's worth it at this point.We could also consider caching the effective mass and angular inertia, which takes
LockedAxes
into account. They are quite cheap to compute though, so I'm not sure if it would be worth it. Needs benchmarking.Migration Guide
InverseMass
andInverseInertia
have been removed, andInertia
has been renamed toAngularInertia
. Use the methods onMass
andAngularInertia
to access the inverse versions.Mass
andAngularInertia
can no longer be constructed manually. Use the constructors such asnew
orfrom_inverse
instead.ColliderMassProperties
now only stores rawmass
,angular_inertia
, andcenter_of_mass
values.RigidBodyQueryItem
methodseffective_inv_mass
andeffective_world_inv_inertia
have been renamed toeffective_inverse_mass
andeffective_global_inverse_inertia
.