Skip to content

Commit

Permalink
Add ColliderDensity (#194)
Browse files Browse the repository at this point in the history
# Objective

Setting the density of a collider is currently unnecessarily verbose and limiting. It requires giving the collider and density to a method that creates the mass property component:

```rust
ColliderMassProperties::new_computed(&Collider::ball(0.5), 2.5)
```

This makes it basically impossible to specify the density when the collider isn't known in advance, like for async colliders #190, and the API is just very inconvenient.

## Solution

Add a `ColliderDensity` component. The above becomes just this:

```rust
ColliderDensity(2.5)
```

Because `ColliderMassProperties` is always overwritten using this density, I also made its properties completely read-only with getter methods.

---

## Changelog

- Added `ColliderDensity`
- Added `Collider::mass_properties`
- Renamed `ColliderMassProperties::new_computed` to `ColliderMassProperties::new`
- Made the properties of `ColliderMassProperties` read-only
- Updated the documentation

## Migration Guide

```rust
// Before
let collider = Collider::ball(0.5);
commands.spawn((
    RigidBody::Dynamic,
    ColliderMassProperties::new_computed(&collider, 2.5),
    collider,
));

// After
commands.spawn((
    RigidBody::Dynamic,
    Collider::ball(0.5),
    ColliderDensity(2.5),
));
```
  • Loading branch information
Jondolf authored Oct 22, 2023
1 parent c9a96b0 commit 476be1e
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 35 deletions.
5 changes: 5 additions & 0 deletions src/components/collider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ impl Collider {
)
}

/// Computes the collider's mass properties based on its shape and a given density.
pub fn mass_properties(&self, density: Scalar) -> ColliderMassProperties {
ColliderMassProperties::new(self, density)
}

/// Creates a collider with a compound shape defined by a given vector of colliders with a position and a rotation.
///
/// Especially for dynamic rigid bodies, compound shape colliders should be preferred over triangle meshes and polylines,
Expand Down
109 changes: 90 additions & 19 deletions src/components/mass_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ pub struct MassPropertiesBundle {
}

