3D Graphics with XNA Game Studio 4.0

By Sean James
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Getting Started with 3D

About this book

XNA is a very powerful API using which it's easy to make great games, especially when you have dazzling 3D effects. This book will put you on course to implement the same 3D graphics used in professional games to make your games shine, and get those gamers addicted! If you think 3D graphics is something that limits your games, then this book is for you.

3D Graphics with XNA Game Studio 4.0 is a step by step companion to implement the effects used in professional 3D games in your XNA games. By the time you're done with this book your games would have eye-catching visuals and 3D effects.

The one thing that can make or break a game is its appearance; players will mostly be attracted to a game if it looks good. With this book you will create your 3D objects and models and make them look more interesting by using shadowing and lighting techniques, and make them move in nasty ways using animation. Want to create realistic terrians for your games? Need some place for your 3D models to engage in battle? This book will enable you to do all that and more, by walking you through the implementation of numerous effects and graphics techniques used in professional games so that you can make them look great.

Publication date:
December 2010
Publisher
Packt
Pages
292
ISBN
9781849690041

 

Chapter 1. Getting Started with 3D

This chapter will provide you with a brief overview of the fundamentals of 3D graphics. We will create a number of useful classes and systems that will make work easier later on and provide us with a flexible framework for building games. This chapter will focus mainly on models, how they work, and how to view them with cameras. We will build a number of different types of camera that can be used in many situations we may encounter while building games. Next, we will look at a way to improve performance with a "view frustum culling" system, and finally, we'll build a small game that allows the player to fly a spaceship using keyboard controls.

 

Setting up a new project


The first step in any game is to set up the XNA game project in Visual Studio.

  1. To begin with, ensure that XNA and Visual Studio are installed by following the guide available at creators.xna.com and launch Visual Studio. Once it has loaded, create a new project:

  2. From the left-hand side, choose the version of XNA you want to work with. Generally, you should pick the most recent version available, unless you are specifically targeting an older version. If you've just installed XNA, there will be only one version of XNA to choose from. At the time of this writing, the most recent version was XNA 3.1. From the box on the right, specify that you want to create a Windows Game, and give it a name:

  3. Click on OK, and you will be taken to the main file of the game project called Game1.cs. XNA will have created this file and added it to your project automatically. The Game1 class is the main class in an XNA game. By default, this class is automatically instantiated when the game is loaded, and its member functions will be called automatically when they should perform their respective tasks.

The automatically generated Game1 class contains a lot of excess comments and code, so once you have familiarized yourself with the class and its functions, simplify it to the following:

public class Game1 : Microsoft.Xna.Framework.Game
{
   GraphicsDeviceManager graphics;
   SpriteBatch spriteBatch;

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

   // Called when the game should load its content
   protected override void LoadContent()
   {
   }

   // Called when the game should update itself
   protected override void Update(GameTime gameTime)
   {
      base.Update(gameTime);
   }

   // Called when the game should draw itself
   protected override void Draw(GameTime gameTime)
   {
      GraphicsDevice.Clear(Color.CornflowerBlue);

      base.Draw(gameTime);
   }
}
 

The 3D coordinate system


One thing that all 3D systems hold in common is a coordinate system. Coordinate systems are important because they allow us to represent points in 3D space in a consistent manner as distances from a center point called the origin along a number of axes. You're probably used to the idea of a 2D coordinate system from your math classes in school—the origin was at (0, 0) and the X and Y axes grew to the right and up respectively. A 3D coordinate system is very similar, except for the addition of a third axis labeled the Z- axis. XNA uses what is called a "right-handed" coordinate system, meaning that the X and Y axes grow the way you're used to (to the right and up respectively), and the Z-axis grows "towards" you. If the X and Y axes were placed flat on your computer screen, you can imagine the Z-axis as growing out of the screen towards you.

With this coordinate system, we can define points in space. For example, let's assume that our coordinate system uses meters as units. Say for a moment, we were sitting at the origin (0, 0, 0) and were facing down the negative portion of the Z-axis. If we wanted to note the location of an object sitting five meters in front of us, three meters to the right, on a table one meter tall, we would say that the object was at (3, 1, -5).

 

Matrices


Matrices are mathematical structures that are used in 3D graphics to represent transformations—operations performed on a point to move it in some way. The three common transformations are translation (movement), rotation, and scaling (changing size). When a transformation is applied to a model, each vertex in the model is multiplied by the transformation's matrix until the entire model has been transformed.

Matrices can be combined by multiplying them together. It is worth noting that matrix multiplication is done from right to left, so the last matrix to be multiplied will be the first to affect the model and so on. This means that rotating and then moving a model will not have the same effect as moving and then rotating it. Generally, unless you mean to do otherwise, the matrices should be multiplied in the following order: scaling * rotation * transformation.

In the 3D graphics world, there are usually three matrices that must be calculated to draw an object onto the screen: the world, view, and projection matrices. The world matrix is the result of all of our transformation matrices multiplied together. Once this transformation has been applied, the model has moved from what is called "local" or "object space" to "world space". Each model in a scene has a different world matrix, as they all have different locations, orientations, and so on. It is also possible that each "piece" of a model (or mesh) may have its own world matrix. For example, the head and leg of a human model will likely have their own matrices to offset them from the center of the model (its root). When the model is drawn, each mesh has its transformation multiplied by the entire model's world matrix to calculate the final world matrix.

