Skip to content

Expose true continuous force/torque application in the Rapier physics API #1495

Description

@wachipayox

Context

Hi! While experimenting with Create: Offroad's Wheel Mount suspension, I ran into a physics behavior that led me to investigate how forces are passed from addons through Sable to Rapier.

I want to disclose upfront that the code investigation and prototype implementation were done with substantial assistance from ChatGPT 5.6 Sol. I personally reproduced the behavior and performed the in-game validation of the different prototypes, but I don't have deep familiarity with Sable/Rapier internals myself.

I'm opening this mainly to share what we found and ask whether exposing a true continuous-force primitive through Sable's public API would make sense.

The behavior that led to this

The problem appeared in a vehicle/sublevel using Offroad Wheel Mount suspension when the wheels were working at an angle while other colliders of the same sublevel were also supporting the vehicle against the ground.

Under those conditions, the suspension could produce an unwanted horizontal force on the sublevel, resulting in a noticeable drift/drag even when that movement should not have been occurring.

Image

Removing the Wheel Mount suspension contribution made the effect disappear.

What became interesting during testing was that the effect also changed dramatically depending on how the exact same suspension contribution was applied over the physics timestep.

This is what led us to investigate the distinction between impulses and continuous forces in Sable.

What we found in Sable

As far as I understand it, Sable currently exposes impulse-based operations for addons, but not a public way to submit an actual continuous Rapier force.

For example, the current Rapier JNI implementation of applyForce ultimately uses:

rb.apply_impulse(force, wake_up > 0);
rb.apply_torque_impulse(torque_impulse, wake_up > 0);

and applyForceAndTorque similarly uses:

rb.apply_impulse(force, wake_up > 0);
rb.apply_torque_impulse(torque, wake_up > 0);

Those are valid operations for instantaneous changes in momentum.

Rapier also has separate continuous-force operations:

rb.add_force(...)
rb.add_torque(...)

which are integrated as forces during the physics step.

I couldn't find a public Sable API that allows an addon to explicitly choose this behavior.

First experiment: distributing the impulse

The Wheel Mount calculation produces a contribution integrated over the timestep, effectively an impulse:

J = F * dt

As an experiment, we kept the same total contribution but distributed it over multiple smaller physics steps.

Conceptually:

J / 15 -> solve
J / 15 -> solve
J / 15 -> solve
...
15 times

In my test setup, the unwanted drift/drag disappeared when the suspension contribution was applied this way.

Importantly, simply having more physics substeps was not enough: the suspension contribution itself needed to be distributed across them.

That made it look like the timing/semantics of the force application were relevant, rather than simply the magnitude of the suspension force.

The obvious problem is performance: using many Rapier solves to approximate this behavior is far too expensive as a real solution.

Second experiment: Rapier continuous forces

We then tried reproducing the same behavior without additional physics solves.

The already-integrated contribution was converted back into its corresponding force:

F = J / dt

and a small experimental patched Sable native exposed Rapier's continuous force accumulator.

The relevant native operation was essentially:

rb.add_force(force, wake_up);
rb.add_torque(torque, wake_up);

The normal Rapier physics step then runs once.

Result

I tested this patched version in-game using the same setup where the unwanted horizontal drift/drag was reproducible.

Applying the Wheel Mount contribution through Rapier's continuous force accumulator made the unwanted force disappear while the suspension continued to work normally.

It reproduced the good behavior of the multi-substep experiment, but required only one normal Rapier solve.

So, for the behavior I was testing:

Current impulse application
    -> unwanted horizontal drift/drag can appear

Same contribution distributed over many smaller solves
    -> unwanted behavior disappears
    -> very expensive

Same contribution through Rapier add_force/add_torque
    -> unwanted behavior disappears
    -> one normal physics solve

I'm not claiming from this experiment that Sable's existing impulse behavior is generally incorrect. Impulses are obviously a valid and necessary physical primitive.

What this made me interested in is allowing addons to explicitly choose between an impulse and a force when the distinction matters.

Possible API

Would Sable be open to exposing continuous-force operations alongside the existing impulse API?

The exact naming and API placement are obviously up to you, but conceptually something like:

RigidBodyHandle.addForce(Vector3dc force);
RigidBodyHandle.addTorque(Vector3dc torque);
RigidBodyHandle.addForceAndTorque(
    Vector3dc force,
    Vector3dc torque
);

and potentially:

RigidBodyHandle.addForceAtPoint(
    Vector3dc position,
    Vector3dc force
);

backed by the corresponding Rapier continuous-force operations.

Existing behavior should remain unchanged

I am not suggesting changing the semantics of any existing impulse methods.

Existing APIs may already have addons depending on their current behavior, so silently changing an impulse into a force could break physics behavior elsewhere.

The idea would only be to expose the missing primitive as an additional opt-in API:

applyImpulse(...) -> instantaneous momentum change

addForce(...) -> force integrated during the physics step

An addon could then explicitly choose whichever primitive matches the system it is implementing.

Potential use cases

Although I found this while experimenting with Wheel Mount suspension, the API itself would not need to be Offroad-specific. It could potentially be useful for other addons that need forces to participate in the normal physics integration

Prototype

I currently have a working prototype based on a patched Sable Rapier native, tested in-game.

I don't think having an addon replace Sable's native library is a good long-term solution, which is the main reason I'm asking whether this primitive could be exposed officially instead.

If this is something you'd be interested in having in Sable, I can share the exact prototype changes and testing details. With AI assistance I can also put together a PR following whatever API design you prefer.

And again, just to be transparent: I can reliably reproduce and test the behavior described above, but the deeper Sable/Rapier code analysis and prototype implementation were done with AI assistance, so I'd rather defer to the maintainers on the appropriate API design and any subtleties of the native implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions