11 January 2025

Interpolation in Unity

Interpolation is a mathematical method used to find an intermediate value between two known points. When those two known points are connected by a straight line, we call it linear interpolation.

Although the mathematical definition might sound a bit technical, the truth is that our daily lives are filled with examples of interpolation:

  • If my automatic car has a maximum speed of 160 km/h, how fast will it go if I press the accelerator halfway?
  • If my plane has a range of 300 km, how many more kilometers can it fly when one-third of the fuel tank is used up?
  • If my fully stocked fridge lasts me 7 days and half of the shelves are empty, when will I need to go shopping?

Although these examples seem different, they are actually not. All of them can be modeled using a linear function, with a graph that looks like this:

A linear function
A linear function

The difference lies in what the axes represent in each case:

In the automatic car example, the X-axis represents the accelerator depth, while the Y-axis represents the car's speed.

  • For the plane, the X-axis is the amount of fuel consumed, and the Y-axis is the distance traveled.
  • In the fridge example, the X-axis is the days that pass, and the Y-axis is the number of empty shelves.
  • In all these cases, the structure of the graph (and the function it represents) is the same: a straight line.

What differentiates one case from another is the slope of the line, which represents the rate of change of the Y-axis as we progress along the X-axis.

If in real life we often need to perform interpolations, you can imagine that in game development we do too. After all, what are video games if not recreations of real life?

Let’s consider a similar example. Suppose I’m developing a simulator and want to provide HOTAS support.

A HOTAS controller
A HOTAS controller

When the player moves the throttle lever (the left one), Unity's Input System will return a value between 0 and 1: 0 when the lever is at its minimum position (closest to the player) and 1 when it's at its maximum position (farthest from the player). With this information, we need to perform interpolation to calculate the vehicle's speed at all intermediate lever positions, knowing that the vehicle should be stationary at 0 and move at maximum speed at 1.

If we return to our graph, we will have the same structure, except that the X-axis will represent the lever position (which ranges from X = 0 to X = 1), and the Y-axis will represent the speed of the game’s vehicle.

How do we perform this calculation? There are several options, depending on the ranges represented on the X and Y axes. Let’s study them from simplest to most complex.

Interpolation with X-axis range between 0 and 1

This is the simplest case: the X-axis varies between 0 and 1, while the Y-axis varies between an initial value (V1) and a final value (V2).

Mathematically, the interpolated value (Y) is calculated as follows:

The formula for a linear function
The formula for a linear function


We could write a function to implement this formula, but it would be reinventing the wheel, because Unity already provides a method that does exactly this: Mathf.Lerp().

Lerp() accepts three parameters:

  • An initial value.
  • A final value.
  • A value between 0 and 1.

If the third parameter is 1, the function returns the final value; if it’s 0, it returns the initial value; and if it’s between 0 and 1, it returns an intermediate value calculated using the line connecting the two values.

In our throttle lever example, we would pass Lerp() the vehicle’s minimum speed, maximum speed, and the value between 0 and 1 returned by the Input System based on the lever’s position. The method’s return value would be the vehicle’s new speed.

Sometimes, we might need the inverse of an interpolation.

For example, suppose the shields on our game’s spaceship have been hit by enemy projectiles. The shield has a strength value, and we’ve been reducing it with each impact. We want to take the current shield value and display it on the cockpit’s control panel as a percentage so the player knows how strong their shields are. That is, we have the shield's maximum value (let's say 300), its minimum value (typically 0), and its current value (let’s say 150). We want to calculate the fraction of the total shield capacity that this current value represents, so we can present a percentage to the player.

Reorganizing the previous formula gives us the inverse function:

The formula for the inverse function
The formula for the inverse function

Using our example:

  • V2, the maximum shield value: 300
  • V1, the minimum shield value: 0
  • Y, the current shield value: 150

This yields a fraction of 0.5. To calculate the percentage, simply multiply by 100, resulting in 50%.

Inverse function, applied to our example
Inverse function, applied to our example

Fortunately, we don’t have to implement this formula manually either. Unity provides the Mathf.InverseLerp() method.

InverseLerp() accepts three parameters:

  • An initial value, 0 in our example.
  • A final value, 300 in our case.
  • The intermediate value for which we want the inverse interpolation, 150 in our example.

With these inputs, InverseLerp() returns 0.5.

It’s important to note that if the intermediate value lies outside the range—either below the initial value or above the final value—InverseLerp() returns 0 or 1, respectively.

Angular Interpolation

When working with angles, it can be tempting to use Lerp(), but this should be avoided. Angles are a special case because they wrap around when they reach their maximum of 360°. For instance, an angle of 380° is equivalent to 20°.

