3D Animation Techniques with XNA Game Studio 4.0

Published February 23, 2012 by Sean James, posted by Gaiiden
Do you see issues with this article? Let us know.
Advertisement
In this article, we will look at several ways to make the objects in our scene move. First, we will look at the animation of objects as a whole. We will do this through simple linear interpolation between start and end values, and through a more complex curve interpolation. We will also look at more complex animations through keyframed animation.

This article by Sean James, author of 3D Graphics with XNA Game Studio 4.0, covers:

  • Object animation
  • Keyframed animation
  • Curve interpolation

Object animation


We will first look at the animation of objects as a whole. The most common ways to animate an object are rotation and translation (movement). We will begin by creating a class that will interpolate a position and rotation value between two extremes over a given amount of time. We could also have it interpolate between two scaling values, but it is very uncommon for an object to change size in a smooth manner during gameplay, so we will leave it out for simplicity's sake.

The ObjectAnimation class has a number of parameters--starting and ending position and rotation values, a duration to interpolate during those values, and a Boolean indicating whether or not the animation should loop or just remain at the end value after the duration has passed:

public class ObjectAnimation
{
Vector3 startPosition, endPosition, startRotation, endRotation;
TimeSpan duration;
bool loop;
}

We will also store the amount of time that has elapsed since the animation began, and the current position and rotation values:

TimeSpan elapsedTime = TimeSpan.FromSeconds(0);

public Vector3 Position { get; private set; }
public Vector3 Rotation { get; private set; }

The constructor will initialize these values:

public ObjectAnimation(Vector3 StartPosition, Vector3 EndPosition,
Vector3 StartRotation, Vector3 EndRotation, TimeSpan Duration,
bool Loop)
{
this.startPosition = StartPosition;
this.endPosition = EndPosition;
this.startRotation = StartRotation;
this.endRotation = EndRotation;
this.duration = Duration;
this.loop = Loop;
Position = startPosition;
Rotation = startRotation;
}

Finally, the Update() function takes the amount of time that has elapsed since the last update and updates the position and rotation values accordingly:

public void Update(TimeSpan Elapsed)
{
// Update the time
this.elapsedTime += Elapsed;

// Determine how far along the duration value we are (0 to 1)
float amt = (float)elapsedTime.TotalSeconds / (float)duration.
TotalSeconds;

if (loop)
while (amt > 1) // Wrap the time if we are looping
amt -= 1;
else // Clamp to the end value if we are not
amt = MathHelper.Clamp(amt, 0, 1);

// Update the current position and rotation
Position = Vector3.Lerp(startPosition, endPosition, amt);
Rotation = Vector3.Lerp(startRotation, endRotation, amt);
}

As a simple example, we'll create an animation (in the Game1 class) that rotates our spaceship in a circle over a few seconds:


0041OT_09_01.png


We'll also have it move the model up and down for demonstration's sake:

ObjectAnimation anim;
We initialize it in the constructor:

models.Add(new CModel(Content.Load("ship"),
Vector3.Zero, Vector3.Zero, new Vector3(0.25f), GraphicsDevice));

anim = new ObjectAnimation(new Vector3(0, -150, 0),
new Vector3(0, 150, 0),
Vector3.Zero, new Vector3(0, -MathHelper.TwoPi, 0),
TimeSpan.FromSeconds(10), true);

We update it as follows:

anim.Update(gameTime.ElapsedGameTime);

models[0].Position = anim.Position;
models[0].Rotation = anim.Rotation;

Keyframed animation


Our ObjectAnimation class allows us to create simple linear animations, but we can't create anything more complex. For example, we can't make our spaceship move in a circle with this class. To achieve more complex animations, we will use what is called keyframed animation. In this method, we specify "key" frames where we want the object to be in a specific position and orientation. We then rely on the code to interpolate between those values to fill in the frames between the key frames.

The following screenshot shows our spaceship at the keyframed positions along a path, and the black line shows the path that would be taken by interpolating between keyframes:

0041OT_09_02.png


Keyframed animation is useful because it is a fast way to create somewhat complex animations without having to animate each frame. For example, birds flying through the air, soldiers on patrol, or even a camera flying through a scene, can all be animated through keyframes. This is probably the easiest way to move the camera during a cutscene, for example. We represent a key frame with the ObjectAnimationFrame class. Like the previous class, it contains position and rotation values. It also, however, contains a time value, marking this frame's time offset from the beginning of the animation.

