1. home
  2. Frame-rate independent interpolation for Unity

Frame-rate independent interpolation for Unity

Published

Back in 2002, Robert Penner published his book, Robert Penner's Programming Macromedia Flash MX. The book was significant in that it unveiled a set of stand-alone "easing functions" for simple animation. You've probably seen these easing functions, they've been ported to every language and websites like easings.net are dedicated to them. These functions make it simple for programmers to apply various eases to tween-based animation. By "tween-based" I'm referring to keyframe animation: interpolation between two fixed points in time.

But what about animations that don't have a start time and an end time? Consider procedural animations that happen in a real-time environment, for example: "every frame ease a game object towards another game object". Those animations can't be keyframed; mathematically, we can't interpolate the in-between values (the 'tweens) if there is no definitive end to the animation.

The Exponential Slide

Penner's book provided a solution for this as well, a solution he dubbed the exponential slide. Though the exponential slide has been extremely significant for my work over the years, the technique was largely overlooked -- partly because the slide is so simple and partly because most Flash developers weren't making real-time content, especially in 2002. Nowadays it seems that most call this technique damping or describe it as asymptotic easing.

Whatever you call it, the idea is dead-simple: move an object a fraction of the way towards a target. Do this once every tick and the result is an object that gracefully eases to a halt. You can visualize a keyframed example of this type of motion at easings.net/#easeOutExpo. Here's some what the exponential slide looks like, implemented in Unity:

void Update(){
    // move 5% of the way to the target:
    transform.position += (target - transform.position) * 0.05f; 
}

It's common to see the exponential slide written using interpolation functions as well:

void Update(){
    // move 5% of the way to the target:
    transform.position = Vector3.Lerp(transform.position, target, 0.05f); 
}

One significant problem with the exponential slide is that the calculation does not account for frame-rate. Many have tried to solve this problem, often by using a scaled delta-time as the percent value in the above equations. And while those solutions can produce better results, they definitely do not produce results that are frame-rate independent. Well, Rory Driscoll did the math and provided us with a far better solution, written succinctly as:

public static float Damp(float a, float b, float lambda, float dt)
{
    return Mathf.Lerp(a, b, 1 - Mathf.Exp(-lambda * dt))
}

As an aside, I'll say that Rory's elegant solution reminds me quite a bit of Robert Penner's original easing functions; it's a linear interpolation with a calculated value for the percentage.

Some Shareable Code

I've extended the logic and combined it with Allen Chou's similarly amazing work with numerical springs. These functions are a perfect starting point for procedural animation. I've provided these functions in the gist below as a class called AnimMath. Here's what it looks like in use:

void Update(){
    // slide towards the target value, reaching 99.9% after 1 second:
    transform.position = AnimMath.Slide(transform.position, target, .001f); 
}

Check out the gist and let me know what you think!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A set of easy-to-implement animation functions. Compiled by Nick Pattison.
/// </summary>
public static class AnimMath {
/// <summary>
/// A function for lerping between 2 values. Yes, Unity has this Mathf.Lerp() and Vector3.Lerp(),
/// but it's nice to have our own implementation as well.
/// </summary>
/// <param name="min">The minimum end of the output range. This value is returned when `p` is 0</param>
/// <param name="max">The maximum end of the output range. This value is returned when `p` is 1</param>
/// <param name="p">Percentage of the interpolation, typically from 0 to 1.</param>
/// <param name="allowExtrapolation">Whether or not to extrapolate. If true, values outside of the range are possible (when `p` > 1 or `p` < 0).
/// If false, the function will NOT output values outside of the range.</param>
/// <returns>The interpolated value</returns>
public static float Lerp(float min, float max, float p, bool allowExtrapolation = true) {
if (!allowExtrapolation) {
if (p < 0) return min;
if (p > 1) return max;
}
return (max - min) * p + min;
}
// 3D version
public static Vector3 Lerp(Vector3 min, Vector3 max, float p, bool allowExtrapolation = true) {
if (!allowExtrapolation) {
if (p < 0) return min;
if (p > 1) return max;
}
return (max - min) * p + min; // the Vector3 class's overloaded operators allow us to do this
}
// 4D version (rotation)
public static Quaternion Lerp(Quaternion min, Quaternion max, float p, bool allowExtrapolation = true) {
if (!allowExtrapolation) {
if (p < 0) return min;
if (p > 1) return max;
}
return Quaternion.Lerp(min, max, p); // just use Unity's Quaternion.Lerp()
}
/// <summary>
/// A function for asymptotic easing, aka the "exponential slide". This modified version is frame-rate independent,
/// and is based on http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
/// </summary>
/// <param name="current">The current value</param>
/// <param name="target">The value to ease towards</param>
/// <param name="percentLeftAfter1Second">How much is left after 1 second? Try small values, around or less than 1%.</param>
/// <param name="dt">The current frame's delta-time. The function will use Time.deltaTime by default, but custom values can be passed in.</param>
/// <returns>A new, eased value</returns>
public static float Slide(float current, float target, float percentLeftAfter1Second = 0.01f, float dt = -1) {
if (dt < 0) dt = Time.deltaTime;
float p = 1 - Mathf.Pow(percentLeftAfter1Second, dt);
return AnimMath.Lerp(current, target, p);
}
// 3D version
public static Vector3 Slide(Vector3 current, Vector3 target, float percentLeftAfter1Second = 0.01f, float dt = -1) {
if (dt < 0) dt = Time.deltaTime;
float p = 1 - Mathf.Pow(percentLeftAfter1Second, dt);
return AnimMath.Lerp(current, target, p);
}
// 4D version (rotation)
public static Quaternion Slide(Quaternion current, Quaternion target, float percentLeftAfter1Second = 0.01f, float dt = -1) {
if (dt < 0) dt = Time.deltaTime;
float p = 1 - Mathf.Pow(percentLeftAfter1Second, dt);
return AnimMath.Lerp(current, target, p);
}
/// <summary>
/// A function for easy implementation of spring-like easing. This is frame-rate independent,
/// and is based on http://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/
/// </summary>
/// <param name="val">The current value of the spring. The function will modify the value.</param>
/// <param name="vel">The current velocity of the spring. The function will modify the velocity.</param>
/// <param name="target">The target value that the spring is easing towards.</param>
/// <param name="damp">How much to dampen the "springiness".</param>
/// <param name="freq">The oscillation-speed in radians/second. The default of 24 is close to 8*PI, which is 4 oscillations/second.</param>
/// <param name="dt">The current frame's delta-time. The function will use Time.deltaTime by default, but custom values can be passed in.</param>
public static void Spring(ref float val, ref float vel, float target, float damp = .23f, float freq = 24, float dt = -1) {
if (dt < 0) dt = Time.deltaTime;
float k = 1 + 2 * dt * damp * freq;
float tff = dt * freq * freq;
float ttff = dt * tff;
val = (k * val + dt * vel + ttff * target) / (k + ttff);
vel = (vel + tff * (target - val)) / (k + ttff);
}
// 3D version
public static void Spring(ref Vector3 val, ref Vector3 vel, Vector3 target, float damp = .23f, float freq = 24, float dt = -1) {
if (dt < 0) dt = Time.deltaTime;
float k = 1 + 2 * dt * damp * freq;
float tff = dt * freq * freq;
float ttff = dt * tff;
val = (k * val + dt * vel + ttff * target) / (k + ttff);
vel = (vel + tff * (target - val)) / (k + ttff);
}
}
view raw AnimMath.cs hosted with ❤ by GitHub