9 min read

Mesh-by-Mesh Animation

Introduction

In a previous tutorial, we talked about simple 3D animation. In this tutorial, we looked at how we could use matrix transformations to place our model in the location that we want it. In this tutorial, we will take this concept a step further. When we render our model, not all meshes in the model have to be transformed in exactly the same way. We can transform each of the parts of the model in a slightly different way, which allows the different pieces to move around in relation to each other.

In this tutorial, we will be working with a helicopter model. The idea is that we will be able to take the meshes for the main rotor and the tail rotor and transform them in such a way that they spin in circles. Then we will transform the whole model to the appropriate location in our world. The process is quite a bit more complicated than before, where we had only one mesh in the model (or we moved them all together), but the results are much cooler.

One of the biggest problems that come up with this method is that we have to know stuff about the model we are using. For instance, we will need to know which part of the helicopter belongs to which mesh. If you’ve made your own model, you will probably know this information. If you got the model from someone else (like an artist on your game development team) then you can talk to them about it. Otherwise, you may need to spend some time playing around with the model, either in a 3D modeling program or in your game in order to figure out the needed information.

Setup

Before we really get going with our project, we will first need to acquire a model that has different parts that will allow us to do the movements that we want. In this tutorial, I will be using the “Helicopter” model in the 3D Model Library . Download the model and add it to your project like we have done before. Make sure you have the texture file in the same directory as your model file as well. The easiest way to do this is to add both the model and the texture file to your project, and then tell XNA Game Studio to exclude the texture from the project.

Let’s go ahead and add all of the instance variables that we will need for this task to our project. Add the following as instance variables to your main game class:

private Model helicopterModel;
private float mainRotorAngle = 0;
private float tailRotorAngle = 0;
private Vector3 position = new Vector3(0, 0, 0);
private float angle = 0f;
private Matrix world = Matrix.CreateTranslation(new Vector3(0, 0, 0));
private Matrix view = Matrix.CreateLookAt(new Vector3(10, 10, 10), new Vector3(0, 0, 0), Vector3.UnitY);
private Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 600f, 0.1f, 100f);

The first variable, helicopterModel will store the model. The next four variables will store different properties about the model so that the model can be drawn correctly later. The last three are Matrix objects that will be used to draw the model. We’ve seen these in several of the earlier tutorials.

Next, in the LoadContent method, add the following line of code to load the model in:

helicopterModel = Content.Load<Model>("Helicopter");

Finally, in the Update() method, let’s update all of the necessary properties for the helicopter with the following:

tailRotorAngle -= 0.15f;
mainRotorAngle -= 0.15f;

angle += 0.02f;
position += Vector3.Transform(new Vector3(0.1f, 0, 0), Matrix.CreateRotationY(MathHelper.ToRadians(90) + angle));

The first thing we do here is to update the angles of both of the rotors on our helicopter. We just make them spin a little bit. The first two lines in this code do this. Then we want to update the helicopter as a whole. Here we are going to have the helicopter fly around in circles, so we change the angle a little, and move the location forward (compared to the direction the helicopter is facing) a little as well.

The Animation Code

Everything should now be set up to have our helicopter moving in circles with the rotors spinning in circles. We are now ready to do the actual drawing code. We are going to create a DrawModel method as we have done in the past, but with a few modifications, to allow each mesh to move differently. The code for our DrawModel method is below, so add it as a method to your class:

private void DrawModel(Model model, Matrix objectWorldMatrix, Matrix[] meshWorldMatrices, Matrix view, Matrix projection)
{
    for(int index = 0; index < model.Meshes.Count; index++)
    {
        ModelMesh mesh = model.Meshes[index];
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = true;

            effect.World = mesh.ParentBone.Transform * meshWorldMatrices[index] * objectWorldMatrix;
            effect.View = view;
            effect.Projection = projection;
        }
        mesh.Draw();
    }
}

Notice that our parameters for the method include the model that we want to draw, the objectWorldMatrix (we’ve been calling this world in the past, but I’m changing the name to make it more clear that this is the world matrix for the entire object), view, and projection matrices like before, and additionally, we have now included an array of Matrix objects called meshWorldMatrices. These are the world matrices for each of the meshes individually. These are what will allow us to move the meshes around individually. The second change that we make from our earlier DrawModel method is that instead of saying foreach (ModelMesh mesh in model.Meshes) we actually use a for loop instead. We do this so that later, we can know what index we are on in the list so that we can get the matching world matrix for the mesh.

Finally, the other difference that we have is where we set the world matrix in the effect. Before we just said effect.World = mesh.ParentBone.Transform * world; Now it is a little more complicated. In addition to applying the parent bone’s transformation, we will want to apply the world matrix for this particular mesh (meshWorldMatrices[index]), and then finally we apply the world matrix for the model as a whole (world). This gets us the transformation that we want for each of the meshes.

Now all that is left is to call this method with the right values for the main worldMatrix, as well as the world matrices for each of the meshes (meshWorldMatrices). To do this, add the following code to your Draw() method:

