Time-based animation

Started by
16 comments, last by ta0soft 13 years, 1 month ago
Hello, I'm writing a simple 3d engine that uses keyframes to animate a mesh. I'm following the book "Advanced Animation with DirectX" by Jim Adams. It's a little confusing but I've managed to make some progress :rolleyes: .

I have a KeyFrame class that stores the time and transformation matrix of each keyframe. My Animation class stores a vector of KeyFrames and an Update() function that is called (before?) every frame update.

Here's a snippet from the book:

void FrameUpdate()
{
static DWORD StartTime = timeGetTime();
DWORD Elapsed = timeGetTime() ? StartTime;

With the elapsed time now in hand, you can scan the key frames to look for the two between which the time value lies. For example, if the current time is 60 milliseconds, the animation is somewhere between key frame #0 (at 0 milliseconds) and key frame #1 (at 400 milliseconds). A quick scan through the key frames determines which to use based on the elapsed time.

DWORD Keyframe = 0; // Start at 1st keyframe
for(DWORD i=0;i<4;i++) {
// If time is greater or equal to a key?frame's time then update the keyframe to use
if(Time >= Keyframes.Time)
Keyframe = i;
}

At the end of the loop, the Keyframe variable will hold the first of the two key frames between which the animation time lies. If Keyframe isn't the last key frame in the array (in which there are four key frames), then you can add 1 to Keyframe to obtain the second key frame. If Keyframe is the last key frame in the array, you can use the same key?frame value in your calculations. Using a second variable to store the next key frame in line is perfect. Remember that if Keyframe is the last key frame in the array, you need to set this new key frame to the same value.

DWORD Keyframe2 = (Keyframe==3) ? Keyframe:Keyframe + 1;

Now you need to grab the time values and calculate a scalar based on the time difference of the keys and the position of the key frame between the keys.

DWORD TimeDiff = Keyframes[Keyframe2].Time ? Keyframes[Keyframe].Time;
// Make sure there's a time difference to avoid divide?by?zero errors later on.
if(!TimeDiff) TimeDiff=1;

float Scalar = (Time ? Keyframes[Keyframe].Time)/TimeDiff;

You now have the scalar value (which ranges from 0 to 1) used to interpolate the transformation matrices of the keys.
[/quote]

I've modified the code a bit, here is my Update function:

D3DXMATRIX Animation::Update(void)
{
DWORD keyFrame, keyFrame2;

if (p_StartTime == 0) p_StartTime = timeGetTime();
p_ElapsedTime = timeGetTime() - p_StartTime;

for (DWORD i = 0; i < Get_KeyFrameCount(); i++)
{
if (p_ElapsedTime >= p_KeyFrames->Get_Time()) keyFrame = i;
}

keyFrame2 = (keyFrame == Get_KeyFrameCount() - 1) ? keyFrame:keyFrame + 1;

DWORD timeDiff = p_KeyFrames[keyFrame2]->Get_Time() - p_KeyFrames[keyFrame]->Get_Time();
if (!timeDiff) timeDiff = 1;

float scalar = (p_ElapsedTime - p_KeyFrames[keyFrame]->Get_Time()) / timeDiff;

//...
};
[/quote]

Everything seems to work but the scalar returns weird numbers like 3209.0, 1203.0, instead of a value between 0-1. Am I doing something wrong? Thanks for the help in advance!
Advertisement
It looks like float scalar may be the result of an integer divide (for you to check). In addition, you have a parentheses out of place. Should be something like:

float scalar = (p_Elapsed - pKeyFrames.Time)/timeDiff;
// .... NOT AS BELOW
float scalar = (p_Elapsed - pKeyFrames.Time/timeDiff);

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

grr I thought I edited the post and fixed the parentheses, my mistake. Either way it still returns the same numbers. I don't know whats wrong :(
Well, looking back at the book it also uses DWORD for Time, so I don't understand how deviding 2 integers returns a float value between 0 and 1? Every time it returns 0 or some odd 4 digit number. Should I cast them to float before deviding? Like this?


float scalar = (float)(p_ElapsedTime - p_KeyFrames[p_KeyFrame]->Get_Time()) / (float)timeDiff;
[/quote]

Well, looking back at the book it also uses DWORD for Time, so I don't understand how deviding 2 integers returns a float value between 0 and 1? Every time it returns 0 or some odd 4 digit number. Should I cast them to float before deviding? Like this?


float scalar = (float)(p_ElapsedTime - p_KeyFrames[p_KeyFrame]->Get_Time()) / (float)timeDiff;

[/quote]
Casting either the numerator or denominator should be sufficient but, yes, a cast to float is required.

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

Well I figured it out, there was code missing from the book but by some miracle I got it working :D. Now I am faced with another problem...

The scalar variable returns the correct values, now I need to "linearly interpolate between the 2 transformation matrices at each keyframe" to get the final transformation matrix. In the book he uses D3DMATRIX in his Keyframe struct and casts it to D3DXMATRIX in the Update function:


// Calculate the difference in transformations
D3DXMATRIX matInt = D3DXMATRIX(Keyframes[Keyframe2].matTransformation) - D3DXMATRIX(Keyframes[KeyFrame].matTransformation);

// Scale the difference
matInt *= Scalar;

// Add scaled transformation matrix back to 1st keyframe matrix
matInt += D3DXMATRIX(Keyframes[KeyFrame].matTransformation);
[/quote]

My KeyFrame class already stores the transform matrix as a D3DXMATRIX so I don't need to cast anything. Will the math still work the same? This is supposed to return the "interpolated" matrix between 2 keyframes but I don't think its working. Nothing happens when I use this matrix to render the mesh, or when I multiply it by the world matrix.

I don't know much about matrix math other than multiplication, this subtraction and addition stuff is confusing. Please help!!


D3DXMATRIX Animation::Update(void)
{
if (Get_KeyFrameCount() == 1) return p_KeyFrames[0]->Get_TransformMatrix();
else
{
if (p_StartTime == 0) p_StartTime = timeGetTime();
p_ElapsedTime = timeGetTime() - p_StartTime;

// Missing from original code
DWORD duration = p_ElapsedTime % p_KeyFrames[Get_KeyFrameCount() - 1]->Get_Time();


for (DWORD i = 0; i < Get_KeyFrameCount(); i++)
{
if (duration >= p_KeyFrames->Get_Time()) p_KeyFrame = i;
}

p_KeyFrame2 = (p_KeyFrame == Get_KeyFrameCount() - 1) ? p_KeyFrame : p_KeyFrame + 1;

DWORD timeDiff = p_KeyFrames[p_KeyFrame2]->Get_Time() - p_KeyFrames[p_KeyFrame]->Get_Time();
if (timeDiff == 0) timeDiff = 1;

float scalar = (float)(duration - p_KeyFrames[p_KeyFrame]->Get_Time()) / (float)timeDiff;

D3DXMATRIX matInt = p_KeyFrames[p_KeyFrame2]->Get_TransformMatrix() - p_KeyFrames[p_KeyFrame]->Get_TransformMatrix();
matInt *= scalar;
matInt += p_KeyFrames[p_KeyFrame]->Get_TransformMatrix();
return matInt;

}
};
[/quote]
First, the formatting of your code snippets isn't nice to read. Please use the "Insert code snippet" button just to the right of "Insert quotation" button for code snippets!

The "addition and subtraction stuff" the author of the book is using is named linear interpolation. The internet gives you a plenty of explanations, and I hope the book explains it, too. Furthermore I hope that the book tells you that the shown code snippet is a no-go as soon as any non-linear transformation is part of the keyframes. E.g. the shown method will work fine for translation and scaling, but it will fail for rotation. Even for small rotations I'd have expected some kind of re-orthonormalization for the rotational part.

Besides that, the shown code snippet seems me correct. May be that the resolution of timeGetTime() is a bit to low (I've read "above 1 millisecond"). And be aware that in the 1st run p_ElapsedTime will be 0, so that p_KeyFrames[0] will be returned. So, have you tested this in a run where p_ElapsedTime lies nicely in the middle of the both first keyframes?
As mentioned by haegarr, rotations can't be interpolated with the method you've posted.

It's more common to use a vector for the translation (and scale, if necessary) and a quaternion for the rotation. Then the interpolation between keyframes is something like (pseudo-code):

D3DXVECTOR3 transVectorInterp = transVectorKeyFrame1 + (1-scalar)*transVectorKeyFrame2; // D3DXVec3Lerp can be used also
D3DXQUATERNION quaternionInterp = D3DXQuaternionSlerp( quatKeyFrame1, quatKeyFrame2, scalar );

D3DXMatrixTranslation( matTrans, transVectorInterp.x, ...);
D3DXMatrixRotationQuaternion( matRot, quaternionInterp );

finalMatrix = matRot * matTrans;


If keyframe data is stored as a matrix, rather than as a translation vector and quaternion for rotation*, it get's a bit inefficient to use this method as you'll have to decompose the keyframe matrix into scale and translation vectors, and a quaternion. You might want to consider storing keyframe data itself with vectors and a quaternion.

*I believe this storage scheme for keyframe data is more common.

EDIT: Be careful about operation precedence. For instance, in the line:
p_KeyFrame2 = (p_KeyFrame == Get_KeyFrameCount() - 1) ? p_KeyFrame : p_KeyFrame + 1;

if I remember correctly, the == operator has a higher precedence than arithmetic operators. That line should probably be:
p_KeyFrame2 = p_KeyFrame == (Get_KeyFrameCount() - 1) ? p_KeyFrame : p_KeyFrame + 1;

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.


First, the formatting of your code snippets isn't nice to read. Please use the "Insert code snippet" button just to the right of "Insert quotation" button for code snippets!

The "addition and subtraction stuff" the author of the book is using is named linear interpolation. The internet gives you a plenty of explanations, and I hope the book explains it, too. Furthermore I hope that the book tells you that the shown code snippet is a no-go as soon as any non-linear transformation is part of the keyframes. E.g. the shown method will work fine for translation and scaling, but it will fail for rotation. Even for small rotations I'd have expected some kind of re-orthonormalization for the rotational part.

Besides that, the shown code snippet seems me correct. May be that the resolution of timeGetTime() is a bit to low (I've read "above 1 millisecond"). And be aware that in the 1st run p_ElapsedTime will be 0, so that p_KeyFrames[0] will be returned. So, have you tested this in a run where p_ElapsedTime lies nicely in the middle of the both first keyframes?


Sorry, I tried using the code snippet tag but it didn't work when I tried -_- I'll use it from now on.

That's the thing, this book doesn't explain anything very well. It jumps around alot and provides lots of code without much information, but it's the only book I could find on keyframe animation :( I guess I'll search the web for some examples/tutorials. I just prefer ebooks.

Thanks for letting me know about the rotation, I probably would've spent hours messing with code before realizing it myself. Thats definately something he should have brought up in the book.


As mentioned by haegarr, rotations can't be interpolated with the method you've posted.

It's more common to use a vector for the translation (and scale, if necessary) and a quaternion for the rotation. Then the interpolation between keyframes is something like (pseudo-code):

D3DXVECTOR3 transVectorInterp = transVectorKeyFrame1 + (1-scalar)*transVectorKeyFrame2; // D3DXVec3Lerp can be used also
D3DXQUATERNION quaternionInterp = D3DXQuaternionSlerp( quatKeyFrame1, quatKeyFrame2, scalar );

D3DXMatrixTranslation( matTrans, transVectorInterp.x, ...);
D3DXMatrixRotationQuaternion( matRot, quaternionInterp );

finalMatrix = matRot * matTrans;


If keyframe data is stored as a matrix, rather than as a translation vector and quaternion for rotation*, it get's a bit inefficient to use this method as you'll have to decompose the keyframe matrix into scale and translation vectors, and a quaternion. You might want to consider storing keyframe data itself with vectors and a quaternion.


Thanks for the advice! Good to know because rotation is the main reason I'm using keyframes.

Technically, my keyframe data isn't stored as a matrix. My Animation class loads the keyframes from a mock INI file that looks like this:


// Key=<int|Time> <float|posX posY posZ> <float|rotX rotY rotZ> <float|scaleX scaleY scaleZ>

[Animation]
Subset=1
Loop=-1
Key=0 0 0 0 0 0 0 1 1 1
Key=1000 0 0 0 0 0.5 0 1 1 1
Key=2000 0 0 0 0 0 0 1 1 1
[/quote]

Then I have a Transform class that stores the position, rotation, and scale in 3 D3DXVECTOR3's, and a function Get_Matrix() that converts them into a transformation matrix:


D3DXMATRIX Transform::Get_Matrix(void)
{
D3DXMATRIXA16 matScale, matRot, matTrans;

D3DXMatrixScaling(&matScale, p_Scale.x, p_Scale.y, p_Scale.z);
D3DXMatrixRotationYawPitchRoll(&matRot, p_Rotation.x, p_Rotation.y, p_Rotation.z);
D3DXMatrixTranslation(&matTrans, p_Position.x, p_Position.y, p_Position.z);

return matScale * matRot * matTrans;
};


Your method above would work great with translation and scale, but I would need a quaternion for rotation.

Is there another alternative to spherical interpolation (slerp) that works with vectors? Wikipedia talks about geometric slerp and quaternion slerp. The reason I ask is because I want to keep my animation files human-readable and easy to edit. I'd like to stay away from quaternions unless there is no other option.


Edit: Is there a way to convert a rotation vector into a quaternion? That would solve my problem.

This topic is closed to new replies.

Advertisement