public class ObjectAnimationFrame
{
public Vector3 Position { get; private set; }
public Vector3 Rotation { get; private set; }
public TimeSpan Time { get; private set; }

public ObjectAnimationFrame(Vector3 Position, Vector3 Rotation,
TimeSpan Time)
{
this.Position = Position;
this.Rotation = Rotation;
this.Time = Time;
}
}

We can now create a new animation class that uses key frames:

public class KeyframedObjectAnimation
{
List frames = new List();
bool loop;
TimeSpan elapsedTime = TimeSpan.FromSeconds(0);

public Vector3 Position { get; private set; }
public Vector3 Rotation { get; private set; }

public KeyframedObjectAnimation(List Frames,
bool Loop)
{
this.frames = Frames;
this.loop = Loop;
Position = Frames[0].Position;
Rotation = Frames[0].Rotation;
}
}

Finally, the Update() function f?"?igures out which frame we are on and interpolates between its values and the next frame's values, based on how far between them we are:

public void Update(TimeSpan Elapsed)
{
// Update the time
this.elapsedTime += Elapsed;

TimeSpan totalTime = elapsedTime;
TimeSpan end = frames[frames.Count - 1].Time;

if (loop) // Loop around the total time if necessary
while (totalTime > end)
totalTime -= end;
else // Otherwise, clamp to the end values
{
Position = frames[frames.Count - 1].Position;
Rotation = frames[frames.Count - 1].Rotation;
return;
}

int i = 0;
// Find the index of the current frame
while(frames[i + 1].Time i++;
// Find the time since the beginning of this frame
totalTime -= frames.Time;

// Find how far we are between the current and next frame (0 to 1)
float amt = (float)((totalTime.TotalSeconds) /
(frames[i + 1].Time - frames.Time).TotalSeconds);

// Interpolate position and rotation values between frames
Position = Vector3.Lerp(frames.Position, frames[i + 1].Position,
amt);
Rotation = Vector3.Lerp(frames.Rotation, frames[i + 1].Rotation,
amt);
}

For example, we can now create a new animation to move our spaceship in a square:

KeyframedObjectAnimation anim;
We set it up as follows:

List frames = new List();

frames.Add(new ObjectAnimationFrame(new Vector3(-1000, 100, -1000),
new Vector3(0, MathHelper.ToRadians(-90), 0),
TimeSpan.FromSeconds(0)));
frames.Add(new ObjectAnimationFrame(new Vector3(1000, 100, -1000),
new Vector3(0, MathHelper.ToRadians(-90), 0),
TimeSpan.FromSeconds(3)));
frames.Add(new ObjectAnimationFrame(new Vector3(1000, 100, -1000),
new Vector3(0, MathHelper.ToRadians(-180), 0),
TimeSpan.FromSeconds(6)));
frames.Add(new ObjectAnimationFrame(new Vector3(1000, 100, 1000),
new Vector3(0, MathHelper.ToRadians(-180), 0),
TimeSpan.FromSeconds(9)));
frames.Add(new ObjectAnimationFrame(new Vector3(1000, 100, 1000),
new Vector3(0, MathHelper.ToRadians(-270), 0),
TimeSpan.FromSeconds(12)));
frames.Add(new ObjectAnimationFrame(new Vector3(-1000, 100, 1000),
new Vector3(0, MathHelper.ToRadians(-270), 0),
TimeSpan.FromSeconds(15)));
frames.Add(new ObjectAnimationFrame(new Vector3(-1000, 100, 1000),
new Vector3(0, MathHelper.ToRadians(-360), 0),
TimeSpan.FromSeconds(18)));
frames.Add(new ObjectAnimationFrame(new Vector3(-1000, 100, -1000),
new Vector3(0, MathHelper.ToRadians(-360), 0),
TimeSpan.FromSeconds(21)));
frames.Add(new ObjectAnimationFrame(new Vector3(-1000, 100, -1000),
new Vector3(0, MathHelper.ToRadians(-450), 0),
TimeSpan.FromSeconds(24)));

anim = new KeyframedObjectAnimation(frames, true);

0041OT_09_03.png


The Update code remains the same. Running the game, you will see the spaceship move from corner to corner of a box, turning towards the next corner at each stop.

Curve interpolation


We now have the ability to make animations with multiple key frames, which allows us to create more complex animations. However, we are still interpolating linearly between those key frames. This looks good for rotations, for example, but it would not look good for an object following a path, as the object would abruptly change direction after reaching a key frame in its animation. Instead, we want to be able to have our objects follow a smooth curve through the positions defined in the key frames. We will do this with what is called Catmull-Rom interpolation. This is a process that will create a curve through our key frame positions, allowing for much smoother object animation:

0041OT_09_04.png


Let's modify the KeyframedObjectAnimation class to use Catmull-Rom interpolation for the position value. XNA has a built-in function to calculate an interpolated position between the second and third points in a set of four points using Catmull-rom interpolation. However, it works only in one dimension, so we'll need to create a function that will interpolate between a set of instances of Vector3:

Vector3 catmullRom3D(Vector3 v1, Vector3 v2, Vector3 v3,
Vector3 v4, float amt)
{
return new Vector3(
MathHelper.CatmullRom(v1.X, v2.X, v3.X, v4.X, amt),
MathHelper.CatmullRom(v1.Y, v2.Y, v3.Y, v4.Y, amt),
MathHelper.CatmullRom(v1.Z, v2.Z, v3.Z, v4.Z, amt));
}

The amt argument specifies how far (0 to 1) between the second and third vectors the new position should be. We can now modify the position calculation to use this new function:

// Interpolate position and rotation values between frames
Position = catmullRom3D(frames[wrap(i - 1, frames.Count - 1)].
Position,
frames[wrap(i, frames.Count - 1)].Position,
frames[wrap(i + 1, frames.Count - 1)].Position,
frames[wrap(i + 2, frames.Count - 1)].Position, amt);

The wrap() function wraps the value that it is given around a certain interval--in this case [0, frames.Count - 1]. This means that we will not have to worry about our indices going out of range when finding the last point, next point, and so on, but it does mean that this type of interpolation will work best with a closed curve--a circle, for example:

// Wraps the "value" argument around [0, max]
int wrap(int value, int max)
{
while (value > max)
value -= max;

while (value value += max;
return value;
}

We could now create the following keyframed animation with a curved path to demonstrate our new interpolation method:

List frames = new List();

frames.Add(new ObjectAnimationFrame(new Vector3(-500, 100, 1000),
new Vector3(0, MathHelper.ToRadians(0), 0),
TimeSpan.FromSeconds(0)));
frames.Add(new ObjectAnimationFrame(new Vector3(500, 100, 500),
new Vector3(0, MathHelper.ToRadians(0), 0),
TimeSpan.FromSeconds(3)));
frames.Add(new ObjectAnimationFrame(new Vector3(-500, 100, 0),
new Vector3(0, MathHelper.ToRadians(0), 0),
TimeSpan.FromSeconds(6)));
frames.Add(new ObjectAnimationFrame(new Vector3(500, 100, -500),
new Vector3(0, MathHelper.ToRadians(0), 0),
TimeSpan.FromSeconds(9)));
frames.Add(new ObjectAnimationFrame(new Vector3(-500, 100, -1000),
new Vector3(0, MathHelper.ToRadians(180), 0),
TimeSpan.FromSeconds(12)));
frames.Add(new ObjectAnimationFrame(new Vector3(-500, 100, 1000),
new Vector3(0, MathHelper.ToRadians(180), 0),
TimeSpan.FromSeconds(15)));
frames.Add(new ObjectAnimationFrame(new Vector3(-500, 100, 1000),
new Vector3(0, MathHelper.ToRadians(360), 0),
TimeSpan.FromSeconds(18)));

anim = new KeyframedObjectAnimation(frames, true);

0041OT_09_05.png


Summary


In this article we have covered the following concepts:

  • Object animation
  • Keyframed animation
  • Curve interpolation
Cancel Save
0 Likes 5 Comments

Comments

FriesBoury
while( value )
{
// some code here
}

checks if the [b]value [/b]is true (boolean) and executes the code between the brackets while this is the case.

It constantly does: check - execute - check - execute - check - execute -... until value turns false
September 28, 2012 03:48 PM
Migi0027

I haven't implemented it yet, but is seems excellent!

Good Job!

May 15, 2013 10:37 AM
Migi0027

I've implemented it successfully!

Great job! Again!

May 15, 2013 08:08 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement