Skip to content

Force Rework#770

Merged
Jondolf merged 38 commits into
mainfrom
force-rework
Jul 17, 2025
Merged

Force Rework#770
Jondolf merged 38 commits into
mainfrom
force-rework

Conversation

@Jondolf

@Jondolf Jondolf commented Jun 30, 2025

Copy link
Copy Markdown
Member

Objective

The current force APIs are very limited, confusing, and cumbersome to use.

  • The "persistent" vs. "not persistent" force split is confusing. You cannot have both persistent and non-persistent forces at the same time.
  • Impulses should be applied to velocity immediately, not accumulated and applied some time later.
  • Impulses should not support persistence. Applying persistent impulses is not good practice; use forces instead.
  • The coordinate spaces are confusing and quite implicit. There is no first-party API for local forces.
  • Applying forces at a world point requires manually providing the global center of mass, which is annoying to compute.
  • Keeping forces and torque in separate components means that ExternalForce also needs to store a torque, just because of apply_force_at_point, duplicating data.
  • There is no way to apply acceleration that is independent of mass properties, beyond manually modifying velocity, which doesn't integrate with substepping.
  • To allow users to query for any rigid body's forces, ExternalForce, ExternalTorque, ExternalImpulse, and ExternalAngularImpulse are all required components for rigid bodies, wasting memory.

It seems like we need to redesign our force APIs from the ground up. Some goals for the new design are:

  • Have a simple and efficient component-driven API for persistent forces and torques.
  • Have another more ad hoc API for non-persistent forces and impulses that are cleared automatically. Something closer to Rigidbody.AddForce in Unity, or similar methods in other engines.
  • Support applying linear and angular acceleration.
  • Support both global and local forces, torques, impulses, and acceleration.
  • Minimize unnecessary memory usage and computational overhead.

To accomplish these goals efficiently, we also need to rework the solver's integration logic. Thus, a secondary goal of this PR is to rework and optimize velocity and position integration.

Solution

Replace ExternalForce, ExternalTorque, ExternalImpulse, and ExternalAngularImpulse with two types of APIs:

  1. Components for constant forces
  2. A Forces helper QueryData

Constant Forces

Constant forces and torques that persist across time steps can be applied using the following components:

  • ConstantForce: Applies a constant force in world space.
  • ConstantTorque: Applies a constant torque in world space.
  • ConstantLinearAcceleration: Applies a constant linear acceleration in world space.
  • ConstantAngularAcceleration: Applies a constant angular acceleration in world space.

They also have local space equivalents:

  • ConstantLocalForce: Applies a constant force in local space.
  • ConstantLocalTorque: Applies a constant torque in local space.
  • ConstantLocalLinearAcceleration: Applies a constant linear acceleration in local space.
  • ConstantLocalAngularAcceleration: Applies a constant angular acceleration in local space.

These components are useful for simulating continuously applied forces that are expected to remain the same across time steps, such as per-body gravity or force fields.

commands.spawn((
    RigidBody::Dynamic,
    Collider::capsule(0.5, 1.0),
    // Apply a constant force of 10 N in the positive Y direction.
    ConstantForce::new(0.0, 10.0, 0.0),
));

The forces are only constant in the sense that they persist across time steps. They can still be modified in systems like normal.

Forces

It is common to apply many individual forces and impulses to dynamic rigid bodies, and to clear them afterwards. This can be done using the Forces helper QueryData.

To use Forces, add it to a Query (without & or &mut), and use the associated methods to apply forces, impulses, and accelerations to the rigid bodies.

fn apply_forces(mut query: Query<Forces>) {
    for mut forces in &mut query {
        // Apply a force of 10 N in the positive Y direction to the entity.
        forces.apply_force(Vec3::new(0.0, 10.0, 0.0));
    }
}

The force is applied continuously during the physics step, and cleared automatically after the step is complete.

Forces can also apply forces and impulses at a specific point in the world. If the point is not aligned with the GlobalCenterOfMass, it will also apply a torque to the body.

// Apply an impulse at a specific point in the world.
// Unlike forces, impulses are applied immediately to the velocity,
forces.apply_linear_impulse_at_point(force, point);

As an example, you could implement radial gravity that pulls rigid bodies towards the world origin with a system like the following:

fn radial_gravity(mut query: Query<(Forces, &GlobalTransform)>) {
    for (mut forces, global_transform) in &mut query {
        // Compute the direction towards the center of the world.
        let direction = -global_transform.translation().normalize_or_zero();
        // Apply a linear acceleration of 9.81 m/s² towards the center of the world.
        forces.apply_linear_acceleration(direction * 9.81);
    }
}

Integration Rework

Currently, the solver runs velocity integration at each substep, applying forces, torques, and damping each time. This involves some math, transformations, and branching at each substep.

However, the velocity increment produced by external forces and torque is actually constant across substeps (excluding changes in the 3D inertia tensor for torque). The same is true for the right hand-side of the velocity damping formula. This means that we can actually precompute these values before the substepping loop, and apply the increments and damping with basic addition and multiplication, without any branching. Sweet! (Note that Rapier also does this.)