Matrix[] meshWorldMatrices = new Matrix[3];
meshWorldMatrices[0] = Matrix.CreateTranslation(new Vector3(0, 0, 0));
meshWorldMatrices[1] = Matrix.CreateRotationY(mainRotorAngle);
meshWorldMatrices[2] = Matrix.CreateTranslation(new Vector3(0, -0.25f, -3.4f)) *
                                            Matrix.CreateRotationX(tailRotorAngle) *
                                            Matrix.CreateTranslation(new Vector3(0, 0.25f, 3.4f));

world = Matrix.CreateRotationY(angle) * Matrix.CreateTranslation(position);

DrawModel(helicopterModel, world, meshWorldMatrices, view, projection);

Our first task here is to create a place to store our world matrices for each mesh. The first line here creates an array of Matrix objects with three elements in it. I’ve chosen three because I know that this model contains three meshes (the helicopter fuselage, the main rotor, and the tail rotor). When you use another model, you will need to figure this information out before you can proceed.

The next thing we do is to create the world transformation matrices for each mesh. The first matrix (index 0) is for the fuselage. We don’t want this to move around, so we just tell it to translate it zero units in each direction. The second matrix (index 1) is for the top rotor. This will simply need to be transformed around the y-axis, which we do here. The third matrix (index 2) is for the tail rotor and is the most complicated of the three. We want the tail rotor to rotate around its center axis. However, it is located out at some random distance down the z-axis, and up a little bit on the y-axis. In order to get it to rotate around its center, we translate the mesh back to the origin, rotate it, and then translate it back out to its starting location. This is why the vector (0, 0.25, 3.4) is used because that is how far away it is located from the center. Because I made the model, I knew where it was located at. When you use a different model, you may need to experiment with values like this until they look right, or better yet, open the model up in a 3D modeling program and take a look at it.

After that, we create the world matrix for the whole model by using the location of the helicopter and the helicopter’s heading. Lastly, we draw the model with all of these values. You should now be able to run the program and see the helicopter flying in circles, with the two rotors spinning as they should.

Screenshot

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace MeshByMeshAnimation
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        private Model helicopterModel;
        private float mainRotorAngle = 0;
        private float tailRotorAngle = 0;
        private Vector3 position = new Vector3(0, 0, 0);
        private float angle = 0f;
        private Matrix world = Matrix.CreateTranslation(new Vector3(0, 0, 0));
        private Matrix view = Matrix.CreateLookAt(new Vector3(10, 10, 10), new Vector3(0, 0, 0), Vector3.UnitY);
        private Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 600f, 0.1f, 100f);

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            base.Initialize();

            this.GraphicsDevice.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true };
        }

        /// <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);

            helicopterModel = Content.Load<Model>("Helicopter");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
        }

        /// <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)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            tailRotorAngle -= 0.15f;
            mainRotorAngle -= 0.15f;

            angle += 0.02f;
            position += Vector3.Transform(new Vector3(0.1f, 0, 0), Matrix.CreateRotationY(MathHelper.ToRadians(90) + angle));

            // This updates the world matrix, so that it reflects the changes to the position
            // and angle.  Remember that this matrix determines where the model will be located
            // at in the 3D world.
            world = Matrix.CreateRotationY(angle) * Matrix.CreateTranslation(position);

            base.Update(gameTime);
        }

        private void DrawModel(Model model, Matrix objectWorldMatrix, Matrix[] meshWorldMatrices, Matrix view, Matrix projection)
        {
            for (int index = 0; index < model.Meshes.Count; index++)
            {
                ModelMesh mesh = model.Meshes[index];
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.World = mesh.ParentBone.Transform * meshWorldMatrices[index] * objectWorldMatrix;
                    effect.View = view;
                    effect.Projection = projection;
                }
                mesh.Draw();
            }
        }

        /// <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)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            Matrix[] meshWorldMatrices = new Matrix[3];
            meshWorldMatrices[0] = Matrix.CreateTranslation(new Vector3(0, 0, 0));
            meshWorldMatrices[1] = Matrix.CreateRotationY(mainRotorAngle);
            meshWorldMatrices[2] = Matrix.CreateTranslation(new Vector3(0, -0.25f, -3.4f)) *
                                                        Matrix.CreateRotationX(tailRotorAngle) *
                                                        Matrix.CreateTranslation(new Vector3(0, 0.25f, 3.4f));

            world = Matrix.CreateRotationY(angle) * Matrix.CreateTranslation(position);

            DrawModel(helicopterModel, world, meshWorldMatrices, view, projection);

            base.Draw(gameTime);
        }

        /// <summary>
        /// Does the work of drawing a model, given specific world, view, and projection
        /// matrices.
        /// </summary>
        /// <param name="model">The model to draw</param>
        /// <param name="world">The transformation matrix to get the model in the right place in the world.</param>
        /// <param name="view">The transformation matrix to get the model in the right place, relative to the camera.</param>
        /// <param name="projection">The transformation matrix to project the model's points onto the screen correctly.</param>
        private void DrawModel(Model model, Matrix world, Matrix view, Matrix projection)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.World = mesh.ParentBone.Transform * world;
                    effect.View = view;
                    effect.Projection = projection;
                }

                mesh.Draw();
            }
        }

    }
}
Download the completed project