Wednesday, July 01, 2009

"XNAVATARS" - PART 1

At the very end of my article "Avatars 101" I said I would post an example with a slightly different approach in order to render avatar's transitions.

Well, I have decided to extend the example and split the end result in a series of -most likely- 3 or 4 articles.

For starters, on this article we will concentrate on the transitions' code I promised.

To simplify the explanation a little bit, the whole code to render the avatar is implemented in the Game class (but this is going to change in the third part of the series).

Ok, let's begin ...

1. Create a XNA GS game project on Visual Studio and named it, say, "AvatarGame".

2. Include the following fields (in addition to the ones that are automatically created for you by XNA GS):

// We will use a font to draw statistics.
private SpriteFont font;
 
// These are needed to handle our avatar.
private AvatarDescription avatarDesc;
private AvatarRenderer avatarRenderer;
private AvatarAnimation currentAnimation, targetAnimation;
 
// Holds an array of all animations available for our avatar.
private AvatarAnimation[] animations;
 
// These are needed to handle transitions between animations.
private bool isInTransition, moveToNextAnimation, moveRandomly;
private float transitionProgress, transitionStep;
private Matrix[] transitionTransforms;
private int currentAnimationId;
private Random randomizer;
 
// These will help to detected pressed buttons.
private GamePadState currentGamePadState, lastGamePadState;

The code is self-explainable. Basically, we are adding the basic fields we need to render avatar plus the helper ones to handle transitions.

3. The constructor:

/// <summary>
/// Initializes a new instance of the <see cref="AvatarGame"/> class.
/// </summary>
public AvatarGame()
{
    // These are implemented for you.
    graphics = new GraphicsDeviceManager( this );
    Content.RootDirectory = "Content";
 
    // Create and add the GamerServices component.
    Components.Add( new GamerServicesComponent( this ) );
 
    // Create the array that will hold the collection of animations.
    this.animations = new AvatarAnimation[ 30 ];
 
    // Create the array of matrices that will hold bone transforms for transitions.
    this.transitionTransforms = new Matrix[ 71 ];
 
    // Create the random-number generator.
    this.randomizer = new Random( DateTime.Now.Millisecond );
}

As usual, you must create the managers/services you will use to render the avatars.

But why do we also need two arrays and a random number generator? The latter, in case we want to change animations in no particular order.

And the arrays? Read on …

4. Loading content:

/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch( GraphicsDevice );
 
    // Load a spritefont.
    this.font = this.Content.Load<SpriteFont>( "MainFont" );
 
    // For each position in the array.
    for ( int i = 0; i < this.animations.Length; i++ )
    {
        // Create and store the corresponding animation.
        this.animations[ i ] = new AvatarAnimation( (AvatarAnimationPreset)i );
    }
 
    // Create a random description (instead, you can try to get the 
    // description of a signedIn gamer).
    this.avatarDesc = AvatarDescription.CreateRandom();
 
    // Create the renderer with a standard loading effect.
    this.avatarRenderer = new AvatarRenderer( avatarDesc, true );
 
    // Just for fun, set the current animation, randomly.
    this.currentAnimationId = this.randomizer.Next( this.animations.Length );
    this.currentAnimation = this.animations[ this.currentAnimationId ];
 
    // Set the "World" value.
    this.avatarRenderer.World =
        Matrix.CreateRotationY( MathHelper.ToRadians( 180.0f ) );
 
    // Set the "Projection" value.
    this.avatarRenderer.Projection =
        Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians( 45.0f ),
            this.GraphicsDevice.Viewport.AspectRatio,
            .01f,
            200.0f );
 
    // Set the "View" value.
    this.avatarRenderer.View =
        Matrix.CreateLookAt(
            new Vector3( 0, 1, 3 ),
            new Vector3( 0, 1, 0 ),
            Vector3.Up );
}

As you can see, in this method we populate the array of animations with each built-in available animation for avatars. The reason? We use this array as a cache so as to speed up the look-up process when phasing out from one animation to the next one.

Then, we set-up the initial transition and set the view and projection fields (note: both matrices are always “fixed“ in this example).

There is no significant code for the Initialize and Unload methods, and thus we move to the Update method.

5. Updating the game:

