Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Table of Contents

Animate()

Interpolates bone fields based on provided animation data, as well as initial states for non-animated fields.

function Animate(
    armature: Armature,
    anims: Animation[],
    frames: Int[],
    smoothFrames: Int[],
) {
    for (let a = 0; a < anims.length; a++) {
        for (let k = 0; k < anims[a].keyframes.length; k++) {
            let kf = anims[a].keyframes[k];

            // only prev keyframes are considered
            if (kf.frame > frames[a]) {
                break;
            }

            // refer keyframe to itself, if there's no next
            if (kf.next_kf == -1) {
                kf.next_kf = k;
            }

            const nextKf = anims[a].keyframes[kf.next_kf];

            // skip keyframe if it's not the last, and would not be animated
            const isLast = kf.next_kf == k;
            const isBeforeFrame = nextKf.frame < frames[a];
            if (isBeforeFrame && !isLast) {
                continue;
            }

            const f = frames[a];
            const sf = smoothFrames[a];

            // animate basic fields
            let bone = armature.bones[kf.bone_id];
            if (kf.element == "PositionX")
                interpolateKeyframes(bone.pos.x, kf, nextKf, f, sf);
            if (kf.element == "PositionY")
                interpolateKeyframes(bone.pos.y, kf, nextKf, f, sf);
            if (kf.element == "Rotation")
                interpolateKeyframes(bone.rot, kf, nextKf, f, sf);
            if (kf.element == "ScaleX")
                interpolateKeyframes(bone.scale.x, kf, nextKf, f, sf);
            if (kf.element == "ScaleY")
                interpolateKeyframes(bone.scale.y, kf, nextKf, f, sf);
            if (kf.element == "Hidden") bone.hidden = kf.value == 1;

            // animate visual fields
            if (bone.visuals_id != -1) {
                let visuals = armature.visuals[bone.visuals_id];

                if (kf.element == "Texture") visuals.tex = kf.value_str;
                if (kf.element == "TintR")
                    interpolateKeyframes(visuals.tint.r, kf, nextKf, f, sf);
                if (kf.element == "TintG")
                    interpolateKeyframes(visuals.tint.g, kf, nextKf, f, sf);
                if (kf.element == "TintB")
                    interpolateKeyframes(visuals.tint.b, kf, nextKf, f, sf);
                if (kf.element == "TintA")
                    interpolateKeyframes(visuals.tint.a, kf, nextKf, f, sf);
            }

            // animate inverse kinematics fields
            if (bone.ik_family_id != -1) {
                let ik = armature.inverse_kinematics[bone.ik_family_id];

                if (kf.element == "IkConstraint") ik.constraint = kf.value_str;
                if (kf.element == "MimicTarget")
                    ik.mimic_target = kf.value == 1;
            }
        }
    }

    // bones that are not being animated should return to their initial states
    resetBones(armature, anims, frames[0], smoothFrames[0]);
}

interpolateKeyframes()

With the provided animation frame, determines the keyframes to interpolate the field by.

The resulting interpolation from the keyframes is interpolated again for smoothing.

function interpolateKeyframes(
    field: Float,
    prevKf: Keyframe,
    nextKf: Keyframe,
    frame: Int,
    smoothFrame: Int,
): Float {
    const totalFrames = nextKf.frame - prevKf.frame;
    const currentFrame = frame - prevKf.frame;

    // interpolate from current to next keyframe value
    const result = interpolate(
        currentFrame,
        totalFrames,
        prevKf.value,
        nextKf.value,
        nextKf.start_handle,
        nextKf.end_handle,
    );

    // using the requested smoothing frames, interpolate the current field to the target value
    const z = { x: 0, y: 0 };
    return interpolate(currentFrame, smoothFrame, field, result, z, z);
}

interpolate()

Interpolation uses a modified bezier spline (explanation below).

Note that 2 helper functions are included below the main function.