The view matrix is used to transform the scene from world space into view space: the world as seen by the camera. The world matrix for each model is simply multiplied by the view matrix to transform the scene. The projection matrix then transforms the three-dimensional position of each vertex in the scene into the two-dimensional projection of the scene that is drawn onto the screen. When the 3D world/view matrix combination is multiplied by the projection matrix, the scene is flattened out so that it can be drawn onto a 2D screen.

 

Loading a model


A model is a file exported from a 3D modeling package such as 3D Studio Max or Blender. The file basically contains a list of points called vertices, which form the edges of polygons that, joined together, give the appearance of a smooth surface:

To load a model, we must add it to our game's content project. XNA will automatically build all of the content in our content project so that we can use it in our game. To add a model to the content project, open the Solution Explorer, right-click on the content project (labeled Content), and click on Add Existing Item.

In addition to building all of the content in the content project, XNA builds any files referenced by a piece of content. Because our model references its texture, we need to exclude the texture from the list of content to build or it will be built twice. Right-click on the texture and then select Exclude From Project. This will remove the texture from the content project but will not delete the file itself, which will allow XNA to find it when building the model but still only build it once.

Now that the content pipeline is building our model for us, we can load it into our game. We do this with the ContentManager class—a class used to access the runtime functionality of the content pipeline. The Game class already has an instance of the ContentManager class built-in, so we can go ahead and use it in the LoadContent() method.

First, an instance of the Model class will be needed. The Model class contains all the data necessary to draw and work with a model. We will also need an array of matrices representing the model's built-in mesh transformations. Add the following member definitions:

Model model;
Matrix[] transforms;

Now, in the LoadContent() method, we can use the ContentManager to load the model. The Load() function of the ContentManager class takes the name of the resource to load—the original filename without its extension. Note that this means we can't have multiple files with the same name only varying by extension.

model = Content.Load<Model>("ship");

transforms = new Matrix[model.Bones.Count];
model.CopyAbsoluteBoneTransformsTo(transforms);

In XNA, a model is made up of pieces called meshes. As described earlier, each model has its own transformation in 3D space, and each mesh also has its own transformation relative to the model's transformation as a whole. The Model class stores these transformations as a skeleton structure with each mesh attached to a bone. The last two lines in the previous code snippet copied that skeleton into the transforms array.

 

Drawing a model


Now that the model has been loaded, we are ready to draw it. We do this in the Draw() function of our game. Games redraw dozens of times per second, and each redraw is called a frame. We need to clear the screen before drawing a new frame, and we do so using XNA's GraphicsDevice.Clear() function. The single argument allows us to change the color to which the screen is cleared:

GraphicsDevice.Clear(Color.CornflowerBlue);

The first step of drawing the model itself is to calculate the view and projection matrices. To calculate the view matrix, we will use the CreateLookAt() static function of the Matrix class, which accepts as arguments a camera position, target, and up direction. The position is simply where in 3D space the camera should be placed, the target is the point the camera should be looking at, and the up vector is literally the direction that is "up" relative to the camera position. This code will go in the Draw() function after the GraphicsDevice is cleared:

Matrix view = Matrix.CreateLookAt(new Vector3(200, 300, 900),new Vector3(0, 50, 0),Vector3.Up);

There are some exceptions, but usually we calculate the projection matrix using the Matrix class' CreatePerspectiveFieldOfView() function, which accepts, in order, a field of view (in radians), the aspect ratio, and the near and far plane distances. These values define the shape of the view frustum as seen in the following figure. It is used to decide which objects and vertices are onscreen and which are not when drawing, and how to squish the scene down to fit onto the two-dimensional screen.

The near plane and far plane determine the distances at which objects will start and stop being drawn. Outside of the range between the two planes, objects will be clipped—meaning they will not be drawn. The field of view determines how "wide" the area seen by the camera is. Most first person shooters use an angle between 45 and 60 degrees for their field of view as anything beyond that range would start to distort the scene. A fish eye lens, on the other hand, would have a field of view closer to 180 degrees. This allows it to see more of the scene without moving, but it also distorts the scene at the edges. 45 degrees is a good starting point as it matches human vision closest without warping the image. The final value, the aspect ratio , is calculated by dividing the width of the screen by the height of the screen, and is used by the CreatePerspectiveFieldOfView() function to determine the "shape" of the screen in regards to its width and height. The GraphicsDevice has a precalculated aspect ratio value available that we can use when calculating the projection matrix:

Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio,0.1f, 10000.0f);

The last matrix needed to draw the model is the world matrix. However, as discussed earlier, each mesh in the model has its own transformation relative to the model's overall transformation. This means that we will need to calculate a world matrix for each mesh. We'll start with the overall transformation and add the transformations from the modelTransformations array per mesh. Each mesh also has what is called an effect. We will look at effects in much more depth in the coming chapters, but for now, just remember that they are used to determine the appearance of a model. We will be using one of XNA's built-in effects (called BasicEffect) for now, so all we need to do is set its World, View, and Projection properties:

// Calculate the starting world matrix
Matrix baseWorld = Matrix.CreateScale(0.4f) * Matrix.CreateRotationY(MathHelper.ToRadians(180));

