14
\$\begingroup\$

To my understanding, a Lerp function interpolates between two values (a and b) using a third value (t) between 0 and 1. At t = 0, the value a is returned, at t = 1, the value b is returned. At 0.5 the value halfway between a and b is returned.

(The following picture is a smoothstep, usually a cubic interpolation)

enter image description here

I have been browsing the forums and on this answer I found the following line of code: transform.rotation = Quaternion.Slerp(transform.rotation, _lookRotation, Time.deltaTime);

I thought to myself, "what a fool, he has no idea" but since it had 40+ upvotes I gave it a try and sure enough, it worked!

float t = Time.deltaTime;
transform.rotation = Quaternion.Slerp(transform.rotation, toRotation, t);
Debug.Log(t);

I got random values between 0.01 and 0.02 for t. Shouldn't the function interpolate accordingly? Why do these values stack? What is it about lerp that I do not understand?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ A is usually position, which changes and therefor sampling at 1/60 (60 fps) would only move the object by interpolation of 0.16 continuously narrowing the distance between A and B ( thus the sample is smaller and smaller each time ). \$\endgroup\$
    – Sidar
    Commented Oct 3, 2017 at 13:38
  • \$\begingroup\$ You logged t and lerped with tt... those are diferent variables. \$\endgroup\$
    – user20574
    Commented Oct 5, 2017 at 0:36

4 Answers 4

23
\$\begingroup\$

See also this answer.

There are two common ways to use Lerp:

1. Linear blending between a start and an end

progress = Mathf.Clamp01(progress + speedPerTick);
current = Mathf.Lerp(start, end, progress);

This is the version you're probably most familiar with.

2. Exponential ease toward a target

current = Mathf.Lerp(current, target, sharpnessPerTick);

Note that in this version the current value appears as both the output and an input. It displaces the start variable, so we're always starting from wherever we moved to on the last update. This is what gives this version of Lerp a memory from one frame to the next. From this moving starting point, we then then move a fraction of the distance toward the target dictated by a sharpness parameter.

This parameter isn't quite a "speed" anymore, because we approach the target in a Zeno-like fashion. If sharpnessPerTick were 0.5, then on the first update we'd move halfway to our goal. Then on the next update we'd move half the remaining distance (so a quarter of our initial distance). Then on the next we'd move half again...

This gives an "exponential ease-out" where the movement is fast when far from the target and gradually slows down as it approaches asymptotically (though with infinite-precision numbers it will never reach it in any finite number of updates - for our purposes it gets close enough). It's great for chasing a moving target value, or smoothing a noisy input using an "exponential moving average," usually using a very small sharpnessPerTick parameter like 0.1 or smaller.


But you're right, there is an error in the upvoted answer you link. It's not correcting for deltaTime the right way. This is a very common mistake when using this style of Lerp.

The first style of Lerp is linear, so we can linearly adjust the speed by multiplying by deltaTime:

progress = Mathf.Clamp01(progress + speedPerSecond * Time.deltaTime);
// or progress = Mathf.Clamp01(progress + Time.deltaTime / durationSeconds);
current = Mathf.Lerp(start, end, progress);

But our exponential easing is non-linear, so just multiplying our sharpness parameter by deltaTime will not give the correct time correction. This will show up as a judder in the movement if our framerate fluctuates, or a change in the easing sharpness if you go from 30 to 60 consistently.

Instead we need to apply an exponential correction for our exponential ease:

blend = 1f - Mathf.Pow(1f - sharpness, Time.deltaTime * referenceFramerate);
current = Mathf.Lerp(current, target, blend);

Here referenceFramerate is just a constant like 30 to keep the units for sharpness the same as we were using before correcting for time.


There's one other arguable error in that code, which is using Slerp - spherical linear interpolation is useful when we want an exactly consistent rate of rotation through the whole movement. But if we're going to be using a non-linear exponential ease anyway, Lerp will give an almost undistinguishable result and it's cheaper. ;) Quaternions lerp much better than matrices do, so this is usually a safe substitution.

\$\endgroup\$
1
\$\begingroup\$

I think the core concept missing would be in this scenario A is not fixed. A is updated with each step, by however much along the interpolation that Time.deltaTime is.

So, with A getting closer to B with each step, the total space of the interpolation changes with each Lerp/Slerp call. Without doing the actual math, I'd suspect that the effect is not the same as your Smoothstep graph, but is a cheap way to approximate a deceleration as A gets closer to B.

Also, this is frequently used because B may not be static either. Typical case might be a camera following a player. You want to avoid jerkiness, having the camera jump to a location or rotation.

\$\endgroup\$
1
\$\begingroup\$

You are right, the method Quaternion Slerp(Quaternion a, Quaternion b, float t) interpolates between a and b by the amount t. But watch the first value, it is not the start value.

Here the first value given to the method is the current object rotation transform.rotation. So for each frame it interpolate between the current rotation and the target rotation _lookRotation by the amount Time.deltaTime.

That is why it produce a smooth rotation.

\$\endgroup\$
0
-2
\$\begingroup\$

Using Time.deltaTime in Lerping functions ensures that the interpolation happens over a smooth and consistent period of time, regardless of the frame rate of the game. Without Time.deltaTime, the interpolation might be too fast or too slow depending on how fast the game is running.

When Lerping, the "t" value represents the percentage of the interpolation between the two values that should have occurred. By multiplying this value with Time.deltaTime, we can ensure that the interpolation happens at a consistent rate, regardless of the frame rate.

The reason why the values of Time.deltaTime stack is because it represents the time that has passed since the last frame. So if the frame rate is constant, the value of Time.deltaTime will be consistent as well. However, if the frame rate fluctuates, the value of Time.deltaTime will also fluctuate accordingly.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Looks like you're missing the point here. \$\endgroup\$
    – Vaillancourt
    Commented Jun 12, 2023 at 3:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .