Jondolf / avian

ECS-driven 2D and 3D physics engine for the Bevy game engine.
https://crates.io/crates/avian3d
Apache License 2.0
1.55k stars 120 forks source link

Rework `Mass` and `Inertia` and add `GlobalAngularInertia` in 3D #500

Closed Jondolf closed 1 month ago

Jondolf commented 3 months ago

Objective

Part of the ongoing mass property rework proposed in #499.

Currently, the inverses of Mass and Inertia (poor name, since mass is also inertia) are cached in the InverseMass and InverseInertia components. They are updated automatically.

This dependency is confusing and adds complexity. The physics engine primarily just needs the inverse mass properties, while Mass and Inertia are mainly a user-facing API. In addition, when the Mass or Inertia 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 and InverseInertia, and instead store the inverses in Mass and AngularInertia directly. This internal representation is abstracted away through constructors and getters.

// Most derives and attributes omitted for brevity
#[derive(Component)]
pub struct Mass {
    inverse: Scalar,
}

let mut mass = Mass::new(5.0);
assert_eq!(mass.value(), 5.0);

mass.set(Mass::from_inverse(0.5));
assert_eq!(mass.value(), 2.0);
assert_eq!(mass.inverse(), 0.5);

2D:

#[derive(Component)]
pub struct AngularInertia {
    inverse: Scalar,
}

let mut angular_inertia = AngularInertia::new(5.0);
assert_eq!(angular_inertia.value(), 5.0);

angular_inertia.set(AngularInertia::from_inverse(0.5));
assert_eq!(angular_inertia.value(), 2.0);
assert_eq!(angular_inertia.inverse(), 0.5);

3D:

#[derive(Component)]
pub struct AngularInertia {
    inverse: Matrix,
}

let mut angular_inertia = AngularInertia::new(Vector::splat(2.0));
assert_eq!(angular_inertia.tensor().diagonal(), Vector::splat(2.0));

Notice how Inertia was also renamed to AngularInertia to be more explicit. Mass and AngularInertia 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