foreach (ModelMesh mesh in model.Meshes)
{
   // Calculate each mesh's world matrix
   Matrix localWorld = modelTransforms[mesh.ParentBone.Index]
        * baseWorld;

   foreach (ModelMeshPart part in mesh.MeshParts)
   {
      BasicEffect e = (BasicEffect)part.Effect;

      // Set the world, view, and projection matrices to the effect
      e.World = localWorld;
      e.View = view;
      e.Projection = projection;

      e.EnableDefaultLighting();
   }

   // Draw the mesh
   mesh.Draw();
}

XNA will already have added the last piece of code for you as well, but it is important to ensure that this code is still in place at the end of the Draw() function. This line of code simply calls the Draw() function of the base Game class, ensuring that the game runs correctly.

base.Draw(gameTime);

The complete code for the Game1 class is now as follows:

public class Game1 : Microsoft.Xna.Framework.Game
{
   GraphicsDeviceManager graphics;
   SpriteBatch spriteBatch;

   Model model;
   Matrix[] modelTransforms;

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

      graphics.PreferredBackBufferWidth = 1280;
      graphics.PreferredBackBufferHeight = 800;
   }

   // Called when the game should load its content
   protected override void LoadContent()
   {
      spriteBatch = new SpriteBatch(GraphicsDevice);

      model = Content.Load<Model>("ship");

      modelTransforms = new Matrix[model.Bones.Count];
      model.CopyAbsoluteBoneTransformsTo(modelTransforms);
   }

   // Called when the game should update itself
   protected override void Update(GameTime gameTime)
   {
       base.Update(gameTime);
   }

   // Called when the game should draw itself
   protected override void Draw(GameTime gameTime)
   {
      GraphicsDevice.Clear(Color.CornflowerBlue);

      Matrix view = Matrix.CreateLookAt(new Vector3(200, 300, 900),new Vector3(0, 50, 0),Vector3.Up);

      Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio,0.1f, 10000.0f);

      // Calculate the starting world matrix
      Matrix baseWorld = Matrix.CreateScale(0.4f) * Matrix.CreateRotationY(MathHelper.ToRadians(180));

     foreach (ModelMesh mesh in model.Meshes)
     {
        // Calculate each mesh's world matrix
        Matrix localWorld = modelTransforms[mesh.ParentBone.Index]* baseWorld;

       foreach (ModelMeshPart part in mesh.MeshParts)
       {
          BasicEffect e = (BasicEffect)part.Effect;

          // Set the world, view, and projection 
          // matrices to the effect
          e.World = localWorld;
          e.View = view;
          e.Projection = projection;

          e.EnableDefaultLighting();
       }
          // Draw the mesh
         mesh.Draw();
      }

      base.Draw(gameTime);
   }
}

Run the game (Debug | Start Debugging, or F5) and you should see our spaceship in all its glory:

 

Creating a Custom Model class


The previous code works for drawing one model, but what if we wanted to draw more than one? More than ten? Writing the previous code out for each model would quickly become unmanageable. To make our lives a little easier, we'll take the previous code and put it into a new class called CModel (for custom model). This class will handle loading the transformations from a model, setting the matrices to the mesh part effects, and so on. Later on, it will handle setting custom effects, manage textures, and more. For now, we will keep it simple:

public class CModel
{
  public Vector3 Position { get; set; }
  public Vector3 Rotation { get; set; }
  public Vector3 Scale { get; set; }

  public Model Model { get; private set; }
  private Matrix[] modelTransforms;

  private GraphicsDevice graphicsDevice;
  public CModel(Model Model, Vector3 Position, Vector3 Rotation,Vector3 Scale, GraphicsDevice graphicsDevice)
  {
    this.Model = Model;

    modelTransforms = new Matrix[Model.Bones.Count];
    Model.CopyAbsoluteBoneTransformsTo(modelTransforms);

    this.Position = Position;
    this.Rotation = Rotation;
    this.Scale = Scale;

    this.graphicsDevice = graphicsDevice;
  }

  public void Draw(Matrix View, Matrix Projection)
  {
     // Calculate the base transformation by combining
     // translation, rotation, and scaling
     Matrix baseWorld = Matrix.CreateScale(Scale)* Matrix.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z)* Matrix.CreateTranslation(Position);

     foreach (ModelMesh mesh in Model.Meshes)
     {
       Matrix localWorld = modelTransforms[mesh.ParentBone.Index]* baseWorld;

       foreach (ModelMeshPart meshPart in mesh.MeshParts)
       {
         BasicEffect effect = (BasicEffect)meshPart.Effect;

         effect.World = localWorld;
         effect.View = View;
         effect.Projection = Projection;

         effect.EnableDefaultLighting();
       }

       mesh.Draw();
     }
  }
}

We can now simplify the Game1 class to draw a list of models:

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

List<CModel> models = new List<CModel>();

As a demonstration, let's add nine copies of our spaceship to that list in the LoadContent() method:

for (int y = 0; y < 3; y++)
  for (int x = 0; x < 3; x++)
  {
    Vector3 position = new Vector3(-600 + x * 600, -400 + y * 400, 0);

    models.Add(new CModel(Content.Load<Model>("ship"), position,new Vector3(0, MathHelper.ToRadians(90) * (y * 3 + x), 0),new Vector3(0.25f), GraphicsDevice));
  }

