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)
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?
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.
No comments:
Post a Comment