Subscribe

RSS Feed (xml)

Powered By

Powered by Blogger

Google
 
xnahelp.blogspot.com

Jumat, 04 April 2008

Creating a Skybox

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.

0 komentar: