Sign in to follow this  

Time-based animation

This topic is 2483 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

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:
[quote]
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[i].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:
[quote]
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[i]->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!

Share this post


Link to post
Share on other sites
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:
[code]
float scalar = (p_Elapsed - pKeyFrames.Time)/timeDiff;
// .... NOT AS BELOW
float scalar = (p_Elapsed - pKeyFrames.Time/timeDiff);
[/code]

Share this post


Link to post
Share on other sites
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?

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

Share this post


Link to post
Share on other sites
[quote name='ta0soft' timestamp='1298335194' post='4777316']
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?

[quote]
float scalar = (float)(p_ElapsedTime - p_KeyFrames[p_KeyFrame]->Get_Time()) / (float)timeDiff;
[/quote]
[/quote]
Casting either the numerator or denominator should be sufficient but, yes, a cast to float is required.

Share this post


Link to post
Share on other sites
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:

[quote]
// 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!!

[quote]
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;

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

for (DWORD i = 0; i < Get_KeyFrameCount(); i++)
{
if ([b]duration[/b] >= p_KeyFrames[i]->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)([b]duration[/b] - p_KeyFrames[p_KeyFrame]->Get_Time()) / (float)timeDiff;

[b] 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;[/b]
}
};
[/quote]

Share this post


Link to post
Share on other sites
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 [i]linear interpolation[/i]. 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?

Share this post


Link to post
Share on other sites
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):
[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;
[/code]

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:
[code]p_KeyFrame2 = (p_KeyFrame == Get_KeyFrameCount() - 1) ? p_KeyFrame : p_KeyFrame + 1;[/code]

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

Share this post


Link to post
Share on other sites
[quote name='haegarr' timestamp='1298369672' post='4777469']
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?
[/quote]

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.

Share this post


Link to post
Share on other sites
[quote name='Buckeye' timestamp='1298390643' post='4777555']
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):
[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;
[/code]

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.
[/quote]

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:

[quote]
// 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:

[code]
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;
};
[/code]

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.

Share this post


Link to post
Share on other sites
[quote]Is there a way to convert a rotation vector into a quaternion? [/quote]
Not sure what you mean by "rotation vector," but look at the D3DXQuaternion... functions to create a quaternion in one of several ways.

Share this post


Link to post
Share on other sites
What I meant was, is there a function that takes yaw/pitch/roll values and returns a quaternion, but you pointed me in the right direction.

