Search code examples
animationkeyframegltfcubic-splinecubic-bezier

What do the in- and out-tangents in glTF's cubic splines visually represent?


Keyframe animations in glTF support "cubic spline" interpolation, with the specification for them simply saying (with my added emphasis):

Let:
n be the total number of keyframes, n>0;
tk be the timestamp of the k-th keyframe, k ∈ [1,n];
vk be the animated property value of the k-th keyframe;
tc be the current (requested) timestamp, tk<tc<tk+1;
td = tk+1 − tk be the duration of the interpolation segment
t = (tc − tk) / td be the segment-normalized interpolation factor.

For each timestamp stored in the animation sampler, there are three associated keyframe values:
in-tangent, property value, and out-tangent.

Let ak, vk, and bk be the in-tangent, the property value, and the out-tangent of the k-th frame respectively.

The interpolated sampler value vt at the timestamp tc is computed as follows.

vt = (2t3 − 3t2 + 1) × vk + td(t3−2t2+t) × bk + (−2t3+3t2) × vk+1 + td(t3−t2) × ak+1

In a traditional two-dimensional cubic Bézier curve each vertex and tangent have a two-dimensional location.
Two-dimensional cubic Bézier curve showing the control points and evaluated curve

However, the glTF specification is associating the in- and out-tangents with the same timestamp as the vertex between them. In a traditional curve, this would represent a vertical section of the graph. Obviously that is incorrect, since the keyframe graph represents value per time, and thus a vertical line graph would represent an ambiguous situation where there are many possible values for the same time.

Two-dimensional cubic Bézier curve with a vertex and control points in the middle of the curve aligned vertically, showing a vertical section in the curve.

My question is, what do these tangent values represent, in a traditional cubic Bézier spline? Or, more to the point:

Given the tangent values per keyframe, how can I construct cubic Bézier control points as part of a Bézier curve that will accurately visually depict the time/value graph that occurs due to the equation specified?


Update: I've created a Desmos graph that performs the above calculation, to experiment with the curve. I've come to believe that the tangent values do not appear to represent actual target values. Instead, they are treated like offsets to the keyframe values on each side:

  • The curve starts at vk and trends towards vk + bk
  • The curve ends at vk+1 and on the way trends towards vk+1 - ak+1

Experimentally sketching cubic Bézier to fit over the Desmos curve shows that the time values of the control points should always be at t=1/3 and t=2/3 (as guessed at by this answer to a related question).

I'm still looking for the formula that incorporates td, but I'm close. It looks like the control points are exactly at (0.333, vk + bk) and (0.666, vk+1 - ak+1) when td = 3, but that may just be a coincidence in my experiments.


Solution

  • The glTF Tutorial on Animations offers this further explanation:

    The input and output tangents are normalized vectors that will need to be scaled by the duration of the keyframe, we call that the deltaTime

    deltaTime = nextTime - previousTime
    

    In other words, a normalized incoming or outgoing direction has been encoded, but it does not contain any magnitude.

    The tutorial goes on to offer this pseudocode implementation of the reference formula given in Appendix C of the glTF specification:

    Point cubicSpline(previousPoint, previousTangent,
                      nextPoint, nextTangent, interpolationValue)
        t = interpolationValue
        t2 = t * t
        t3 = t2 * t
        
        return (2 * t3 - 3 * t2 + 1) * previousPoint +
               (t3 - 2 * t2 + t) * previousTangent +
               (-2 * t3 + 3 * t2) * nextPoint +
               (t3 - t2) * nextTangent;
    

    The above code can be treated as all "floats" (or doubles) when animating scalar values, or can be treated as 3D vectors for interpolating X, Y, and Z animations in one pass. The "interpolationValue" is a normalized zero-to-one timeline position between adjacent keyframes.