On the surface, it still seems a bit annoying to store an additional 32 bytes (or 20 bytes in 2D) per SolverBody for this, since the computations are still quite cheap, and integration is far from a bottleneck for us. However, these cached velocity increments can actually serve a secondary objective: we can accumulate forces and acceleration applied via Forces to the increments! This means that we don't need to repeat a bunch of computations for each type of force, and can instead funnel it all into a single optimized output.

Note that we do still need to handle local accelerations separately, because they can change as the body rotates between substeps.

Other Changes

  • Added GlobalCenterOfMass component for the global center of mass (used when applying forces at a world point, and in some other places)
  • Added locked axes to SolverBodyFlags
  • Renamed Torque type alias to more general AngularVector name
  • Reworked and cleaned up some mass property code

Performance

In a 3D scene with 10k dynamic bodies without colliders, just flying in the air, with 10 substeps, we can observe a small but noticeable performance improvement. Before and after:

Performance comparison

However the timers here are very unstable for some reason, and seem to change quite a lot between runs, so take the results with a grain of salt. The main thing is that this at least shouldn't regress performance.


Migration Guide

Force Rework

Avian's force APIs have been overhauled, and ExternalForce, ExternalTorque, ExternalImpulse, and ExternalAngularImpulse have been removed.

  • For persistent forces and torques, use the new ConstantForce and ConstantTorque components.
  • For non-persistent forces that get cleared automatically, use the new Forces helper QueryData.
  • Impulses can no longer be persistent. Use persistent forces instead.

The new ForcePlugin must be enabled for forces to function properly. It is included in PhysicsPlugins by default.

Jondolf added 27 commits May 5, 2025 18:39
We use a `ForceHelper` system param again, this time exposing a faster and more capable API that only does one `get_mut` query per entity.

Local forces and acceleration are also supported!
@Jondolf Jondolf added this to the 0.4 milestone Jun 30, 2025
@Jondolf Jondolf added C-Feature A new feature, making something new possible C-Performance Improvements or questions related to performance labels Jun 30, 2025
@Jondolf Jondolf added A-Dynamics Relates to rigid body dynamics: motion, mass, constraint solving, joints, CCD, and so on M-Migration-Guide A breaking change to Avian's public API that needs to be noted in a migration guide C-Usability A quality-of-life improvement that makes Avian easier to use D-Modest A moderate level of difficulty: suitable for simple features or challenging fixes labels Jun 30, 2025
@Jondolf Jondolf marked this pull request as ready for review June 30, 2025 16:40
@Jondolf Jondolf merged commit b5742e1 into main Jul 17, 2025
6 checks passed
@Jondolf Jondolf deleted the force-rework branch July 17, 2025 13:43
This was referenced Jul 17, 2025
Jondolf added a commit that referenced this pull request Jul 17, 2025
# Objective

#770 added a `Forces` helper. We should have tests to verify that it actually produces the correct results!

## Solution

Add some unit tests for `Forces`.
Jondolf added a commit that referenced this pull request Jul 18, 2025
# Objective

Whoops, #770 broke kinematic bodies! Gravity and external forces now affect them 😬

## Solution

Skip kinematic bodies during velocity integration.
Jondolf added a commit that referenced this pull request Jul 19, 2025
# Objective

#770 added a `Forces` helper `QueryData` for applying forces, impulses, and accelerations to dynamic bodies. The intent was that forces would wake up bodies by default, but this was actually not the case, as `Forces` has some components that sleeping bodies don't.

However, we should *also* have a way to allow forces not to wake up bodies. This can be important for optimization in some cases.

## Solution

Rework `Forces` to wake up bodies by default when applying non-zero forces, impulses, or accelerations. Additionally, add a `ForcesItem::non_waking` method that returns a `NonWakingForcesItem`. The two force types share a `RigidBodyForces` trait and the same API, but the former wakes up bodies while the latter doesn't.

The API looks like this:

```rust
// This does not wake up the body.
forces.non_waking().apply_force(force);

// This wakes up the body if it is sleeping.
forces.apply_force(force);
```

Many existing physics engines instead take a boolean argument in all force methods to control whether the force should wake the body up. However, I am of the opinion that sleeping should primarily be transparent to the user and just an internal optimization, and that intuitively you would expect a force or impulse to *always* have an effect on a body by default. Disabling waking for a force should be a more advanced use case, so it should not affect the common API.

Game engines like Unity and Godot actually seem to always wake up bodies for non-zero forces, with no option to configure this. I took the middle route, defaulting to waking up, but having an additional `non_waking` method to opt in to the non-waking behavior.

I considered some component-driven approaches like a `ForceWakingMode` component, but I think this needs to be easily configurable per force and not a persisted per-entity setting.
@6TELOIV

6TELOIV commented Oct 13, 2025

Copy link
Copy Markdown

This is awesome! I had tried making a Mario Galaxy-esq platformer before and it was quite tough to get it going, but this seems much easier to use!

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-Feature A new feature, making something new possible C-Performance Improvements or questions related to performance C-Usability A quality-of-life improvement that makes Avian easier to use D-Modest A moderate level of difficulty: suitable for simple features or challenging fixes M-Migration-Guide A breaking change to Avian's public API that needs to be noted in a migration guide

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants