Theory, Uncategorized

Lerp: Understanding Linear Interpolation

If you’ve ever wanted to change a player’s health bar from green to red as they take damage, figure out the damage of an attack based on the power level, or smoothly transition from darkness to light in your level design, chances are you’ve used a linear interpolation, or “lerp”, operation. Having linear interpolation on your toolbelt and being confident in its application can be invaluable in game programming, but I’ve noticed that there can be a few pitfalls to be aware of as well, so let’s talk about what, exactly, it means to “lerp” something, and how we can apply it in our code.

The idea behind linear interpolation is fairly straightforward: it is finding an unknown value that lies some percentage of the distance between two known values (technically, it can be an arbitrarily large set of known values, but in game dev, I’ve found that it’s most often two). For the sake of clarity and consistency, let’s call the first known value “a”, the second known value “b”, and the percentage of distance “t”. In function form, we would write it as something like this: lerp(a, b, t).

Here to demonstrate this is a little visualization tool that I built. You can drag the points around to experiment with linear interpolation between two points (the green point represents the interpolated value); hovering or touching a point or colored part of the equation at the top will bold all the matching values:

See the Pen Linear Interpolation (lerp) by Derek Stobbe (@Problematic) on CodePen.

A couple things to note. First, the arguments are all either numbers, or things composed of numbers. This means, naturally, that you can lerp between floats and integers, but you can also lerp between things like colors, numeric vectors (like the Vector2 and Vector3 types in Unity), and custom data types composed of numbers, by lerping each component value individually (Unity provides wrappers to do this for the first two via the static methods Vector3.Lerp and Color.Lerp).

Second, notice an interesting property of interpolation: when the (blue) t value is 0, the green interpolated point is equal to the first (red) point. When t = 1, the interpolated point is equal to the second (purple) point. Based on that, you can surmise that when t = 0.5, the interpolated point is halfway between the two, t = 0.75 is three-quarters of the distance from a to b, and so on.

Note that the visualization tool allows you to explore values of t that are less than 0 or greater than 1, but at that point, you’re no longer interpolating a value, you’re extrapolating one. Many implementations of the lerp function (including Unity’s Mathf.Lerp) clamp the t value between 0 and 1, meaning you won’t be able to reproduce the results above where 0 > t > 1: when clamping input, lerp(a, b, 1.12358), for example, gives the same result as lerp(a, b, 1.0).

With this in mind, let’s talk about one of the places I frequently see developers get tripped up: using lerp to interpolate between values, where the t value is driven by time. Consider this pseudocode:

In this example, while the player holds a button, we’d like some value representing the player’s attack power to be lerped from an initial value of 0 to target_value over a period of time, say one second. However, there’s a bug in this implementation. Remember how the t value of the lerp function is clamped to the range [0, 1]? Assuming our elapsed_game_time value (which would be represented by Time.time in Unity, for example) is a float that represents the number of seconds the game has been running, and begins incrementing from 0 the moment the game starts, this code will only work as expected in the first second of gameplay. After that, the “elapsed time” will be some number greater than one, and the call to charge_attack will immediately set attack_power to the target value, instead of ramping it up over time, letting the player make fully-charged attacks with impunity, thus destroying our perfectly-tuned game balance.

How can we address this problem? We need to remap our t value to be between 0 and 1 inclusive, and the easiest way to do that is to cache the time that the user started pressing the button. In our example implementation, that might look like this:

The only thing we do differently is to cache the time that the button was pressed, and subtract that from the currently elapsed game time when we call charge_attack to update attack_power during the game loop. We can see that this works for our desired duration of one second, because say that the button is pressed at 3.1 seconds of elapsed gameplay, for example. After a half second, so at 3.6 seconds on the game clock, our calculation for the t value is 3.6 - 3.1, which is 0.5… precisely the value we wanted.

What if instead of changing attack_power to target_value over one second, we want to lerp it over a longer period of time, say 2.5 seconds? Our implementation fails once more, because one second after the button is pressed, elapsed_game_time - button_pressed_at exceeds 1.0, and is clamped. How can we solve this?

Well, if we look at our current solution, we can see that elapsed_game_time - button_pressed_at gives us a fractional number representing our current progress toward “filling” attack_power by making it equal to target_value. We know that any number divided by 1 is equal to the number itself, so we can represent our progress with the formula (elapsed_game_time - button_pressed_at) / 1.0. Coincidentally (or not), 1.0 happens to represent the old requirements for the duration of the charge effect, so if we swap out that constant for a variable representing the charge duration, we’re back in business:

Everything works as expected, and we have the added benefit of being able to easily tune charge_duration to control how long it takes the player’s attack to lerp up to full power. Game balance remains intact!

As I’ve hopefully demonstrated, the problem domain for successfully using linear interpolation in your games mostly involves figuring out how to map your t progress variable to a floating point number between 0 and 1. I’ll talk about Unity-specific implementations in another post, but I hope that’s enough to get you going for now!