Enhancing Skeletal Animation Objects
Now that you've seen how simple it is to blend multiple skeletal animations,
why not take this new knowledge and add on to the skeletal animation objects?
Sounds like a great idea; by adding a single function to the
cAnimationCollection class, you can be on your way to blending animations like
the pros.
In fact, rather than messing with the code from cAnimationCollection, just
derive a new class that handles blended animations. This new derived class,
cBlendedAnimationCollection, is defined as follows:
class cBlendedAnimationCollection : public cAnimationCollection
{
public:
void Blend(char *AnimationSetName,
DWORD Time, BOOL Loop,
float Blend = 1.0f);
};
Wow, that's a small class! The one and only function declared in
cBlendedAnimationCollection is Blend, which is meant to take over the
cAnimationCollection::Update function. Why not just derive a new Update
function, you ask? Well, with cBlendedAnimationCollection, you can use the
regular animation sets you used, as well as your (soon to be) newly developed
blended animation sets.
Take a close look at the Blend function to see what's going on, and then I'll
show you how to put your new class to good use.
void cBlendedAnimationCollection::Blend(
char *AnimationSetName,
DWORD Time, BOOL Loop,
float Blend)
{
The Blend function prototype takes four parameters, the first of which is
AnimationSetName. When calling Blend, you need to set AnimationSetName to the
name of the animation set you are going to blend into the animation. Each
animation set in your source .X file has a unique animation name (as defined by
the AnimationSet data object's instance name). You must set AnimationSetName to
a matching name from the .X file.
I'll get back to the animation sets in a bit. For now, I want to get back to
the Blend prototype. The second parameter of Blend is Time, which represents the
time in the animation that you are using to blend the animation. If you have an
animation that is 1000 milliseconds in length, then Time can be any value from 0
to 999. Specifying a value larger than the animation's length forces the Blend
function to use the last key frame in the list to blend the animation.
What about looping the animation? Well, that's the purpose of the third
parameter, Loop. If you set Loop to FALSE, then your animations will refuse to
update if you try to update using a time value that is greater than the length
of the animation. However, if you set Loop to TRUE, the Blend function
bounds−checks the time value (Time) to always fall within the range of the
animation's time.
The previous paragraph may not make perfect sense at first, so to help you
understand, imagine the following function:
void UpdateAnimation(DWORD Elapsed)
{
static DWORD AnimationTime = 0; // Animation time
// Call Blend, using AnimationTime as the time in the animation
AnimationBlend.Blend("Walk", AnimationTime, FALSE, 1.0f);
// Update the time of the animation
AnimationTime += ELapsed;
}
In the UpdateAnimation function, you are tracking the animation time via a
static variable. Every time UpdateAnimation is called, the Blend function is
used to blend in an animation called Walk, using a time value specified as
AnimationTime. Assuming the Walk animation is 1000 milliseconds in length and
the elapsed time between calls to UpdateAnimation is 50 ms, you can see tell
that the animation would reach its end after 20 calls to the function. This
means after you call UpdateAnimation 20 times, the animation will stop (because
you set Loop to FALSE).
Going back and changing the Loop value to TRUE forces Blend to bounds−check
the timing value and make sure it always uses a valid timing value. When I say
bounds−check, I mean to use a modulus calculation. I'll show you how to use the
modulus calculation in a moment; for now I want to get back to the fourth and
final parameter.
The last parameter is Blend, which is a floating−point value that represents
a scalar value used to modify the blended transformation matrix before it is
applied to the skeletal structure. For example, if you are blending a walking
animation, but you only want 50 percent of the transformation to be applied,
then you would set Blend to 0.5.
Okay, that's enough for the parameters; let's get into the function code! If
you've perused the cAnimationCollection::Update function, you'll notice that the
majority of code in the Blend function is the same. Starting off, you'll find a
bit of code that scans the linked list of animation sets to find the one that
matches the name you provided as AnimationSetName.
cAnimationSet *AnimSet = m_AnimationSets;
// Look for matching animation set name if used
if(AnimationSetName) {
// Find matching animation set name
while(AnimSet != NULL) {
// Break when match found
if(!stricmp(AnimSet−>m_Name, AnimationSetName))
break;
// Go to next animation set object
AnimSet = AnimSet−>m_Next;
}
}
// Return no set found
if(AnimSet == NULL)
return;
If you set AnimationSetName to NULL, then Blend will use the first animation
set in the linked list of animation sets. If you specified a name in
AnimationSetName and none was found in the linked list, then Blend will return
without any further ado.
Now that you have a pointer to the appropriate animation set object, you can
bounds−check the time according to the value set in Time and the looping flag
Loop.
// Bounds time to animation length
if(Time > AnimSet−>m_Length)
Time = (Loop==TRUE)?Time %
(AnimSet−>m_Length+1):AnimSet−>m_Length;
Quite an ingenious little bit of code, the previous tidbit does one of two
things, depending on the Loop flag. If Loop is set to FALSE, then Time is
checked against the length of the animation (AnimSet−>m_Length). If Time is
greater than the length of the animation, then Time is set to the length of the
animation, thus locking it at the last millisecond (and later on, the last key
frame) of the animation. If you set Loop to TRUE, then a modulus calculation
forces Time to always lie within the range of the animation's length (from 0 to
AnimSet−>m_Length).
After you have calculated the appropriate Time to use for the animation, it
is time to scan the list of bones in your skeletal structure. For each bone, you
are going to track the combined transformations from the appropriate key frames.
For each key frame found in the animation, you need to add (not multiply) the
transformation to the skeletal structure's transformations.
// Go through each animation
cAnimation *Anim = AnimSet−>m_Animations;
while(Anim) {
//Only process if it's attached to a bone
if(Anim−>m_Bone) {
// Reset transformation
D3DXMATRIX matAnimation;
D3DXMatrixIdentity(&matAnimation);
// Apply various matrices to transformation
From here, scan each key frame (depending on the type of keys used) and
calculate the transformation to apply to your skeletal structure. For the sake
of space, I'm only going to list the code that scans matrix keys.
// Matrix
if(Anim−>m_NumMatrixKeys && Anim−>m_MatrixKeys) {
// Loop for matching matrix key
DWORD Key1 = 0, Key2 = 0;
for(DWORD i=0;i<Anim−>m_NumMatrixKeys;i++) {
if(Time >= Anim−>m_MatrixKeys[i].m_Time)
Key1 = i;
}
// Get 2nd key number
Key2 = (Key1>=(Anim−>m_NumMatrixKeys−1))?Key1:Key1+1;
// Get difference in keys' times
DWORD TimeDiff = Anim−>m_MatrixKeys[Key2].m_Time − Anim−>m_MatrixKeys[Key1].m_Time;
if(!TimeDiff)
TimeDiff = 1;
// Calculate a scalar value to use
float Scalar = (float)(Time − Anim−>m_MatrixKeys[Key1].m_Time) / (float)TimeDiff;
// Calculate interpolated matrix
D3DXMATRIX matDiff;
matDiff = Anim−>m_MatrixKeys[Key2].m_matKey − Anim−>m_MatrixKeys[Key1].m_matKey;
matDiff *= Scalar;
matDiff += Anim−>m_MatrixKeys[Key1].m_matKey;
// Combine with transformation
matAnimation *= matDiff;
}
Basically, the code is searching the key frames and calculating an
appropriate transformation to use. This transformation is stored in
matAnimation.
From this point on, things take a decidedly different course than the
cAnimationCollection::Update function code. Instead of storing the
transformation matrix (matAnimation) in the skeletal structure's frame object,
you will calculate the difference in the transformation from matAnimation to the
skeletal structure's initial transformation (stored in matOriginal when the
skeletal structure was loaded). This difference in transformation values is
scaled using the floating−point Blend value you provided, and the resulting
transformation is then added (not multiplied, as you do with concoction) to the
skeletal structure's frame transformation. This ensures that the transformations
are properly blended at the appropriate blending values.
After that, the next bone's key frames are scanned, and the loop continues
until all bones have been processed.
// Get the difference in transformations
D3DXMATRIX matDiff = matAnimation − Anim−>m_Bone−>matOriginal;
// Adjust by blending amount
matDiff *= Blend;
// Add to transformation matrix
Anim−>m_Bone−>TransformationMatrix += matDiff;
}
// Go to next animation
Anim = Anim−>m_Next;
}
}
Congratulations, you've just completed your Blend function'! Let's put this
puppy to work! Suppose you have a mesh and frame hierarchy already loaded, and
you want to load a series of animations from an .X file. Suppose this .X file
(called Anims.x) has four animation sets: Stand,Walk, Wave, and Shoot. That's
two animations for the legs and two for the arms and torso. Here's a bit of code
to load the animation sets:
// pFrame = root frame in frame hierarchy
cBlendedAnimationSet BlendedAnims;
BlendedAnims.Load("Anims.x");
// Map animations frame hierarchy
BlendedAnims.Map(pFrame);
Now that you have an animation collection loaded, you can begin blending the
animations before updating and rendering your skinned mesh. Suppose you want to
blend the Walk and Shoot animations, both using 100 percent of the
transformations. To start, you must reset your frame hierarchy's transformations
to their original states. This means you need to copy the
D3DXFRAME_EX::matOriginal transformation into the
D3DXFRAME_EX::TransformationMatrix transformation. This is very important
because it serves as a base to which your animation set transformations are
added during the blending operation.
// Use D3DXFRAME_EX::Reset to reset transformations
pFrame−>Reset();
Once the transformations have been reset to their original states, you can
blend your animation sets.
// AnimationTime = time of animation, which is the
elapsed time since start of the animation
// Blend in the walk animation
BlendedAnims.Blend("Walk", AnimationTime, TRUE, 1.0f);
// Blend in the shoot animation
BlendedAnims.Blend("Shoot", AnimationTime, TRUE, 1.0f);
Once you've blended all the animation sets you're going to use, you need to
update your frame hierarchy, which is then used to update your skinned mesh.
// Update the frame hierarchy
pFrame−>UpdateHierarchy();
After you have updated the hierarchy (the transformation matrices have been
combined and the results are stored in D3DXFRAME_EX::matCombined), you can
update your skinned mesh and render away!