We can now update our Draw() method to draw a list of models, then we can run the game and see the result:

// Called when the game should draw itself
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);

  Matrix view = Matrix.CreateLookAt(new Vector3(0, 300, 2000),new Vector3(0, 0, 0),
        Vector3.Up);

  Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio,0.1f, 10000.0f);

  foreach (CModel model in models)
    model.Draw(view, projection);

  base.Draw(gameTime);
}
 

Creating a Camera class


Much like we did with the CModel class, let's create a reusable Camera class. We'll start with a base class that represents a camera at its lowest level: simply the view and projection matrices. We use a base class that all camera types will inherit from because we want to be able to use all camera types interchangeably. This Camera base class will also take care of calculating the projection matrix unless derived classes choose to do so themselves.

public abstract class Camera
{
  public Matrix View { get; set; }
  public Matrix Projection { get; set; }
  protected GraphicsDevice GraphicsDevice { get; set; }
        
  public Camera(GraphicsDevice graphicsDevice)
  {
     this.GraphicsDevice = graphicsDevice;
     generatePerspectiveProjectionMatrix(MathHelper.PiOver4);
  }

  private void generatePerspectiveProjectionMatrix(float FieldOfView)
  {
    PresentationParameters pp = GraphicsDevice.PresentationParameters;

    float aspectRatio = (float)pp.BackBufferWidth / (float)pp.BackBufferHeight;
    this.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), aspectRatio, 0.1f, 1000000.0f);
 }

  public virtual void Update()
  {
  }
}

Creating a target camera

Now that we have our base class, let's create the most basic type of camera—the target camera. This is simply a camera with two components—a position and a target. The camera points from the position towards the target, similar to the "camera" we used when we first drew our model:

This data is more or less directly passed into the CreateLookAt() function in the Matrix class to calculate the view matrix. The call in the constructor to the Camera base class' constructor ensures that the projection matrix and other matrices are calculated for us:

public class TargetCamera : Camera
{
  public Vector3 Position { get; set; }
  public Vector3 Target { get; set; }

  public TargetCamera(Vector3 Position, Vector3 Target,GraphicsDevice graphicsDevice) : base(graphicsDevice)
  {
     this.Position = Position;
     this.Target = Target;
  }

  public override void Update()
  {
    Vector3 forward = Target - Position;
    Vector3 side = Vector3.Cross(forward, Vector3.Up);
    Vector3 up = Vector3.Cross(forward, side);
    this.View = Matrix.CreateLookAt(Position, Target, up);
  }
}

As you can see, the Position and Target values can be set freely through their public properties or through the constructor to position the camera any way desired at any time, as the Update() function will update the view matrix as necessary. We can now once again update the Game1 class to use the TargetCamera instead of doing all the camera calculations itself. In addition to our list of models, we will also need a camera:

List<CModel> models = new List<CModel>();
Camera camera;

We will need to initialize the camera along with any models in the LoadContent() method:

models.Add(new CModel(Content.Load<Model>("ship"),Vector3.Zero, Vector3.Zero, new Vector3(0.6f), GraphicsDevice));

camera = new TargetCamera(new Vector3(300, 300, -1800),Vector3.Zero, GraphicsDevice);

We need to update the camera in the Update() method:

// Called when the game should update itself
protected override void Update(GameTime gameTime)
{
  camera.Update();

  base.Update(gameTime);
}

Finally, we can use the View and Projection properties of camera in the Draw() method:

// Called when the game should draw itself
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);

  foreach (CModel model in models)
     model.Draw(camera.View, camera.Projection);

  base.Draw(gameTime);
}

We now have two classes, TargetCamera and CModel, which can be reused easily, and a base class for creating any type of camera we could need. We'll use the rest of this chapter to look at other types of cameras, and to add a system that will speed up the game by keeping it from drawing (culling) objects that are not in the camera's view.

Upgrading the camera to a free camera

At the moment, we can put our camera at one point and aim it at another. This works for a static scene, but it doesn't allow for any freedom in a real game. Most games would be pretty difficult to play if the camera can't move around, so this will be our next task. The free camera we will create will operate like those found in First-Person-Shooter (FPS) games, which allow the player to move around with the W, S, A, and D keys (for forward, back, left, and right respectively), and look around with the mouse. On the Xbox 360, we would use the left joystick for movement and the right joystick to rotate the camera.

public class FreeCamera : Camera
{
  public float Yaw { get; set; }
  public float Pitch { get; set; }

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

  private Vector3 translation;

  public FreeCamera(Vector3 Position, float Yaw, float Pitch,GraphicsDevice graphicsDevice) : base(graphicsDevice)
  {
    this.Position = Position;
    this.Yaw = Yaw;
    this.Pitch = Pitch;

    translation = Vector3.Zero;
  }
}

The FreeCamera class adds two new values that the TargetCamera class didn't have—yaw and pitch. These two values (in radians) determine the amount that the camera has been rotated around the Y and X axes, respectively. The yaw and pitch values can be modified (usually based on mouse movements) through the new Rotate() method. There is also a new value called translation that accumulates the amount that the camera has moved (in the direction the camera is facing) between frames. The Move() function modifies this value (usually based on keyboard or joystick input):

public void Rotate(float YawChange, float PitchChange)
{
  this.Yaw += YawChange;
  this.Pitch += PitchChange;
}

