Imagine you're standing at one end of a straight path, and your friend is standing at the other. Now you want to walk halfway to your friend - where do you end up? What if you walk 25% of the way? This simple idea of moving partway between two points is at the heart of linear interpolation, or lerp for short.

Beginners often conflate the Lerp function with a function to produce smoothing or animation. In truth, it's nothing more than an equation, a simple formula. At its core, Lerp is defined like so:

\[ \text{lerp}(a, b, t) = a + t(b - a) \]

This may seem a little confusing at first, so let's use an example. What value is halfway (i.e. 50%) between 10 and 20? If you answered 15, then you just did \(\text{lerp}(10, 20, 0.5)\) in your head!

In Unity, this expression can be written as:

float result = Mathf.Lerp(10, 20, 0.5f);
// result == 15

The \(\text{lerp}\) function simply calculates what value lies at a given percentage between two other values. If we were to plug in our values to the function, we'd get:

\[ \begin{equation} \begin{split} \text{lerp}\left( 10, 20, 0.5 \right) &= 10 + 0.5 \left( 20 - 10 \right) \\ &= 10 + 0.5 \left( 10 \right) \\ &= 10 + 5 \\ &= 15 \end{split} \end{equation} \]

When \(t=0\) the result is \(a\). When \(t=1\) the result is \(b\). Any value in between \(0\) and \(1\) returns a value partway between \(a\) and \(b\). This also means that we can provide values greater than \(1\) to get even larger differentials. Using the same values for \(a=10\) and \(b=20\), let's work out what \(\text{lerp}\left( 10, 20, 2 \right)\) comes to.

We can think of this as "what if I walked twice the distance toward my friend?". If you are standing at \(10\), and your friend is standing at \(20\), this means they are \(10\) units away. Now imagine walking those \(10\) units, then doing it again. You've now walked \(20\) units total. Since we started at \(10\) and walked \(20\) units, we expect the result to be \(10 + 20 = 30\). Let's see how the math pans out:

\[ \begin{equation} \begin{split} \text{lerp}\left( 10, 20, 2 \right) &= 10 + 2 \left( 20 - 10 \right) \\ &= 10 + 2 \left( 10 \right) \\ &= 10 + 20 \\ &= 30 \end{split} \end{equation} \]

Indeed we get the result \(30\) as expected. Do it again with \(t=3\), and you walk yet another \(10\) units.

\[ \begin{equation} \begin{split} \text{lerp}\left( 10, 20, 3 \right) &= 10 + 3 \left( 20 - 10 \right) \\ &= 10 + 3 \left( 10 \right) \\ &= 10 + 30 \\ &= 40 \end{split} \end{equation} \]

Below is an interactive example which lets you adjust the values of \(a\), \(b\), and \(t\).

Linearly interpolating 2D vectors

When it comes to utilising vectors, the exact same operation is performed component-wise.

This means that given two vectors:

\[ \begin{align} \overrightarrow{v_{1}} = \left( x_{1}, y_{1} \right) \\ \overrightarrow{v_{2}} = \left( x_{2}, y_{2} \right) \end{align} \]

We perform the \(\text{lerp}\) function on the \(x\), and \(y\) components before taking those results to create a new vector. Let's use some real values to demonstrate how this works. We'll define \(v_{1}\) as the point \((1, 0)\), and \(v_{2}\) at the point \((1, 1)\).

Intuitively, we can deduce the halfway point as lying at \((1, 0.5)\) but let's follow the math to see where it leads us. Since we want to compute the value halfway between these points, \(t=0.5\) for this demonstration.

We'll start by computing the \(x\) coordinate. In this case, the \(x\) coordinate of both vectors are the same, so the output should in fact result in the same value for \(x\).

\[ \begin{equation} \begin{split} \text{lerp}(1, 1, 0.5) &= 1 + 0.5(1 - 1) \\ &= 1 + 0.5(0) \\ &= 1+0 \\ x &= 1 \end{split} \end{equation} \]

We get the result \(x=1\). Halfway between \(1\) and \(1\) is still \(1\). Perfect. Now let's solve for \(y\).

