Tweening in Zig
Lerp, or linear interpolation, is a convenient way to animate stuff in games without having to store much state:
const position = lerp(start, end, time);
This will move position
linearly from start
to end
as time moves from 0
to 1
.
Unfortunately, linear motion often appears robotic and unnatural. In fact–even a cartoony robot should overshoot and rattle a bit, right?
Thankfully there’s an easy fix. You don’t have to give up lerp, you can just play with the t
value that you pass in:
This isn’t actually game 2, it’s a side project. Not quite ready to announce game 2 yet sorry. :P
The pictured asteroid is being linearly interpolated, but the t
value is being modified by an elastic easing function causing it to act like a spring:
const position = lerp(start, end, elasticOut(t, .{}));
Pretty nifty for a one liner, right?
Functions that play with a lerp’s t
value are called “easing functions”, you can find visualizations for many commonly used easing functions on easings.net.
It’s nice to have a bunch of these on hand to pick from, so I’ve published a Zig library of all the popular ones. The rest of this post is a brief description of the contents of the library at the time of writing.
Lerp & Clamp01
The provided lerp function differs from std.math.lerp
in that it guarantees exact results at 0 and 1 at the cost of slightly more arithmetic.
In addition, the implementation provided here supports a larger variety of types:
- Floats
- Vectors
- Arrays
- Structures containing other supported types (so you can lerp your custom math types, for example)
clamp01
is provided for convenience to allow easily turning lerp into clamped lerp.
Ilerp & Remap
If these functions aren’t familiar to you, I highly recommend watching The Simple Yet Powerful Math We Don’t Talk About by Freya Holmer.
In short, ilerp
is the inverse of lerp
, and remap
uses a combination of ilerp
and lerp
to remap a value from one range to another.
Damp
Smoothed camera motion in my last game, Way of Rhea.
It’s often tempting to pass delta time into lerp to create smooth motion, e.g. to smoothly move a follow camera towards a position near the player each frame:
camera_pos = lerp(camera_pos, player_pos, dt);
Unfortunately, this code is not actually framerate independent. It takes a little thought to figure out how to fix this.
Damp is provided to do this work for you. It processes a delta time value paired with a smoothing value between 0 and 1, returning a t
value that can be used for framerate independent lerp:
camera_pos = lerp(camera_pos, player_pos, damp(dt, smoothing));
A nice write up on this concept can be found here, though deriving damp()
yourself can be a fun exercise. Way of Rhea uses this trick in a number of places, though the actual camera motion is actually slightly more involved.
Easing Functions
Onto the fun stuff.
Using an easing function is as easy as a single extra function call. If you’re new to easing, smootherstep
is a good default to slap on everything you want to look natural:
const position = lerp(start, end, smootherstep(t));
Smootherstep is a quintic function passing through (0, 0)
and (1, 1)
whose first and second derivatives are 0 at both points.
This tends to result in very natural looking motion. You can play with the coefficients here if you want to understand how it works:
If you want more customization, or are going for something more stylized, the following easing functions are supported. The more stylized ones were invented by Robert Penner and are now used everywhere easing is relevant:
I’ve opted to not include GIFs demonstrating the easing styles here, as easings.net already has a great visualizer for almost all of these:
Implementations may very slightly. In, out, and in-out variations of each are provided when relevant, functions are exact at 0 and 1 for 32 bit floats unless otherwise noted.
You can adapt easing functions with mix
, combine
, reflect
, and reverse
.
Post Decrement
Fun bit of trivia–if you look up other library’s implementations of these easing functions, a lot of them make this bizarre use of the post-decrement operator:
v = 7.5625 * (v -= 1.5 / 2.75) * v + 0.75;
Check out the v -= ...
in the middle of the expression, followed by a reference to v
later the same line.
What the heck is going on here?? Wouldn’t it be simpler to just pull this out into a local, or do the subtraction in both places?
As it turns out, this was originally an optimization for the ActionScript stack machine.
I guess everyone else has just been uncritically copy pasting these functions for the last 23 years. :)
FMA
If you’re shipping binaries, you probably have a min spec CPU in mind.
Easing is unlikely to be your bottleneck, but for peak performance I recommend making sure muladd
is enabled for your baseline so it doesn’t end up getting emulated in software–more info here.
On x86 this means enabling fma
, on arm/aarch64 this means enabling either neon
or vfp4
.
I’m working on a number of other libraries that I’ll announce soon. If you want to stay up to date on my game dev work in Zig, consider signing up for my newsletter.
I’ll be sending out an update on my work once a month. When I have major news (e.g. a new game announcement) I may email more often.
Happy tweening!