function interpolate(
    current: Int,
    max: Int,
    startVal: Float,
    endVal: Float,
    startHandle: Vec2,
    endHandle: Vec2,
): Float {
    // snapping behavior for Snap transition preset
    if(startHandle.y == 999.0 && endHandle.y == 999.0) {
        return startVal;
    }
    if(max == 0 || current >= max) {
        return endVal;
    }

    // solve for t with Newton-Raphson
    const initial = current / max;
    let t = initial;
    for(let i = 0; i < 5; i++) {
        let x = cubicBezier(t, startHandle.x, endHandle.x);
        let dx = cubicBezierDerivative(t, startHandle.x, endHandle.x);
        if(Math.abs(dx) < 1e-5 {
            break;
        }
        t -= (x - initial) / dx;
        t = clamp(t, 0.0, 1.0);
    }

    const progress = cubic_bezier(t, startHandle.y, endHandle.y)
    return startVal + (endVal - startVal) * progress
}

// for both functions below, p0 and p3 are always 0 and 1 respectively

function cubicBezier(t: Float, p1: Float, p2: Float): Float {
    const u = 1. - t;
    return 3. * u * u * t * p1 + 3. * u * t * t * p2 + t * t * t;
}

function cubicBezierDerivative(t: Float, p1: Float, p2: Float): Float {
    const u = 1. - t;
    return 3. * u * u * p1 + 6. * u * t * (p2 - p1) + 3. * t * t * (1. - p2);
}

Bezier Explanation

Note: the following explanation is incomplete, as it doesn’t include Newton-Rapshon. However, understanding this is not required to implement the code above.

A Primer on Bezier Curves

The bezier spline uses the following polynomial:

value =
    a * (1 - t)^3 +
    b * 3 * (1 - t)^2 * t +
    c * 3 * (1 - t) * t^2 +
    d * t^3

This can be simplified into 4 points:

FormulaCoefficient (a, b, c, d)
h00(1 - t)^3startVal
h013 * (1 - t)^2 * tstartHandle
h103 * (1 - t) * t^2endHandle
h11t^3endVal

The above is for a generic bezier spline, however.

In interpolation, startVal and endVal should be 0 and 1 respectively to represent 0% to 100% of the end value. This allows the algorithm to have a persistent curve regardless of the actual values being interpolated.

Simplified points:

FormulaCoefficient (b, c, d)
h013 * (1 - t)^2 * tstartHandle
h103 * (1 -t) * t^2endHandle
h11t^31

Notice that h00 is now gone, as its coefficient, startVal, is always 0 and would have no effect on the algorithm.

The actual start and end values are applied at the end:

progress = h10 * startHandle + h01 * endHandle + h11
value = start + (end - start) * progress

resetBones()

Animate bones back to their initial states, if they’re not being animated.

This makes use of each bones’ init_ fields (init_pos, init_rot, etc).

Example: animation 1 was played and rotated the arm bone, but animation 1 is not being played anymore. That arm bone must now return to its initial rotation.

function resetBones(
    armature: Armature, animations: Animation[], frame: Uint, smoothFrame: Uint
) {
    let elementMap = {}

    // add every element that's being animated for each bone
    anims.forEach(anim => {
        anim.keyframes.forEach(kf => {
            if(!elementMap[kf.bone_id].contains(kf.element)) {
                elementMap[kf.bone_id].push(kf.element)
            }
        })
    jj

    const f = frame;
    const sf = smoothFrame;

    // reset every element that's not in the map back to its initial state
    armature.bones.forEach(bone => {

        // reset basic fields
        if (!elementMap[bone.id]["PositionX"])
            interpolate(f, sf, bone.pos.x, bone.init_pos.x, z, z)
        if (!elementMap[bone.id]["PositionY"])
            interpolate(f, sf, bone.pos.y, bone.init_pos.y, z, z)
        if (!elementMap[bone.id]["Rotation"])
            interpolate(f, sf, bone.rot, bone.init_rot, z, z)
        if (!elementMap[bone.id]["ScaleX"])
            interpolate(f, sf, bone.scale.x, bone.init_scale.x, z, z)
        if (!elementMap[bone.id]["ScaleY"])
            interpolate(f, sf, bone.scale.y, bone.init_scale.y, z, z)
        if (!elementMap[bone.id]["Hidden"])
            bone.hidden = bone.init_hidden

        // reset visual fields
        if bone.visuals_id != -1 {
            let visuals = armature.visuals[bone.visual_id]

            if (!elementMap[bone.id]["Texture"])
                visuals.tex = visuals.init_tex;
            if (!elementMap[bone.id]["TintR"])
                interpolate(f, sf, visuals.tint.r, visuals.init_tint.r, z, z)
            if (!elementMap[bone.id]["TintG"])
                interpolate(f, sf, visuals.tint.g, visuals.init_tint.g, z, z)
            if (!elementMap[bone.id]["TintB"])
                interpolate(f, sf, visuals.tint.b, visuals.init_tint.b, z, z)
            if (!elementMap[bone.id]["TintA"])
                interpolate(f, sf, visuals.tint.a, visuals.init_tint.a, z, z)
        }

        // reset inverse kinematics fields
        if (bone.ik_family_id != -1) {
            let ik = armature.inverse_kinematics[bone.ik_family_id]

            if (!elementMap[bone.id]["IkConstraint"])
                ik.constraint = ik.init_constraint;
            if (!elementMap[bone.id]["MimicTarget"])
                ik.mimic_target = ik.init_mimic_target;
        }]
    })
}

Note: most runtimes will have this functionality at the end of Animate(). The separation of functions is purely for documentation purposes.