Interpolation & Easing

Interpolation means finding a value between two others. It's behind almost every smooth motion in games: doors opening, cameras following, health bars draining, colors fading. This guide covers the four main ways to do it, which to pick, and how to make it feel good with easing.

A dimmer switch between "off" (A) and "full brightness" (B). The knob position is t: 0 is fully A, 1 is fully B, 0.5 is halfway. Interpolation is just "set the knob to t, what's the brightness?" Easing is changing how fast you turn the knob: slowly at first, then faster, then slowly again.

Lerp: the core idea

Lerp (linear interpolation) is simply: a + (b - a) * t.

Mathf.Lerp(0f, 10f, 0.25f);                  // 2.5
Vector3.Lerp(pointA, pointB, 0.5f);          // halfway between the points
Color.Lerp(Color.white, Color.red, 0.8f);    // mostly red
Quaternion.Slerp(rotA, rotB, 0.5f);          // halfway rotation

Lerp clamps t to 0-1. LerpUnclamped doesn't, so t = 1.2 overshoots past b (useful for bouncy effects).

The four patterns

PatternFeels likeArrives?Use forTool
1. Fixed duration"Takes exactly 0.5 s"Yes, on timeDoors, UI transitions, cutscenesLerp with t = elapsed / duration
2. Constant speed"Moves at 5 m/s"Yes, exactlyEnemies walking, turrets turningMoveTowards, RotateTowards
3. Smooth follow"Fast when far, slows as it gets close"Almost (gets very close)Cameras, UI following a target, smoothing inputLerp with a decay factor
4. Spring"Has momentum, eases in and out"Yes (approximately)Cameras, zoom, aim smoothingSmoothDamp
00.511.5starttargettime (seconds)valueFixed + easeon timeMoveTowardsconstant speedexp-decay Lerpnever quite arrivesSmoothDampspring-like
The four patterns moving from a start value to a target. The eased fixed-duration move (highlighted) lands on time; MoveTowards is a straight line; the exp-decay Lerp jumps quickly then crawls; SmoothDamp starts gently because it builds up speed like a spring.

1. Fixed duration (with easing)

using System.Collections;
using UnityEngine;

public class SlidingDoor : MonoBehaviour
{
    [SerializeField] private Vector3 openOffset = new Vector3(2f, 0f, 0f);
    [SerializeField] private float duration = 0.6f;
    [SerializeField] private AnimationCurve ease = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);

    public IEnumerator Open()
    {
        Vector3 start = transform.position;
        Vector3 end = start + openOffset;

        for (float elapsed = 0f; elapsed < duration; elapsed += Time.deltaTime)
        {
            float t = elapsed / duration;                        // 0 -> 1 over "duration" seconds
            transform.position = Vector3.Lerp(start, end, ease.Evaluate(t));
            yield return null;
        }
        transform.position = end; // make sure we land exactly
    }
}

start and end are stored once. Only t changes. This is what DOTween does for you in one line.

2. Constant speed

// Never overshoots, arrives exactly, same speed on every machine
transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, degreesPerSec * Time.deltaTime);
health = Mathf.MoveTowards(health, targetHealth, drainPerSec * Time.deltaTime); // draining bar

3. Smooth follow (frame-rate independent)

You'll often see this in tutorials:

// Common, but frame-rate DEPENDENT: faster at high FPS
transform.position = Vector3.Lerp(transform.position, target.position, 0.1f);

It moves 10% of the remaining distance per frame, so 144 FPS catches up much faster than 30 FPS. Multiplying by Time.deltaTime (0.1f * Time.deltaTime) is closer but still not exact. The correct version uses an exponential:

[SerializeField] private float sharpness = 10f; // higher = snappier

