Architecting a 3D Animation Engine
The interface to a 3D animation system can be very simple or very complex. If you've written one, you know what I mean. If you haven't written one, you probably will someday soon. Here is a set of C/C++ interface guidelines for an animation engine.
It always helps to know what you're getting into. About three years ago, I was asked to volunteer my time at a junior high school (7th and 8th graders) to speak with groups of kids. They were having Career Day, and most of the people coming in to speak about their careers were parents of the children in school. I had attended this particular junior high school years ago, and a friend of mine who now works there contacted me a few days before Career Day to see if I'd come talk about working in the video game industry.
When I arrived at the school, I checked in at the principal's office, and amidst some confusion, they were able to tell me where I'd be presenting. I thought it was strange that they had chosen the girls' gym as my presentation room, but it didn't concern me too much. I hadn't been in a junior high school in quite some time, and from the moment I walked in the front doors, I realized I was in a different, mostly shorter, world.
I made my way to the girls' gym. The gym teacher was there to greet me, and she explained how the day's schedule worked. Then she dropped the bomb. Nobody had told me this yet, but the reason I was asked to come in was because another presenter had canceled. This didn't strike me as horrific until I learned two more facts. The other presenter had canceled after the kids had chosen the careers they were interested in. The other presenter was scheduled to talk about sewing.
That day, I learned that 13-year-old girls who are interested in sewing and junior-high kids who are interested in video games are mutually exclusive groups. The talk was a disaster. I bombed. The discussion quickly degenerated to questions like, "Are you a surfer?" The only thing that saved me was the girls' gym teacher, who kept the group of giggling girls from exploding into full-on 13-year-old giggling girl anarchy. By the end of the day, the results were in. Everybody loved the firefighter (he brought in lots of cool equipment), and everybody liked the clown (everybody except, perhaps, the clown's kid). Nobody mentioned video games. So much for my first attempt at being a positive role model.
The moral of this story is: Know what you're getting into beforehand. Applied to 3D animation engines, this lesson dictates that you determine exactly what you want your engine to do before you begin developing it. If you don't have a good grasp of the requirements, your initial engine will have only the most basic features and won't be sufficient to support your game. You could find yourself adding various capabilities to the engine during the course of developing your game. Why not get it right from the start? This article describes a fairly full-featured interface for just such an engine.
There are a number of informational resources and sources of inspiration to investigate before you begin developing your 3D animation engine:
If you've already written your own 2D or 3D animation API, consider what features you'll be able to add to it.
Get documentation on commercial high-level 3D APIs that support animation. They'll inspire you to think in new directions and develop new features.
Look at some other 3D game titles in development. What features do they use that you would like to implement in your next title?
Discuss your ideas with someone who is knee deep in a large project. Their experience and perspective will undoubtedly expose ideas that are very useful and not obvious at the start of a project.
The 3D system that you start with will likely fall somewhere close to OpenGL on the spectrum of high- to low-level APIs. The high-level end of this spectrum includes all retained-mode systems, while the lowest level of this spectrum is you, the hardware, and an assembler. Whichever end of the spectrum you're on, however, you'll want to write your own animation control system.
At the high level of the API spectrum, there's usually support for animation, but it's unlikely that it will do you any good for the following reasons: performance, licensing fees, and feature set. Retained-mode APIs have a pretty consistent reputation for being slow and bulky. Some retained-mode APIs require licensing fees that you may not want to pay. And finally, animation support is often very basic.
For example, Direct3D Retained Mode allows you to define keyframes and the current display time for your animated object. You'll have to add a lot more code to make this system useful to you, and you have no control over important things such as the internal representation (storage size and accuracy) of the keyframes. Direct3D Retained Mode stores rotational keyframes as quaternions defined as four floats (16 bytes per keyframe). Using a custom 16-bit value in place of the 32-bit floats may provide you all the accuracy you want, and it will cut your animation data size in half. However, retained mode APIs won't provide you with this option.
At the low-level end of the spectrum, animation support is nonexistent and completely up to you. In the midrange, where OpenGL falls, animation support is also nonexistent. OpenGL, Direct3D Immediate Mode, 3Dfx's Glide, and consoles provide a rendering pipeline only. Hierarchical animation takes place in the steps before your data is taken over by the rendering pipeline.
So let's get down in it. Here, I will propose an interface to a 3D animation system. The only assumption that I'll make about the underlying engine is that you will implement the capability of interpolating between arbitrary keyframes.
The first part of our animation interface will deal with playing animations. With one very simple function, many different modes of animation playback are covered.
For all code listings, I'll use a C++ style that assumes we have a class that implements this animation interface. Every function interface listed in this article is actually a member function (or method) of our AnimatedObject class (Listing 1). If you're using C, just mentally insert a pointer to the animated object struct as the first argument of each function, then grumble about the lameness of C++ for a bit. Likewise, you can use your imagination to eliminate the default arguments used in the examples if you are using a strictly C compiler that doesn't support default arguments.
The PlayAnimation function,
void PlayAnimation(int animNum, float startTime=0f, float transitionTime=0f);
gives us the very basic ability to start playing any animation that our character is capable of playing, as defined by animNum. It also allows us to supply a starting point, startTime, within that animation; depending on the game, there may be many occasions when you want to start playing an animation "in progress" by skipping a few milliseconds of that animation. Finally, we allow ourselves the ability to define the transition time between animations with the transitionTime argument. This time can mean different things to different people. For some, it can define the number of milliseconds inserted between the previous animation and the one that you are attempting to play. For others, it can define a period of time during which both animations are played and averaged together. Either way you look at it, this time will normally be a standard value that makes the transition between animations look good. The other common value for transitionTime will be zero, indicating that the next animation must start immediately without any interpolation at all.
From PlayAnimation, we can easily come up with a few commonly used variations to define within our animated object class.
The function in Listing 2 will start a particular animation immediately. There will be no transition between the currently playing animation and the animation specified in this function call. The starting point of the new animation will be startTime milliseconds into the new animation.
The TransitionIntoAnimation function in Listing 3 will be the most frequently used function to start a new animation. All you need to specify is the new animation to play. The transition period between the currently playing animation and this new animation is set to the StandardTransitionTime (a const defined in a header file, typically equivalent to two or three frames' worth of animation). This function provides the smooth transitions that you'll normally want to see between animations.
The TransitionIntoAnimationAtTime function in Listing 4 will transition (interpolate) into a new animation and will skip the first few milliseconds of that new animation as specified by startTime.
All three of these functions are just convenient ways to call PlayAnimation; they are all ideal candidates for inlining. Each function states exactly what its purpose is, so you can tell what's going on in the code without trying to interpret individual arguments to PlayAnimation.
What happens when an animation is finished playing? Our animation engine has two options. It can loop the current animation, or it can transition into the next animation. We need a way to define what the next animation is. This can be achieved with the following function:
void SetNextAnimation(int next);
The SetNextAnimation function can be called at any time, but it is most appropriate to call it immediately after playing an animation. That way, you know exactly what will happen after an animation is finished even if you don't get around to playing another animation. Our engine can make some fairly intelligent decisions regarding looping animations at this point. As soon as an animation is played with any variant of PlayAnimation, assume that the animation will be looped. Then, as soon as a next animation is defined via SetNextAnimation, disable the looped status of the current animation (otherwise the next animation would never be reached), and assume that the next animation will be looped. If these assumptions ever change, we'll conveniently have functions to manipulate or examine the looped status of the currently playing animation.
The functions in Listing 5 do exactly what you would expect. If the currently playing animation is looped (recall that our engine will automatically loop any played animation), you can change that status with a call to DisableLoopedAnimation. If you then change your mind, you can re-enable looping with EnableLoopedAnimation. IsAnimationLooped returns the current status.
What would happen if you played an animation, set the next animation, and then called EnableLoopedAnimation, like this?
// Interesting code snippet
PlayAnimation(animNum, startTime);
SetNextAnimation(next);
EnableLoopedAnimation();
In this case, the initial animation will be looped. When you call SetNextAnimation, the playing animation will cease to loop. When you re-enable looping on the current animation via EnableLoopedAnimation, the next animation that you set will never be played. This is a perfectly reasonable sequence of actions that could arise in your game, and no harm will come of it. What happens if your next animation is playing looped, and you disable its looping? When that animation ends, your engine won't know what to play. This case can either be considered a bug (an error message appears with something clever such as "Should never get here!"), or you can define a default animation for each animated object that is played and looped when no more animations are instructed to play.
You can exert even more control over what happens with the next animation to be played. There will be times when you want the currently playing animation to end early, but you don't yet have another animation to play. Usually, the animation that was set with SetNextAnimation is a safe follow-on to the current animation (otherwise you wouldn't have set it as the next animation). You can skip directly to the next animation by calling either of these functions:
void PlayNextAnimation();
void TransitionIntoNextAnimation();
PlayNextAnimation will start the next animation immediately with no transition. TransitionIntoNextAnimation will start the next animation with the default amount of interpolated transition frames. You could add a transitionTime parameter to the TransitionIntoNextAnimation function, which would allow you to specify the transition time each time this function is used. However, in general you want to use your default transition time, so don't bother with this parameter unless it's a must.
The final functions related to playing animations allow you to temporarily disable the entire animation system. They are
void StopAnimation();
void StartAnimation();
If you want the animated object to freeze, or if you want to take algorithmic control over the animation, then you need to temporarily disconnect animation playback. StopAnimation stops animation playback in its tracks. Any updates to the current animation will be ignored. Even calls to PlayAnimation should be ignored until the animation is re-enabled with a call to StartAnimation.
The animations that you play back with this interface will have keyframes, and those keyframes will have timestamps that define the animation's actual playback speed. Those timestamps will most likely be in terms of frames, where a frame is about 1/30 second. What if you want to play an animation back at a different speed? Give yourself an easy way to do this with functions like these:
void SetMsPerFrame(float newRate=DefaultMsPerFrame);
float GetMsPerFrame();
The preceding functions allow you to set "milliseconds per frame" to define the playback rate of an animation. If your animations were originally 30 frames per second, the DefaultMsPerFrame value about 33.33. By calling SetMsPerFrame with a value of 66.66, you cut the animation playback rate in half and everything will play back slower than normal. Smaller values will make animations play faster. Note that the playback rate set in the preceding functions should be set on a per-object basis, and it should stay set for that object until it is changed again by you. Setting a global playback rate for all objects won't have the flexibility that you will need.
If you're accustomed to basing your entire life around a vertical blank interrupt, the idea of setting 33.33 milliseconds per frame is going to hurt your brain in some way. Sorry about that. There is some overhead to using milliseconds instead of an integral frame count, but it's minimal on today's machines. Basing everything on milliseconds rather than frames makes more sense if you ever plan on having the game run on more than one machine configuration. For consoles, there's a discrepancy between PAL (50 FPS) and NTSC (60 FPS). On a PC, the frame rate you can to achieve will fluctuate according to the performance of the machine on which your game is running. Basing everything in your game on milliseconds instead of frames is hard to get used to for some people (including me), but it's worth the effort.
An advanced feature that you may want to add to your animation engine is the ability to capture a channel. If you have a hierarchical animation system, you will have multiple channels of data for each animation, such as a root translation channel and a joint rotation channel for each joint in the hierarchy. There may come a time in your game when you want to take algorithmic control over one part of the character's animation and allow the rest of the character to animate normally. For example, you might want your character to look in a certain direction while running. To do this, you have to capture the channel for the neck rotation.
Capturing the channel itself is easy if you have a method of identifying translation and rotation channels by number. To accomplish this, you should add the following functions to your engine:
void CaptureRotationChannel(int which);
void CaptureTranslationChannel(int which);
The numbers that you use to identify channels should correspond to the numbering or ordering convention of your 3D animation tools. Your engine will respond to this capture request by simply stopping the animation for that channel only. For example, you may have 16 joints in a model being animated with rotation information every frame. Capturing joint 10 means that joint 10 will no longer be updated with rotation information every frame, but the other 15 joints will continue to be updated.
Once a channel is captured, you can do with it what you like, safe in the knowledge that changes you make to a captured joint's rotation or translation won't be clobbered by animation data. How you actually manipulate that joint depends on how your animation system is implemented.
As your animation engine grows in complexity, it becomes increasingly important to know about the state of the engine at various times during game execution. The solution, of course, is to create functions that retrieve this state information. Let's look at some of these.
Information regarding the current animation being played is certainly important. To identify the currently playing animation by number, use this function:
int GetCurrentAnimation();
To return the current position in the animation being played relative to the start of that animation (this value will continually reset to zero for looped animations), use:
float GetAnimationTimeInMs();
Similarly, you can return the current position in the animation being played relative to the start of that animation in terms of frames with
float GetAnimationTimeInFrames();
Why create two functions that return the same information in different formats? Recall that the playback speed of an animation can be changed. In some cases, you might be interested in knowing whether a particular frame of an animation has passed yet. Using GetAnimationTimeInFrames, you can determine this information regardless of the animation's current playback speed setting.
The InTransition function should return True if the animation engine is currently interpolating between two different animations:
bool InTransition();
The functions in Listing 6 provide you with additional state information that you will likely need. The first function, GetNextAnimation, will tell you what animation is set to be played next. Sometimes, you need to know very specific information about the current state of the animation, such as the global position of a particular joint. Using your joint numbering convention, you can ask for the position of any joint in terms of world coordinates. In this example, the GetGlobalPosition function returns the x, y, and z values individually, but you'll probably use your own vector type here. The global position of any joint is essential for collision detection.
The velocity of an animation is an implementation-dependent concept. If you've extracted a velocity from your animations and stored this velocity with the animation, you can use GetCurrentVelocity to find out what the current velocity is. This information is useful when trying to predict when an animated character will reach a certain location, for example. The corresponding SetCurrentVelocity can come in handy if you've determined that an animated character is moving too slowly to get where it needs to be. Get the velocity, scale it up, and set it again. SetCurrentVelocity should override the current animation's velocity only. When a new animation starts, the correct velocity for that animation should be used. Note the SetMsPerFrame function that I already discussed will affect the velocity as well as playback rate, so if a persistent change in speed is what you're after, use SetMsPerFrame.
Often, you'll need to know how long an animation will take to play back. GetAnimLengthInMs will return that information for any animation that a character is capable of playing. This information can be useful in hundreds of situations. This call should take into account the current milliseconds per frame setting.
In addition to the length of a particular animation, there is a long list of other information that you may want to know before playing an animation. For example, you might want to know which frame of an animation has the largest z translation from the starting position. For custom information such as this, write a routine that can query individual frames of individual animations. Note that in a keyframed, interpolation-based system, the frame that you inquire about might not physically exist. In this case, the query will have to create the interpolated frame in order to return the information that you're after.
Perhaps you have an animation scripting system that plays back sequences of animations defined in a text file. Or you may need to queue up more than one animation at a time. These needs are simple extensions to the system presented in this article. If you start out with a fairly complete foundation to your animation system, new and advanced features will come easily.
Many ideas are presented in this discussion of a theoretical 3D animation system interface. All of the ideas are from real-world examples. Nonetheless, your real world will always be different. The entire set of features that you need can only be determined by you; hopefully, by now you are thinking well beyond the ideas presented here and picturing your ideal animation system. And if anyone asks you to speak to kids about any of this, walk away. Just walk away.
Scott Corley is a developer at High Voltage Software. The author would like to acknowledge fellow High Voltage guy Dwight Luestcher for his part in developing the concepts discussed in this article
Listing 1
class AnimatedObject {
// An interface to an animated object. Most comments are
// left out for brevity. See accompanying article for
// full descriptions of everything here.
// StandardTransitionTime is 1/10 second, 100 milliseconds
static const float StandardTransitionTime=100f;
// DefaultMsPerFrame is 1/30 second
static const float DefaultMsPerFrame=33.333333f;
// The intent is to provide an interface only, but some of the data
// mentioned in the article is presented below. Obviously, in a full
// animation engine implementation, there will be much more data
// in this object.
int currentAnimation; // ID of animation playing now
float currentAnimationTime; // current frame of current animation
int nextAnimation; // ID of next animation to play, -1 if none set
float msPerFrame; // current playback rate setting
bool loopCurrentAnim; // true if current animation should loop
// Every animated object has to have a pointer to the animation data
// it is capable of playing. Actual implementation is up to you.
AnimationData *animationData;
public:
AnimatedObject(AnimationData *animDataIn); // constructor
void PlayAnimation(int animNum, float startTime=0f, float transitionTime=0f);
inline void PlayAnimationAtTime(int animNum, float startTime)
{ PlayAnimation(animNum, startTime); }
inline void TransitionIntoAnimation(int animNum)
{ PlayAnimation(animNum, 0f, StandardTransitionTime); }
inline void TransitionIntoAnimationAtTime(int animNum, float startTime)
{ PlayAnimation(animNum, startTime, StandardTransitionTime); }
void SetNextAnimation(int next);
void EnableLoopedAnimation();
void DisableLoopedAnimation();
bool IsAnimationLooped();
void PlayNextAnimation();
void TransitionIntoNextAnimation();
void StopAnimation();
void StartAnimation();
void SetMsPerFrame(float newRate=DefaultMsPerFrame);
float GetMsPerFrame();
void CaptureRotationChannel(int which);
void CaptureTranslationChannel(int which);
int GetCurrentAnimation();
float GetAnimationTimeInMs();
float GetAnimationTimeInFrames();
bool InTransition();
bool IsAnimationLooped();
int GetNextAnimation();
void GetGlobalPosition(int joint, float &x, float &y, float &z);
void GetCurrentVelocity(float &x, float &y, float &z);
void SetCurrentVelocity(float x, float y, float z);
float GetAnimLengthInMs(int animNum);
};
Listing 2
inline void PlayAnimationAtTime(int animNum, float startTime)
{ PlayAnimation(animNum, startTime); }
Listing 3
inline void TransitionIntoAnimation(int animNum)
{ PlayAnimation(animNum, 0f, StandardTransitionTime); }
Listing 4
inline void TransitionIntoAnimationAtTime(int animNum, float startTime)
{ PlayAnimation(animNum, startTime, StandardTransitionTime); }
Listing 5
void EnableLoopedAnimation();
void DisableLoopedAnimation();
bool IsAnimationLooped();
Listing 6
int GetNextAnimation();
void GetGlobalPosition(int joint, float &x, float &y, float &z);
void GetCurrentVelocity(float &x, float &y, float &z);
void SetCurrentVelocity(float x, float y, float z);
float GetAnimLengthInMs(int animNum);
bool IsAnimationLooped();
Read more about:
FeaturesAbout the Author
You May Also Like