public void Move(Vector3 Translation)
{
  this.translation += Translation;
}

The Update() function does the math to calculate the new view matrix:

public override void Update()
{
  // Calculate the rotation matrix
  Matrix rotation = Matrix.CreateFromYawPitchRoll(Yaw, Pitch, 0);

  // Offset the position and reset the translation
  translation = Vector3.Transform(translation, rotation);
  Position += translation;
  translation = Vector3.Zero;

  // Calculate the new target
  Vector3 forward = Vector3.Transform(Vector3.Forward, rotation);
  Target = Position + forward;

  // Calculate the up vector
  Vector3 up = Vector3.Transform(Vector3.Up, rotation);

  // Calculate the view matrix
  View = Matrix.CreateLookAt(Position, Target, up);
}

Once again we are ready to modify the Game1 class, this time to switch to our new camera and add keyboard and mouse control. First, we need to change the type of camera we are creating in the LoadContent() function:

camera = new FreeCamera(new Vector3(1000, 0, -2000), MathHelper.ToRadians(153), // Turned around 153 degreesMathHelper.ToRadians(5), // Pitched up 13 degreesGraphicsDevice);

Let's start by adding mouse control. In order to measure the amount of mouse movement between frames, we need to be able to store the last frame's mouse state. We can then compare that mouse state to the current mouse state. To store the last frame's mouse state, we will add a MouseState member variable:

List<CModel> models = new List<CModel>();
Camera camera;

MouseState lastMouseState;

This value needs to be initialized once before the Update method so that the game doesn't crash when trying to access it on the first frame, so we will grab the mouse state at the end of the LoadContent method:

lastMouseState = Mouse.GetState();

Now, we can create a new function that the Update() method will use to update the camera:

// Called when the game should update itself
protected override void Update(GameTime gameTime)
{
  updateCamera(gameTime);

  base.Update(gameTime);
}

void updateCamera(GameTime gameTime)
{
  // Get the new keyboard and mouse state
  MouseState mouseState = Mouse.GetState();
  KeyboardState keyState = Keyboard.GetState();

  // Determine how much the camera should turn
  float deltaX = (float)lastMouseState.X - (float)mouseState.X;
  float deltaY = (float)lastMouseState.Y - (float)mouseState.Y;

  // Rotate the camera
  ((FreeCamera)camera).Rotate(deltaX * .01f, deltaY * .01f);

  Vector3 translation = Vector3.Zero;
  // Determine in which direction to move the camera
  if (keyState.IsKeyDown(Keys.W)) translation += Vector3.Forward;
  if (keyState.IsKeyDown(Keys.S)) translation += Vector3.Backward;
  if (keyState.IsKeyDown(Keys.A)) translation += Vector3.Left;
  if (keyState.IsKeyDown(Keys.D)) translation += Vector3.Right;

  // Move 3 units per millisecond, independent of frame rate
  translation *= 3 * (float)gameTime.ElapsedGameTime.TotalMilliseconds;

  // Move the camera
  ((FreeCamera)camera).Move(translation);

  // Update the camera
  camera.Update();

  // Update the mouse state
  lastMouseState = mouseState;
}

Run the game again, and you should be able to move and rotate the camera with the mouse and W, S, A, and D keys.

 

Calculating bounding spheres for models


It is often convenient for us to have a simplified representation of the geometry of a model. Because complex models are made of hundreds if not thousands of vertices, it is often too inefficient to check for object intersection per vertex between every object in the scene when doing collision detection, for example. To simplify collision checks, we will use what is called a bounding volume , specifically a bounding sphere. Let's add some functionality to our CModel class to calculate bounding spheres for us. To start, we need to add a new BoundingSphere member variable to the CModel class:

private Matrix[] modelTransforms;
private GraphicsDevice graphicsDevice;
private BoundingSphere boundingSphere;

Next, we will create a function to calculate this bounding sphere based on our model's geometry:

private void buildBoundingSphere()
{
  BoundingSphere sphere = new BoundingSphere(Vector3.Zero, 0);

  // Merge all the model's built in bounding spheres
  foreach (ModelMesh mesh in Model.Meshes)
  {
     BoundingSphere transformed = mesh.BoundingSphere.Transform(
        modelTransforms[mesh.ParentBone.Index]);

     sphere = BoundingSphere.CreateMerged(sphere, transformed);
  }

  this.boundingSphere = sphere;
}

We need to be sure to call this function in our constructor:

public CModel(Model Model, Vector3 Position, Vector3 Rotation,Vector3 Scale, GraphicsDevice graphicsDevice)
{
  this.Model = Model;

  modelTransforms = new Matrix[Model.Bones.Count];
  Model.CopyAbsoluteBoneTransformsTo(modelTransforms);

  buildBoundingSphere();

  ...

However, there is one problem with this approach: this bounding sphere is centered at the origin, so if we were to move our model, the bounding sphere would no longer contain the model. To solve this problem, we will add a public property that translates our origin-centered boundingSphere value to the model's current position and scales it based on our model's scale:

public BoundingSphere BoundingSphere
{
  get
  {
    // No need for rotation, as this is a sphere
    Matrix worldTransform = Matrix.CreateScale(Scale)* Matrix.CreateTranslation(Position);

   BoundingSphere transformed = boundingSphere;
   transformed = transformed.Transform(worldTransform);

   return transformed;
  }
}
 

View frustum culling


One usage of our new bounding sphere system is to determine which objects are onscreen. This is useful when, for example, we are drawing a large number of objects: if we first check whether an object is onscreen before drawing it, we have a chance of improving the performance of our games if some of the objects in the scene leave the player's view. This is called view frustum culling because we are checking if an object's bounding sphere intersects the view frustum, and if it doesn't, we cull it (refrain from drawing it). It is worth noting that this method of culling is not an all-encompassing method of optimization—if you are drawing many small objects, it may not be worth the time to cull objects because the graphics card won't process pixels that are offscreen anyway.

The first thing we need to do is to actually calculate a view frustum. We can do this very simply once we know our view and projection matrices. Let's go back to our abstract Camera class and add a BoundingFrustum. The BoundingFrustum class has a function to check if a BoundingSphere is in view.

public BoundingFrustum Frustum { get; private set; }

The view and projection matrices can change frequently, so we need to update the frustum whenever they change. We will do this by calling the generateFrustum() function that we will write shortly in the View and Projection properties' set accessors:

Matrix view;
Matrix projection;

public Matrix Projection
{
  get { return projection; }
  protected set
  {
     projection = value;
     generateFrustum();
  }
}

public Matrix View
{
  get { return view; }
  protected set
  {
    view = value;
    generateFrustum();
  }
}

private void generateFrustum()
{
  Matrix viewProjection = View * Projection;
  Frustum = new BoundingFrustum(viewProjection);
}

Finally, we will make things a little easier on ourselves by adding some shortcuts to check if bounding boxes and bounding spheres are visible. Because we are doing this in the base class, these functions will conveniently work for any camera type.

public bool BoundingVolumeIsInView(BoundingSphere sphere)
{
  return (Frustum.Contains(sphere) != ContainmentType.Disjoint);
}

public bool BoundingVolumeIsInView(BoundingBox box)
{
  return (Frustum.Contains(box) != ContainmentType.Disjoint);
}

Finally, we can update the Game1 class to use this new system. Our new Draw() function will check if the bounding sphere of each model to be drawn is in view, and if so, it will draw the model. If it doesn't end up drawing any models, it will clear the screen to red so we can tell if models are being culled correctly:

// Called when the game should draw itself
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);

  int nModelsDrawn = 0;

  foreach (CModel model in models)
   if (camera.BoundingVolumeIsInView(model.BoundingSphere))
   {
      nModelsDrawn++;
      model.Draw(camera.View, camera.Projection);
   }

   if (nModelsDrawn == 0)
    GraphicsDevice.Clear(Color.Red);

   base.Draw(gameTime);
}

