You are on page 1of 8

XNA Game Studio Express is intended to allow you to create games for the PC, for the XBox

360, Zune devices and now Windows Phone 7. Most of the easy to follow examples of game creation are for 2D sprite-based games - 3D XNA seems hard. However, if you know a little about how 3D works getting started with XNA isn't that difficult. This project's end result might not seem very impressive if you were hoping for a 3D game but it is exactly what you need to get started on such a project. The aim is to create a 3D rotating cube. This is the 3D graphics equivalent of the familiar hello world program because once you can create a rotating 3D cube you can create just about any shape and movement you care to attempt. In most cases when creating any 3D model you would use a 3D graphics design program and export the model to XNA. However, every 3D programmer should know how to create a 3D model from scratch and so in this case we start off by programmatically building the cube. What might surprise you is that even this simple object isn't a simple to create in code. Once we have the mesh that defines the cube we move on to solve the problem of displaying it.

Prerequisites
Before you get started on this project you will need to download and install C# Express 2010 or Visual Studio 2010. After this you can download and install XNA Game Studio - XNA Creators Club Online - downloads This which provides a new project types within C# Express 2010. Its worth knowing that you cant use XNA Express from Visual Studio 2010 You also need a graphics card that supports Shader Model 2.0 or better - which covers most modern machines.

Getting started with XNA


Open C# Express (or Visual Studio) and create a new Windows Game (XNA) project and call it XNACube. If you take a look at the generated code you will discover that you have a single class called Game1 (or similar). This provides you with the basic framework to create a game, be it in 2D or 3D. As well as the constructor, Initialise and LoadContent which can be used to initialise anything you need in the game, there are two important methods that are core to the functioning of the game: protected override void Update(GameTime gameTime) which is called at regular intervals and is where you place all of the games logic and protected override void Draw(GameTime gameTime) which is also called at regular intervals and is where you place all of the code that actually draws the game scene. The parameter passed to both methods gameTime is an object that can be used to discover how long it has been since the last iteration. The constructor initialises the graphics device manager which looks after the way graphics hardware is used. public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } You can spend time customising the way this works but for this simple example we can simple accept the defaults. There are also some lines of generated code which handle the game pad keys and a few other things - but once again for this simple example we can ignore them. The cube The first thing we need is to create the geometry of the cube. There are a number of ways of defining 3D model geometry but it all comes down to defining triangles.

Each triangle has three vertexes and has a clockwise and an anticlockwise face according to the order in which the vertexes are listed. To create a square face we need to define two triangles.

A single cube face consists of two triangles - the order of the vertexes gives each face an anti-clockwise sense. To create a cube we need six faces and hence a total of 12 triangles and 36 vertexes. Creating the data needed to define a cube isnt difficult, but you have to get it exactly right. We are going to create a function called MakeCube which returns an array of vertexes that define the triangles that make it up. This can be done simply by entering the coordinates for each vertex and many people do use this method - it's simple and direct but it' s alot of typing. In order to reduce the amount of typing we can make use of symmetries of the cube. If you define a square face centred on the origin then a cube can be constructed by moving the face and rotating it see the figure below:

