Loading a skeletal animation? Linearize it!

When I first studied the principles behind skeletal animations, I decided to implement it by interpolating between poses on CPU and by having the skinning on the vertex shader for performance reasons. I was thrilled to see some computational weight being transferred from CPU to GPU, where it could be handled with heavy parallelization.

As performance was my main concern, I also tried to squeeze the most of my CPU in the pose interpolation process by avoiding conditional logic (which, in turn, avoids branch mispredictions), interpolating quaternions with LERP instead of SLERP (the animation difference is not noticeable if you have a large amount of samples), normalizing vectors with the invsqrt procedure and using function pointers to discriminate between bones that do require interpolation from the ones that do not — which, eventually, I realized was not such a good idea: although a procedure call may be decoded in the fetch pipeline stage, an indirect function call depends on an address that might not be available on cache, which could cause the pipeline to stall for a (very slow) memory fetch.

When I wrote my first implementation, I found out that there was a myriad of possible interpolation methods, which would have been chosen by the designer in the modelling software: linear, constant, bezier curves and so on. It didn’t sound like a good idea to implement all those methods, as this would clutter my code with features that could fit very well in a large game engine, but not in my time constrained demo. Also, implementing some of these techniques would defeat my purpose of having a blazing fast skeletal animation demo.

The simpler, the better

Facing this issue, I decided to approximate whatever curve the designer used to animate the model into a series of line segments, with equally spaced joints. This way, I would only have to implement a linear interpolation method, and at the same time, would be capable of loading and animating any model.

functions

As I was using Autodesk’s FBX SDK, sampling the interpolation function was straightforward, since it provides a function to sample poses from each bone. My core sampling function (which only had to be run once, after loading the model) looks like this:

for (int i = 0; i < numSamples; ++i) {
    // Fetches keyframe
    Eigen::Map<Matrix4d> pose_double((double*)
                         evaluator->GetNodeLocalTransform
                                      (node, sampleTime));
    Matrix4f pose = pose_double.cast<float>();

    // Grabs translation
    Vector3f translation = pose.block<3,1>(0,3);

    // Grabs scale
    Vector3f scale(pose.block<3,1>(0,0).norm(),
                    pose.block<3,1>(0,1).norm(),
                    pose.block<3,1>(0,2).norm());

    // Grabs rotation
    float quat_data[4];
    Map<Quaternionf> rot_quat(quat_data);
    Map<Vector4f> rot_vec4(quat_data);
    rot_quat = Quaternionf(pose.block<3,3>(0,0));

    // Pushes key frames and their corresponding time
    translations.push_back(translation);
    scales.push_back(scale);
    attitudes.push_back(rot_vec4);

    sampleTime += skipTime;
}

Interpolating between keyframes

After transforming the animation function of all bones into a sequence of linear functions, your interpolation code can be greatly simplified. This is how my implementation looks like:

// Compute current and next keyframe indices
int current_index = int(t*framerate);
int next_index = (current_index+1)%translations.size();

// Compute interpolation factor
float alpha = t*framerate-float(current_index);

// Interpolate translation
Vector3f translation = translations[current_index]+
                       (translations[next_index]-
                        translations[current_index])*alpha;

// Interpolate rotation
Vector4f attitude = attitudes[current_index]+
                    (attitudes[next_index]-
                     attitudes[current_index])*alpha;
attitude = attitude*invsqrt(attitude[0]*attitude[0] +
                            attitude[1]*attitude[1] +
                            attitude[2]*attitude[2] +
                            attitude[3]*attitude[3]);

// Interpolate scale
Vector3f scale = scales[current_index]+
                 (scales[next_index]-
                  scales[current_index])*alpha;

// Return matrix
output << 0,0,0, translation[0],
          0,0,0, translation[1],
          0,0,0, translation[2],
          0,0,0, 1;

Matrix3f rs = Map<Quaternionf>(attitude.data()).matrix();
rs.col(0) *= scale[0];
rs.col(1) *= scale[1];
rs.col(2) *= scale[2];
output.block<3,3>(0,0) = rs;

Obviously, this ignores a large amount of concepts (some of which are FBX-specific), but none fall within the scope of this article.

Final thoughts

It turns out that loading an FBX model takes quite some time, and even more so if you linearize your animations. This may also be true for many other model formats out there, including .blend. That’s one of the reasons why it’s good to have a custom model format to ship with your games, designed in such a way that you don’t need to pre-process your model before it’s ready to be rendered.

One thing I plan to study deeper is how this linearization process plays with animation blending. I currently expect this to be straightforward, but some hardcore optimization can make it a little bit tricky.

Finally, in this post I overlooked two very important libraries, Eigen and the FBX SDK. In the future, I will be writing more detailed posts about each of them.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s