Once you have tried this out and are convinced this system works, feel free to remove the extra code that clears the screen to red:

// Called when the game should draw itself
protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);

  foreach (CModel model in models)
    if (camera.BoundingVolumeIsInView(model.BoundingSphere))
       model.Draw(camera.View, camera.Projection);

  base.Draw(gameTime);
}
 

Additional camera types: Arc-Ball


We will spend the rest of this chapter looking at two other camera types, and finally at the end we will use one of them to build a small game where the player gets to fly the spaceship that we've been looking at thus far around. The first camera type is the Arc-Ball camera.

An Arc-Ball camera is essentially the opposite of a free camera. Instead of sitting at a point and looking at a target, the target sits at one point and the camera rotates around it. As an example, this type of camera is commonly used when editing the appearance of a vehicle or character in a 3D game. The camera is free to "float around" the object, but it can't turn around or leave the object, as the target of the camera is at the center of the sphere the camera is allowed to travel on.

Most games limit the vertical and zoom range of this type of camera, so our camera will allow this as well. The code for the ArcBall Camera class is as follows:

public class ArcBallCamera : Camera
{
  // Rotation around the two axes
  public float RotationX { get; set; }
  public float RotationY { get; set; }

  // Y axis rotation limits (radians)
  public float MinRotationY { get; set; }
  public float MaxRotationY { get; set; }

  // Distance between the target and camera
  public float Distance { get; set; }

  // Distance limits
  public float MinDistance { get; set; }
  public float MaxDistance { get; set; }

  // Calculated position and specified target
  public Vector3 Position { get; private set; }
  public Vector3 Target { get; set; }

  public ArcBallCamera(Vector3 Target, float RotationX,float RotationY, float MinRotationY, float MaxRotationY,float Distance, float MinDistance, float MaxDistance,GraphicsDevice graphicsDevice) : base(graphicsDevice)
    {
       this.Target = Target;

       this.MinRotationY = MinRotationY;
       this.MaxRotationY = MaxRotationY;

       // Lock the y axis rotation between the min and max values
       this.RotationY = MathHelper.Clamp(RotationY, MinRotationY, MaxRotationY);

       this.RotationX = RotationX;

       this.MinDistance = MinDistance;
       this.MaxDistance = MaxDistance;

       // Lock the distance between the min and max values
       this.Distance = MathHelper.Clamp(Distance, MinDistance, MaxDistance);
    }


  public void Move(float DistanceChange)
  {
     this.Distance += DistanceChange;

     this.Distance = MathHelper.Clamp(Distance, MinDistance, MaxDistance);
  }