To handle this peculiarity, Unity provides the Mathf.LerpAngle() method.

This method accepts three parameters:

  • The initial angle in degrees.
  • The final angle in degrees.
  • The intermediate value between 0 and 1.

Be cautious, as the method doesn’t operate within the 0° to 360° range but instead within -180° to 180°.

For example, calling LerpAngle(0.0f, 190.0f, 1.0f) will return -170.0f because, at 180°, it assumes the remaining 10° were obtained by rotating 170° to the left. In other words, LerpAngle() always returns the shortest rotation. If you’re interested in the longer rotation (e.g., rotating the full 190° to the right), you would need to use Lerp().

Linear Interpolation with X-axis Values Beyond 0 and 1

By default, Lerp() only accepts intermediate values between 0 and 1, but there may be cases where we want values outside this range. Unity provides the Mathf.LerpUnclamped() method for this.

Its parameters are the same as Lerp(), but it allows intermediate values below 0 and above 1. The method simply extends the line beyond the [0,1] range to calculate the resulting value.

Non-Linear Interpolation

So far, we have assumed that our range on the X-axis was limited to 0 and 1. All the intermediate values we fed into Lerp() were constrained to that range.

However, there may be situations where we want the X-axis to represent a different range, for example, because we want to apply different linear functions as we progress along the X-axis.

Imagine we have a spaceship, and we want its speed to be affected by accumulated damage, slowing the ship down as more damage is taken. However, we don't want the slowdown to be uniform; instead, we prefer that beyond a certain damage threshold, the slowdown accelerates.

We would have a graph where damage is represented on the X-axis and the slowdown on the Y-axis. From 0 to the damage threshold, the graph would be a line ascending gradually in the first segment, but at the threshold, it would have a bend, and from there, a second segment would begin, ascending much more steeply. Instead of a continuous straight line, we would have one that bends at a certain point. These types of functions are called nonlinear.

There are several ways to implement something like this. We could normalize the X-axis for the first segment using InverseLerp() with the initial, final, and current values of that segment on the X-axis, and use the resulting value to perform a Lerp() for the minimum and maximum values of the Y-axis for that segment. The problem is that we would need to repeat all those operations for the second segment.

We could simplify the calculations slightly by using the remap() function from Unity's Mathematics package. This method allows us to pass a source range (on the X-axis), a destination range (on the Y-axis), and an intermediate value within the source range to obtain the equivalent value in the destination range. This would save us from having to chain InverseLerp() and Lerp(), but it would still require applying the remap() method to each segment. Additionally, it would require us to install the Mathematics package via the Package Manager.

This method becomes unsustainable if our graph has multiple segments, and moreover, the transition between segments might be too abrupt.

In such cases, the best approach is to use AnimationCurves. As their name implies, they are designed to be used in the animation window, but we can also use them in our code. AnimationCurves allow us to define our function graphically, including as many segments, curves, and straight lines as we want.

For example, let's say we want to implement a vehicle with smooth acceleration and braking. If these were linear, they would feel unnatural. Instead, we are going to apply curves. To include them in our code, we could do something like this:

Including AnimationCurves in our code
Including AnimationCurves in our code

The above fields would appear in the inspector as follows:

AnimationCurves in the inspector
AnimationCurves in the inspector

Clicking on any of the AnimationCurves allows us to edit its shape. For example, the acceleration curve might look like this:

Acceleration curve
Acceleration curve

If you observe, the curve above is normalized on both the X and Y axes, as both are confined to values between 0 and 1. However, this shouldn't be a problem given what we've learned so far. For example, to calculate the speed during acceleration, we could do:

Sampling an AnimationCurve
Sampling an AnimationCurve

Given that the variable accelerationRadius defines the distance from the start at which the vehicle reaches its maximum speed, we know that this distance corresponds to X=1 on the acceleration curve. Therefore, to determine where we are on the acceleration curve, we perform an InverseLerp(), passing our distance from the starting point as the intermediate value (line 2).

The point obtained from the InverseLerp() is then passed to the Evaluate() method of the AnimationCurve, which returns the graph value at that point (line 3). Since the Y-axis of the graph corresponds to the speed, and Y=1 represents the maximum speed, we simply multiply the value returned by the curve by the maximum speed.

As you can see, using an AnimationCurve spares you from having to apply different calculations for each segment and makes transitions much more natural and smooth.

Conclusion

Interpolations are invaluable in game development. The simplest cases can be resolved using Lerp and InverseLerp, with LerpAngle for angles. For more complex scenarios, AnimationCurve provides a powerful and flexible solution, especially when exposed in the inspector for visual editing.