bjornbytes/lovr

Fixed Timestep Physics Interpolation

bjornbytes opened this issue · 1 comments

Fixed timesteps are important to keep physics stable, and allow physics updates to happen at a rate independent of rendering.

VR renders at a high framerate.

The easiest and most common way to do physics in LÖVR is like this:

function lovr.update(dt)
  world:update(dt)
end

Or this:

function lovr.update(dt)
  world:update(1 / lovr.headset.getRefreshRate())
end

Both have issues. The first not using a fixed timestep. The second is based on headset frameloop speed instead of wallclock time, and risks discontinuities during frame drops or e.g. if the user looks at an expensive part of a scene and the VR runtime steps app submission rate down to half the refresh rate, the physics will become slow motion.

Both of them also use an unnecessarily high tick rate for many use cases, which leads to high CPU usage.

A better approach is to use a fixed timestep and interpolate collider poses. Physics will add delta times to an accumulator and perform zero or more updates depending on how much time remains in the accumulator. Then, for rendering, colliders will use poses interpolated between their 2 most recent states.

LÖVR should be minimal, can't people just do this in Lua or use a library?

It's possible, but there are a few reasons why it's better done in C:

  • Doing a fixed timestep and interpolating is usually what you want.
  • The math is straightforward, it doesn't depend on anything specific that would differ from project-to-project.
  • Doing it in Lua requires tables or permanent vectors for every Collider, and will likely be slower or impact GC.
  • People are lazy. If someone's prototyping quickly, they aren't going to stop to do this.

Bullet has interpolation built-in. Here's a discussion with recommendations about doing interpolation in Jolt.

Rough implementation details:

  • Some way to set the tick rate on a World e.g. World:setTickRate(30).
  • World:update(dt) will feed dt to an accumulator, performing zero or more updates based on the tick rate.
    • Being careful to avoid a death spiral for high delta times (this may be best left for Lua to monitor and adjust).
  • Before a tick, the World saves the pose of each active Collider (if it's the last tick for a given :update call).
  • After running all the ticks, the World computes interpolated/extrapolated poses for each active Collider.
  • Methods to get the interpolated pose of a Collider (instead of, or in addition to, the raw pose).
  • Possibly ways to enable/disable interpolation for a World or Collider.
  • If a Collider is positioned manually with :setPose, it should probably not be interpolated (or there should be a way to disable interpolation for it). That way things like VR hands stay as responsive as possible.

This has been implemented.