Tutorial 1: Displaying a 3D Model on the Screen
This article details how to use the XNA Framework Content Pipeline to load a 3D model and its associated textures, and it presents the code necessary to display the model on the screen.
- Introduction
- Step 1: Create a Spacewar Project
- Step 2: Create a New Project and Use the Spacewar Model
- Step 3: Load the Model by Using the Content Pipeline
- Step 4: Display the Model on the Screen (and Make It Rotate)
- Congratulations!
- Ideas to Expand
- The Complete Example (contents of Game1.cs)
Introduction
In Your First Game: Microsoft XNA Game Studio Express in 2D, you saw a simple example that used the XNA Framework Content Pipeline to load a sprite, represented by a Texture2D object. You also used the XNA Framework to draw the sprite on the screen. This tutorial goes beyond that simple sample to help introduce you to many concepts that XNA Game Studio Express makes easy, so you can focus on making fun, interactive games.
This first tutorial will introduce you to the Content Pipeline in a little more detail, and will introduce you to some of the XNA Framework API calls you'll use to draw 3D objects on the screen. When you complete this tutorial, you'll have a 3D model drawing on your screen. The model will have textures and lighting. Let's get started!
Step 1: Create a Spacewar Project
The first thing that you'll need before you start coding are some art assets to play around with. In this case, you want a 3D model, and an associated texture file so that the model has some detail. These assets will be loaded into your game using the XNA Framework Content Pipeline, which is a feature built right into the Visual C# 2005 Express Edition user interface.
With XNA Game Studio Express, there is a rich source of art right at your fingertips, in the Spacewar Starter Kit. But you only want the art, not all the prebuilt code for the Spacewar game, so your first step is to isolate the art. Start by creating a Spacewar Starter Kit project.
On your Windows computer, open the Start Menu and navigate to All Programs, the Microsoft XNA Game Studio Express folder, and then XNA Game Studio Express.
Visual C# 2005 Express Edition will launch. When it appears, click the File menu, and then click New Project to create a new project.
From the list of templates that appears, click either Spacewar Windows Starter Kit or Spacewar Xbox 360 Starter Kit, depending on whether you're developing on the Xbox 360 or Windows. Either way, the art assets will be the same.
Type a path into the Location field that you can remember. You'll be copying art from this location.
Leave the rest of the fields at their default values. The dialog box should look similar to the following:
Click OK.
The Spacewar project will be populated at the path you typed into the Location field. Once the project code opens up on the screen, click the File menu, and then click Close Solution. You don't need the Spacewar solution anymore.
Step 2: Create a New Project and Use the Spacewar Model
Now that the art assets are available to you, the next step is to create the actual code project that you'll be writing.
- Click the File menu, and then click New Project to create a new project.
- From the list of templates that appears, click either Windows Game or Xbox 360 Game, depending on whether you're developing on the Xbox 360 or Windows. If you develop for Xbox 360, be sure you have a membership in the XNA Creators Club as described in Connecting to Your Xbox 360 Console with XNA Game Studio Express; otherwise, you will not be able to play your game!
- Type a name for your game into the Name field, and a path to where you want the game files stored in the Location field.
- Click OK.
The code for your new game will be displayed. The project already contains many of the methods that are needed to start and run a game. Right now, however, you need to make sure your art assets are being loaded. Then you can modify the game to display them on the screen. Follow these steps to get some art into your project.
- Make sure you can see the Solution Explorer for your project on the right side of the window. If you cannot see it, click the View menu, and then click Solution Explorer. When it appears, you will see files associated with your project in a tree structure.
- In Solution Explorer, right-click the Project icon (one level below the Solution icon) and click Add and then New Folder. Name this folder Content. This is the root folder for your art. You must add two more folders underneath this one.
- Right-click the Content folder you just created, click Add, and then click New Folder. This will create a new folder underneath Content. Name this folder Models.
- Repeat the last step, creating a new folder under Content. This time, call the folder Textures.
Your project structure should look similar to this:
Now you need two pieces of art. The first is the 3D model that will go into this new Content\Models folder, and the second is a texture that will be drawn on the 3D model; this will go in the Content\Textures folder. The files you need are back in the path you placed the Spacewar project in. To get them:
- Right-click the Models folder in the Solution Explorer, click Add, and then click Existing Item. Using the dialog box that appears, browse back to the path you placed the Spacewar project in, and find the Contents\Models folder. Select p1_wedge.fbx. If you can't see any files, make sure you change the Files of type selection box to read Content Pipeline Files. Click OK.
- Now, copy the textures associated with the model into the Textures folder. To do this, open a Windows Explorer window and browse to the Spacewar project path, and the Content\Textures folder. Copy wedge_p1_diff_v1.tga, and then browse to your project folder, then into the Content\Textures folder, and paste in the .tga you just copied.
Your project structure should now look similar to this:
Note that you don't see the texture you added in Solution Explorer. When you add a model, the textures that the model uses do not need to be added to the Content Pipeline. If you need to add textures that you will access manually (such as textures used for 2D sprite drawing), do so via Solution Explorer. Otherwise, you can simply copy the texture files to the appropriate folder.
When the files are added to the project, the Content Pipeline automatically identifies them as content files and sets the appropriate processors to run when you build your project. This will happen silently; you won't need to do anything. If you'd like to learn more about the Content Pipeline, see Content Pipeline.
Note
Model files contain path information for the textures they use. All models in the Spacewar Starter Kit expect to find their textures in a Textures folder that exists alongside the folder the models are in. For this tutorial, and any time you intend to use Spacewar content, you must replicate the folder structure in Solution Explorer noted in this tutorial. Models you create or retrieve from other sources may have different path requirements and therefore may require different folder setups. You can determine the correct texture paths by examining the model files, or by compiling the model as part of your game using XNA Game Studio Express and noting any Content Pipeline path errors that are returned.
At this point, you're ready to code!
Step 3: Load the Model by Using the Content Pipeline
Take a look at the code for Game1.cs; it should still be on your screen from opening up your project. You'll see a lot already done for you. Each of the methods already in the code are waiting for you to drop in your own calls to the XNA Framework. For now, start by modifying the LoadGraphicsContent method.
Double-click the Game1.cs file in Solution Explorer to bring up the code for your game.
In the code, find the LoadGraphicsContent method.
Modify the code (including adding the lines shown above the method) to look like this:
// Set the 3D model to draw. Model myModel; // The aspect ratio determines how to scale 3d to 2d projection. float aspectRatio; protected override void LoadGraphicsContent( bool loadAllContent ) { if (loadAllContent) { myModel = content.Load<Model>( "Content\\Models\\p1_wedge" ); } aspectRatio = graphics.GraphicsDevice.Viewport.Width / graphics.GraphicsDevice.Viewport.Height; }
In that step, you have told the Content Pipeline to load your model into your game when LoadGraphicsContent is called at the beginning of your game. Note how you have to pass in the path to the asset relative to your project directory. Also note that there is no extension on the asset anymore. The name of the asset can be anything you want, but by default it is the name of the asset file minus its extension. To see more information on how to change the name of your asset, see Game Asset Properties.
The code now loads the model. Your next step is to get it showing on the screen.
Step 4: Display the Model on the Screen (and Make It Rotate)
You'll want to modify two of the methods in your Game1.cs file.
- In the Draw method, you will draw the model on the screen with texture and lighting.
- In the Update method, you will make the model change its orientation based on time, so it appears to rotate over time.
Do the harder work first: drawing the model. The first step is to use some XNA Framework methods to set up the model's position and lighting.
In the code, find the Draw method.
Modify the code (including adding the lines shown above the method) to look like this:
// Set the position of the model in world space, and set the rotation. Vector3 modelPosition = Vector3.Zero; float modelRotation = 0.0f; // Set the position of the camera in world space, for our view matrix. Vector3 cameraPosition = new Vector3( 0.0f, 50.0f, 5000.0f ); protected override void Draw( GameTime gameTime ) { graphics.GraphicsDevice.Clear( Color.CornflowerBlue ); // Copy any parent transforms. Matrix[] transforms = new Matrix[myModel.Bones.Count]; myModel.CopyAbsoluteBoneTransformsTo( transforms ); // Draw the model. A model can have multiple meshes, so loop. foreach (ModelMesh mesh in myModel.Meshes) { // This is where the mesh orientation is set, as well as our camera and projection. foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = transforms[mesh.ParentBone.Index] * Matrix.CreateRotationY( modelRotation ) * Matrix.CreateTranslation( modelPosition ); effect.View = Matrix.CreateLookAt( cameraPosition, Vector3.Zero, Vector3.Up ); effect.Projection = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians( 45.0f ), aspectRatio, 1.0f, 10000.0f ); } // Draw the mesh, using the effects set above. mesh.Draw(); } }
This code uses helper methods provided by the XNA Framework to set up the necessary 3D math and lighting to display the model on the screen. Use the World matrix to change the position of the model in the world, the View matrix to change the position and direction of the camera (your eye), and the Projection matrix to control how the view of the 3D world is turned into a 2D image (projected) on your screen.
The call to CopyAbsoluteBoneTransformsTo and associated code inside the setup of the World matrix are not strictly necessary for this model, but when using more complicated models, which often use heirarchical structure (where mesh positions, scales, and rotations are controlled by "bones"), this code ensures that any mesh is first transformed by the bone that controls it, if such a bone exists. The mesh is then transformed relative to the bone transformation.
If you compile and run your code now, you will see your model on screen! It is a spaceship with detail texture. But if you can resist the urge to compile your project and run, you can easily make the model rotate in real-time so you can see all of it:
In the code, find the Update method.
Modify the code (including adding the lines shown above the method) to look like this:
protected override void Update( GameTime gameTime ) { if (GamePad.GetState( PlayerIndex.One ).Buttons.Back == ButtonState.Pressed) this.Exit(); modelRotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds * MathHelper.ToRadians( 0.1f ); base.Update( gameTime ); }
And that's it. Compile and run your project by hitting the F5 key or clicking the Debug menu, and then clicking Start Debugging.
Congratulations!
You did it. There's a lot to making games, but you've taken the first step. A 3D model with lighting and movement in real time. From here, there's no limit to where you could go!
For simplicity's sake, there have been some shortcuts taken that could be optimized for better performance. An obvious improvement would be to precalculate the View and Projection matrices instead of calculating them every time Draw is called, since they do not change. Try out this optimization as a first step. When you're ready to make your game interactive, go to the next tutorial.
Next...
Tutorial 2: Making Your Model Move Using Input
Ideas to Expand
Got the urge to tinker with the project a bit? Try these ideas.
- Modify the lighting parameters in the Draw call. Look at BasicEffect for an idea of what you can modify.
- Instead of looking at a blue background, try adding an image as your background. See How to: Make a Scrolling Background for guidance. Hint: Make sure you use a call to SpriteBatch.Draw that allows you to specify a layerDepth parameter, and set that depth to 1.0f.
The Complete Example (contents of Game1.cs)
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
#endregion
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
ContentManager content;
public Game1()
{
graphics = new GraphicsDeviceManager( this );
content = new ContentManager( Services );
}
protected override void Initialize()
{
base.Initialize();
}
// Set the 3D model to draw.
Model myModel;
// The aspect ratio determines how to scale 3d to 2d projection.
float aspectRatio;
protected override void LoadGraphicsContent( bool loadAllContent )
{
if (loadAllContent)
{
myModel = content.Load<Model>( "Content\\Models\\p1_wedge" );
}
aspectRatio = graphics.GraphicsDevice.Viewport.Width /
graphics.GraphicsDevice.Viewport.Height;
}
protected override void UnloadGraphicsContent( bool unloadAllContent )
{
if (unloadAllContent == true)
{
content.Unload();
}
}
protected override void Update( GameTime gameTime )
{
if (GamePad.GetState( PlayerIndex.One ).Buttons.Back == ButtonState.Pressed)
this.Exit();
modelRotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds * MathHelper.ToRadians( 0.1f );
base.Update( gameTime );
}
// Set the position of the model in world space, and set the rotation.
Vector3 modelPosition = Vector3.Zero;
float modelRotation = 0.0f;
// Set the position of the camera in world space, for our view matrix.
Vector3 cameraPosition = new Vector3( 0.0f, 50.0f, 5000.0f );
protected override void Draw( GameTime gameTime )
{
graphics.GraphicsDevice.Clear( Color.CornflowerBlue );
// Copy any parent transforms.
Matrix[] transforms = new Matrix[myModel.Bones.Count];
myModel.CopyAbsoluteBoneTransformsTo( transforms );
// Draw the model. A model can have multiple meshes, so loop.
foreach (ModelMesh mesh in myModel.Meshes)
{
// This is where the mesh orientation is set, as well as our camera and projection.
foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.World = transforms[mesh.ParentBone.Index] * Matrix.CreateRotationY( modelRotation )
* Matrix.CreateTranslation( modelPosition );
effect.View = Matrix.CreateLookAt( cameraPosition, Vector3.Zero, Vector3.Up );
effect.Projection = Matrix.CreatePerspectiveFieldOfView( MathHelper.ToRadians( 45.0f ),
aspectRatio, 1.0f, 10000.0f );
}
// Draw the mesh, using the effects set above.
mesh.Draw();
}
}
}