Skip to content

Interpolation

Simulation runs at a fixed timestep (default 1/60s); rendering runs at the variable frame rate. Interpolation smooths component values between fixed frames to prevent visual stuttering.

How it works

The four steps below map onto the per-frame phase order:

  1. Before each fixed update: the previous component value is saved (InterpolatedPrevious<T>).
  2. During fixed update: systems update the component normally.
  3. During variable update: Interpolated<T> is computed by blending previous → current based on progress through the fixed frame.
  4. Rendering: read Interpolated<T> for smooth visuals.

Setup

1. Mark fields as interpolated

In your template, use the [Interpolated] attribute:

public partial class SmoothEntity : ITemplate, ITagged<MyTags.Smooth>
{
    [Interpolated]
    Position Position = default;

    [Interpolated]
    Rotation Rotation = default;

    OrbitParams OrbitParams;
}

Each interpolated field generates three components: Position, Interpolated<Position>, and InterpolatedPrevious<Position>.

2. Define interpolation functions

Write static methods with [GenerateInterpolatorSystem] defining how each component type blends. The source generator creates a presentation-phase system with a nested Burst-compiled job for each:

public static class MyInterpolators
{
    const string GroupName = "MyGameInterpolators";

    [GenerateInterpolatorSystem("PositionInterpolatedUpdater", GroupName)]
    [BurstCompile]
    public static void InterpolatePosition(
        in Position a, in Position b, ref Position result, float t)
    {
        result.Value = math.lerp(a.Value, b.Value, t);
    }

    [GenerateInterpolatorSystem("RotationInterpolatedUpdater", GroupName)]
    [BurstCompile]
    public static void InterpolateRotation(
        in Rotation a, in Rotation b, ref Rotation result, float t)
    {
        // nlerp is cheaper than slerp and sufficient for small angular deltas
        result.Value = math.nlerp(a.Value, b.Value, t);
    }
}

GroupName groups related interpolators so they can be registered together.

3. Register with WorldBuilder

The source generator creates Add{GroupName}, registering all interpolators in the group — both the previous-frame savers and the variable-update blending systems:

var world = new WorldBuilder()
    .AddTemplate(SmoothEntity.Template)
    .AddMyGameInterpolators()  // Generated
    .Build();

4. Create entities with interpolated values

SetInterpolated() initializes all three components (current, interpolated, previous) in sync:

world.AddEntity<MyTags.Smooth>()
    .SetInterpolated(new Position(startPos))
    .SetInterpolated(new Rotation(startRot))
    .Set(new OrbitParams { ... });

5. Read interpolated values for rendering

In a variable-update rendering system, read Interpolated<T> instead of the raw component:

[ExecuteIn(SystemPhase.Presentation)]
public partial class RenderSystem : ISystem
{
    [ForEachEntity(typeof(MyTags.Smooth))]
    void Execute(in Interpolated<Position> pos, in Interpolated<Rotation> rot, in GameObjectId id)
    {
        var go = _goManager.Resolve(id);
        go.transform.position = (Vector3)pos.Value.Value;
        go.transform.rotation = rot.Value.Value;
    }
}

What gets generated

For each [GenerateInterpolatorSystem] method, the source generator produces:

  • An ISystem with a nested Burst-compiled job that runs during the presentation phase, iterating entities with the component and blending previous → current via your function.
  • An extension method on WorldBuilder (one per group) that registers all InterpolatedPreviousSaver<T> instances and interpolator systems in the group.

You write the math; scheduling, dependency tracking, and registration are handled.

Best practices

  • Only interpolate visual components — positions, rotations, scales, colors. Don't interpolate gameplay state like health or ammo.
  • Use SetInterpolated() at creation — keeps all three components in sync and avoids a first-frame visual pop. If you forget and the component has no default, Trecs throws.
  • Group interpolators by project — share a GroupName constant so one Add{GroupName}() registers everything.
  • Prefer nlerp over slerp for rotations — angular deltas between fixed frames are small enough that the difference is imperceptible, and nlerp is much cheaper.

See also