  public void Rotate(float RotationXChange, float RotationYChange)
  {
     this.RotationX += RotationXChange;
     this.RotationY += -RotationYChange;

     this.RotationY = MathHelper.Clamp(RotationY, MinRotationY, MaxRotationY);
  }

  public void Translate(Vector3 PositionChange)
  {
     this.Position += PositionChange;
  }

  public override void Update()
  {
     // Calculate rotation matrix from rotation values
     Matrix rotation = Matrix.CreateFromYawPitchRoll(RotationX, -RotationY, 0);

      // Translate down the Z axis by the desired distance
      // between the camera and object, then rotate that
      // vector to find the camera offset from the target
      Vector3 translation = new Vector3(0, 0, Distance);
      translation = Vector3.Transform(translation, rotation);


      Position = Target + translation;

      // Calculate the up vector from the rotation matrix
      Vector3 up = Vector3.Transform(Vector3.Up, rotation);

      View = Matrix.CreateLookAt(Position, Target, up);
  }
}

To implement this camera in the Game1 class, we first instantiate our camera as an ArcBallCamera in the LoadContent() method:

camera = new ArcBallCamera(Vector3.Zero, 0, 0, 0, MathHelper.PiOver2, 1200, 1000, 2000, GraphicsDevice);

Second, we need to update the updateCamera() function to reflect the way this new camera type moves:

void updateCamera(GameTime gameTime)
{
  // Get the new keyboard and mouse state
  MouseState mouseState = Mouse.GetState();
  KeyboardState keyState = Keyboard.GetState();

  // Determine how much the camera should turn
  float deltaX = (float)lastMouseState.X - (float)mouseState.X;
  float deltaY = (float)lastMouseState.Y - (float)mouseState.Y;

  // Rotate the camera
  ((ArcBallCamera)camera).Rotate(deltaX * .01f, deltaY * .01f);

  // Calculate scroll wheel movement
  float scrollDelta = (float)lastMouseState.ScrollWheelValue -(float)mouseState.ScrollWheelValue;

  // Move the camera
  ((ArcBallCamera)camera).Move(scrollDelta);

  // Update the camera
  camera.Update();

  // Update the mouse state
  lastMouseState = mouseState;
}

Run the game, and you will be able to rotate around the ship with the mouse, and move towards and away from it with the scroll wheel.

 

Additional camera types: chase camera


The last camera type we will look at is the chase camera. A chase camera is designed to "chase" an object. Generally, the camera follows the object at some distance and turns with it. This is the type of camera used, for example, in most third person situations—racing games, third person shooters, flight simulators, and so on. The chase distance and view direction are generally determined using an offset for the camera position and an offset for the target position from the object's position. The view matrix is then calculated as usual based on those values.

In this case, the chase camera also has a relative rotation value that allows the camera to rotate independently of the object, and a "springiness" value that allows the camera to "bend" from side-to-side instead of rigidly following the object. This can create more of a feeling of movement than simply following the object because the camera responds more like it would under real-life physics. The code for the ChaseCamera class is as shown:

public class ChaseCamera : Camera
{
  public Vector3 Position { get; private set; }
  public Vector3 Target { get; private set; }

  public Vector3 FollowTargetPosition { get; private set; }
  public Vector3 FollowTargetRotation { get; private set; }

  public Vector3 PositionOffset { get; set; }
  public Vector3 TargetOffset { get; set; }

  public Vector3 RelativeCameraRotation { get; set; }

    float springiness = .15f;

  public float Springiness
  {
    get { return springiness; }
    set { springiness = MathHelper.Clamp(value, 0, 1); }
  }

  public ChaseCamera(Vector3 PositionOffset, Vector3 TargetOffset,Vector3 RelativeCameraRotation, GraphicsDevice graphicsDevice): base(graphicsDevice)
  {
     this.PositionOffset = PositionOffset;
     this.TargetOffset = TargetOffset;
     this.RelativeCameraRotation = RelativeCameraRotation;
  }

  public void Move(Vector3 NewFollowTargetPosition,Vector3 NewFollowTargetRotation)
  {
     this.FollowTargetPosition = NewFollowTargetPosition;
     this.FollowTargetRotation = NewFollowTargetRotation;
  }

  public void Rotate(Vector3 RotationChange)
  {
     this.RelativeCameraRotation += RotationChange;
  }

  public override void Update()
  {
     // Sum the rotations of the model and the camera to ensure it 
     // is rotated to the correct position relative to the model's 
     // rotation
     Vector3 combinedRotation = FollowTargetRotation + RelativeCameraRotation;

     // Calculate the rotation matrix for the camera
     Matrix rotation = Matrix.CreateFromYawPitchRoll(combinedRotation.Y, combinedRotation.X, combinedRotation.Z);

     // Calculate the position the camera would be without the spring
     // value, using the rotation matrix and target position
     Vector3 desiredPosition = FollowTargetPosition +Vector3.Transform(PositionOffset, rotation);

     // Interpolate between the current position and desired position
     Position = Vector3.Lerp(Position, desiredPosition, Springiness);

     // Calculate the new target using the rotation matrix
     Target = FollowTargetPosition +  Vector3.Transform(TargetOffset, rotation);

    // Obtain the up vector from the matrix
    Vector3 up = Vector3.Transform(Vector3.Up, rotation);

    // Recalculate the view matrix
    View = Matrix.CreateLookAt(Position, Target, up);
  }
}





 