/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update( GameTime gameTime )
{
    // Update the gamepad state for player one.
    this.lastGamePadState = this.currentGamePadState;
    this.currentGamePadState = GamePad.GetState( PlayerIndex.One );
 
    // Allow the game to exit.
    if ( this.currentGamePadState.Buttons.Back == ButtonState.Pressed )
    {
        this.Exit();
    }
 
    // Force moving to the next animation without waiting for the 
    // current animation to end.
    if ( this.currentGamePadState.Buttons.A == ButtonState.Pressed &&
        this.lastGamePadState.Buttons.A == ButtonState.Released &&
        !this.isInTransition )
    {
        this.moveToNextAnimation = true;
    }
 
    // Change the type of selection: ascending order or randomly.
    if ( this.currentGamePadState.Buttons.B == ButtonState.Pressed &&
        this.lastGamePadState.Buttons.B == ButtonState.Released )
    {
        this.moveRandomly = !this.moveRandomly;
    }
 
    // Is there any animation to update?
    if ( currentAnimation != null )
    {
        // Is the current animation just about to finish or forced to end?
        if ( !this.isInTransition &&
            this.targetAnimation == null &&
            ( this.currentAnimation.RemainingTime().TotalSeconds <= 1 ||
                this.moveToNextAnimation ) )
        {
            // Are we moving randomly?
            if ( this.moveRandomly )
            {
                // If so, select a new animation at random.
                this.currentAnimationId = this.randomizer.Next( this.animations.Length );
            }
            else
            {
                // If not, point to the next animation.
                this.currentAnimationId++;
 
                // Keep the id pointing to a valid position in the array.
                this.currentAnimationId %= this.animations.Length;
            }
 
            // Set the corresponding target animation.
            this.targetAnimation = this.animations[ this.currentAnimationId ];
        }
 
        // Has the animation reached its last frame? Or is it forced 
        // to change by the user?
        if ( this.currentAnimation.LastFrameReached() || this.moveToNextAnimation )
        {
            // If so, start by resetting this marker flag to false.
            this.moveToNextAnimation = false;
 
            // State that we will process a transition.
            this.isInTransition = true;
 
            // Make a copy of the readonly transforms to the transition array.
            this.currentAnimation.BoneTransforms.CopyTo( this.transitionTransforms, 0 );
 
            // Reset the current animation.
            this.currentAnimation.CurrentPosition = TimeSpan.Zero;
 
            // Set the target one as the current one.
            this.currentAnimation = this.targetAnimation;
            this.targetAnimation = null;
 
            // You can tweak this value to meet your game's need,
            // considering the lenght of your animations (it could vary).
            this.transitionStep = .5f;
        }
 
        // Update the current animation (in this example, there is no looping).
        this.currentAnimation.Update( gameTime.ElapsedGameTime, false );
 
        // Are we processing a transition?
        if ( this.isInTransition )
        {
            // If so, update the progress value.
            this.transitionProgress += this.transitionStep * (float)gameTime.ElapsedGameTime.TotalSeconds;
 
            // Is the progress below 100%?
            if ( transitionProgress < 1f )
            {
                // Calculate the proper bone transforms.
                this.CalculateTransition();
            }
            else
            {
                // When the progress reaches 100%, reset everything.
                this.isInTransition = false;
                this.transitionProgress = 0;
            }
        }
    }
 
    // As usual, call the base method.
    base.Update( gameTime );
}

At runtime, if you press the ‘A’ button and no transition is being executed, then you force “the avatar” to start a transition to the next animation.

By pressing the ‘B’ button, you change the way the next animation is selected: in ascending order or randomly.

Notice that when the current animation is about to end, the game selects the next animation, in the current order method selected by the user.

When either the last frame of the animation being played is reached (no looping) or the user forces the transition, we then copy the bone transforms for the position in that animation, reset the position to zero and change the animation to the “target” one.

You may wondering what’s the purpose of the field “transitionStep”. This factor states the time we will spend to move from “the last” position of the ending animation to the “current” position of the starting one.

Why to the “current”? The behavior I decided to choose in order to create a believable look’n’feel for the transition was to avoid going from “last to first” and instead from “last to current” (notice that the starting animation will play from the moment we execute the transition out, and so the current position will change).

Of course that you can change this behavior if you prefer going “from last to first” position behavior before playing the entering animation.

Finally, we call the “CalculateTransition” method which I will explain after the Draw one.

6. Drawing the avatar:

/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw( GameTime gameTime )
{
    // As usual, clear the backbuffer (or the current render target).
    GraphicsDevice.Clear( Color.CornflowerBlue );
 
    // Can we draw the avatar?
    if ( avatarRenderer != null && currentAnimation != null )
    {
        // If we can, is the animation in transition?
        if ( this.isInTransition )
        {
            // If so, draw it with the interpolated transforms.
            avatarRenderer.Draw(
                this.transitionTransforms,
                currentAnimation.Expression );
        }
        else
        {
            // If not, draw it with the actual transforms.
            avatarRenderer.Draw(
                this.currentAnimation.BoneTransforms,
                currentAnimation.Expression );
        }
    }
 
    // The following is used to show some statistics and other info
    // on screen. It can be omitted (or optimized).
    this.spriteBatch.Begin();
 
    // No need for further explanation.
    this.spriteBatch.DrawString(
        this.font,
        "Press 'A' to force changing animations or 'Back' to exit.",
        new Vector2( 50, 25 ),
        Color.White );
 
    // No need for further explanation.
    this.spriteBatch.DrawString(
        this.font,
        "Press 'B' to change the type of selection : " +
        ( this.moveRandomly ? "RANDOMLY" : "IN ASCENDING ORDER" )
        + ".",
        new Vector2( 50, 55 ),
        Color.White );
 
    // Draw the animation pointer, whether we are processing a transition and
    // the current transition time. Please notice that in this implementation
    // when the current animation is about to end (that is, 1 second or less),
    // the pointer "currentAnimationId" will change even if the animation is still
    // the same, so you will see a different number and name during 1 second or so.
    this.spriteBatch.DrawString(
        this.font,
        this.currentAnimationId + " : " +
            ( (AvatarAnimationPreset)this.currentAnimationId ).ToString() +
            " (" +
            ( !this.isInTransition ? "no transition" : this.transitionProgress.ToString() + " processed" ) +
            ").",
        new Vector2( 50, 85 ),
        Color.White );
 
    // Draw the current position and length of the animation being rendered.
    if ( currentAnimation != null )
    {
        this.spriteBatch.DrawString(
            this.font,
            "Processed " +
            this.currentAnimation.CurrentPosition.ToString() +
                " of " +
                this.currentAnimation.Length.ToString() +
                ".",
            new Vector2( 50, 115 ),
            Color.White );
    }
 
    // Flush the batch.
    this.spriteBatch.End();
 
    // As usual, call the base method.
    base.Draw( gameTime );
}