Building a cube by defining a face and then moving and rotating it to build up the cube. The function starts off by defining an array to hold the 36 vertexes we are about to create: protected VertexPositionNormalTexture[] MakeCube() { VertexPositionNormalTexture[] vertexes = new VertexPositionNormalTexture[36]; XNA allows the use of a range of different vertex types according to what sort of data is stored. The simplest is VertexPositionColor which stores the position of each vertex and its color. In our example the vertex type is VertexPositionNormalTexture rather than something simpler because this is what the default renderer uses. As its name suggests, this type of vertex stores the position; the normal to the surface, i.e. the direction at right angles to the surface; and a texture coordinate.The basic idea is that the normal allows lighting calculations to be performed quickly and the texture coordinate allows a bitmap texture to be mapped onto the surface. A texture is a bitmap that is normalised to use u,v co-ordinates that are in the range 0 to 1. Texture bitmaps are mapped onto the surfaces defined by the triangles and you specify texture co-ordinates for each vertex giving the position within the bitmap to be used for that vertex. We arent going to use the texture feature at the moment, for simplicity, but it still has to be set to a value which again for simplicity is (0,0) : Vector2 Texcoords = new Vector2(0f, 0f); We now need to code the positions of the six points that make up the two triangles that represent a face: Vector3[] face = new Vector3[6]; //TopLeft face[0] = new Vector3(-1f, 1f, 0.0f); //BottomLeft face[1] = new Vector3(-1f, -1f, 0.0f); //TopRight face[2] = new Vector3(1f, 1f, 0.0f); //BottomLeft

face[3] = new Vector3(-1f, -1f, 0.0f); //BottomRight face[4] = new Vector3(1f, -1f, 0.0f); //TopRight face[5] = new Vector3(1f, 1f, 0.0f); This slightly wasteful repeated storing of the points makes the logic of constructing the cube much easier because reading the array in sequence gives the two triangles with the vertexes in anticlockwise order. Notice also that the face is defined at z=0 and centered on the origin. The first three vertexes, 0,1 and 2, correspond to the light green triangle:

The second three vertexes, 3, 4 and 5, correspond the the dark green triangle:

With this we can now move on and create the front face. Notice that all we have created so far is an array of vectors which give the location of the points that make up a flat face consisting of two triangles. We now have to use this basic geometry to create vertexes. To do this we use the constructor which accepts the position of the vertex as a Vector3 as the first parameter, the normal to the surface as a Vector3 as the second parameter and a Vector2 given the texture co-ordinates of the point as the third parameter. By default the XNA co-ordinate system is such that positive x is to the right, positive y is up and positive z is out of the screen towards the viewer. The front face is constructed by moving the points forward in the z direction i.e. out of the screen -i.e. we simply add (0,0,1) or Vector3.UnitZ to the location of each point. The normal for the front face is simply a vector pointing out of the screen so it is Vector3.UnitZ for each vertex: //front face for (int i = 0; i <= 2; i++) { vertexes[i] = new VertexPositionNormalTexture( face[i] + Vector3.UnitZ, Vector3.UnitZ, Texcoords); vertexes[i+3] = new VertexPositionNormalTexture( face[i + 3] + Vector3.UnitZ, Vector3.UnitZ, Texcoords); } The back face is constructed in the same way but now the translation is in the -Vector3.UnitZ direction and the normal is also -Vector3.UnitZ: //Back face for (int i = 0; i <= 2; i++) { vertexes[i + 6] = new VertexPositionNormalTexture( face[2 - i] - Vector3.UnitZ,

-Vector3.UnitZ, Texcoords); vertexes[i + 6 + 3] = new VertexPositionNormalTexture( face[5 - i] - Vector3.UnitZ, -Vector3.UnitZ, Texcoords); } These definitions might look complicated but all that is happening is that the front face is created by moving the face one unit forward in the z direction and the back face one unit back in the z direction. The front face has its vertexes listed in an anticlockwise order but the back face needs them in reverse order because it presents its clockwise back face to us in its present position. The left, right and top and bottom faces are created in the same way but after rotating the initial face through 90 degrees about the Y and X axis respectively. Again we need to pay attention to the order of the vertexes to make sure that they are anticlockwise on side of the face the faces out of the cube. For example for the left face we need to rotate each point 90 degrees around the Y axis and then move the face in the X negative direction: //left face Matrix RotY90 = Matrix.CreateRotationY( -(float)Math.PI / 2f); for (int i = 0; i <= 2; i++) { vertexes[i + 12] = new VertexPositionNormalTexture( Vector3.Transform(face[i], RotY90) - Vector3.UnitX, -Vector3.UnitX, Texcoords); vertexes[i + 12 + 3] = new VertexPositionNormalTexture( Vector3.Transform(face[i + 3], RotY90) - Vector3.UnitX, -Vector3.UnitX, Texcoords); } The rest of the faces are computed in the same way - a rotation and a translation: //Right face for (int i = 0; i <= 2; i++) { vertexes[i + 18] = new VertexPositionNormalTexture( Vector3.Transform(face[2 - i], RotY90) - Vector3.UnitX, Vector3.UnitX, Texcoords); vertexes[i + 18 + 3] = new VertexPositionNormalTexture( Vector3.Transform(face[5 - i], RotY90) - Vector3.UnitX, Vector3.UnitX, Texcoords); } //Top face Matrix RotX90 = Matrix.CreateRotationX( -(float)Math.PI / 2f); for (int i = 0; i <= 2; i++) { vertexes[i + 24] = new VertexPositionNormalTexture( Vector3.Transform(face[i], RotX90) + Vector3.UnitY, Vector3.UnitY, Texcoords); vertexes[i + 24 + 3] = new VertexPositionNormalTexture( Vector3.Transform(face[i + 3], RotX90) + Vector3.UnitY,