\[ \begin{equation} \begin{split} \text{lerp}(0, 1, 0.5) &= 0 + 0.5(1 - 0) \\ &= 0 + 0.5(1) \\ &= 0 + 0.5 \\ y &= 0.5 \end{split} \end{equation} \]

Now we have both coordinates which compose the result. Halfway between \(0\) and \(1\) is indeed \(0.5\), and so our final interpolated point is \((1, 0.5)\). I've included another interactive demo below which allows you to move the \(A\) and \(B\) points around, so you can get a feel for how it works.

When you interpolate 2D vectors in a game engine like Unity, this is all that's really happening!

var start  = new Vector2(1, 0);
var end    = new Vector2(1, 1);
var result = Vector2.Lerp(start, end, 0.5f);

// result == (1, 0.5)

Linearly interpolating 3D vectors

When you add a third dimension to the mix, the only thing that changes is now you have to compute the lerp value for the \(z\) axis. Let's choose 2 random points in 3D space, and to make things interesting let's find out the point that lies only 10% (i.e. \(0.1\)) of the distance between them instead of the halfway point.

\[ \begin{align} A &= (1, 6, -4) \\ B &= (7, 2, 3) \end{align} \]

First we solve for \(x\).

\[ \begin{equation} \begin{split} \text{lerp}(1, 7, 0.1) &= 1 + 0.1(7 - 1) \\ &= 1 + 0.1(6) \\ &= 1 + 0.6 \\ x &= 1.6 \end{split} \end{equation} \]

Then \(y\).

\[ \begin{equation} \begin{split} \text{lerp}(6, 2, 0.1) &= 6 + 0.1(2 - 6) \\ &= 6 + 0.1(-4) \\ &= 6 - 0.4 \\ y &= 5.6 \end{split} \end{equation} \]

And lastly \(z\).

\[ \begin{equation} \begin{split} \text{lerp}(-4, 3, 0.1) &= -4 + 0.1(3 + 4) \\ &= -4 + 0.1(7) \\ &= -4 + 0.7 \\ z &= -3.3 \end{split} \end{equation} \]

And so our final interpolated point is at \((1.6, 5.6, -3.3)\). You can experiment with interpolating 3D points below.

Just like with 2D vectors, interpolating 3D vectors in a game engine like Unity simply performs this calculation for each \(x\), \(y\), and \(z\) component.

var start  = new Vector3(1, 6, -4);
var end    = new Vector3(7, 2, 3);
var result = Vector3.Lerp(start, end, 0.1f);

// result == (1.6, 5.6, -3.3)

Inverse linear interpolation

Another function that game engines offer is “inverse lerp”. Let's focus on how Mathf.InverseLerp works in Unity. We know that \(\text{lerp}\) returns some value \(n\) that is at \(t\) between \(a\) and \(b\):

\[ \text{lerp}(a, b, t) = n \]

As the name suggests, InverseLerp the inverse of this! That is, given a start value \(a\), end value \(b\), and some other value \(n\), how far along the journey from \(a\) to \(b\) is \(n\)? What percentage \(t\) do I need to give to \(\text{lerp}\) to produce the value \(n\)?

\[ \text{invlerp}(a, b, n) = t \]

We can work this out very easily. We simply need to rearrange the lerp formula to be in terms of \(t\).

\[ \begin{equation} \begin{split} n &= a + t(b - a) \\ \\ n - a &= t(b - a) \\ \\ \frac{n-a}{b - a} &= t \end{split} \end{equation} \]

The only case we need to look out for is if \(b=a\), because otherwise we will end up with a division by \(0\). In that situation, we could theoretically return any value as \(\text{lerp}(x, x, t)=x\) for all \(t\).

And that's it! We now have a definition:

\[ \text{invlerp}(a,b,n) = \begin{cases} 0 & \text{ if } b = a \\ \frac{n -a}{b - a} & \text{ if } b \neq a \end{cases} \]

Taking a look at Unity's implementation of the function, we can see this is exactly how they did it.

public static float InverseLerp(float a, float b, float value)
{
    if (a != b)
        return Clamp01(<mark>(value - a) / (b - a)</mark>);
    else
        return 0.0f;
}

Lerp is not an animation function

