In my last article, I provided some overall details on how to load Quake 2 model files (also known as md2 files) and described how these models would store its animations in their so called keyframes.
By the end of the post I discussed the fact that (as you could see on video) the model animation looks rather “choppy”. In this post I will explain what we can do to improve the animation rendering process for models animated through keyframes.
What we are going to do in order to smooth the animation is, instead of just rendering the keyframe data directly, take advantage of a factor that we haven’t completely explored yet: time.
If by the end of the last post you started working on rendering md2 models, chances are so far you have some sort of time control that allows you to determine which keyframe to render.
The rendering problem that we face is characterized by the fact that sometimes, for a given elapsed time, we determine that the actual keyframe we have to draw lays between two keyframes. Let’s call these the nth and nth+1 keyframes.
In order to draw the mode to the screen, we face two choices: select the nth keyframe or the nth+1 keyframe. What’s important to know here is that in either case our choice will be wrong. This is because the actual data that we need does not exist, and so, it’s impossible to render the model correctly. Forcing the choice to the nth or n+1 keyframe is what makes our model animation look choppy.
Not everything is lost though. Even if we don’t have the data we need, we can infer it.
Should our needed keyframe actually exist, we know it would lay between two other keyframes. Now, as we have some sort of time control mechanism, we can determine which ones these two actually are. Under these conditions, something we can do is determine the “distance” from the current time mark to each keyframe’s.
Using this information, we can guess the keyframe that’s supposed to lay in between by interpolating both keyframes in time. The idea is to perform a convex sum. The following C++ pseudocode depicts the concept:
for (size_t i = 0; i < _numvertices; i++)
guess[i] = _keyframes[n] * (1-t) +
t * _keyframes[n+1];
Here, guess holds our guessed keyframe, Vertex is a struct composed of three floats and t is a parameter dependent on the elapsed time that has been adjusted to the [0,1] interval. When t=0, it means the keyframe to render is actually the nth keyframe. Conversely, t=1 indicates the keyframe to render is nth+1. Finally, notice that when t=0.5, the guessed frame will be halfway between keyframes n and n+1.
The following video shows the improved rendering process in action:
Notice how smooth the animation actually looks. This video was generated by running the animation at just 2 frames per second.
Interpolating between keyframes is certainly possible and, in our conducted tests, it really helped smooth the animation process.
This added visual quality does come at a cost, though. Every time we have to animate the model, we will have to iterate over the keyframe array, interpolating the values corresponding to two consecutive keyframes. The additional CPU overhead is well worth it nevertheless, dramatically improving the quality of our animation.
Many factors have been left out of this post. In particular, time control, elapsed time calculation and how to determine which keyframe should be rendered at a given time were all omitted. I believe these features are dependent on the kind of application being written and different formulae will or will not work in different situations. The best thing to do is to design a model that works for your application and produce a simple way to obtain the data needed to apply these concepts.
See you next time!