void LateUpdate()
{
    // Same feel at any frame rate
    float t = 1f - Mathf.Exp(-sharpness * Time.deltaTime);
    transform.position = Vector3.Lerp(transform.position, target.position, t);
}
Why exp?This pattern closes a fixed fraction of the gap per second, not per frame. Two 8 ms frames then do exactly the same as one 16 ms frame. It never quite reaches the target mathematically, but gets within a tiny distance very quickly.

4. SmoothDamp (spring)

private Vector3 velocity; // SmoothDamp stores its speed here between frames

void LateUpdate()
{
    transform.position = Vector3.SmoothDamp(transform.position, target.position, ref velocity, 0.25f);
    // 0.25 = roughly how long it takes to reach the target
}

Unlike pattern 3, SmoothDamp accelerates from rest too, so it feels heavier and more natural. Great for cameras. Also available as Mathf.SmoothDamp and Mathf.SmoothDampAngle.

Easing

Easing reshapes t before you use it. t still goes 0 → 1, but not in a straight line.

public static class Ease
{
    public static float InQuad(float t)    => t * t;                          // slow start
    public static float OutQuad(float t)   => 1f - (1f - t) * (1f - t);       // slow end
    public static float InOutQuad(float t) => t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f;
    public static float OutCubic(float t)  => 1f - Mathf.Pow(1f - t, 3f);     // strong slow end
    public static float SmoothStep(float t)=> t * t * (3f - 2f * t);           // gentle in and out

    public static float OutBack(float t)   // overshoots slightly, then settles
    {
        const float c1 = 1.70158f, c3 = c1 + 1f;
        return 1f + c3 * Mathf.Pow(t - 1f, 3f) + c1 * Mathf.Pow(t - 1f, 2f);
    }
}

// Usage (use LerpUnclamped for OutBack so the overshoot isn't cut off)
transform.localScale = Vector3.LerpUnclamped(Vector3.zero, Vector3.one, Ease.OutBack(t));
Lineart = 01OutQuadt = 01InOutQuadt = 01OutBackt = 01
Easing reshapes t: the horizontal axis is time, the vertical axis is how far along the value is. The faint line marks the target (1); steep means fast. OutBack briefly goes past the target, then settles.
EaseFeelsGood for
LinearMechanicalRotating fans, progress bars, conveyor belts
Out (Quad/Cubic)Fast then gentle stopMost UI and things arriving: the default choice
InSlow then fast exitThings leaving the screen, falling
InOut / SmoothStepGentle both endsCamera moves, doors, elevators
OutBackPlayful overshootPop-ups, pickups, buttons

AnimationCurve fields let designers draw their own easing in the Inspector: [SerializeField] AnimationCurve curve; then curve.Evaluate(t).

Other helpers

InverseLerp and remapping

// "Where is value between a and b?" -> 0..1
float t = Mathf.InverseLerp(10f, 50f, speed);   // speed 30 -> 0.5

// Remap one range to another: speed 10..50 -> engine pitch 0.8..2.0
float pitch = Mathf.Lerp(0.8f, 2f, Mathf.InverseLerp(10f, 50f, speed));

Angles and rotations

Mathf.Lerp(350f, 10f, 0.5f);       // 180: the LONG way around
Mathf.LerpAngle(350f, 10f, 0.5f);  // 0:   the short way. Use this for angles.

Quaternion.Slerp(a, b, t);   // spherical: constant angular speed, best for rotations
Quaternion.Lerp(a, b, t);    // slightly faster, slightly uneven speed for big angles
Vector3.Slerp(dirA, dirB, t);// rotate a DIRECTION, keeping its length

What happens at runtime

  1. All these functions are small pieces of math. They're very cheap and create no garbage.
  2. The "smoothness" comes from calling them every frame with a t that changes. Pattern 1 changes t over time; patterns 2-4 move the current value a bit each frame.
  3. Anything multiplied by Time.deltaTime, or driven by elapsed time, stops when Time.timeScale = 0. Use Time.unscaledDeltaTime for pause-menu animations.

Common Mistakes

See also