impl MassPropertiesBundle {
/// Computes the mass properties from a given [`Collider`] and density.
/// Computes the mass properties for a [`Collider`] based on its shape and a given density.
pub fn new_computed(collider: &Collider, density: Scalar) -> Self {
let ColliderMassProperties {
mass,
Expand All @@ -223,7 +223,7 @@ impl MassPropertiesBundle {
inverse_inertia,
center_of_mass,
..
} = ColliderMassProperties::new_computed(collider, density);
} = collider.mass_properties(density);

Self {
mass,
Expand All @@ -234,27 +234,61 @@ impl MassPropertiesBundle {
}
}
}

/// The mass properties derived from a given collider shape and density.
/// The density of a [`Collider`], 1.0 by default. This is used for computing
/// the [`ColliderMassProperties`] for each collider.
///
/// ## Example
///
/// These will be added to the body's actual [`Mass`], [`InverseMass`], [`Inertia`], [`InverseInertia`] and [`CenterOfMass`] components.
/// ```
/// use bevy::prelude::*;
/// # #[cfg(feature = "2d")]
/// # use bevy_xpbd_2d::prelude::*;
/// # #[cfg(feature = "3d")]
/// # use bevy_xpbd_3d::prelude::*;
///
/// // Spawn a body with a collider that has a density of 2.5
/// fn setup(mut commands: Commands) {
/// commands.spawn((
/// RigidBody::Dynamic,
/// Collider::ball(0.5),
/// ColliderDensity(2.5),
/// ));
/// }
/// ```
#[derive(Reflect, Clone, Copy, Component, Debug, Deref, DerefMut, PartialEq, PartialOrd)]
#[reflect(Component)]
pub struct ColliderDensity(pub Scalar);

impl ColliderDensity {
/// The density of the [`Collider`] is zero. It has no mass.
pub const ZERO: Self = Self(0.0);
}

impl Default for ColliderDensity {
fn default() -> Self {
Self(1.0)
}
}

/// An automatically added component that contains the read-only mass properties of a [`Collider`].
/// The density used for computing the mass properties can be configured using the [`ColliderDensity`]
/// component.
///
/// You should generally not create or modify this directly. Instead, you can generate this automatically using a given collider shape and density with the associated `from_shape_and_density` method.
/// These mass properties will be added to the [rigid body's](RigidBody) actual [`Mass`],
/// [`InverseMass`], [`Inertia`], [`InverseInertia`] and [`CenterOfMass`] components.
#[derive(Reflect, Clone, Copy, Component, Debug, PartialEq)]
#[reflect(Component)]
pub struct ColliderMassProperties {
/// Mass given by collider.
pub mass: Mass,
pub(crate) mass: Mass,
/// Inverse mass given by collider.
pub inverse_mass: InverseMass,
pub(crate) inverse_mass: InverseMass,
/// Inertia given by collider.
pub inertia: Inertia,
pub(crate) inertia: Inertia,
/// Inverse inertia given by collider.
pub inverse_inertia: InverseInertia,
pub(crate) inverse_inertia: InverseInertia,
/// Local center of mass given by collider.
pub center_of_mass: CenterOfMass,
/// Density used for calculating other mass properties.
pub density: Scalar,
pub(crate) center_of_mass: CenterOfMass,
}

impl ColliderMassProperties {
Expand All @@ -265,13 +299,13 @@ impl ColliderMassProperties {
inertia: Inertia::ZERO,
inverse_inertia: InverseInertia::ZERO,
center_of_mass: CenterOfMass::ZERO,
density: 0.0,
};
}

impl ColliderMassProperties {
/// Computes mass properties from a given [`Collider`] and density.
pub fn new_computed(collider: &Collider, density: Scalar) -> Self {
///
/// Because [`ColliderMassProperties`] is read-only, adding this as a component manually
/// has no effect. The mass properties will be recomputed using the [`ColliderDensity`].
pub fn new(collider: &Collider, density: Scalar) -> Self {
let props = collider.shape_scaled().mass_properties(density);

Self {
Expand All @@ -289,10 +323,47 @@ impl ColliderMassProperties {
inverse_inertia: InverseInertia(props.reconstruct_inverse_inertia_matrix().into()),

center_of_mass: CenterOfMass(props.local_com.into()),

density,
}
}

/// Get the [mass](Mass) of the [`Collider`].
pub fn mass(&self) -> Scalar {
self.mass.0
}

/// Get the [inverse mass](InverseMass) of the [`Collider`].
pub fn inverse_mass(&self) -> Scalar {
self.inverse_mass.0
}

/// Get the [inerta](Inertia) of the [`Collider`].
#[cfg(feature = "2d")]
pub fn inertia(&self) -> Scalar {
self.inertia.0
}

/// Get the [inertia tensor](InverseInertia) of the [`Collider`].
#[cfg(feature = "3d")]
pub fn inertia(&self) -> Matrix3 {
self.inertia.0
}

/// Get the [inverse inertia](InverseInertia) of the [`Collider`].
#[cfg(feature = "2d")]
pub fn inverse_inertia(&self) -> Scalar {
self.inverse_inertia.0
}

/// Get the [inverse inertia](InverseInertia) of the [`Collider`].
#[cfg(feature = "3d")]
pub fn inverse_inertia(&self) -> Matrix3 {
self.inverse_inertia.0
}

/// Get the [local center of mass](CenterOfMass) of the [`Collider`].
pub fn center_of_mass(&self) -> Vector {
self.center_of_mass.0
}
}

impl Default for ColliderMassProperties {
Expand Down
48 changes: 38 additions & 10 deletions src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,54 @@ use derive_more::From;
///
/// ## Adding mass properties
///
/// You should always give dynamic rigid bodies mass properties. The easiest way to do this is to [add a collider](Collider), since colliders
/// by default have [their own mass properties](ColliderMassProperties) that are added to the body's own mass properties.
/// You should always give dynamic rigid bodies mass properties so that forces are applied to them correctly.
///
/// The easiest way to add mass properties is to simply [add a collider](Collider):
///
/// ```ignore
/// // The mass properties will be computed from a ball shape with a radius of 0.5 and a density of 1.
/// commands.spawn((RigidBody::Dynamic, Collider::ball(0.5)));
/// ```
///
/// If you don't want to add a collider, you can instead add a [`MassPropertiesBundle`] with the mass properties computed from a collider
/// shape using the [`MassPropertiesBundle::new_computed`](MassPropertiesBundle#method.new_computed) method.
/// This will automatically compute the [collider's mass properties](ColliderMassProperties)
/// and add them to the body's own mass properties like [`Mass`], [`Inertia`] and so on.
///
/// By default, each collider has a density of `1.0`. This can be configured with
/// the [`ColliderDensity`] component:
///
/// ```ignore
/// commands.spawn((
/// RigidBody::Dynamic,
/// Collider::ball(0.5),
/// ColliderDensity(2.5),
/// ));
/// ```
///
/// If you don't want to add a collider, you can instead add a [`MassPropertiesBundle`]
/// with the mass properties computed from a collider shape using the
/// [`MassPropertiesBundle::new_computed`](MassPropertiesBundle#method.new_computed) method.
///
/// ```ignore
/// // This is equivalent to the earlier approach, but no collider will be added.
/// commands.spawn((RigidBody::Dynamic, MassPropertiesBundle::new_computed(&Collider::ball(0.5), 1.0)));
/// commands.spawn((
/// RigidBody::Dynamic,
/// MassPropertiesBundle::new_computed(&Collider::ball(0.5), 2.5),
/// ));
/// ```
///
/// If you want, you can also define the mass properties explicitly by adding the components manually.
/// Note that the mass properties of colliders are added on top of the existing mass properties, so if you
/// want to define the body's mass properties explicitly, you might want to add
/// [`ColliderMassProperties::ZERO`](ColliderMassProperties#associatedconstant.ZERO) to the colliders.
/// You can also specify the exact values of the mass properties by adding the components manually.
/// To avoid the collider mass properties from being added to the body's own mass properties,
/// you can simply set the collider's density to zero.
///
/// ```ignore
/// // Create a rigid body with a mass of 5.0 and a collider with no mass
/// commands.spawn((
/// RigidBody::Dynamic,
/// Collider::ball(0.5),
/// ColliderDensity(0.0),
/// Mass(5.0),
/// // ...the rest of the mass properties
/// ));
/// ```
#[derive(Reflect, Clone, Copy, Component, Debug, Default, PartialEq, Eq)]
#[reflect(Component)]
pub enum RigidBody {
Expand Down
3 changes: 1 addition & 2 deletions src/components/world_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ mod tests {
app.world.spawn(original_mass_props.clone());

// Create collider mass properties
let collider_mass_props =
ColliderMassProperties::new_computed(&Collider::capsule(7.4, 2.1), 14.3);
let collider_mass_props = Collider::capsule(7.4, 2.1).mass_properties(14.3);

// Get the mass properties and then add and subtract the collider mass properties
let mut query = app.world.query::<MassPropertiesQuery>();
Expand Down
23 changes: 19 additions & 4 deletions src/plugins/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl Plugin for PreparePlugin {
.chain()
.run_if(any_new_physics_entities),
update_mass_properties,
clamp_collider_density,
clamp_restitution,
// all the components we added above must exist before we can simulate the bodies
apply_deferred,
Expand Down Expand Up @@ -373,16 +374,21 @@ fn init_colliders(
Entity,
&Collider,
Option<&ColliderAabb>,
Option<&ColliderDensity>,
Option<&ColliderMassProperties>,
Option<&PreviousColliderMassProperties>,
),
Added<Collider>,
>,
) {
for (entity, collider, aabb, mass_properties, previous_mass_properties) in &mut colliders {
for (entity, collider, aabb, density, mass_properties, previous_mass_properties) in
&mut colliders
{
let density = *density.unwrap_or(&ColliderDensity::default());
commands.entity(entity).insert((
*aabb.unwrap_or(&ColliderAabb::from_shape(collider.shape_scaled())),
*mass_properties.unwrap_or(&ColliderMassProperties::new_computed(collider, 1.0)),
density,
*mass_properties.unwrap_or(&collider.mass_properties(density.0)),
*previous_mass_properties.unwrap_or(&PreviousColliderMassProperties(
ColliderMassProperties::ZERO,
)),
Expand Down Expand Up @@ -504,12 +510,14 @@ fn update_mass_properties(
&mut PreviousColliderTransform,
&ColliderParent,
&Collider,
&ColliderDensity,
&mut ColliderMassProperties,
&mut PreviousColliderMassProperties,
),
Or<(
Changed<Collider>,
Changed<ColliderTransform>,
Changed<ColliderDensity>,
Changed<ColliderMassProperties>,
)>,
>,
Expand All @@ -521,6 +529,7 @@ fn update_mass_properties(
mut previous_collider_transform,
collider_parent,
collider,
density,
mut collider_mass_properties,
mut previous_collider_mass_properties,
) in &mut colliders
Expand All @@ -539,8 +548,7 @@ fn update_mass_properties(

// Update previous and current collider mass props
previous_collider_mass_properties.0 = *collider_mass_properties;
*collider_mass_properties =
ColliderMassProperties::new_computed(collider, collider_mass_properties.density);
*collider_mass_properties = collider.mass_properties(density.max(Scalar::EPSILON));

// Add new collider mass props to the body's mass props
mass_properties += ColliderMassProperties {
Expand Down Expand Up @@ -602,3 +610,10 @@ fn clamp_restitution(mut query: Query<&mut Restitution, Changed<Restitution>>) {
restitution.coefficient = restitution.coefficient.clamp(0.0, 1.0);
}
}

/// Clamps [`ColliderDensity`] to be above 0.0.
fn clamp_collider_density(mut query: Query<&mut ColliderDensity, Changed<ColliderDensity>>) {
for mut density in &mut query {
density.0 = density.max(Scalar::EPSILON);
}
}
1 change: 1 addition & 0 deletions src/plugins/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl Plugin for PhysicsSetupPlugin {
.register_type::<Inertia>()
.register_type::<InverseInertia>()
.register_type::<CenterOfMass>()
.register_type::<ColliderDensity>()
.register_type::<ColliderMassProperties>()
.register_type::<PreviousColliderMassProperties>()
.register_type::<LockedAxes>()
Expand Down

0 comments on commit 476be1e

Please sign in to comment.