Example—spaceship simulator


Let's use the concepts and classes learned and created so far to create a simple game in which the player flies our spaceship around using the keyboard. You'll notice that the example uses the ChaseCamera to follow the spaceship and uses two models to represent the ground and spaceship.

  1. We'll start by instantiating these values in the LoadContent() method:

    models.Add(new CModel(Content.Load<Model>("ship"),new Vector3(0, 400, 0), Vector3.Zero, new Vector3(0.4f), GraphicsDevice));
    models.Add(new CModel(Content.Load<Model>("ground"),Vector3.Zero, Vector3.Zero, Vector3.One, GraphicsDevice));
    camera = new ChaseCamera(new Vector3(0, 400, 1500), new Vector3(0, 200, 0),new Vector3(0, 0, 0), GraphicsDevice);
  2. Next, we will create a new function that updates the position and rotation of our model based on keyboard input, which is called by the Update() function:

    // Called when the game should update itself
    protected override void Update(GameTime gameTime)
    {
      updateModel(gameTime);
      updateCamera(gameTime);
      base.Update(gameTime);
    }
    void updateModel(GameTime gameTime)
    {
      KeyboardState keyState = Keyboard.GetState();
      Vector3 rotChange = new Vector3(0, 0, 0);
      // Determine on which axes the ship should be rotated on, if any
      if (keyState.IsKeyDown(Keys.W))
         rotChange += new Vector3(1, 0, 0);
      if (keyState.IsKeyDown(Keys.S))
         rotChange += new Vector3(-1, 0, 0);
      if (keyState.IsKeyDown(Keys.A))
         rotChange += new Vector3(0, 1, 0);
      if (keyState.IsKeyDown(Keys.D))
         rotChange += new Vector3(0, -1, 0);
         models[0].Rotation += rotChange * .025f;
      // If space isn't down, the ship shouldn't move
      if (!keyState.IsKeyDown(Keys.Space))
         return;
      // Determine what direction to move in
      Matrix rotation = Matrix.CreateFromYawPitchRoll(models[0].Rotation.Y, models[0].Rotation.X, models[0].Rotation.Z);
      // Move in the direction dictated by our rotation matrix
      models[0].Position += Vector3.Transform(Vector3.Forward, rotation) * (float)gameTime.ElapsedGameTime.TotalMilliseconds * 4;
    }
    
  3. We can now greatly simplify the updateCamera() function:

    void updateCamera(GameTime gameTime)
    {
      // Move the camera to the new model's position and orientation
      ((ChaseCamera)camera).Move(models[0].Position, models[0].Rotation);
      // Update the camera
      camera.Update();
    }
  4. And with that, we're finished! Run your game one last time and you should be able to fly the ship around with W, S, A, D, and the Space bar:

XNA Graphics Profiles

In an effort to allow developers to easily maintain compatibility with a wide range of devices, XNA provides two "graphics profiles." A graphics profile is a set of features that are guaranteed to work on a certain machine, as long as the machine meets all of the requirements of that graphics profile. The two profiles XNA provides are "Reach" and "HiDef." Games developed with the Reach profile will work on a very large range of devices, but are limited in which graphics features they can use. Games developed with the HiDef profile will be able to use a large range of graphics features but they will only work on a much more limited range of devices. The Xbox 360 supports the HiDef profile.

In order to implement many of the examples in this book, you will need to be developing under the Hidef profile. Some examples may work under the Reach profile but this book assumes you are working on a computer supporting HiDef. If you encounter errors while trying to build or run an example, you should first ensure that the game is set to run under the HiDef profile. To do this, right click on your game's project in the solution explorer in Visual Studio (labelled "MyGame" if you have been following along) and click "Properties." Under the "XNA Game Studio" tab, select "Use HiDef to access the complete API (including features unavailable for Windows Phone." Rebuild the game and run it and any errors relating to the graphics profile will be fixed. More information can be found at http://msdn.microsoft.com/en-us/library/ff604995.aspx.

 

Summary


Now that you have completed this chapter, you have an understanding of the fundamentals of 3D graphics. You know how to create a new game project with Visual Studio, how to add content to its content project, and how to remove content. You also have a basic understanding of the content pipeline and how to interact with it through code and the ContentManager. You have also created a number of useful classes that will be reused later, including the CModel and Camera classes, and all of the derived camera classes. Finally, you also have a way to determine which objects intersect others or which are onscreen.

In the coming chapters, we will learn how to add new special effects to our games. We will start with "Effects" (which were mentioned earlier) and HLSL to implement some lighting and texturing effects.

About the Author

  • Sean James

    Sean James is a computer science student who has been programming for many years. He started with web design, learning HTML, PHP, Javascript, etc. Since then has created many websites including his personal XNA and game development focused blog http://www.innovativegames.net. In addition to web design he has interests in desktop software development and development for mobile devices such as Android, Windows Mobile, and Zune. However, his passion is for game development with DirectX, OpenGL, and XNA.

    Sean James lives in Claremont, CA with his family and two dogs. He would like to thank his family and friends who supported him throughout the writing of this book, and all the people at Packt Publishing who worked hard on the book and to support him. He would also like to thank the XNA community for providing such amazing resources, without which this book would not have been possible.

    Browse publications by this author