You’re Using Lerp Wrong

Matt Harzewski
Jan 24 · 5 min read
Image for post
Image for post

Linear interpolation, or “lerp” for short, is a technique commonly used when programming things like games or GUIs. In principle, a lerp function “eases” the transition between two values over time, using some simple math. This could be used to slide a character between two coordinates, or to animate a change in size or opacity of a UI element. If you lerped between two colors, you’d get a gradient. The Unity game engine includes helper functions for Lerp in the Vector, Quaternion and floating point Math classes.

The use case I’ve been working with lately is smoothing player movement from coordinate to coordinate in network code. Since there are inherent latency and bandwidth limitations over the Internet, and you can only reasonably send so many packets per second, simply updating a player’s transform from one vector to another would result in very jerky movement. The solution: for every pair of packets that come in, in which the player will only move a few meters, you lerp between the vectors over a time roughly equal to the time between the packets. This slides the character smoothly, so it doesn’t stutter around.

In my Internet travels, I’ve noticed that a lot of examples in the Unity community–that’s the game engine I’m using–are bad. (And in the words of Zoidberg, perhaps they should feel bad.)

This is the very popular antipattern I see all over the place:

public void Update() {
// [...]
pos = Vector3.Lerp(obj.transform.position, targetPosition, 0.1f);
obj.transform.position = pos;
}

This is a misuse of the Lerp() function, and while it may look good enough at a glance, it’s mathematically bad and can lead to unexpected behavior. Unfortunately, in typical internet fashion, something that is wrong but mostly works will inevitably be cargo culted and spread like an invasive weed.

The Lerp function, in the Unity docs, is defined with the following signature:

public static Vector3 Lerp(Vector3 a, Vector3 b, float t);

As designed, the a value and the b value should not change during the interpolation. The only value that should change is t, which is a percentage–expressed as a value from 0.0 to 1.0–of the distance between the two values.

  • When t is 0.0, the function returns the value of a.
  • When t is 1.0, it returns the value of b.
  • When t is 0.5, the returned value is halfway between the two.

The t value should absolutely not be a constant, as it represents where, on the continuous line, the return value falls.

“But it works, doesn’t it? Why is it bad?” you may ask.

Well, the way that code snippet works, the a vector is shortened every frame. So the model is moved 10% closer to its target, then on the next frame it’s moved 10% further along the remaining distance. This results in the value telescoping, eventually moving incredibly tiny distances toward the target. Depending on use case, this can cause weirdly slow movement near the end, excessive iterations, gradual drift from imprecision, or other undesirable behavior.

The Lerp function, mathematically, is defined as lerp(a, b, t) = a + (b — a) * t. I’m using the single float version of this (from the Mathf class), since it’s simpler than the Vector3 version. For the algebra-averse, the English version is “the starting position, plus some percentage of the distance between the two values.”

Image for post
Image for post

If you add mental parentheses around the terms in the box, it should be easily understood: we’re just adding a variably-sized chunk of the distance between the start and end value onto the start value, moving us toward the destination.

Vectors are the same idea, only in more dimensions. Break the components apart and lerp them independently, then you can put the vector back together…and be thankful that Unity vectors just give you the components, instead of notating them as an angle and magnitude, like you run into in Physics.

To illustrate proper vs. bad lerping, I wrote up some quick and dirty Python code.

def lerp(a, b, t):
return a + (b-a) * t

This shows the correct way to do it, linearly interpolating from 2 to 5 as our t value goes from 0.0 to 1.0.

t=0.0: 2.0
t=0.1: 2.3
t=0.2: 2.6
t=0.3: 2.9
t=0.4: 3.2
t=0.5: 3.5
t=0.6: 3.8
t=0.7: 4.1
t=0.8: 4.4
t=0.9: 4.7
t=1.0: 5.0

In contrast, this is an implementation of the “bad way” that keeps cropping up all over the Unity community. We’re still starting at 2 and moving to a target value of 5. However, this time the t value is a constant 10% and the a value is being changed on every iteration.

def lerp(a, b, t):
return a + (b-a) * t

The output is not at all what we expect…

Iteration 0: 2.3
Iteration 1: 2.57
Iteration 2: 2.8129999999999997
Iteration 3: 3.0317
Iteration 4: 3.2285299999999997
Iteration 5: 3.405677
Iteration 6: 3.5651093
Iteration 7: 3.70859837
Iteration 8: 3.837738533
Iteration 9: 3.9539646797
Iteration 10: 4.05856821173
Iteration 11: 4.152711390557
Iteration 12: 4.2374402515013
Iteration 13: 4.31369622635117
Iteration 14: 4.382326603716053
Iteration 15: 4.4440939433444475
Iteration 16: 4.499684549010003
Iteration 17: 4.5497160941090025
Iteration 18: 4.5947444846981025
Iteration 19: 4.635270036228293

Yikes. We ran that almost twice as many times, and it’s still not reaching the target value. It wouldn’t even after 1000 iterations. It would reach 4.999 repeating, but would never actually hit the target value.

So, from that initial example, you could be moving that game object every frame for a long time. Unless you did some voodoo coding and slapped in a conditional to make it stop when it’s less than 0.001 units off from the target, like many of those questionable implementations do. But that’s just sweeping dirt under the rug.

So, how do you correctly move a game object in Unity with lerp? The following isn’t a definitive answer–after all, every use case and implementation is different–but it’s on the right track:

Vector3 startPosition = gameObject.transform.position;
Vector3 targetPosition = ... ;
  • The start and target positions are properties outside of the Update function, because they do not change. We don’t want the start position and the game object’s position to be the same at any time other than the very first iteration.
  • The t value is calculated as a percentage. In this case, I’m just using a counter, so t progresses from 0.0 to 1.0 over 3 seconds (30 frames per second).

This example moves the game object over 3 seconds (30 frames per second). There are more robust ways to do this, but it gets the point across. The important thing is that t is calculated as the elapsed time over the desired duration of the movement, however you measure those.

Please, if you’re going to copy and paste something, steal that one.

The Startup

Medium's largest active publication, followed by +773K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store