Animation Blending

I was reading the chapter Animation Blending of Game Engine Architecture and decided to put in practice what I learned in a simple demo. The result can be seen here, where you can play around with the X and Z speeds by moving the slidebars:

I’m using 5 animations, each one moving the character in some direction: left, right, forward, backwards and one animation for idle pose. Then, using the x and z speeds, I blend them to create the final pose. This blend can result in an animation movement pointing to any direction covered by the animation’s set. So basically, with this technique you don’t need a specific animation when the player is moving diagonally or at certain speed, you can just blend two or more and compose the desired movement.

This can be expanded to situations where the upper body play a different animation than the lower body, for example: you can have the character reloading his weapon while he’s playing soccer (is this a good idea?).

Of course, the result is not so refined as a proper animation to the given direction at the given velocity, what is very perceptible when playing just one of the 5 animations (set one of the speeds to 0 and note the higher quality). However, this solution can be a good work around when dealing with a small set of animations.

Foundation

This technique take advantage of the Affine Invariant property of the transformations used to animate the skeleton. By saying that all key transformations are Affine (i.e. translation, rotation and scaling) we can make a weighted sum of any number of them, with the only requirement that all wheights sum to one, and the result transformation still affine.

So we just needs to interpolate the key transformations in every frame like any other animation system. This can be done by using linear interpolation to translations/scalings and spherical interpolation to quaternion rotations. This way, we will have one transformation matrix for each animation we wanna blend, and to actually blend them, we just use the weighted sum. Note that is better to interpolate the transformation components at each bone local space because apart from getting a more naturally movement for the bones,all the interpolations can be performed in parallel.

Implementation

First, I implemented a simple interface on top of WebGL to create shaders and buffers, very simple because I knew I would need to draw just lines between points. Then, I got some animations from Mixamo (which has very good free content in my opinion) and using this tool I converted the FBX files to JSON that I can read in my code.

After I spent some time hacking the JSON files to understand how the key frames are organized, I made the function to interpolate single animations. Then, add the Animation Blending was surprinsingly easy… I ended up with this code to interpolate and blend the animation:

// skeleton: map of each bone's local and global transformations
// animation: map of each bone's key transformations
// now: time since program begin
// weight: this animation weight
function updateSkeleton(skeleton, animation, now, weight) {
  
  // calculate the current frame
  var frame = Math.round(now/animation.ms) % animation.duration;
  
  // for each bone transformed by this animation
  for(var bone in animation.keyframes) {
    var map = animation.keyframes[bone];
    
    // get the key frames before and after the current frame
    var [prevFrame, nextFrame] = getKeyframe(map, frame);
    
    // initialize components
    var pos = null;
    var scl = null;
    var rot = null;
    
    // if the animation has only 1 key frame
    if(prevFrame == nextFrame) {
      // don't need to interpolate
      pos = vec3.clone(map[prevFrame].position);
      scl = vec3.clone(map[prevFrame].scale);
      rot = quat.clone(map[prevFrame].rotation);
    
    } else {
      // set the frame time between 0 and 1 for the interpolation parameter 
      var t = (frame-prevFrame)/(nextFrame - prevFrame);
    
      // inteporlate each component
      pos = vec3.lerp(vec3.create(), map[prevFrame].position, map[nextFrame].position, t);  
      scl = vec3.lerp(vec3.create(), map[prevFrame].scale, map[nextFrame].scale, t);  
      rot = quat.slerp(quat.create(), map[prevFrame].rotation, map[nextFrame].rotation, t);
    };
    
    // mix the components into a single matrix
    var trans = mat4.fromRotationTranslationScale(mat4.create(), rot, pos, scl);
    
    // make the weighted sum with the skeleton transformation
    mat4.multiplyScalarAndAdd(trans, skeleton[bone].global, trans, weight);
    
    // set both local and global transformations of the bone
    // when traversing the skeleton is possible to multiply child.local with parent.global
    // and get the correct global transform for each bone
    skeleton[bone].local = trans;
    skeleton[bone].global = mat4.clone(trans);
  }
}