We want to add a skybox to our code. We are going to
create a project that will contain the content, content
processor, and content compiler. After creating this project
we will create another file inside of our XELibrary to read
the skybox data. Finally, we will create a demo that will
utilize the XELibrary’s Skybox Content Reader, which the
Content Manager uses to consume the skybox.
Before we actually create the project we should first
examine a skybox and its purpose in games. A skybox
keeps us from having to create complex geometry for
objects that are very far away. For example, we do not need
to create a sun or a moon or some distant city when we use
a skybox. We can create six textures that we can put into
our cube. Although there are skybox models we could use,
154 CHAPTER 8 Extending the Content Pipeline
for this chapter we are going to build our own skybox. It is simply a cube and we already
have the code in place to create a cube. We know how to create rectangles and we know
how to position them where we want them. We can create six rectangles that we can use
as our skybox. When each texture is applied to each side of the skybox we get an effect
that our world is much bigger than it is. Plus, it looks much better than the cornflower
blue backdrop we currently have!
Creating the Skybox Content Object
To start, let’s create a new Windows library project called SkyboxPipeline. There is no
need to create an Xbox 360 version of the project because this will only be run on the PC.
This SkyboxPipeline project will have three files. The first file is the SkyboxContent.cs
code file, shown in Listing 8.1.
LISTING 8.1 SkyboxContent.cs holds the design time class of our skybox
using System;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
namespace SkyboxPipeline
{
public class SkyboxContent
{
public ModelContent Model;
public Texture2DContent Texture;
}
}
The SkyboxContent object holds our skybox data at design time. We need to add a reference
to Microsoft.Xna.Framework.Content.Pipeline to our project to utilize the namespaces
needed.
Creating the Skybox Processor
The SkyboxContent object is utilized by the processor, shown in Listing 8.2.
LISTING 8.2 SkyboxProcessor.cs actually processes the data it gets as input from the
Content Pipeline
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
namespace SkyboxPipeline
{
[ContentProcessor]
class SkyboxProcessor : ContentProcessor
{
private int width = 1024;
private int height = 512;
private int cellSize = 256;
public override SkyboxContent Process(Texture2DContent input,
ContentProcessorContext context)
{
MeshBuilder builder = MeshBuilder.StartMesh(“XESkybox”);
CreatePositions(ref builder);
AddVerticesInformation(ref builder);
// Create the output object.
SkyboxContent skybox = new SkyboxContent();
// Finish making the mesh
MeshContent skyboxMesh = builder.FinishMesh();
//Compile the mesh we just built through the default ModelProcessor
skybox.Model = context.Convert
skyboxMesh, “ModelProcessor”);
skybox.Texture = input;
return skybox;
}
private void CreatePositions(ref MeshBuilder builder)
{
Vector3 position;
//————-front plane
//top left
position = new Vector3(-1, 1, 1);
builder.CreatePosition(position); //0
//bottom right
155
8
LISTING 8.2 Continued
Creating a Skybox
position = new Vector3(1, -1, 1);
builder.CreatePosition(position); //1
//bottom left
position = new Vector3(-1, -1, 1);
builder.CreatePosition(position); //2
//top right
position = new Vector3(1, 1, 1);
builder.CreatePosition(position); //3
//————-back plane
//top left
position = new Vector3(-1, 1, -1); //4
builder.CreatePosition(position);
//bottom right
position = new Vector3(1, -1, -1); //5
builder.CreatePosition(position);
//bottom left
position = new Vector3(-1, -1, -1); //6
builder.CreatePosition(position);
//top right
position = new Vector3(1, 1, -1); //7
builder.CreatePosition(position);
}
private Vector2 UV(int u, int v, Vector2 cellIndex)
{
return(new Vector2((cellSize * (cellIndex.X + u) / width),
(cellSize * (cellIndex.Y + v) / height)));
}
private void AddVerticesInformation(ref MeshBuilder builder)
{
//texture locations:
//F,R,B,L
//U,D
//Front
Vector2 fi = new Vector2(0, 0); //cell 0, row 0
156 CHAPTER 8 Extending the Content Pipeline
LISTING 8.2 Continued
//Right
Vector2 ri = new Vector2(1, 0); //cell 1, row 0
//Back
Vector2 bi = new Vector2(2, 0); //cell 2, row 0
//Left
Vector2 li = new Vector2(3, 0); //cell 3, row 0
//Upward (Top)
Vector2 ui = new Vector2(0, 1); //cell 0, row 1
//Downward (Bottom)
Vector2 di = new Vector2(1, 1); //cell 1, row 1
int texCoordChannel = builder.CreateVertexChannel
(VertexChannelNames.TextureCoordinate(0));
//————front plane first column, first row
//bottom triangle of front plane
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, fi));
builder.AddTriangleVertex(4); //-1,1,1
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, fi));
builder.AddTriangleVertex(5); //1,-1,1
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, fi));
builder.AddTriangleVertex(6); //-1,-1,1
//top triangle of front plane
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, fi));
builder.AddTriangleVertex(4); //-1,1,1
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, fi));
builder.AddTriangleVertex(7); //1,1,1
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, fi));
builder.AddTriangleVertex(5); //1,-1,1
//————-right plane
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, ri));
builder.AddTriangleVertex(3);
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, ri));
builder.AddTriangleVertex(1);
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, ri));
builder.AddTriangleVertex(5);
Creating a Skybox 157
8
LISTING 8.2 Continued
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, ri));
builder.AddTriangleVertex(3);
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, ri));
builder.AddTriangleVertex(5);
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, ri));
builder.AddTriangleVertex(7);
//————-back pane //3rd column, first row
//bottom triangle of back plane
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, bi)); //1,1
builder.AddTriangleVertex(2); //-1,-1,1
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, bi)); //0,1
builder.AddTriangleVertex(1); //1,-1,1
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, bi)); //1,0
builder.AddTriangleVertex(0); //-1,1,1
//top triangle of back plane
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, bi)); //0,1
builder.AddTriangleVertex(1); //1,-1,1
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, bi)); //0,0
builder.AddTriangleVertex(3); //1,1,1
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, bi)); //1,0
builder.AddTriangleVertex(0); //-1,1,1
//————-left plane
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, li));
builder.AddTriangleVertex(6);
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, li));
builder.AddTriangleVertex(2);
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, li));
builder.AddTriangleVertex(0);
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, li));
builder.AddTriangleVertex(4);
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, li));
builder.AddTriangleVertex(6);
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, li));
builder.AddTriangleVertex(0);
//————upward (top) plane
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, ui));
builder.AddTriangleVertex(3);
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, ui));
builder.AddTriangleVertex(4);
158 CHAPTER 8 Extending the Content Pipeline
LISTING 8.2 Continued
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, ui));
builder.AddTriangleVertex(0);
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, ui));
builder.AddTriangleVertex(3);
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, ui));
builder.AddTriangleVertex(7);
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, ui));
builder.AddTriangleVertex(4);
//————downward (bottom) plane
builder.SetVertexChannelData(texCoordChannel, UV(1, 0, di));
builder.AddTriangleVertex(2);
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, di));
builder.AddTriangleVertex(6);
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, di));
builder.AddTriangleVertex(1);
builder.SetVertexChannelData(texCoordChannel, UV(1, 1, di));
builder.AddTriangleVertex(6);
builder.SetVertexChannelData(texCoordChannel, UV(0, 1, di));
builder.AddTriangleVertex(5);
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, di));
builder.AddTriangleVertex(1);
}
}
}
The SkyboxProcessor contains a lot of code, but the vast majority of it is actually building
and texturing our skybox. We can go ahead and create this file in our pipeline project
now.
To begin we used the [ContentProcessor] attribute for our class so the Content Pipeline
could determine which class to call when it needed to process a resource with our type.
We inherit from the ContentProcessor class stating that we are going to be taking a
Texture2D as input and outputting our skybox content type. In the Process method we
take in two parameters: input and context. To create our skybox we are going to pass in a
single texture in our game projects. The processor creates a new MeshBuilder object,
which is a helper class that allows us to quickly create a mesh with vertices in any order
we wish and then apply different vertex information for those vertices that can contain
things like texture coordinates, normals, colors, and so on. For our purposes we will be
storing the texture. We create our actual eight vertices of our skybox cube in the
CreatePositions method. We are simply passing a vertex position into the
CreatePosition method of the MeshBuilder method.
Creating a Skybox 159
8
LISTING 8.2 Continued
Next up is our call to AddVerticesInformation. This method contains the bulk of the
code but it is not doing anything fancy. It is simply creating triangles in the mesh by
passing the vertices index values to the AddTriangleVertex method of the MeshBuilder
object. These vertices need to be called in the right order. We can think of this as
building the indices of the mesh. The idea is that we created our unique vertices (in
CreatePositions) and although we could have stored the value CreatePosition returned
to us, we know that it will return us the next number starting with 0. Instead of using up
memory for the index being passed back, we just made a note in the comment next to
that vertex so we could build our triangles.
Before we actually add a vertex to a triangle of our mesh, we pass in our vertex channel
information. We created a vertex channel before we started creating triangles with the
following code:
int texCoordChannel = builder.CreateVertexChannel
(VertexChannelNames.TextureCoordinate(0));
We can have multiple vertex channels. Although we are only going to store texture coordinates,
we could also store normals, binormals, tangents, weights, and colors. Because we
could store all of these different pieces of information we need to tell the vertex channel
which type of data we are storing. We then store an index to that particular channel.
Once we have that channel index we can call the SetVertexChannelData method for each
triangle vertex we add. In fact, set the channel data for the builder before adding the
vertex. If we had more than one vertex channel to apply to a vertex, we would call all of
them in succession before finally calling the AddTriangleVertex method. The following
code shows the order in which this needs to take place:
builder.SetVertexChannelData(texCoordChannel, UV(0, 0, fi));
builder.AddTriangleVertex(4);
SetVertexChannelData takes in the vertex channel ID followed by the appropriate data
for that channel. When we set up the vertex channel to handle texture coordinates we
did so by passing the generic Vector2 because texture coordinates have an x and a y
component. This means that the SetVertexChannelData for our texture coordinate
channel is expecting a type of Vector2.
For this texture mapping code to make sense, we need to discuss how the texture asset we
are going to pass into our demo or game needs to be laid out. Instead of requiring six
different textures to create a skybox, we are requiring only one with each plane of the
cube to have a specific location inside of the texture. The texture size is 1024 x 512 to
keep with the power-of-two restriction most graphic cards make us live by. We put four
textures on the top row and two textures on the bottom row. The top row will have the
cube faces Front, Right, Back, and Left in that order. The bottom row will have Up (Top)
and Down (Bottom). If we have skyboxes in other formats we can use a paint program to
get them in this format. We can also use tools to generate skybox images and output
them into this format or one we can easily work with. The great thing about this being an
extension of the Content Pipeline is that we have free reign over how we want to read in
160 CHAPTER 8 Extending the Content Pipeline
data and create content that our games can easily use. If we stick with the current single
texture it leaves part of the texture unused. We could utilize these two spots for something
else. For example, we could create one or two cloud layers to our skybox, so instead
of just rendering a cube, it would render a cube with two additional layers that could
prove to be a nice effect. We could use it for terrain generation by reading in the values
from a gray-scaled image in one of those spots to create a nice ground layout. We do not
discuss terrain generation in this book, but there are many excellent articles on the Web
about generating terrains.
Now that we know how the texture is laid out we can discuss some of the details of the
code that is applying the texture to the different panels of the cube. In the following code
we declared a variable to hold our index of the right panel in the texture:
//Right
Vector2 ri = new Vector2(1, 0); //cell 1, row 0
We are storing 1,0 in a vector signifying that the right panel’s portion of the large texture
is in the first cell in row zero (this is zero based). In Chapter 4, “Creating 3D Objects,”
we discussed how to texture our rectangle (quad) by applying different u and v coordinates
to the different vertices of our rectangle. We are using the exact same concept here.
The only difference is that we have to take into account the fact that we are extracting
multiple textures from our one texture. For example, to texture the right-side panel of
our skybox using just one texture we could simply tell the top left vertex to use texture
coordinates 0,0 and the bottom right vertex to use texture coordinate 1,1. However, our
right-side panel’s texture is not the entire texture we have in memory; instead it is from
pixels 256,0 to 512,256. We can see this in Figure 8.1 where the right panel texture is not
grayed out.
Creating a Skybox 161
8
To handle the offset issue we created a UV method that takes in the typical 0 and 1 along
with index cell from which we need to get our updated u and v coordinates. The UV
method that calculates our u and v values is as follows:
private Vector2 UV(int u, int v, Vector2 cellIndex)
{
return(new Vector2((cellSize * (cellIndex.X + u) / width),
(cellSize * (cellIndex.Y + v) / height)));
}
This method simply takes in the u and v coordinates we would normally map on a full
texture along with the cell index we want to access in the texture and it returns the calculated
u and v coordinates. The cellSize, width, and height are private member fields. We
take the size of the cell, 256, and multiply that by the sum of our x value of our cell
index and the u value passed in. We take that value and divide it by width to come up
with the correct u position of the large texture. We do the same thing to get our v value.
We pass those actual values to SetVertexChannelData so it will associate the right texture
coordinates with that vertex.
After actually creating the Skybox vertices and setting up all of the triangles needed and
applying our texture coordinates, we can finally save the mesh. We do this by calling the
FinishMesh method on our MeshBuilder object, which returns a MeshContent type back to
us. This is convenient as that is the type of object we need to pass to the default
ModelProcessor to process our mesh (just as if we loaded a .X file through the Content
Pipeline). This is done with the following code:
MeshContent skyboxMesh = builder.FinishMesh();
skybox.Model = context.Convert
skyboxMesh, “ModelProcessor”);
After setting our texture to the texture (our input) that was actually loaded to start this
process, we return the skybox content and the compiler gets launched. We discuss the
compiler in the next section.
Creating the Skybox Compiler
This brings us to our third and final file for our pipeline project. We need to create
another code file with the name SkyboxCompiler.cs. The code for this file is found in
Listing 8.3.
LISTING 8.3 SkyboxCompiler.cs compiles and writes out the content it is passed from the
processor
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
162 CHAPTER 8 Extending the Content Pipeline
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
namespace SkyboxPipeline
{
[ContentTypeWriter]
public class SkyboxWriter : ContentTypeWriter
{
protected override void Write(ContentWriter output, SkyboxContent value)
{
output.WriteObject(value.Model);
output.WriteObject(value.Texture);
}
public override string GetRuntimeType(TargetPlatform targetPlatform)
{
return “XELibrary.Skybox, “ +
“XELibrary, Version=1.0.0.0, Culture=neutral”;
}
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
return “XELibrary.SkyboxReader, “ +
“XELibrary, Version=1.0.0.0, Culture=neutral”;
}
}
}
We start off this class much like the last in that we associate an attribute with it. This
time we need to use the [ContentTypeWriter] attribute as it tells the Content Pipeline
this is the compiler or writer class. We inherit from the ContentTypeWriter with the
generic type of SkyboxContent (which we created in the first file of this project). This way
when the Content Pipeline gets the skybox content back from the processor it knows
where to send the data to be compiled.
We override the Write method and save our skybox as an .xnb file. The base class does all
of the heavy lifting and all we need to do is write our object out. The next method,
GetRuntimeType, tells the Content Pipeline the actual type of the skybox data that will be
loaded at runtime. The last method, GetRuntimeReader, tells the Content Pipeline which
object will actually be reading in and processing the .xnb data. The contents of these two
methods are returning different classes inside of the same assembly. They do not need to
reside in the same assembly, but it definitely made sense in this case. We store the
runtime type and runtime reader in a separate project. We do not add them to the
pipeline project because the pipeline project is Windows dependent and our actual
skybox type and reader object needs to be platform independent. We are going to set
Creating a Skybox 163
8
Creating the Skybox Reader
Let’s copy and open our Load3DObject project from Chapter 6, “Loading and Texturing
3D Objects.” Our XELibrary should already be inside of this project and we can add a
SkyboxReader.cs file to our XELibrary projects. This file will contain both our Skybox type
and our SkyboxReader type. We could have created separate files if we desired. If we had
them in different assemblies, however, we would need to update our GetRunttimeType
and GetRuntimeReader methods in our content writer. The code contained in
SkyboxReader.cs can be found in Listing 8.4.
LISTING 8.4 SkyboxReader.cs inside of our XELibrary allows for our games to read the
compiled .xnb files generated by the Content Pipeline
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
namespace XELibrary
{
public class SkyboxReader : ContentTypeReader
{
protected override Skybox Read(ContentReader input, Skybox existingInstance)
{
return new Skybox(input);
}
}
public class Skybox
{
private Model skyboxModel;
private Texture2D skyboxTexture;
internal Skybox(ContentReader input)
{
skyboxModel = input.ReadObject
skyboxTexture = input.ReadObject
}
public void Draw(Matrix view, Matrix projection, Matrix world)
{
foreach (ModelMesh mesh in skyboxModel.Meshes)
{
foreach (BasicEffect be in mesh.Effects)
{
be.Projection = projection;
be.View = view;
be.World = world;
be.Texture = skyboxTexture;
be.TextureEnabled = true;
}
mesh.Draw(SaveStateMode.SaveState);
}
}
}
}
Our SkyboxReader class is pretty small. It derives from the ContentTypeReader and uses a
Skybox type that we will see in a moment. We override the Read method of this class,
which gets passed in the skybox data as input as well as an existing instance of the object
that we could write into if needed. We take the input and actually create an instance to
our Skybox object by calling the internal constructor.
Inside of the Skybox class we take the input that was just passed to us and store the model
embedded inside. We expose a Draw method that takes in view, projection, and world
matrices as parameters. We then treat the model as if we loaded it from the pipeline
(because we did) and set the basic effect on each mesh inside of the model to use the
projection, view, and world matrices passed to the object. Finally, we actually draw the
object onto the screen.
Jumat, 04 April 2008
Creating a Skybox
Creating a Sound Demo
Now we need to add in another Windows game project to our XELibrary solution. We can
call this new project SoundDemo. We can also set up our solution for our Xbox 360
project if we want to test it on the console. Now we need to make sure our game is referencing
the XELibrary project.
Once we have our XELibrary referenced correctly, we can start writing code to test out our
new sound class (and updated input class). We need to use the library’s namespace at the
top of our game class as follows:
using XELibrary;
We should also add a folder called Content with a Sounds subfolder to our solution. We
can then paste our XACT project file into our Sounds folder. The wave files should be put
in the folder, but do not need to be included in the project. When we compile our code
later, the Content Pipeline will find all of the waves from the wave bank and wrap them
into a wave bank .xwb file. It also creates a sound bank .xsb file while the audio engine is
stored in Chapter7.xgs (as that is what we had as our XACT project name).
We will now add in our InputHandler game component so we can kick off sound events
based on our input. We need to declare our private member field to hold the component
as well as adding it to our game’s components collection:
Creating a Sound Demo 147
7
private InputHandler input;
private SoundManager sound;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
content = new ContentManager(Services);
input = new InputHandler(this);
Components.Add(input);
sound = new SoundManager(this, “Chapter7”);
Components.Add(sound);
}
We passed in “Chapter7” to our constructor because that is what we called our XACT
project. The next thing we need to do is set up our playlist. We can do this inside of our
Initialize method because we added the sound component in our constructor:
string[] playList = { “Song1”, “Song2”, “Song3” };
sound.StartPlayList(playList);
The code tells our sound manager we will be playing three different songs. The library
will keep checking to see if they are playing. If not, it will automatically play the next
one, looping back to the beginning song when it reaches the end of the list.
Now we can actually populate our Update method to check for our input to play all of the
sounds and songs we set up in XACT. We need to add the following code to our Update
method:
if (input.KeyboardState.WasKeyPressed(Keys.D1) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.A))
sound.Play(“gunshot”);
if (input.KeyboardState.WasKeyPressed(Keys.D2) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.B))
sound.Play(“hit”);
if (input.KeyboardState.WasKeyPressed(Keys.D3) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.LeftShoulder))
sound.Play(“attention”);
if (input.KeyboardState.WasKeyPressed(Keys.D4) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.LeftStick))
sound.Play(“explosion”);
if (input.KeyboardState.WasKeyPressed(Keys.D5) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.RightShoulder))
148 CHAPTER 7 Sounds and Music
sound.Play(“bullet”);
if (input.KeyboardState.WasKeyPressed(Keys.D6) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.RightStick))
sound.Play(“crash”);
if (input.KeyboardState.WasKeyPressed(Keys.D7) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.X))
sound.Play(“complex”);
if (input.KeyboardState.WasKeyPressed(Keys.D8) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.Y))
sound.Toggle(“CoolLoop”);
if (input.KeyboardState.WasKeyPressed(Keys.D9) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.LeftShoulder))
sound.Toggle(“CoolLoop 2”);
if (input.KeyboardState.WasKeyPressed(Keys.P) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.Start))
{
sound.Toggle(“CoolLoop”);
}
if (input.KeyboardState.WasKeyPressed(Keys.S) ||
(input.GamePads[0].Triggers.Right > 0))
sound.StopPlayList();
We are simply checking to see if different keys were pressed or different buttons were
pushed. Based on those results we play different cues that we set up in the XACT project.
A good exercise for us would be to run the demo and reread the section of this chapter
where we set up all of these sounds and see if they do what we expect when we press the
appropriate keys or buttons. In particular we can press the B button or number 2 key
repeatedly and see that the “hit” cue is queuing up as we told it to limit itself to only
playing once and to queue failed requests. We can also click down on our right thumb
stick or press the number 6 key to hear our crash. If the playlist is hindering our hearing
of the sounds we can stop it by pressing the S key or pushing on our right trigger.
The final piece of code for our sound demo is where we can set a global variable for the
RPC we set up as well as the volume of our default category cues. To start, we need to add
two more private member fields:
private float currentVolume = 0.5f;
private float value = 0;
Now we can finish up our Update method with the following code:
if (input.KeyboardState.IsHoldingKey(Keys.Up) ||
input.GamePads[0].DPad.Up == ButtonState.Pressed)
Creating a Sound Demo 149
7
Now we need to add in another Windows game project to our XELibrary solution. We can
call this new project SoundDemo. We can also set up our solution for our Xbox 360
project if we want to test it on the console. Now we need to make sure our game is referencing
the XELibrary project.
Once we have our XELibrary referenced correctly, we can start writing code to test out our
new sound class (and updated input class). We need to use the library’s namespace at the
top of our game class as follows:
using XELibrary;
We should also add a folder called Content with a Sounds subfolder to our solution. We
can then paste our XACT project file into our Sounds folder. The wave files should be put
in the folder, but do not need to be included in the project. When we compile our code
later, the Content Pipeline will find all of the waves from the wave bank and wrap them
into a wave bank .xwb file. It also creates a sound bank .xsb file while the audio engine is
stored in Chapter7.xgs (as that is what we had as our XACT project name).
We will now add in our InputHandler game component so we can kick off sound events
based on our input. We need to declare our private member field to hold the component
as well as adding it to our game’s components collection:
Creating a Sound Demo 147
7
private InputHandler input;
private SoundManager sound;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
content = new ContentManager(Services);
input = new InputHandler(this);
Components.Add(input);
sound = new SoundManager(this, “Chapter7”);
Components.Add(sound);
}
We passed in “Chapter7” to our constructor because that is what we called our XACT
project. The next thing we need to do is set up our playlist. We can do this inside of our
Initialize method because we added the sound component in our constructor:
string[] playList = { “Song1”, “Song2”, “Song3” };
sound.StartPlayList(playList);
The code tells our sound manager we will be playing three different songs. The library
will keep checking to see if they are playing. If not, it will automatically play the next
one, looping back to the beginning song when it reaches the end of the list.
Now we can actually populate our Update method to check for our input to play all of the
sounds and songs we set up in XACT. We need to add the following code to our Update
method:
if (input.KeyboardState.WasKeyPressed(Keys.D1) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.A))
sound.Play(“gunshot”);
if (input.KeyboardState.WasKeyPressed(Keys.D2) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.B))
sound.Play(“hit”);
if (input.KeyboardState.WasKeyPressed(Keys.D3) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.LeftShoulder))
sound.Play(“attention”);
if (input.KeyboardState.WasKeyPressed(Keys.D4) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.LeftStick))
sound.Play(“explosion”);
if (input.KeyboardState.WasKeyPressed(Keys.D5) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.RightShoulder))
148 CHAPTER 7 Sounds and Music
sound.Play(“bullet”);
if (input.KeyboardState.WasKeyPressed(Keys.D6) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.RightStick))
sound.Play(“crash”);
if (input.KeyboardState.WasKeyPressed(Keys.D7) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.X))
sound.Play(“complex”);
if (input.KeyboardState.WasKeyPressed(Keys.D8) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.Y))
sound.Toggle(“CoolLoop”);
if (input.KeyboardState.WasKeyPressed(Keys.D9) ||
input.ButtonHandler.WasButtonPressed(0,
InputHandler.ButtonType.LeftShoulder))
sound.Toggle(“CoolLoop 2”);
if (input.KeyboardState.WasKeyPressed(Keys.P) ||
input.ButtonHandler.WasButtonPressed(0, InputHandler.ButtonType.Start))
{
sound.Toggle(“CoolLoop”);
}
if (input.KeyboardState.WasKeyPressed(Keys.S) ||
(input.GamePads[0].Triggers.Right > 0))
sound.StopPlayList();
We are simply checking to see if different keys were pressed or different buttons were
pushed. Based on those results we play different cues that we set up in the XACT project.
A good exercise for us would be to run the demo and reread the section of this chapter
where we set up all of these sounds and see if they do what we expect when we press the
appropriate keys or buttons. In particular we can press the B button or number 2 key
repeatedly and see that the “hit” cue is queuing up as we told it to limit itself to only
playing once and to queue failed requests. We can also click down on our right thumb
stick or press the number 6 key to hear our crash. If the playlist is hindering our hearing
of the sounds we can stop it by pressing the S key or pushing on our right trigger.
The final piece of code for our sound demo is where we can set a global variable for the
RPC we set up as well as the volume of our default category cues. To start, we need to add
two more private member fields:
private float currentVolume = 0.5f;
private float value = 0;
Now we can finish up our Update method with the following code:
if (input.KeyboardState.IsHoldingKey(Keys.Up) ||
input.GamePads[0].DPad.Up == ButtonState.Pressed)
Creating a Sound Demo 149
7
currentVolume += 0.05f;
if (input.KeyboardState.IsHoldingKey(Keys.Down) ||
input.GamePads[0].DPad.Down == ButtonState.Pressed)
currentVolume -= 0.05f;
currentVolume = MathHelper.Clamp(currentVolume, 0.0f, 1.0f);
sound.SetVolume(“Default”, currentVolume);
if (input.KeyboardState.WasKeyPressed(Keys.NumPad1))
value = 5000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad2))
value = 25000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad3))
value = 30000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad4))
value = 40000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad5))
value = 50000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad6))
value = 60000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad7))
value = 70000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad8))
value = 80000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad9))
value = 90000;
if (input.KeyboardState.WasKeyPressed(Keys.NumPad0))
value = 100000;
if (input.GamePads[0].Triggers.Left > 0)
value = input.GamePads[0].Triggers.Left * 100000;
sound.SetGlobalVariable(“SpeedOfSound”, value);
This completes our sound demo code and now if we run it and press the Up and Down
arrow keys or on the Dpad we can hear the volume of the sounds associated with our
“Default” category go up and down. We clamp our value between 0 and 1 because that is
what the engine takes to set the volume. This is an example of how checking for the
input state without considering the previous state can get us into trouble. The code will
turn the volume up and down very quickly because it is getting back a true on every call
every frame. Feel free to add Dpad to the updated InputHandler code.
Not only does this code let us test the volume settings, it also lets us set a global variable.
In the XACT project, if we used SpeedOfSound as one of the parameters when setting up
our curve then we can actually modify the way the cue sounds here at runtime. That is
pretty powerful.
Creating a Split Screen
Now that we know how to set up our camera and accept input, we can look into how our
code will need to change to handle multiple players in a split-screen game. To start, we
need to make a copy of the InputDemo project we just finished. We can rename the
project SplitScreen. After we have our solution and projects renamed (complete with our
assembly GUID and title) we can look at the code we will need to change to accomplish a
split-screen mode of play.
To create a split screen, we need to two different viewports. We have only been using one
up until now and we actually retrieved it in our camera’s Initialization method. We
simply grabbed the GraphicsDevice.Viewport property to get our camera’s viewport.
Because we want to display two screens in one we need to define our two new viewports
and then let the cameras (we will need two cameras) know about them so we can get the
desired effect. To start we will need to add the following private member fields to our
Game1.cs code:
private Viewport defaultViewport;
private Viewport topViewport;
private Viewport bottomViewport;
private separatorViewport;
private bool twoPlayers = true;
private FirstPersonCamera camera2;
Then at the end of our LoadGraphicsContent method we will need to define those viewports
and create our cameras and pass the new values. We do this in the following code:
if (twoPlayers)
{
defaultViewport = graphics.GraphicsDevice.Viewport;
topViewport = defaultViewport;
bottomViewport = defaultViewport;
topViewport.Height = topViewport.Height / 2;
separatorViewport.Y = topViewport.Height – 1;
separatorViewport.Height = 3;
bottomViewport.Y = topViewport.Height + 1;
bottomViewport.Height = (bottomViewport.Height / 2) - 1;
camera.Viewport = topViewport;
106 CHAPTER 5 Input Devices and Cameras
camera2 = new FirstPersonCamera(this);
camera2.Viewport = bottomViewport;
camera2.Position = new Vector3(0.0f, 0.0f, -3.0f);
camera2.Orientation = new Vector3(0.0f, 0.0f, 1.0f);
camera2.PlayerIndex = PlayerIndex.Two;
Components.Add(camera2);
}
We discussed briefly that we would need more than one camera to pull this off. This is
because we have our view and projection matrices associated with our camera class
(which we should). It makes sense that we will have two cameras because the camera is
showing what the player is seeing. Each player needs his or her own view into the game.
Our initial camera is still set up in our game’s constructor but our second camera will get
added here. Our first camera gets the default viewport associated with it. The first thing
the preceding code is doing is checking to see if we are in a two-player game. For a real
game, this should be determined by an options menu or something similar, but for now
we have just initialized the value to true when we initialized the twoPlayer variable.
Inside of the two-player condition the first thing we do is set our default viewport to what
we are currently using (the graphic device’s viewport). Then we set our top viewport to
the same value. We also initialize our bottomViewport to our defaultViewport value. The
final thing we do with our viewports is resize them to account for two players. We divide
the height in two (we are making two horizontal viewports) on both. We then set our
bottom viewport’s Y property to be one more than the height of our bottom. This effectively
puts the bottom viewport right underneath our top viewport.
While still in the two-player condition we change our first camera’s viewport to use the
top viewport. Then we set up our second camera by setting more properties. Not only
do we set the viewport for this camera to the bottom viewport we have, but we also
set a new camera position as well as the orientation of the camera. Finally, we set the
player index.
None of these properties is exposed from our camera object, so we need to open our
Camera.cs file and make some changes to account for this. First, we need to add a new
private member field to hold our player index. We just assumed it was player 1 before. We
can set up our protected (so our FirstPersonCamera class can access it) index as an
integer as follows:
protected int playerIndex = 0;
Now, we can actually modify our input code that controls our camera to use this index
instead of the hard-coded value 0 for our game pads. In the camera’s Update method we
can change any instance of input.GamePads[0] to input.GamePads[playerIndex]. We also
need to do the same for the FirstPersonCamera object. We did not update the keyboard
code and will not for the sake of time. However, to implement multiple users where both
can use the keyboard we should create a mapping for each player and check accordingly.
In general, it is a good practice to have a keyboard mapping so that if gamers do not like
Creating a Split Screen 107
5
the controls we have defined in our games then they have a way to change them so it
works more logically for them. The same can be said about creating a mapping for the
game pads but many games simply give a choice of a couple of layouts. Because the code
does not implement a keyboard mapping, the only way for us to control the separate
screens differently is by having two game pads hooked up to our PC or Xbox 360.
After we have changed our camera to take the player index into consideration before
reading values from our game pad, we can add the following properties to our code:
public PlayerIndex PlayerIndex
{
get { return ((PlayerIndex)playerIndex); }
set { playerIndex = (int)value; }
}
public Vector3 Position
{
get { return (cameraPosition); }
set { cameraPosition = value; }
}
public Vector3 Orientation
{
get { return (cameraReference); }
set { cameraReference = value; }
}
public Vector3 Target
{
get { return (cameraTarget); }
set { cameraTarget = value; }
}
public Viewport Viewport
{
get
{
if (viewport == null)
viewport = graphics.GraphicsDevice.Viewport;
return ((Viewport)viewport);
}
set
{
viewport = value;
InitializeCamera();
}
}
108 CHAPTER 5 Input Devices and Cameras
We are simply exposing the camera’s position, orientation (reference), and target variables.
For the player index property we are casting it to a PlayerIndex enumeration type.
The final property is the Viewport property. We first check to see if our viewport variable
is null and if so we set it to the graphics device’s viewport. When we set our Viewport
property, we also call our InitializeCamera method again so it can recalculate its view
and projection matrices. We need to set up a private member field for our viewport. We
will allow it to have a default null value so we can declare it as follows:
private Viewport? viewport;
Because we are utilizing the Viewport type we will need to add the following using statement
to our code:
using Microsoft.Xna.Framework.Graphics;
The only thing left for us to do now is to actually update our game’s drawing code to
draw our scene twice. Because we are going to have to draw our scene twice (once for
each camera) we will need to refactor our Draw code into a DrawScene method and pass in
a camera reference. Our new code for the new Draw method is as follows:
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Viewport = camera.Viewport;
DrawScene(gameTime, camera);
if (twoPlayers)
{
graphics.GraphicsDevice.Viewport = camera2.Viewport;
DrawScene(gameTime, camera2);
//now clear the thick horizontal line between the two screens
graphics.GraphicsDevice.Viewport = separatorViewport;
graphics.GraphicsDevice.Clear(Color.Black);
}
base.Draw(gameTime);
}
We took all of the code that was inside of this method and put it into a new method
DrawScene(GameTime gameTime, Camera camera). The code we put into the DrawScene
method did not change from how it looked when it resided inside of the Draw method.
The first thing we do with the preceding code is set our graphics device’s viewport to be
what our camera’s viewport is. We then draw the scene passing in our camera. Then we
check to see if we have two players; if so we set the viewport appropriately and finally
draw the scene for that camera. We can run our application and see that it is using a split
screen! The screen shot of this split screen demo can be seen in Figure 5.1.
Creating a Split Screen 109
Creating a First Person Camera
We can build on our stationary camera by adding a first person camera. The main thing
we want to do is to add in a way to move back and forth and to each side. Before we
start, we should create a new camera class and inherit from the one we have. We can call
this new class FirstPersonCamera. The following is the Update method for this new class:
public override void Update(GameTime gameTime)
{
// TODO: Add your update code here
//reset movement vector
movement = Vector3.Zero;
if (input.KeyboardState.IsKeyDown(Keys.A) ||
(input.GamePads[0].ThumbSticks.Left.X < 0))
{
movement.X—;
Creating a First Person Camera 103
3
LISTING 5.1 Continued
}
if (input.KeyboardState.IsKeyDown(Keys.D) ||
(input.GamePads[0].ThumbSticks.Left.X > 0))
{
movement.X++;
}
if (input.KeyboardState.IsKeyDown(Keys.S) ||
(input.GamePads[0].ThumbSticks.Left.Y < 0))
{
movement.Z++;
}
if (input.KeyboardState.IsKeyDown(Keys.W) ||
(input.GamePads[0].ThumbSticks.Left.Y > 0))
{
movement.Z—;
}
//make sure we don’t increase speed if pushing up and over (diagonal)
if (movement.LengthSquared() != 0)
movement.Normalize();
base.Update(gameTime);
}
The conditional logic should look familiar. It is identical to our stationary camera, except
we changed where we were reading the input and what values it updated. We are reading
the A, S, W, D keys and the left thumb stick. We are not looking at the mouse for movement.
The value we are setting is a movement vector. We are only setting the X (left and
right) and Z (back and forth) values. At the end of the conditions we are normalizing our
vector as long as the length squared of the vector is not zero. This makes sure that we are
not allowing faster movement just because the user is moving diagonally. There is no
more code in the FirstPersonCamera object. The rest of the changes were made back on
our original camera object.
We declared the movement as a protected member field of type Vector3 called movement
inside of our original Camera object. We also declared a constant value for our movement
speed. Both of these are listed here:
protected Vector3 movement = Vector3.Zero;
private const float moveRate = 120.0f;
We also set the access modifier of our input field to protected so our FirstPersonCamera
could access it:
protected IInputHandler input;
104 CHAPTER 5 Input Devices and Cameras
Finally we updated the last part of our Update method to take the movement into
account when transforming our camera:
//update movement (none for this base class)
movement *= (moveRate * timeDelta);
Matrix rotationMatrix;
Vector3 transformedReference;
Matrix.CreateRotationY(MathHelper.ToRadians(cameraYaw), out rotationMatrix);
if (movement != Vector3.Zero)
{
Vector3.Transform(ref movement, ref rotationMatrix, out movement);
cameraPosition += movement;
}
//add in pitch to the rotation
rotationMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(cameraPitch)) *
rotationMatrix;
// Create a vector pointing the direction the camera is facing.
Vector3.Transform(ref cameraReference, ref rotationMatrix,
out transformedReference);
// Calculate the position the camera is looking at.
Vector3.Add(ref cameraPosition, ref transformedReference, out cameraTarget);
Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget, ref cameraUpVector,
out view);
Besides just moving our local variables closer together, the only things that changed are
the items in bold type. We take our movement vector and apply our move rate to it
(taking into account our time delta, of course). The second portion is the key. We transformed
our movement vector by our rotation matrix. This keeps us from just looking in a
direction but continuing to move straight ahead. By transforming our movement vector
by our rotation matrix we actually move in the direction we are looking! Well, the movement
actually happens in the next statement when we add this movement vector to our
current camera position. We wrapped all of this in a condition to see if any movement
happened because we do not want to take a performance hit to do the math if we did not
move.
Another thing to note is that because we were creating a first person camera we only
transformed our movement vector by the yaw portion of our rotation matrix. We did
not include the pitch, as that would have allowed us to “fly.” If we did want to create a
flying camera instead, we could simply move this following statement before the code
that is bold:
105
5
//add in pitch to the rotation
rotationMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(cameraPitch)) *
rotationMatrix;
Kamis, 03 April 2008
Creating a Camera
That is enough theory for a bit. We are going to create a camera so we can view our
world. Now we can create a new Windows game project to get started with this section.
We can call this project XNADemo. To begin, we will need to create the following private
member fields:
private Matrix projection;
private Matrix view;
private Matrix world;
We will then need to add a call to InitializeCamera in the beginning of our
LoadGraphicsContent method. The InitializeCamera method will have no return
value. We will begin to populate the method, which can be marked as private, in the next
three points.
Projection
The Matrix struct has a lot of helper functions built in that we can utilize. The
Matrix.CreatePerspectiveFieldOfView is the method we want to look at now.
float aspectRatio = (float)graphics.GraphicsDevice.Viewport.Width /
(float)graphics.GraphicsDevice.Viewport.Height;
Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio,
0.0001f, 1000.0f, out projection);
First we set up a local variable aspectRatio. This is to store, you guessed it, the aspect
ratio of our screen. For the Xbox 360 the aspect ratio of the back buffer will determine
how the game is displayed on the gamer’s TV. If we develop with a widescreen aspect ratio
and the user has a standard TV, the game will have a letterbox look to it. Conversely, if we
develop with a standard aspect ratio and the user has a wide screen, the Xbox 360 will
stretch the display. To avoid this we should account for both situations and then adjust
the value of our aspect ratio variable to the default values of the viewport of the graphics
device like in the preceding code. If we needed to query the default value to which the
gamer had his or her Xbox 360 set, we can gather that information by querying the
DisplayMode property of the graphics device during or after the Initialization method
is called by the framework.
Creating a Camera 61
4
However, if we want to force a widescreen aspect ratio on the Xbox 360 we could set the
PreferredBackBufferWidth and PreferredBackBufferHeight properties on the graphics
object right after creating it. Many gamers do not care for the black bars, so we should use
this with caution. To force a widescreen aspect ratio on Windows is a little more complicated,
but the XNA Game Studio Express documentation has a great “How to” page
explaining how to do it. Once in the documentation, you can find “How to: Restrict
Graphics Devices to Widescreen Aspect Ratios in Full Screen” under the Application
Model in the Programming Guide.
Second, we create our field of view. The first parameter we pass in is 45 degrees. We could
have used MathHelper.ToRadians(45.0f) but there is no need to do the math because the
MathHelper class already has the value as a constant. The second parameter is the aspect
ratio, which we already calculated. The third and fourth parameters are our near and far
clipping planes, respectively. The plane values represent how far the plane is from our
camera. It means anything past the far clipping plane will not be drawn onto the screen.
It also means anything closer to us than the near clipping plane will not be drawn either.
Only the points that fall in between those two planes and are within a 45-degree angle of
where we are looking will be drawn on the screen. The last parameter is where we populate
our projection matrix. This is an overloaded method. (One version actually returns
the projection, but we will utilize the overload that has reference and out parameters, as
they are faster because it doesn’t have to copy the value of the data.)
View
Now that we have our projection matrix set, we can set up our view matrix. To do this we
are going to use another XNA matrix helper method. The Matrix.CreateLookAt method
takes three parameters. Let’s create and initialize these private member fields now.
private Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 3.0f);
private Vector3 cameraTarget = Vector3.Zero;
private Vector3 cameraUpVector = Vector3.Up;
Now we can actually call the CreateLookAt method inside of our InitializeCamera
method. We should add the following code at the end of the method:
Matrix.CreateLookAt(ref cameraPosition, ref cameraTarget,
ref cameraUpVector, out view);
The first parameter we pass in is our camera position. We are passing in the coordinates
(0,0,3) for our camera position to start with, so our camera position will remain at the
origin of the x and y axis, but it will move backward from the origin 3 units. The second
parameter of the CreateLookAt method is the target of where we are aiming the camera.
In this example, we are aiming the camera at the origin of the world Vector3.Zero
(0,0,0). Finally, we pass in the camera’s up vector. For this we use the Up property on
Vector3, which means (0,1,0). Notice we actually created a variable for this so we can pass
it in by reference. This is also an overloaded method, and because we want this to be fast
we will pass the variables in by reference instead of by value. Fortunately, we do not lose
much readability with this performance gain.
62 CHAPTER 4 Creating 3D Objects
World
At this point if we compiled and ran the demo we would still see the lovely blank cornflower
blue screen because we have not set up our world matrix or put anything in the
world to actually look at. Let’s fix that now.
As we saw, the templates provide a lot of methods stubbed out for us. One of these very
important methods is the Draw method. Find this method and add this line of code right
below the TODO: Add your drawing code here comment:
world = Matrix.Identity;
This simply sets our world matrix to an identity matrix, which means that there is no
scaling, no rotating, and no translating (movement). The identity matrix has a translation
of (0,0,0) so this will effectively set our world matrix to the origin of the world.
At this point we have our camera successfully set up but we have not actually drawn
anything. We are going to correct that starting with the next section.
Vertex Buffers
3D objects are made up of triangles. Every object is one triangle or more. For example, a
sphere is just made up of triangles; the more triangles, the more rounded the sphere is.
Take a look at Figure 4.1 to see how this works. Now that we know that every 3D object
we render is made up of triangles and that a triangle is simply three vertices in 3D space,
we can use vertex buffers to store a list of 3D points. As the name implies, a vertex buffer
is simply memory (a buffer) that holds a list of vertices.
Vertex Buffers 63
4
FIGURE 4.1 All 3D objects are made up of triangles.
XNA uses a right-handed coordinate system. This means that the x axis goes from left to
right (left being negative and right being positive), the y axis goes up and down (down
being negative and up being positive), and z goes forward and backward (forward being
negative and backward being positive). We can visualize this by extending our right arm
out to our right and positioning our hand like we are holding a gun. Now rotate our wrist
so our palm is facing the sky. At this point our pointer finger should be pointing to the
right (this is our x axis going in a positive direction to the right). Our thumb should be
pointing behind us (this is our z axis going in a positive direction backward). Now, we
uncurl our three fingers so they are pointing to the sky (this represents the y axis with a
positive direction upward). Take a look at Figure 4.2 to help solidify how the right-handed
coordinate system works.
Now that we know what the positive direction is for each axis we are ready to start plotting
our points. XNA uses counterclockwise culling. Culling is a performance measure
graphic cards take to keep from rendering objects that are not facing the camera. XNA has
three options for culling: CullClockwiseFace, CullCounterClockwiseFace, and None. The
default culling mode for XNA is CullCounterClockwiseFace, so to see our objects we have
to set up our points in the opposite order—clockwise.
TIP
It is helpful to use some graph paper (or regular notebook paper for that matter) to
plot out points. Simply put points where we want them and make sure when we put
them into the code, we do it in a clockwise order.
Let’s plot some points. Ultimately, we want to make a square. We know that all 3D
objects can be made with triangles and we can see that a square is made up of two triangles.
We will position the first triangle at (-1,1,0); (1,-1,0); (-1,-1,0). That means the first
point (-1,1,0) will be positioned on the x axis 1 unit to the left and it will be 1 unit up the
y axis and will stay at the origin on the z axis. The code needed to set up these points is
as follows:
private void InitializeVertices()
{
Vector3 position;
Vector2 textureCoordinates;
vertices = new VertexPositionNormalTexture[3];
//top left
position = new Vector3(-1, 1, 0);
textureCoordinates = new Vector2(0, 0);
vertices[0] = new VertexPositionNormalTexture(position, Vector3.Forward,
textureCoordinates);
//bottom right
position = new Vector3(1, -1, 0);
textureCoordinates = new Vector2(1, 1);
vertices[1] = new VertexPositionNormalTexture(position, Vector3.Forward,
textureCoordinates);
//bottom left
position = new Vector3(-1, -1, 0);
textureCoordinates = new Vector2(0, 1);
vertices[2] = new VertexPositionNormalTexture(position, Vector3.Forward,
textureCoordinates);
}
As we look at this function we can see three variables that have been created: vertex,
position, and textureCoordinates. The vertex variable will store our vertex (point) data.
XNA has different structs that describe the type of data a vertex will hold. In most cases
for 3D games we will need to store the position, normal, and texture coordinates. We
discuss normals later, but for now it is sufficient to know they let the graphics device
know how to reflect light off of the face (triangle). The most important part of the vertex
variable is the position of the point in 3D space. We saw earlier that XNA allows us to
store that information in a Vector3 struct. We can either set the data in the constructor as
we did in this code, or we can explicitly set its X, Y, and Z properties.
We skip over explaining the texture coordinates momentarily, but notice it uses the
Vector2 struct that XNA provides for us.We need to add the following private member
field to our class that we have been using to store our vertices:
private VertexPositionNormalTexture[] vertices;
We need to call this method in our application. The appropriate place to call the
InitializeVertices method is inside of the LoadGraphicsContent method.
Vertex Buffers 65
4
If we compile and run our application now we still do not see anything on the screen.
This is because we have not actually told the program to draw our triangle! We will want
to find our Draw method and before the last call to the base class base.Draw(gameTime),
we need to add the following code:
graphics.GraphicsDevice.VertexDeclaration = new
VertexDeclaration(graphics.GraphicsDevice,
VertexPositionNormalTexture.VertexElements);
BasicEffect effect = new BasicEffect(graphics.GraphicsDevice, null);
world = Matrix.Identity;
effect.World = world;
effect.Projection = projection;
effect.View = view;
effect.EnableDefaultLighting();
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives(
PrimitiveType.TriangleList, vertices, 0,
vertices.Length / 3);
pass.End();
}
effect.End();
You might think there is a lot of code here just to draw the points we have created on the
screen. Well, there is, but it is all very straightforward and we can plow on through.
Before we do, though, let’s take a minute and talk about effects.
Creating 3D Objects
In this chapter we examine 3D concepts and how the
XNA Framework exposes different types and objects that
allow us to easily create 3D worlds. We will create a couple
of 3D demos that explain the basics. We will also create 3D
objects directly inside of our code. Finally, we will move
these objects on the screen.
Vertices
Everything in a 3D game is represented by 3D points. There
are a couple of ways to get 3D objects on the screen. We
can plot the points ourselves or we can load them from a
3D file (which has all of the points stored already). Later, in
Chapter 6, “Loading and Texturing 3D Objects,” we will
learn how to load 3D files to use in our games. For now,
we are going to create the points ourselves.
We defined these points with an x, y, and z coordinate (x,
y, z). In XNA we represent a vertex with a vector, which
leads us to the next section.
Vectors
XNA provides three different vector structs for us—
Vector2, Vector3, and Vector4. Vector2 only has an x
and y component. We typically use this 2D vector in 2D
games and when using a texture. Vector3 adds in the z
component. Not only do we store vertices as a vector, but
we also store velocity as a vector. We discuss velocity in
Chapter 19, “Physics Basics.” The last vector struct that
XNA provides for us is a 4D struct appropriately called
Vector4. Later examples in this book will use this struct to
pass color information around as it has four components.
We can perform different math operations on vectors,
which prove to be very helpful. We do not discuss 3D math
in detail in this book as there are many texts out there that
60 CHAPTER 4 Creating 3D Objects
cover it. Fortunately, XNA allows us to use the built-in helper functions without having to
have a deep understanding of the inner workings of the code. With that said, it is
extremely beneficial to understand the math behind the different functions.
Matrices
In XNA a matrix is a 4x 4 table of data. It is a two-dimensional array. An identity matrix,
also referred to as a unit matrix, is similar to the number 1 in that if we multiply any
other number by 1 we always end up with the number we started out with (5 * 1 = 5 ).
Multiplying a matrix by an identity matrix will produce a matrix with the same value as
the original matrix. XNA provides a struct to hold matrix data—not surprisingly, it is
called Matrix.
Transformations
The data a matrix contains are called transformations. There are three common transformations:
translation, scaling, and rotating. These transformations do just that: They transform
our 3D objects.
Translation
Translating an object simply means we are moving an object. We translate an object from
one point to another point by moving each point inside of the object correctly.
Scaling
Scaling an object will make the object larger or smaller. This is done by actually moving
the points in the object closer together or further apart depending on if we are scaling
down or scaling up.
Rotation
Rotating an object will turn the object on one or more axes. By moving the points in 3D
space we can make our object spin.
Transformations Reloaded
tag: transformation,reloaded,xna
An object can have one transformation applied to it or it can have many transformations
applied to it. We might only want to translate (move) an object, so we can update the
object’s world matrix to move it around in the world. We might just want the object to
spin around, so we apply a rotation transformation to the object over and over so it will
rotate. We might need an object we created from a 3D editor to be smaller to fit better in
our world. In that case we can apply a scaling transformation to the object. Of course, we
might need to take this object we loaded in from the 3D editor and scale it down and
rotate it 30 degrees to the left so it will face some object and we need to move it closer to
the object it is facing. In this case we would actually do all three types of transformations
to get the desired results. We might even need to rotate it downward 5 degrees as well,
and that is perfectly acceptable.
We can have many different transformations applied to an object. However, there is a
catch—there is always a catch, right? The catch is that because we are doing these transformations
using matrix math we need to be aware of something very important. We are
multiplying our transformation matrices together to get the results we want. Unlike
multiplying normal integers, multiplying matrices is not commutative. This means that
Matrix A * Matrix B != Matrix B * Matrix A. So in our earlier example where we want to
scale our object and rotate it (two different times) and then move it, we will need to be
careful in which order we perform those operations. We will see how to do this a little
later in the chapter.