The only important code to notice here is the one in charge of rendering either the animation as is, or the calculated transition.

Since the Draw method of the AvatarAnimation expects an instance of type “IList”, we can pass an array of matrices directly without creating a ReadOnlyCollection.

7. Finally, the magic:

/// <summary>
/// Calculates the proper bone transoforms for the current transition.
/// </summary>
private unsafe void CalculateTransition()
{
    // If so, declare all needed helper primitives.
    // For debugging purposes you can use local fields marked as "final",
    // which in this example are commented out.
    Matrix currentMatrix, targetMatrix;
    Vector3 currentScale, targetScale; //, finalScale;
    Quaternion currentRotation, targetRotation; //, finalRotation;
    Vector3 currentTranslation, targetTranslation; //, finalTranslation;
 
    // For each transform's matrix.
    for ( int i = 0; i < this.transitionTransforms.Length; i++ )
    {
        // Since we are pointing to a managed struct we must use the 
        // reserved word "fixed" with an "unsafe" method declaration,
        // if we want to avoid traversing the array several times.
        fixed ( Matrix* matrixPointer = &this.transitionTransforms[ i ] )
        {
            // Get both, the current and target matrices.
            // Declaring these two local fields could be omitted 
            // and be used directly in the calcs below, but 
            // they are really useful when debugging.
            currentMatrix = *matrixPointer;
            targetMatrix = this.currentAnimation.BoneTransforms[ i ];
 
            // Get the components for the current matrix.
            currentMatrix.Decompose(
                out currentScale,
                out currentRotation,
                out currentTranslation );
 
            // Get the components for the target matrix.
            targetMatrix.Decompose(
                out targetScale,
                out targetRotation,
                out targetTranslation );
 
            // There's no need to calculate the blended scale factor, since we
            // are mantaining the current one, but I include it in the example
            // for learning purposes in case you need it.
            /*Vector3.Lerp(
                ref currentScale,
                ref targetScale,
                this.transitionProgress,
                out currentScale );*/
 
            // Interpolate a rotation value from the current an target ones,
            // taking into account the progress of the transition.
            Quaternion.Slerp(
                ref currentRotation,
                ref targetRotation,
                this.transitionProgress,
                out currentRotation );
 
            // Interpolate a translation value from the current an target ones,
            // taking into account the progress of the transition.
            Vector3.Lerp(
                ref currentTranslation,
                ref targetTranslation,
                this.transitionProgress,
                out currentTranslation );
 
            // Calculate the corresponding matrix with the final components.
            // Again, in this example, the creation of the scale matrix can be omitted
            // from the formula below (you may only use the rotation and tranlsation
            // factors obtaining the same result and save some processing power per loop).
            //this.transitionTransforms[ i ] =
            *matrixPointer =
                //Matrix.CreateScale( currentScale ) *
                Matrix.CreateFromQuaternion( currentRotation ) *
                Matrix.CreateTranslation( currentTranslation );
        }
    }
}

Every time we need to calculate and update the interpolated transition transforms, we’ll have to decompose the matrices of both, the last position in the ending animation and the updated position in the entering animation.

If the last position of the old animation is “fixed”, why do we need to decompose its matrices every time we call this method? Good question. Short answer: we don’t. We could calculate the old transforms once and store them in a private field for the game, but I include them here in case you want to change the default behavior (for instance, if you decide to keep the ending animation playing as well as the entering one).

What’s with the words “unsafe” and “fixed”? We must declare those two in order to use pointers to value types (plus mark the project to allow unsafe code). Fixing the field will prevent the GC from collecting it until we do not need it anymore, plus, in this case, it will allow us to directly use and set the value store in the stack without traversing the array of transition transforms twice per “for” loop to first get and then update the value type.

Phew! That’s it for now; when you compile and run the program you will see a random created avatar do some nice transitions either when the last animation ends or when you press the ‘A’ button.

You can download an example project for this article from here (you will find an extra class with two extension methods for the AvatarAnimation, named “LastFrameReached” & “RemainingTime”).

You can tweak the behavior and optimize the code a little more (say the rendering, create a specific class for the avatar and so on), or wait for the upcoming parts on the series …

Enjoy!
~Pete

> Link to Spanish version.

No comments:

Post a Comment

Any thoughts? Post them here ...