Vector3.UnitY, Texcoords); } //Bottom face for (int i = 0; i <= 2; i++) { vertexes[i + 30] = new VertexPositionNormalTexture( Vector3.Transform(face[2 - i], RotX90) - Vector3.UnitY, -Vector3.UnitY, Texcoords); vertexes[i + 30 + 3] = new VertexPositionNormalTexture( Vector3.Transform(face[5 - i], RotX90) - Vector3.UnitY, -Vector3.UnitY, Texcoords); } return vertexes; } There a few subtle points to notice. The first is that XNA provides a lot of useful classes and methods to make working with vectors and matrices easy. It is very important that you understand how vectors and matrices work if you want to do 3D programming. The surface normals have been defined to point out of the cube and, as we will see later effect the way light reflects off each surface. In more sophisticated examples the surface normals don't always have to be the true geometric normals to the surface. Think of surface normals as defining the angle that light would have to hit the surface and bounce off at the same angle. By manipulating the surface normals you can make flat surfaces look curved. In case you are wondering for large shapes you wouldnt hand code the vertex array but use a modelling program to create a mesh that you would load into you XNA program. This said - every 3D programmer should know how to create a mesh programmatically and the cube is a good illustration of how this can be done and how geometry can be created.

Basic effects
With the cube built and ready to go what we now need to do is work out how to display it. XNA has no default graphics processing pipeline and you are supposed to create vertex and pixel shaders to provide whatever special effects are essential to your game. This is a reasonable approach in that most graphics cards have supported programmable shaders for a long time and its the reason graphics have improved so much. However, having to master High Level Shader Language (HLSL) makes it difficult to get started. Fortunately a pair of predefined vertix and pixel shaders are provided and they are combined in a single BasicEffect class. This implements a number of colouring, texturing and lighting options and is a good way to begin learning about shaders. If you have used 3D graphics systems before you will recognise much of what has to be initialised in BasicEffect as being the standard way we have to set up a view. The big difference with XNA is that the pipeline that creates the view and renders the vertexes is fully programmable and the BasicEffect is just one way to do the job. Moving to the Initialize routine we need to create a BasicEffect instance: private BasicEffect effect; protected override void Initialize() { effect = new BasicEffect( graphics.GraphicsDevice); There are lots of lighting effects we could set, but for simplicity lets keep to a white ambient light colour and a single directional white light coming from the bottom right and out of the screen: effect.AmbientLightColor = Vector3.One; effect.DirectionalLight0.Enabled = true; effect.DirectionalLight0.DiffuseColor = Vector3.One; effect.DirectionalLight0.Direction = Vector3.Normalize(Vector3.One); All that remains is to switch the lights on: effect.LightingEnabled = true;

Lights, model, camera, action!


An important part of understanding any 3D system is how the objects can be placed in the 3D world being constructed. When you create a model such as the cube you can nearly always create it so that is centered on the origin. You can think of each model as having its own model space that only it occupies. Before it is displayed the model is rotated, scaled and moved by the World transformation which is specified by the effects World matrix. The perspective transformation determines how the 3D world is converted into a 2D rendered bitmap. It defines the viewing cone and essentially determines how much perspective is used in rendering the 3D scene. You can think of the view cone as being something like the lens on a camera. A large view angle corresponds to a wide angle lens and a small view angle to a telephoto lens. By default the viewpoint of the rendering is at the origin of the world coordinate system. You can think of this as being equivalent to the camera being at the origin and pointing along the z axis. A third and final transformation supported by the effect is implemented by the view matrix. You can think of this as moving and possibly rotating the camera so that it can see different parts of the 3D world.

The perspective transformation is defined by the view cone

Pointing the camera


The next job is to set up the projection and view transformations. You can think of this as selecting a lens for the camera and pointing it at what we want to look at: Matrix projection = Matrix. CreatePerspectiveFieldOfView( (float)Math.PI / 4.0f, (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height, 1f, 10f); effect.Projection = projection; Matrix V = Matrix.CreateTranslation(0f,0f,-10f); effect.View = V; The projection matrix is set to a 45 degree view which is a moderately wide angle lens and it will render anything within 1 to 10 units of the camera anything close or further away is ignored. The view matrix moves the cameras location to (0,0,10) and doesnt change its orientation so it is still looking straight down the z axis toward the origin. Beginners are often confused that a translation of minus 10 moves the camera to 10 the reason is that the view transformation actually moves the 3D world and the camera stays put! Now that we have an effect object created and initialize we can move to the Draw method. The next few steps in rendering the scene are fairly standard. First we clear the display window to a nice colour and use the BeginScene method to start drawing:

protected override void Draw( GameTime gameTime) { GraphicsDevice.Clear( Color.CornflowerBlue); Finally we have to set the cull mode so that faces that have their vertexes listed in clockwise order arent drawn i.e they are culled, because they are faces pointing into the cube and never visible: RasterizerState rs = new RasterizerState(); graphics.GraphicsDevice.RasterizerState = rs;

As we are using an effect we have to start processing vertexes using it. The only complication is that an effect can modify different stages in the rendering pipeline and all have to be processed. In the case of BasicEffect it only apply to one pass and we could perform this without a for loop, however the following code is completely generally and works with any effect:

foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); graphics.GraphicsDevice.DrawUserPrimitives <VertexPositionNormalTexture>( PrimitiveType.TriangleList, cube, 0, 12); } Now you DrawUserPrimitives "knows" how to draw vertexes as specified by the generic type parameter i.e. it will draw an array of VertexPositionNormalTexture vertexes. Notice that it is the number of triangles that you specify to draw i.e. 12, rather than the number of vertexes. Finally we have to create an instance of the cube to draw and this is best done in the LoadContent method, even though the content isn't going to be loaded but generated: private VertexPositionNormalTexture[] cube; protected override void LoadContent() { // Create a new SpriteBatch, // which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); cube = MakeCube(); } If you now run the program you will see the overall result - a white square in the middle of the view area. This is all that you can expect as the cube is face on to the viewer, has no texture properties and so defaults to white under the white ambient lighting.

The first view of the cube

Update
To see something a little more interesting we can make the cube rotate by adding some code to the Update method to change the World matrix. We can set the World matrix so that the cube is rotated through a small angle about the Z axis and moved back into the screen 5 units so that it is more within the camera's view: private float angle=0; protected override void Update( GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One). Buttons.Back== ButtonState.Pressed) this.Exit(); angle = angle + 0.005f; if (angle > 2 * Math.PI) angle = 0; Matrix R = Matrix.CreateRotationY(angle) * Matrix.CreateRotationX(.4f); Matrix T =

Matrix.CreateTranslation(0.0f, 0f, 5f); effect.World = R * T; base.Update(gameTime); } It is important which order these transformations are carried out in because rotations are always about the origin. So if you rotate and then move what you see is a cube rotated about its centre. If you move and rotate then what you see is a cube orbiting the origin. Try it out to see how it works. What you should see is a rotating white cube.

A rotating white cube To make the cube look more interesting we should now start to assign textures to the faces so that the cube has some material properties but this would take us into the next stage in developing a 3D world. An alternative is to modify the lighting so that the white cube looks different as it rotates. Change the AmbientColor property in the Initialize method to read: effect.AmbientColor = new Vector3(0.0f, 1.0f, 0.0f); The colour is specified as Red, Green, Blue components and so in this case the surfaces will be coloured green. If you now run the program you will see a rotating green cube with a directional white light coming from the left. As the cube rotates you will see the side on the left reflect the white directional light.

Thats all there is too it and now you can go on to animate a whole range of objects. You can also move the camera as well as the objects to implement fly bys. The BasicEffect object is limited but you can learn a lot using it before you eventually need to move on to your own custom shaders but thats another story. Explore the basic program to learn how things work. Try modifying the lighting. Move so that the direction lighting is from the top. Change the color. Add a specular color. Try moving the world view to see the cube move. Change the rotation to be about a diagonal axis. Another basic skill is working with more than one object so add multiple cubes at different locations. All of these are fairly easy but they do require a good grasp of the geometry, the co-ordinate system and vectors. To access the code for this project, once you haveregistered, click on CodeBin.

You might also like