There's a certain simple animation thing that I've been using almost since I've ever started doing anything related to graphics. I use it for rotating & moving the camera, for moving figures in a turn-based game, for moving UI elements, for smoothing volume changes in my audio lib, everywhere! So I decided I'll write about it. The trick itself is nothing new, – in fact, you've probably already heard about or even used it, – but I'll also show it in some examples and explain how it works mathematically :)
Speaking of UI, say you're making some UI component, maybe a toggle button. Something like this (click it!):
This simply computes the position of the switch as a function of its state:
position.x = turned_on ? max_x : min_x;
This works perfectly, but feels a bit lifeless. Adding some animation to it would be cool! Animations are not just a fancy visual thing, they help the user understand what's going on. Instead of teleporting the toggle indicator to its new position, let's move it smoothly:
The downside is that we need to run some updating animation now:
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
However, this still looks a bit clumsy due to having a constant speed (i.e. the position is a linear function of time). Let's add some easing function on top of that, like the classic cubic \( 3t^2-2t^3 \):
or a square root \( \sqrt t \):
The difference between these may be hard to see, so let's slow down the animation by a factor of 8:
Linear: | ||
Cubic: | ||
Square root: |
This time, instead of just updating the switch position, we have to keep track of some extra animation state:
t += (turned_on ? 1 : -1) * speed * dt;
t = clamp(t, 0, 1);
ease = (3 * t * t - 2 * t * t * t);
position.x = lerp(min_x, max_x, ease);
Here, I'm using the fact that smoothstep
is symmetric in the following sense: 1 - f(t) = f(1 - t)
, meaning the forward and backward animations can use the same code. With sqrt
things are a bit different: we have to explicitly use a different easing function depending on the animation's direction:
ease = turned_on ? sqrt(t) : 1 - sqrt(1 - t);
Whichever looks best is arguably a matter of taste, but of all these sqrt
is my favourite: the switch starts moving really fast (this is because sqrt
has infinite derivative at zero), but then slows down nicely as it reaches the destination (the cubic one is my second favourite, though). The downside of this version is that we need quite a lot of bookkeeping even in the simplest possible case of a two-state toggle button (later in the article I'll show how this becomes a nightmare in more complicated scenarios). Another downside is that it has a discontinuity: it jumps suddenly if the user clicks on it the middle of animation (try it!).
Thankfully there's a similar version which uses the minimal possible state and doesn't have the "jumping" problem:
I call it exponential smoothing (for reasons that will become clear later). I've also heard it being called approach, and I'm certain it has it's own name in every engine. Here it is slowed down 8x and compared to sqrt
:
Square root: | ||
Exponential: |
Here's the code for the exponential version:
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
Intuitively, on each frame we nudge the current position towards its target position (which is determind by the on/off state). However, the amount of nudging (1 - exp(- dt * speed))
looks really weird, doesn't it? Before we see where it comes from, let's have a look at some more complicated animations.
Say we have some kind of map, and a camera scrolling/moving around.
Yes I've made a whole procedural map generator & renderer just for this example, and I have zero regrets.
Again, this begs us to add some animation. Let's interpolate it with constant speed:
Here's the code:
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
See this jittering after the animation completes? That's because target.x - position.x
keeps alternating between being positive and negative. Instead of sign(delta)
we need some function that clamps the delta:
float update(float & value, float target, float max_delta)
{
float delta = target - value;
delta = min(delta, max_delta);
delta = max(delta, -max_delta);
value += delta;
}
update(position.x, target.x, speed * dt);
update(position.y, target.y, speed * dt);
Quite a mouthful for such a simple thing! And here's the result:
Much better, although it's still a bit clumsy and also weird if we move the camera faster than the animation completes. We could, as before, add some easing function, like the cubic one:
although this time it gets really complicated: we have to maintain a queue of requested movement events, and animate them one by one (otherwise I have no idea how to slap the easing function here). This still looks a bit weird when moving the camera fast enough. We could just ignore user's input while the animation is active, but this is a deadly sin as it is infuriatingly frustrating from the user's perspective.
The perfect solution? Why, exponential smoothing of course! The code barely changes compared to the toggle button example:
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
and here's how it looks like:
Pretty nice, if you ask me! Notice how it speeds up naturally if you click fast enough.
Ok, so what's up with this 1 - exp(- speed * dt)
, what on Earth is that?
Let's start with a simplified version: we have some animation, it has a current position
and the new position target
which it must move towards with some speed
. To make the movement faster when the difference between position
and target
is large, we make the speed proportional to this difference:
position += (target - position) * speed * dt;
Notice how it doesn't require maintaining any state other than the current and the target position! (speed
is usually a constant.) It even doesn't need to keep track of time that elapsed since the start of the animation, and it adjusts automatically if the target
suddenly changes.
Now this already works perfectly in many situations, but there's a small catch. Here's the toggle button again, with the above udpate code:
See the jittering? That's because I've set the speed
value so high that speed * dt
became larger than 1! Specifically, I used speed = 220
and dt = 1 / 125
.
To understand what's happening, it is useful to rewrite the code above using lerp
:
position = lerp(position, target, speed * dt);
You can check that this is ultimately the same formula. We can clearly see what's going on: the formula interpolates between the current value and the target value. The closer the interpolation parameter speed * dt
to zero, the slower the interpolation. The closer it is to one, the faster the movement.
Now, what happens when speed * dt
is larger than 1 is that the interpolation overshoots! The only reason it still works is that speed * dt
is less than 2, so that the absolute delta between position
and target
still decreases with time. Here's an example with speed * dt = 248 / 125 < 2
:
and here's one with speed * dt = 252 / 125 > 2
:
The last one doesn't do anything useful at all.
To solve this, we could simply clamp the value by 1:
position = lerp(position, target, min(1, speed * dt));
However, this doesn't seem like the right thing to do in all scenarios. Consider why speed * dt
might actually happen to be so large?
One reason is that your speed
value is too large because you want a really quick animation. However, as we've seen with the above toggle buttons, this is actually way too quick for any reasonable user – the actual animation is impossible to notice. So, our speed
value is usually not that high.
The other reason is that dt
is too large. Maybe because your code runs too slow, and your framerate is dropping. Maybe because the user moved to a different tab/window and your code was sleeping, and now it got woken up with a dt
of many seconds.
When applying such a dt
to something like physics, you certainly want to clamp it, or subdivide into several updates, etc. With animations, however, wouldn't it be cool if everything worked perfectly even in this case? Even if your physics might lag, at least the camera & buttons would still work nicely – as a user, I would really appreciate such care.
Ok, we want to solve the problem, but how? Here's the two-step recipe:
Time-dependent update that works for small dt
but breaks for large dt
is pretty typical for numerical solvers of differential equations. What equation does position += (target - position) * speed * dt
solve? Whenever you see A += B * dt
, this corresponds to an equation
\[ \frac{d}{dt}A=B \]
In our case, the equation is
\[ \frac{d}{dt}\text{position} = (\text{target} - \text{position})\cdot\text{speed} \]
I will die if I keep typing these formulas with all words spelled out, so let's make a few variable changes: call \( x = \text{position} \), \( a = \text{target} \), and \( c = \text{speed} \):
\[ \frac{d}{dt}x = (a-x)\cdot c \] Solving this needs just a few tricks: \[ \frac{d}{dt}(x-a) = \frac{d}{dt}x = (a-x)\cdot c = -(x-a)\cdot c \] \[ x-a = (x_0-a)\cdot\exp{-c\cdot t} \] \[ x = a - (a-x_0)\cdot\exp{-c\cdot t} \] \[ x = x_0 + (a-x_0)\cdot\left(1-\exp{-c\cdot t}\right) \]Btw, a similar exponent appears in e.g. volumetric rendering for pretty similar reasons.
It's not important to understand exactly where this all comes from. The point is that if we believe that position += (target - position) * speed * dt
is the right formula for small dt
, then the formula position += (target - position) * (1 - exp(- speed * dt))
is the right formula to use for any dt
. This is further supported by expanding the latter equation in terms of Taylor series for the exponent: \( \exp(x) \approx 1 + x \), so that
i.e. we get exactly the former equation.
The cool thing is that it doesn't care about old values: if you have your previous value \( x_0 \) and you know how much time has passed between the previous and the current iteration, you can compute the new value. (This is a direct consequence of being a first-order differential equation.)
So, the TL;DR is that
position += (target - position) * (1 - exp(- speed * dt))
is the right formula that works for any speed
and dt
. Even if the product speed * dt
is too large, exp(- speed * dt)
handles it nicely, since exp
of a large negative number is just something close to zero, so 1 - exp
will be close to one.
We can, as before, rewrite this using lerp
: position = lerp(position, target, 1 - exp(- speed * dt))
or even position = lerp(target, position, exp(- speed * dt))
. There are many ways to rewrite this equtaion.
Usually, we think of animation in terms of its duration. Like, the toggle button should move to the new place in 0.125 seconds (the actual value used in the examples in the beginning of the post), after that it stops moving. With this exponential formula, however, the animation technically takes infinite time to complete! exp(- speed * time)
gets smaller with time, but it never equals zero, so that position
technically never equals target
(provided they were different to start with).
However, in practice we have a ton of limitations. If position
is a floating-point number, it quickly reaches the precision limit, and it becomes equal to target
in practice. If it is, say, the camera position, the user probably won't notice that the animation is still going since the delta target - position
gets ridiculously small even before it hits floating-point precision limits.
So, what does the speed
parameter mean, exactly? It means the following: 1 / speed
is the time in which position
becomes closer to target
by a factor of e = 2.71828...
exactly. Do whatever you want with this information.
I usually set the speed
to something in the range 5..50. For a linear/cubic animation of a certain speed
, I usually set the exponential version speed to be 2 * speed
, this feels about right (again, this is what was used in the examples above).
If you google "exponential smoothing" (or "exponential moving average"), you might find the wiki article on something completely unrelated which, nevertheless, features some pretty similar formulas. It actually is the discrete analogue of what we were talking about in this post!
Suppose that our dt
is always the same; also suppose that target
changes as often as every iteration. Then, indexing the values with the iteration number, we compute something like position[i] = (target[i] - position[i - 1]) * factor
, where factor = 1 - exp(- speed * dt)
. In this case, one typically sets factor
directly to some value between 0 and 1 instead of deriving it from other values (although the aforementioned wiki article does explain what this factor
actually means).
People use it in signal processing for the same reasons I do for animations: it doesn't require maintaining previous values or any other obscure state, just the current averaged value. They also use it in digital audio, where you typically have a fixed dt
of 1 / freq
the inverse sampling frequency (e.g. 1/44100
or 1/48000
).
I had the idea of this post for many months, glad to finally get it done :)
As always, thanks for reading!