Lerp is a formula. Nothing more, and nothing less. A formula to calculate a point that lies somewhere along a straight line path between two other points. Despite this, I still see countless tutorials teaching Lerp incorrectly, giving misinformation to beginners.

The correct way to use it is to calculate a new position between a start point and end point. In other words:

\[ c = \text{lerp}(a, b, t) \]

Where \(a\) is the start position, \(b\) is the end position, \(t\) is the fraction of the journey, and \(c\) is the new position. \(a\) and \(b\) remain fixed, whereas \(t\) increases with each timestep based on how much time has elapsed since the journey began. In Unity, it may look something like this:

private void Start()
{
    StartCoroutine(MoveToTarget(4));
}

private IEnumerator MoveToTarget(float duration)
{
    Vector3 startPos = transform.position;
    Vector3 endPos = target.position;

    float start = Time.time;

    while (Time.time <= start + duration)
    {
        float completion = (Time.time - start) / duration;
        transform.position = Vector3.Lerp(startPos, endPos, completion);
        yield return null;
    }

	// this line is not strictly necessary, but it can be good practice
    transform.position = endPos;
}

Unfortunately, there are many tutorials out there which teach the following:

\[ c = lerp(a, b, x\Delta t) \]

Where \(a\) represents the current position - not the start position - and the value for percentage of completion is given as some scaled value of delta time as if to represent some kind of speed factor. In Unity, this would look like this:

private void Update()
{
    transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime);
}

An invocation like this makes no sense. It is akin to someone asking you “what percentage of your homework have you completed?” and you answering “0.2 seconds”. A unit of time is not a percentage, and treating it as one is nonsensical.

The other problem with this approach is that in normal gameplay, the object will - in fact - never reach its destination of target.position. Instead, you'll find yourself in a twisted version of Zeno's paradox of the dichotomy where the position asymptotically approaches the destination, but never reaches it. Time.deltaTime represents the number of seconds since the previous frame, and in a typical 60fps situation that means Time.deltaTime will be \(\frac{1}{60}\) or \(0.01\overline{6}\). Since \(\frac{1}{60} < 1\), there will never be a point where the \(\text{lerp}\) function returns the value of \(b\). The object will never reach target.position.

Below is a video which demonstrates this very effect. The ball on top follows the incorrect usage of Vector3.Lerp - changing \(a\) (the position) and keeping \(t\) relatively fixed (Time.deltaTime). The ball on the bottom implements the correct usage, where \(a\) is kept fixed (the start position) and it's \(t\) that changes (the percentage of completion). After 4 seconds, the green ball overlaps the red ball completely and the interpolation is complete.

I recorded this 5 years ago. Who'd have thought it's still relevant?

Exponential decay

One common critique of using the correct implementation of linear interpolation is that it is, no surprise, linear. This means that as time progresses, the value of \(t\) increases at a constant rate, resulting in evenly spaced changes in position over equal intervals of time.

In essence, the correct usage of Lerp results in a “curve” like so:

However, the incorrect usage has an inherent form of smoothing due to exponential decay. You can zoom in on this graph to see how \(f(x) \rightarrow 1\), but at no point does \(f(x) = 1\).

Fret not, for this can be reconciled. We can achieve the same effect using an exponential easing function.

\[ f(x) = \begin{cases} 1 &\text{ if } x = 1 \\ 1 - 2^{-10x} &\text{ if } x \neq 1 \\ \end{cases} \]
private void Start()
{
    StartCoroutine(MoveToTarget(4));
}

private float EaseOutExponential(float x)
{
	return x == 1 ? 1 : 1 - MathF.Pow(2, -10 * x);
}

private IEnumerator MoveToTarget(float duration)
{
    Vector3 startPos = transform.position;
    Vector3 endPos = target.position;

    float start = Time.time;

    while (Time.time <= start + duration)
    {
        float completion = (Time.time - start) / duration;
        float eased = EaseOutExponential(completion);
        transform.position = Vector3.Lerp(startPos, endPos, eased);
        yield return null;
    }

	// this line is not strictly necessary, but it can be good practice
    transform.position = endPos;
}

This function produces a curve that resembles that of exponential decay that we'd see from the incorrect usage of Lerp.