It works now! But something is still wrong. Translation and rotation seem to be working fine but the interpolated scale is off. My keyframes are all set to 1.0, 1.0, 1.0 scale, but when I render the animation the mesh shrinks... No idea why :( Am I supposed to normalize something?

[code]
Transform* Animation::Update(void)
{
if (Get_KeyFrameCount() == 1) return p_KeyFrames[0]->Get_Transform();

D3DXVECTOR3 transInterp, scaleInterp;
D3DXQUATERNION rotInterp;

if (p_StartTime == 0) p_StartTime = timeGetTime();
p_ElapsedTime = timeGetTime() - p_StartTime;
p_Duration = p_ElapsedTime % p_KeyFrames[Get_KeyFrameCount() - 1]->Get_Time();

for (DWORD i = 0; i < Get_KeyFrameCount(); i++)
{
if (p_Duration >= p_KeyFrames[i]->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)(p_Duration - p_KeyFrames[p_KeyFrame]->Get_Time()) / (float)timeDiff;

transInterp = p_KeyFrames[p_KeyFrame]->Get_Position() + (1 - scalar) * p_KeyFrames[p_KeyFrame2]->Get_Position();
scaleInterp = p_KeyFrames[p_KeyFrame]->Get_Scale() + (1 - scalar) * p_KeyFrames[p_KeyFrame2]->Get_Scale();
D3DXQuaternionSlerp(&rotInterp, &p_KeyFrames[p_KeyFrame]->Get_Rotation(), &p_KeyFrames[p_KeyFrame2]->Get_Rotation(), scalar);

return new Transform(transInterp, rotInterp, scaleInterp);
};
[/code]

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

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

return matScale * matRot * matTrans;
};
[/code]


Share this post


Link to post
Share on other sites
Well I've tried using different animation files with a different amount of keyframes and they all seem to perform the same. I've also tried animating different subsets of the mesh. Translation and rotation work exacly as expected, but the mesh (or subset) shrinks from the first keyframe to the last.

[quote]
// Move mesh 5 points along z axis and back again
[Animation]
Subset=-1
Loop=-1
Key=0 0 0 0 0 0 0 1 1 1
Key=1000 0 0 5 0 0 0 1 1 1
Key=2000 0 0 0 0 0 0 1 1 1

// Rotate subset[1] 1.5 radians along x axis
[Animation]
Subset=1
Loop=-1
Key=0 0 0 0 0 0 0 1 1 1
Key=1000 0 0 0 1.5 0 0 1 1 1

// Move and rotate subset[2]
[Animation]
Subset=2
Loop=-1
Key=0 0 0 0 0 0 0 1 1 1
Key=2000 -5 0 0 1.5 0 0 1 1 1
Key=4000 0 0 0 0 0 0 1 1 1
Key=6000 5 0 0 -1.5 0 0 1 1 1
[/quote]

Share this post


Link to post
Share on other sites
Okay, it appears the keys all have scales of ( 1,1,1 ) - if the last 3 digits are scale - as you said. As mentioned above, what have you done to determine that your Update function works as expected? Have you stepped through the code, looking at values (such as scaleInterp) when they're calculated?

Share this post


Link to post
Share on other sites
After looking at the values more closely, scale isn't the only thing acting strange. Consider the following animation:

[quote]
// Move 3 points along x axis
[Animation]
Subset=1
Loop=-1
Key=0 0 0 0 0 0 0 1 1 1
Key=100 3 0 0 0 0 0 1 1 1
[/quote]

I used OutputDebugString to look at the interpolated values, transInterp should start at 0, 0, 0 and end at 3, 0, 0 but it looks like it's doing the opposite. scaleInterp starts at 2, 2, 2 and ends at 1, 1, 1 (no idea why). rotInterp works as expected, so I'm guessing it has something to do with the equasion (keyframe1Scale + (1 - scalar) * keyframe2Scale)

[quote]
Time: 0 ms
transInterp: 3.0000 0.0000 0.0000
rotInterp: 0.0000 0.0000 0.0000 1.0000
scaleInterp: 2.0000 2.0000 2.0000

Time: 40 ms
transInterp: 1.8000 0.0000 0.0000
rotInterp: 0.0000 0.0000 0.0000 1.0000
scaleInterp: 1.6000 1.6000 1.6000

Time: 50 ms
transInterp: 1.5000 0.0000 0.0000
rotInterp: 0.0000 0.0000 0.0000 1.0000
scaleInterp: 1.5000 1.5000 1.5000

Time: 70 ms
transInterp: 0.9000 0.0000 0.0000
rotInterp: 0.0000 0.0000 0.0000 1.0000
scaleInterp: 1.3000 1.3000 1.3000

Time: 90 ms
transInterp: 0.3000 0.0000 0.0000
rotInterp: 0.0000 0.0000 0.0000 1.0000
scaleInterp: 1.1000 1.1000 1.1000

Time: 100 ms
transInterp: 3.0000 0.0000 0.0000
rotInterp: 0.0000 0.0000 0.0000 1.0000
scaleInterp: 2.0000 2.0000 2.0000
[/quote]

Share this post


Link to post
Share on other sites
Linear [size=2]int[/size]erpolation is done as
[size=2]g( w ) := f[/size][sub]1[/sub] [size=2]* ( 1 - w ) + f[/size][sub]2[/sub][size=2] * w == f[/size][sub]1[/sub][size=2] + ( f[/size][sub]2[/sub][size=2] - f[/size][sub]1[/sub][size=2] ) * w[/size]
[size=2]with w being the weight, f[/size][sub]1[/sub][size=2] being the "start" value and f[/size][sub]2[/sub][size=2] the "end" value, so that[/size]
[size=2]g( w=0 ) == f[/size][sub][size=2]1[/size][/sub][size=2]
g( w=1 ) == f[/size][sub][size=2]2[/size][/sub][size=2]
[/size][sub][/sub]
[sub][size="2"]Use that for translation as well as for scaling.[/size][/sub]
[sub] [/sub]
[sub][size="2"]For comparison: Your (wrong) equation seems to be [/size][/sub]
[sub][size="2"][size=3][size=2]f[/size][sub]1[/sub] [size=2]+ ( 1 - w ) * f[/size][sub]2[/sub][/size][/size][/sub]

Share this post


Link to post
Share on other sites

This topic is 2483 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this