This week in the XNA 4.0 workshop we're covering 2D graphics using the SpriteBatch class. For those that used XNA 3.1 or earlier, and especially those that have never used DirectX, the new arguments to SpriteBatch.Begin can be a bit off-putting.
When using the SpriteBatch class there are really just four functions that'll you'll use most of the time. These are
- Draw()
- DrawString()
- Begin()
- End()
The first two are fairly obvious. Any time you want to draw a sprite or a string you call the corresponding function. The latter two need some explanation.
XNA is built on DirectX which is a hardware accelerated API for manipulating 3D primitives. This means that everything we do with XNA is really in 3D using triangles. Drawing sprites is no exception. Every time you call Draw or DrawString it's really just queuing a texture to be drawn on a 3D quad rendered in pre-transformed screen space.
Because XNA/DirectX is really 3D, the API has to do some initial setup with the Graphics Pipeline to make sure things are drawn with the appearance of 2D. In XNA 3.1 and prior this was done with shader code that was called from SpriteBatch during the drawing process. When that happened depended on which sort mode you were using. But ultimately your textures, combined with specific device states, would be drawn to the screen in a series of pre-transformed quads.
In XNA 3.1 you had a little bit of control over this process, but not much. When you called Begin you could pass it a number of arguments depending on which overload you chose. The most complicated overload looked like this:
Begin(SpriteBlendMode, SpriteSortMode, SaveStateMode, Matrix)
In the above overload, SpriteBlendMode, SpriteSortMode, and SaveStateMode were all just simple enumerations.
SpriteBlendMode was used to determine how sprites you drew were combined with data already in the target buffer. The options were either SpriteBlendMode.Additive, SpriteBlendMode.AlphaBlend, or SpriteBlendMode.None.
SaveStateMode was used to determine whether or not device states would be reset after calling Begin/End on the SpriteBatch class. The options were SaveStateMode.None and SaveStateMode.SaveState. The default was None, and would often cause confusion for people when switching back and forth between drawing with SpriteBatch and without in the same draw pass. Suddenly, people's 3D models would draw without respect for the depth buffer and all of their alpha blending states would be reset.
SpriteSortMode was used to determine in what order your sprites were drawn. The possible values were: BackToFront, Deferred, FrontToBack, Immediate, Texture.
In deferred mode all of your draw calls were deferred, and nothing was actually flushed to the hardware until End was called. Once End was called, the device had all of its render states set and all of your sprites were drawn to the target surface in the same order as they were "drawn". Of course, if you drew your sprites in something other than a painter's algorithm, or if you didn't intelligently split up your sprites by texture, it could result in undesirable layering or poorer performance, especially if you were rendering sprite-based particles.
The BackToFront and FrontToBack sort modes were designed to handle the painter's algorithm. That is, by passing a z-value in with your Draw calls you had more control over the order in which sprites were drawn. In these modes, it first sorted all your sprites by the z-value, ascending or descending, and then drew them. So it didn't matter the order in which you called Draw.
The Texture mode is/was for performance. If you drew from several different texture sheets within the same Begin/End pair, each switch from one texture to another would cause a flush and the device had to make a unique DrawPrimitive DirectX call. This is bad m'kay. By using the Texture mode it first sorted all of your sprites by texture, and then called them with as few DrawPrimitive calls as possible. Note, if all your sprites are/were from the same (or relatively few) texture sheet(s), then Texture sorting is useless, and you're better off using BackToFront or just Deferred.
The Immediate mode is interesting. With Immediate sort mode the device state was initialized as soon as you called Begin, and then each time you called Draw the quad was immediately pushed to the target surface. This is bad performance, but gave you control over your sprite rendering in a way none of the previous sort modes did. This is because you could call Begin, then manually make changes to the GraphicsDevice, such as changing blending values, filtering modes, depth buffer flags, etc... and THEN draw your quads. The problem was, now all of those quads were being drawn individually.
Enter XNA 4.0. With XNA 4.0, Microsoft acknowledged a need to give greater control over sprite drawing, while still maintaining performance. So the SpriteBatch.Begin method has a different set of arguments. The corresponding overload now looks like this:
Begin(SpriteSortMode, BlendState, SamplerState, DepthStencilState,
RasterizerState, Effect, Matrix)
Notice that gone are most of the enumerations. While there is still an enumeration for SpriteSortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, and even Effect are all fully realized objects. This allows you to set properties on the device at the time you call Begin, and then have those states be set for the duration of the Begin/End pair. So you can set certain properties, set your Sort mode to Defferred, and gain the benefit of both custom settings and batched rendering.
If you're like most people, when you see that set of object parameters your first instinct is to go..."uh, how do I set all those." Fortunately for us, Microsoft provided static fields on each of those classes which grants access to instances of the most common scenarios you'll need when rendering with SpriteBatch.
- For BlendState, there are static fields for Additive, AlphaBlend, NonPremultiplied, and Opaque Blend States.
- For SamplerState there are several static fields, but you'll most often use the default, which is LinearClamp.
- For DepthStencilState there are Default, DepthRead, and None. Interestingly enough, Default is pre-configured with the default states you'll want set if you want to use the stencil buffer in your 2D rendering. In truth, None is actually the "default" setting.
- And last, RasterizerState includes CullClockwise, CullCounterClockwise, and CullNone. The default is CullCounterClockwise.
You'll also notice from the above prototype the addition of the Effect parameter. This allows you to pass in your own custom effect, so you could even do your 2D drawing in 3D if you wanted to. It helps to have something to start from though if you're going to provide your own custom effect. That's why Microsoft has released the Source Code to the SpriteEffect, which can be downloaded from the AppHub.
If at the end of the day you just want to draw in 2D and don't need any of the options you can just call the simpler version of the Begin method, or use the above but pass in null for as many arguments as you like. Since those are objects and not enumerations, it's fine to just let the SpriteBatch class determine what the most appropriate paramter is.
So you see, with the transition from XNA 3.1 to XNA 4.0 Microsoft has given users a lot more control over how they do 2D rendering with the SpriteBatch class. By messing with the different state objects you can make your sprite blend with the surface in new and unique ways, have it draw to the stencil buffer, ignore (or not) the depth buffer, or even have sprites wrap within the target rectangle. Because you can also provide your own effect it's now also possible to do things such as having your sprites cast shadows, or react in unique ways to custom lighting, all without sacrificing performance.
In this tutorial, we go from using user-provided arrays with simple, colored primitives, to using buffered primitives with full texture mapping and filtering. We will also look at some of the changes in the new GraphicsDevice class.
In this series of tutorials, I have so far been following the path of the NeHe OpenGL tutorials. The idea was to demonstrate how easy it is to complete those same tutorials using XNA. However, starting with this fourth tutorial I am diverging from the NeHe tutorials because they, at times, develop a little slower than many would like, and also don’t go into enough depth. I will on occasion go back and use the NeHe tutorials for inspiration, but in general, this tutorial series will now follow its own path.
For those who are familiar with the NeHe series of tutorials, this tutorial will cover roughly the same material as covered in the NeHe tutorials 4-6 (and some of 7). In specific, we are going to be drawing full 3D objects now instead of just single primitives, we are going to be doing a bit more rotation and manipulation of the world transforms of our objects, and we are going to be performing both texture sampling and filtering as part of our coverage of texture mapping.
As usual, all rendering will be done with HLSL and our own custom effect classes, rather than using the pre-built *Effect classes which ship with XNA 4.0. Note, however, that everything we are going to do in this tutorial is possible with the BasicEffect class. With no further ado, let's get started.
The Sample Framework
The first thing you will need to do is grab hold of the sample framework. If you have been following along with the previous tutorials, you have seen the first version of the sample framework, which was provided as part of Tutorial #1. However, because we will usually call the ResetProjection method from within our OnResize handler, I have gone ahead, added that to the base code, and provided a second download. You will need to get the updated base code here.
The Game Class
With the new base code in hand, it is time to get started making changes to Game1. The first thing we are going to do is add some new fields. We need an instance of the DefaultEffect class, which we will use to perform our rendering, we will need some matrices to perform some transformations, and we will need a pair of buffers for each of our 3D objects.
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
KeyboardState prevKeyboardState = Keyboard.GetState();
DefaultEffect effect;
Matrix triangleTransform;
Matrix rectangleTransform;
VertexBuffer pyramidVB;
IndexBuffer pyramidIB;
VertexBuffer boxVB;
IndexBuffer boxIB;
TextureFilter textureFilter = TextureFilter.Linear;
private float triangleAngle;
private float rectangleAngle;
bool isAnimating = true;
float depth = -6.0f;
Part of the purpose of this tutorial is for you to see how a filtering mode changes the appearance of textures on your primitives. In order to accomplish this, you need to be able to switch between different filtering methods at run-time. The textureFilter field will identify what the current filtering method is. Another objective of this tutorial is to have a box and pyramid rotating around in circles. We will use the two angle fields to identify the current rotation of the objects. The associated isAnimating field will determine whether the game "is animating".
The first method we are going to look at is the ResetTranslation method. In previous projects, the transformation matrices were built once in the Initialize method. In this project, we are going to allow the user to move the object closer or further away in order to get a better look at the texture mapping. To make this easier, I have provided a ResetTranslation method which will be called both from within Initialize, as well as from within Update.
protected void ResetTranslation()
{
triangleTransform = Matrix.CreateTranslation(new Vector3(-1.5f, 0.0f, depth));
rectangleTransform = Matrix.CreateTranslation(new Vector3(1.5f, 0.0f, depth));
}
Now let's take a look at the Initialize method. The first thing we do is call ResetTranslation. This creates the world transformations for both the pyramid and the box, based on the starting depth of -6.0f. The next part of Initialize is the creation of two vertex arrays and index lists - one set for the pyramid, and one set for the box. In the previous tutorial, we used the built-in VertexPositionColor structure to represent our 3D vertices. However, because we are planning to use texture mapping in this project, we will need a different vertex structure. Specifically, one that has a position for each vertex and a set of texture coordinates (sometimes called UV coordinates), which identify which texel within a texture we'd like to map to the current vertex. Fortunately, XNA once again comes through with a pre-built structure called VertexPositionTexture.
protected override void Initialize()
{
ResetTranslation();
Vector2 topLeft = new Vector2(0.0f, 0.0f);
Vector2 topRight = new Vector2(1.0f, 0.0f);
Vector2 bottomLeft = new Vector2(0.0f, 1.0f);
Vector2 bottomRight = new Vector2(1.0f, 1.0f);
VertexPositionTexture[] triangleData = new VertexPositionTexture[]
{
new VertexPositionTexture(new Vector3(1.0f, -1.0f, 1.0f), bottomRight),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, 1.0f), bottomLeft),
new VertexPositionTexture(new Vector3(0.0f, 1.0f, 0.0f), topRight),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, -1.0f), bottomRight),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, 1.0f), bottomLeft),
new VertexPositionTexture(new Vector3(0.0f, 1.0f, 0.0f), topRight),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, -1.0f), bottomRight),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, -1.0f), bottomLeft),
new VertexPositionTexture(new Vector3(0.0f, 1.0f, 0.0f), topRight),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, 1.0f), bottomRight),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, -1.0f), bottomLeft),
new VertexPositionTexture(new Vector3(0.0f, 1.0f, 0.0f), topRight),
};
VertexPositionTexture[] boxData = new VertexPositionTexture[]
{
// Front Surface
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, 1.0f),bottomLeft),
new VertexPositionTexture(new Vector3(-1.0f, 1.0f, 1.0f),topLeft),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, 1.0f),bottomRight),
new VertexPositionTexture(new Vector3(1.0f, 1.0f, 1.0f),topRight),
// Front Surface
new VertexPositionTexture(new Vector3(1.0f, -1.0f, -1.0f),bottomLeft),
new VertexPositionTexture(new Vector3(1.0f, 1.0f, -1.0f),topLeft),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, -1.0f),bottomRight),
new VertexPositionTexture(new Vector3(-1.0f, 1.0f, -1.0f),topRight),
// Left Surface
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, -1.0f),bottomLeft),
new VertexPositionTexture(new Vector3(-1.0f, 1.0f, -1.0f),topLeft),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, 1.0f),bottomRight),
new VertexPositionTexture(new Vector3(-1.0f, 1.0f, 1.0f),topRight),
// Right Surface
new VertexPositionTexture(new Vector3(1.0f, -1.0f, 1.0f),bottomLeft),
new VertexPositionTexture(new Vector3(1.0f, 1.0f, 1.0f),topLeft),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, -1.0f),bottomRight),
new VertexPositionTexture(new Vector3(1.0f, 1.0f, -1.0f),topRight),
// Top Surface
new VertexPositionTexture(new Vector3(-1.0f, 1.0f, 1.0f),bottomLeft),
new VertexPositionTexture(new Vector3(-1.0f, 1.0f, -1.0f),topLeft),
new VertexPositionTexture(new Vector3(1.0f, 1.0f, 1.0f),bottomRight),
new VertexPositionTexture(new Vector3(1.0f, 1.0f, -1.0f),topRight),
// Bottom Surface
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, -1.0f),bottomLeft),
new VertexPositionTexture(new Vector3(-1.0f, -1.0f, 1.0f),topLeft),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, -1.0f),bottomRight),
new VertexPositionTexture(new Vector3(1.0f, -1.0f, 1.0f),topRight),
};

If you look at the last argument to the constructor of each of the VertexPositionTexture structures you will see one of four variables; topLeft, bottomLeft, topRight, and bottomRight. These correspond with the four corners of a texture. For those unfamiliar with texture mapping, the goal of texture mapping is to map the image found in a 2D texture onto the surface of a 3D primitive. In order to do this, we assign UV coordinates to each of the vertices of a triangle and then let the interpolator, executed between the vertex shader and the pixel shader, determine the UV coordinates of all the pixels in-between. Unlike world coordinates, or even screen coordinates, texels are represented as (u,v) pairs, in which each coordinate is a percent of the texture from 0.0f to 1.0f. For a visual representation of this, see the image to the right.
Ok, after that sojourn into texture mapping, let us return to the code at hand. Just after the collection of VertexPositionTexture structures there are two small arrays of short integers. These will be the indices for our 3D primitives.
short[] pyramidIndices = new short[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
short[] boxIndices = new short[] {
0, 1, 2, 2, 1, 3,
4, 5, 6, 6, 5, 7,
8, 9, 10, 10, 9, 11,
12, 13, 14, 14, 13, 15,
16, 17, 18, 18, 17, 19,
20, 21, 22, 22, 21, 23
};
For those not familiar with indices, consider this: A box has six sides, with two triangles per side. If we were to include each of the vertices for each of the triangles in our triangle list, we would have 36 vertices! If we think about it carefully, many of the vertices (two per side) are duplicates. They have the exact same position and UV coordinates as a vertex on other triangle of the same quad. By using index buffers, we can reduce the number of vertices pushed to the device, and re-use the ones needed by a different triangle.
With all that said, our current vertex structure has only two fields and our buffer is only 36 vertices long. That is not many vertices to send to the video card, nor is it an overly complex format. So why use index buffers? Simple, when you do not use index buffers, the vertex caching mechanism is disabled on most video cards, which forces all the vertices to be re-ran through your shaders. So not only does using index buffers reduce the number of vertices sent to the video card, but it can also reduce the number of vertices that must be shaded.
The next part of the Initialize method is the creation and assignment of the vertex and index buffers for both the pyramid and the box. For those people coming from a previous version of XNA, you may notice that in XNA 4.0 the VertexBuffer constructor now takes an argument of type VertexDeclaration. This is new, and you’ll see why when we get to the Draw method.
pyramidVB = new VertexBuffer(GraphicsDevice, VertexPositionTexture.VertexDeclaration, 12, BufferUsage.WriteOnly);
pyramidIB = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, 12, BufferUsage.WriteOnly);
pyramidVB.SetData(triangleData);
pyramidIB.SetData(pyramidIndices);
triangleData = null;
pyramidIndices = null;
boxVB = new VertexBuffer(GraphicsDevice, VertexPositionTexture.VertexDeclaration, 24, BufferUsage.WriteOnly);
boxIB = new IndexBuffer(GraphicsDevice, IndexElementSize.SixteenBits, 36, BufferUsage.WriteOnly);
boxVB.SetData(boxData);
boxIB.SetData(boxIndices);
boxData = null;
boxIndices = null;
base.Initialize();
}
The LoadContent method has a couple new changes in it. First, after the ResetProjection method, which is responsible for computing and setting the projection matrix, we use the ContentManager to load a Texture2D object and set it on the effect. Simply put, this tells the effect which image you want to draw with.
protected override void LoadContent()
{
Effect tempEffect = Content.Load("Effects/Default");
effect = new DefaultEffect(tempEffect);
tempEffect = null;
ResetProjection();
effect.Texture = Content.Load("Textures/Crate");
GraphicsDevice.SamplerStates[0] = new SamplerState()
{
Filter = textureFilter
};
}
If we were to render all primitives perpendicular to the camera, and we were to draw them all at such a distance that there was precisely one texel for each corresponding pixel on the screen, then texture mapping would be simple. However, this is rarely (read never) the case (except in pre-transformed 2D rendering). In reality, 3D primitives are drawn at odd angles, sometimes with only a sliver on the screen. Additionally, objects are constantly moving back and forth, closer and further from the screen. When this happens, the video card and driver must apply a filter to the texture in order to determine how texels will be mapped to pixels.
When an object is positioned or scaled in such a way that multiple pixels on the screen map to a single texel, this is called texture magnification, as it appears as though the textures are being magnified. When a single pixel on the screen maps to multiple texels on the texture it is called texture minification. In previous editions of XNA, it was necessary to set the filtering method for magnification and minification separately. In XNA 4.0, however, both are set with a single property on the SamplerState class called Filter. This property can have any of the values of the TextureFilter enumeration, which as of XNA 4.0 includes:
- Linear
- Point
- Anisotropic
- LinearMipPoint
- PointMipLinear
- MinLinearMagPointMipLinear
- MinLinearMagPointMipPoint
- MinPointMagLinearMipLinear
- MinPointMapLinearMipPoint
The first three values are straightforward and instruct the device to use Linear, Point, or Anisotropic Filtering when doing minification, magnification, or mipmapping (which I will talk about more in a later tutorial). The latter six are a bit more difficult to read, but are no more complex, and instruct the device which filtering methods to use for each of the different filtering modes. For example, MinLinearMagPointMipLinear tells the device to lerp for both texture minification and mipmapping, but to use nearest-neighbor filtering for magnification.
The next method to look at is the Update method. This project adds several new conditions to the Update method. First, we check to see if any of the following keys were pressed: A, R, F, PageUp, and PageDown. The A key is used to toggle whether or not the shapes are being animated and the R key is used to "reset" the objects back to their original orientations. The PageUp and PageDown keys are used to adjust at what depth the object are being drawn at, with PageUp moving the objects further away, and PageDown moving them closer. Finally, the F key is used to iterate between the different filtering methods so you can see what impact they have on the rendering of our objects.
The last thing the Update method does is, if animating, update the pyramid and box rotation angles so the objects appear to move.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
KeyboardState keyboard = Keyboard.GetState();
if (keyboard.IsKeyDown(Keys.Escape))
Exit();
if (keyboard.IsKeyDown(Keys.F11) && prevKeyboardState.IsKeyUp(Keys.F11))
IsFullScreen = !IsFullScreen;
if (keyboard.IsKeyDown(Keys.A) && prevKeyboardState.IsKeyUp(Keys.A))
isAnimating = !isAnimating;
if (keyboard.IsKeyDown(Keys.R) && prevKeyboardState.IsKeyUp(Keys.R))
{
triangleAngle = 0f;
rectangleAngle = 0f;
}
if (keyboard.IsKeyDown(Keys.F) && prevKeyboardState.IsKeyUp(Keys.F))
{
textureFilter = textureFilter + 1;
if (textureFilter > TextureFilter.MinPointMagLinearMipPoint)
textureFilter = TextureFilter.Linear;
GraphicsDevice.SamplerStates[0] = new SamplerState()
{
Filter = textureFilter
};
}
if (keyboard.IsKeyDown(Keys.PageUp))
{
depth -= 0.05f;
ResetTranslation();
}
if (keyboard.IsKeyDown(Keys.PageDown))
{
depth += 0.05f;
ResetTranslation();
}
if (isAnimating)
{
triangleAngle += MathHelper.ToRadians(1.0f);
if (triangleAngle > MathHelper.TwoPi)
triangleAngle -= MathHelper.TwoPi;
rectangleAngle -= MathHelper.ToRadians(0.75f);
if (rectangleAngle < 0.0f)
rectangleAngle += MathHelper.TwoPi;
}
prevKeyboardState = keyboard;
}
The final method we will look at of the Game1 class is the Draw method. While arguably one of the most important methods of our class, it is in many ways the least remarkable as there are relatively few changes to it.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
effect.World = Matrix.CreateRotationY(triangleAngle) * triangleTransform;
effect.CurrentTechnique.Passes[0].Apply();
GraphicsDevice.SetVertexBuffer(pyramidVB);
GraphicsDevice.Indices = pyramidIB;
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 5, 0, 4);
effect.World = Matrix.CreateFromYawPitchRoll(rectangleAngle,
rectangleAngle, rectangleAngle) * rectangleTransform;
effect.CurrentTechnique.Passes[0].Apply();
GraphicsDevice.SetVertexBuffer(boxVB);
GraphicsDevice.Indices = boxIB;
GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12);
}
}
First, there are two additional statements before each of our Draw calls. The first is to SetVertexBuffer and the latter is the assignment of our Indices. In previous versions of XNA, the vertex buffer was set similarly to the Indices with the assignment of Vertices[slot], where "slot" identified the stream number you wanted to bind to. In XNA 4.0, however, we use a method call. It is important to note that SetVertexBuffer is overloaded and provides a second version that takes an index as the second parameter, which is used to specify the stream number to bind to.
Another new addition to XNA 4.0 with respect to vertices is the SetVertexBuffers method. SetVertexBuffers takes a VertexBufferBinding[] array, and can be used to set all the vertex streams in a single call. This could be useful if you are serious about performance and are breaking up your streams, or if you are doing hardware instancing and you need to reset multiple streams frequently.
One thing you should notice missing from this method is the assignment of the VertexDeclaration. In previous versions of XNA it was required that you set those manually so the device knew the format of the vertices. In XNA 4.0, however, the vertex declaration is bound to the vertex buffer, so it is set automatically when you call SetVertexBuffer.
The other thing that is new in the Draw method is which Draw method we call on the device. In previous tutorials, we called DrawUserPrimitive, but in this tutorial, since we are using indexed, buffered vertices, we call the corresponding DrawIndexedPrimitives method. This method is far more performant, especially for Draw calls with a larger number of primitives.
The DefaultEffect Class
With the Game1 class out of the way, we can finally turn our attention to the DefaultEffect class. Absent from this class are the shader index and diffuse material color properties which were added in tutorial 3. They are unnecessary in this tutorial because we are only using a single technique, and pulling our diffuse material color from a texture, rather than setting it via an EffectParameter. What is new in this version, however, is the addition of the Texture property. As you can see in the constructor, the EffectParameter for the texture is obtained and cached in the same method as any other shader parameter, and is used by the Texture property to set the texture for the effect. Overall, there is nothing particularly new or exciting here if you have been following along.
class DefaultEffect : Effect, IEffectMatrices
{
EffectParameter world;
EffectParameter projection;
EffectParameter texture;
public DefaultEffect(Effect effect)
: base(effect)
{
world = Parameters["World"];
projection = Parameters["Projection"];
texture = Parameters["DiffuseTexture"];
}
public Matrix World
{
get { return world.GetValueMatrix(); }
set { world.SetValue(value); }
}
public Matrix View
{
get { return Matrix.Identity; }
set { }
}
public Matrix Projection
{
get { return projection.GetValueMatrix(); }
set { projection.SetValue(value); }
}
public Texture2D Texture
{
get { return texture.GetValueTexture2D(); }
set { texture.SetValue(value); }
}
}
The DefaultEffect.fx File
The last part of this tutorial is the effect file for our default effect. There are two new global variables in this effect file. The first is an object of type Texture2D, which stores the texture object we will sample. The second is an object of type Sampler2D.
float4x4 World;
float4x4 Projection;
Texture2D DiffuseTexture;
sampler2D DiffuseSampler = sampler_state
{
Texture = ;
};
Samplers are a complex type that can have many different properties in them. However, we use only one in this tutorial, the Texture property, which we set to be a reference to our DiffuseTexture object. The other properties, which can be set in a sampler state, are the same as they are for XNA versions prior to 4.0, as well as DirectX 9. These include:
- AddressU
- AddressV
- AddressW
- BorderColor
- Filter
- MaxAnisotropy
- MaxLOD
- MinLOD
- MipLODBias
The next part of the effect file is the vertex declaration. Using the same convention as XNA, I named the struct after the elements that make up the vertex format. In this case, I call it VertexPositionTexture, just like that provided by XNA.
struct VertexPositionTexture
{
float4 Position : POSITION0;
float2 UV : TEXCOORD0;
};
Finally, we get to the vertex and pixel shaders. The vertex shader is very similar to the previous shaders. The first line creates an instance of the VertexPositionTexture structure, and is then followed by a simple computation which converts the input vertex position into projection space. The new part of the function is the assignment of the UV coordinates to the output structure. As we discussed in the previous tutorial, anything returned from the vertex shader is interpolated over the triangle and passed on to the pixel shader for further processing. This is precisely what we want, since we need each projected pixel on the screen to sample a texel within the texture.
Looking ahead to the pixel shader you can see that it has only a single line. In the pixel shader, we use the intrinsic function tex2D to sample a 2D texture given a sampler state and a texture coordinate. As our sampler, we use the global variable DiffuseSampler previously discussed. For the texture coordinate, we pass in the interpolated UV values by the interpolator before being passed into the pixel shader.
VertexPositionTexture TexturedVertexShader(VertexPositionTexture input)
{
VertexPositionTexture output;
output.Position = mul(input.Position, World);
output.Position = mul(output.Position, Projection);
output.UV = input.UV;
return output;
}
float4 TexturedPixelShader(VertexPositionTexture input) : COLOR0
{
return tex2D(DiffuseSampler, input.UV);
}
technique DefaultEffect
{
Pass
{
VertexShader = compile vs_2_0 TexturedVertexShader();
PixelShader = compile ps_2_0 TexturedPixelShader();
}
}
If you obtain the base code and make all of the previous changes, or, if you download the complete project file located at the bottom of this tutorial and compile/run it, you will see an image that looks similar to the following:

In tutorial #3 we're going to be duplicating a lot of the code which was written in tutorial #2 in order to implement the same triangle and quad as last time; but this time with vertex and diffuse material COLOR! As I've stated in the last few tutorials, part of my goal for this series of tutorials is to help aspiring XNA programmers become more familiar with Real-Time Shaders and HLSL. As a result, rather than just using the BasicEffect class which has its own DiffuseColor and VertexColorEnabled properties, we're going to be doing the vertex and material coloring ourselves; using our own custom effect and our own custom DefaultEffect class. So let's get started!
As usual, the first change we're going to make to the Game1 class is to add a few fields. As with tutorial #2, we're going to add an instance of the DefaultEffect class, which we'll be implementing ourselves, in addition to a Vertex Declaration and vertex data for our triangle and rectangle.
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
KeyboardState prevKeyboardState = Keyboard.GetState();
DefaultEffect effect;
VertexDeclaration vertexDecl;
Matrix triangleTransform;
Matrix rectangleTransform;
VertexPositionColor[] triangleData;
Vector3[] rectangleData;
The main thing to note about these fields is the difference between the triangle and rectangle data. In Tutorial #2, both the triangle and the rectangle were created using Vector3. In this tutorial we plan to color the triangle with vertex coloring. In order for the video card to know what color each vertex will be we must supply it with not only a position, but a color as well. So a simple Vector3 is no longer sufficient. In general, when creating vertices which contain more than just positions, we must create a custom structure comprised of all the data associated with each individual vertex. But, as with XNA 3.1 (and before), XNA provides built-in structures for many of the most common vertex formats. Each one is named after the data it contains and in this case, we're going to utilize the VertexPositionColor structure in order to send the video card a position and a color for each vertex.
The next part of the code is a repeat from Tutorial 2. This tells the Game Window that any time the window is resized we want to adjust the projection matrix so our objects can be drawn with the new aspect ratio.
protected void OnClientSizeChanged(object sender, EventArgs e)
{
ResetProjection();
}
protected void ResetProjection()
{
Viewport viewport = graphics.GraphicsDevice.Viewport;
effect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
(float)viewport.Width / viewport.Height,
0.1f,
100.0f);
}
The next method we're going to look at is the Initialize method. As with Tutorial #2, we're essentially doing two things here. The first thing we're going to do is initialize two matrices which will act as the world transformations for both our triangle and rectangle. For an explanation of why the transformations have a -6.0f z-coordinate, and why we have no View transformation, see Tutorial #2.
protected override void Initialize()
{
triangleTransform = Matrix.CreateTranslation(new Vector3(-1.5f, 0.0f, -6.0f));
rectangleTransform = Matrix.CreateTranslation(new Vector3(1.5f, 0.0f, -6.0f));
triangleData = new VertexPositionColor[3];
triangleData[0] = new VertexPositionColor(new Vector3(1.0f, -1.0f, 0.0f), Color.Red);
triangleData[1] = new VertexPositionColor(new Vector3(-1.0f, -1.0f, 0.0f), Color.Green);
triangleData[2] = new VertexPositionColor(new Vector3(0.0f, 1.0f, 0.0f), Color.Blue);
rectangleData = new Vector3[4];
rectangleData[0] = new Vector3(-1.0f, -1.0f, 0.0f);
rectangleData[1] = new Vector3(-1.0f, 1.0f, 0.0f);
rectangleData[2] = new Vector3(1.0f, -1.0f, 0.0f);
rectangleData[3] = new Vector3(1.0f, 1.0f, 0.0f);
base.Initialize();
}
The second thing we do in the Initialize method is to create two data arrays. The first is a set of three VertexPositionColor structures which will be used as the vertices for our triangle. You can see that we initialize both a Vector3 (for the position) as well as a color for each vertex. This is unnecessary in the quad, because while we do intend on coloring it, using a diffuse material color, the color does not change per-vertex. As such, it's not necessary to specify the color on each vertex. As you'll see shortly, we'll just pass the diffuse material color to the device directly, using effect parameters.
With the Initialize method out of the way it's time to look at the LoadContent method.
protected override void LoadContent()
{
Effect tempEffect = Content.Load("Effects/Default");
effect = new DefaultEffect(tempEffect);
tempEffect = null;
ResetProjection();
vertexDecl = new VertexDeclaration(new VertexElement[] {
new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0)
}
);
effect.DiffuseColor = new Vector3(0f, 0f, 1.0f);
}
protected override void UnloadContent()
{
Content.Unload();
}
LoadContent looks almost identical to that which was provided in Tutorial #2. The only difference is the inclusion of the final line of the method. This last line tells the effect what the diffuse material color is for our primitives. When we get to the code listing for the DefaultEffect you'll see how this is transferred to the vertex shader.
The final method of the Game1 class is the Draw method. This again is almost identical to the Draw method discussed in Tutorial #2 with two differences - one subtle, and one less subtle.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
effect.World = triangleTransform;
effect.IsVertexColoringEnabled = true;
effect.CurrentTechnique.Passes[0].Apply();
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip,
triangleData, 0, 1, VertexPositionColor.VertexDeclaration);
effect.World = rectangleTransform;
effect.IsVertexColoringEnabled = false;
effect.CurrentTechnique.Passes[0].Apply();
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip,
rectangleData, 0, 2, vertexDecl);
}
The first difference are the assignments of IsVertexColoringEnabled. On the third line of the method we set this value to true so the device knows which vertex shader to use (the one which uses vertex coloring). We reset this value back to false on line 7 so that our quad does not use the per-vertex coloring. Instead, its behavior is to use the vertex and pixel shaders which access the global material color set by our custom DefaultEffect class.
The other, more subtle difference between this Draw method and that provided in Tutorial #2, is the first call to DrawUserPrimitives. This invocation has two changes. First, the generic method now applies to type VertexPositionColor rather than Vector3. This tells the method what type of vertices are going to be pushed to the device. The other difference is the passed in VertexDeclaration. Instead of passing in our custom vertex declaration "vertexDecl", we pass in VertexPositionColor.VertexDeclaration. Note: Unlike XNA 3.1, in which each of the custom vertex format structures contained an array of VertexElements, XNA 4.0 cuts out the middle man and each custom vertex format explicitly provides a VertexDeclaration. This is a lot more convenient, as it means when using the built-in vertex formats, you don't even need to create a vertex declaration.
Ok, with Game1 out of the way it's time to look at our DefaultEffect class. This again resembles what was created in Tutorial #2, with the addition of a few new EffectParameters and matching properties.
class DefaultEffect : Effect, IEffectMatrices
{
EffectParameter world;
EffectParameter projection;
EffectParameter shaderIndex;
EffectParameter diffuseColor;
bool isVertexColoringEnabled;
public DefaultEffect(Effect effect)
: base(effect)
{
world = Parameters["World"];
projection = Parameters["Projection"];
shaderIndex = Parameters["ShaderIndex"];
diffuseColor = Parameters["DiffuseColor"];
}
public Matrix World
{
get { return world.GetValueMatrix(); }
set { world.SetValue(value); }
}
public Matrix View
{
get { return Matrix.Identity; }
set { }
}
public Matrix Projection
{
get { return projection.GetValueMatrix(); }
set { projection.SetValue(value); }
}
public Vector3 DiffuseColor
{
get { return diffuseColor.GetValueVector3(); }
set { diffuseColor.SetValue(value); }
}
public bool IsVertexColoringEnabled
{
get { return isVertexColoringEnabled; }
set
{
isVertexColoringEnabled = value;
if (isVertexColoringEnabled)
shaderIndex.SetValue(1);
else
shaderIndex.SetValue(0);
}
}
}
The constructor continues to initialize the object by accessing the parameters from the real-time shader. In this case, it also grabs the shaderIndex and diffuseColor parameters. The first is going to be used to tell the device which vertex and pixel shaders to use, and the second will be used by the default pixel shader to tell it what color to return for each pixel.
You can see that the DiffuseColor property follows the same pattern as the transformation matrices, with get and set methods which return the effect parameter value or set it on the device. The IsVertexColoringEnabled property is a little different. Rather than using bool, this property internally maps the bool to an index, either 0 or 1, which will act as an array index to identify which of a set of shaders the effect framework should set on the device. This is an alternative approach to using if-statements in the vertex and pixel shaders as it is only performed once, when setting the shaders on the device, rather than per-vertex or worse, per-pixel.
Since we're talking about the shaders, let's take a look at the .fx file now.
float4x4 World;
float4x4 Projection;
int ShaderIndex = 0;
float3 DiffuseColor;
struct VertexPositionColor
{
float4 Position : POSITION0;
float4 Color : COLOR0;
};
float4 DiffuseVertexShader(float4 input : POSITION0 ) : POSITION0
{
float4 output = mul(input, World);
return mul(output, Projection);
}
float4 DiffusePixelShader(float4 input : POSITION0 ) : COLOR0
{
return float4( DiffuseColor, 1.0 );
}
VertexPositionColor VertexColorVertexShader(VertexPositionColor input)
{
VertexPositionColor output;
output.Position = mul(input.Position, World);
output.Position = mul(output.Position, Projection);
output.Color = input.Color;
return output;
}
float4 VertexColorPixelShader(VertexPositionColor input) : COLOR0
{
return input.Color;
}
VertexShader VertexShaders[2] =
{
compile vs_2_0 DiffuseVertexShader(),
compile vs_2_0 VertexColorVertexShader(),
};
PixelShader PixelShaders[2] =
{
compile ps_2_0 DiffusePixelShader(),
compile ps_2_0 VertexColorPixelShader()
};
technique DefaultEffect
{
Pass
{
VertexShader = (VertexShaders[ShaderIndex]);
PixelShader = (PixelShaders[ShaderIndex]);
}
}
This effect file is a bit more complex than that previously implemented. First, it provides additional global variables, ShaderIndex and DiffuseColor, which provide access to the EffectParamters utilized on the CPU. The other difference is the addition of a new structure called VertexPositionColor. You'll note that it shares the same name as the structure used in the C# code, and refers to the vertex format used by the vertex coloring shaders. I could have chosen another name, but I find that describing the vertex structure in the shader by format prevents the common habit of creating multiple structures with the same format for different purposes, such as vertexformat_in and vertexformat_out.
Unlike the previous effect file this one contains two methods which will be used as vertex shaders, and two methods which will be used as pixel shaders. In each case, there is one method that implements per-vertex coloring, and one that performs simple diffuse material coloring.
The first set of methods are the diffuse material methods. The DiffuseVertexShader method is identical to that implemented in Tutorial #2 as it only acts to transform the position of the vertex into projection space, and then pass it off to be interpolated and processed by the pixel shader. The DiffusePixelShader is similar to that provided in Tutorial #2, which only returned the color white, but this one instead returns the color provided in the global variable DiffuseColor. The idea being that it returns each pixel as whatever color was specified in the C# code.
The next method in the file is VertexColorVertexShader. This vertex shader again transforms the position into projection space, but it additionally sets the output color to the input color. For those not already familiar with the relationship between vertex and pixel shaders. Any data which is returned out of the vertex shader will be interpolated across the surface of the primitive to be processed per-pixel in the pixel shader. Because each of our vertices have different colors, and because we're returning it to the device for interpolation, it will result in each pixel in the pixel shader being sent an interpolated color (a blend of the three vertex colors). We can then use this in the pixel shader to determine the color of the pixel.
The VertexColorPixelShader function is the next in the .fx file. You can see that it has precisely one line, returning the input color back to the caller. This may seem a little silly, but remember that the caller has no idea what the pixel-shader is going to do. It only knows the inputs being sent to the pixel shader and the output - always a float4, representing the color of the pixel. If we wanted to we could return the interpolated vertex color weighted by the position. Or, we could completely ignore the color and still return a diffuse material color. In any event, the method just returns the interpolated color for the pixel back to the device to be used as the pixel color.
The final part of the effect file may seem a bit strange. We declare two arrays, one of vertex shaders, and one of pixel shaders, with the individual elements being equal to the compiled functions. This serves to give us a way to refer to the shaders by indices. If you look at the technique definition you'll see that in Pass 0 (the only pass), the vertex shader and pixel shader on the device are set to one of the indices of each of the two arrays, determined by the ShaderIndex constant. Remember what I said before. We could have just put if-statements in a single vertex and pixel shader, ignoring the vertex-coloring data when it was unnecessary, but this method is a bit cleaner when you've got a large number of vertex and pixel shaders, such as is done in BasicEffect. In future effect files I'll probably just use if-statements for compactness.
If you enter all of the previous code into the starter game shown in Tutorial #1, or just download the completed project below and compile/run it, you should see the following:

Hopefully it's not a surprise that the triangle is colored via an interpolation of the three vertex colors, while the rectangle is colored by the single diffuse material color we passed in as a shader constant. If you want to play with this project more, feel free to change the diffuse color passed in by the effect, or change the vertex colors on each of the vertices.
In Lesson 2 of this tutorial series we make our first attempt at working with the XNA 4.0 3D Graphics Pipeline. By the time this tutorial is complete you should have a working knowledge of how to get 3D primitives onto the screen, how to work with Effects to perform Real-Time shading on the GPU, and as usual, some of the differences between XNA 3.1 and XNA 4.0. So Let's get started....More...
As I stated in my previous post, over the next couple of weeks I'm going to be working my way through ALL 48 of the NeHe OpenGL tutorials (in addition to some of my own) in an effort to both show how easy it is to use XNA, as well as to highlight some of the differences between XNA 3.1 and XNA 4.0.
To get us off to the right start, it's necessary to create a "framework" for our applications. This is a basic "game" which we can add to in later demos in order to minimize the amount of work required. Fortunately for us, XNA does most of this for us with the XNA Game API. XNA already handles creation of the main game window, the creations of the graphics device, and the update and draw phases of the main game loop. The only things we need to add to the starting game to put us in line with the NeHe tutorials are:
- The ability to toggle between fullscreen and windowed mode
- The ability to see the mouse when in windowed mode
- The ability to resize the window and handle projection accordingly
So let's get started!
To begin the process of creating the sample demo we just need to launch XNA Game Studio 4.0. This is where we see the first difference. The whole thing is navy blue and .NET-looking. This is because, as you probably noticed during installation, XNA Game Studio 4.0 is built on Visual C# 2010 Express Edition. The new look you see when developing with XNA GS 4.0 is from the newer version of Visual C#. Another thing which was immediately obvious to me was the list of references. As you can see from the image below, XNA Game Studio 4.0 includes a lot more assemblies. More to the point, XNA 4.0 breaks the XNA Framework Library into mutiple, smaller assemblies. This makes it possible for you to completely remove assemblies from the project which you're not going to be using, thus reducing the loading time and the number of assemblies loaded during game execution.
This also makes it easier for the developers. XNA is a cross-platform framework, and by breaking the API into multiple assemblies it allows them to factor out the platform-specific implementations more carefully, while allowing assemblies which are compatible across multiple platforms to remain intact. I should note, I'm not sure Microsoft is actually doing this, as a number of classes in Xna.Framework probably still require multiple implementations. For example, Xna.Framework still contains all of the audio classes (not including the Xact stuff), as well as all of the input classes (not including the new Touch library). So while Xna.Framework.dll could conceptually only contain classes which are shared across all platforms, I doubt that's the case....yet. Moving on.

Another thing you'll likely notice from the screenshot is the way Content Projects are now handled. In Xna 3.1 content projects were embedded/sub-projects of the current project. Now they're isolated projects with "Content References" from the game projects. I'm not sure all of the implications of this yet, but I'll explore it more in a few weeks when I start delving into the content pipeline more aggressively. For now, it's just an observation.
Ok, with the project/file structure stuff out of the way, let's look at some code. This is the code for our starting game, which I will use as the basis for all additional NeHe style demos.
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
As with before, XNA Game Studio likes to include every XNA namespace as well as the kitchen sink when starting a new project. However, in an effort to limit the scope, I removed the namespaces I'm not going to need here. Incidentally, this means you can remove some of the referenced assemblies as well. Don't forget to do that!
namespace JWalsh.XnaSamples
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
KeyboardState prevKeyboardState = Keyboard.GetState();
The next thing you see is the usual namespace, the Game1 class definition, and a few fields. You'll notice that I've added a KeyboardState right away. I've done this because the demos/tutorials in this series will all focus (initially) on the PC. I'll be re-adding some WinPhone 7 and Xbox 360 versions at a later date as well. Anyways... we'd like to be able to toggle between fullscreen and windowed mode, as well as hit ESC in order to exit the program. At least one of them requires a previous state. We can ignore previous state when the user hits ESC.
Two things you'll notice missing from the Game class are the previously deprecated LoadGraphicsContent and UnloadGraphicsContent methods. Yup, that's right. If you were still using those out-dated methods it's time to make the switch, because your code isn't going to compile otherwise.
Next, XNA continues to put the SpriteBatch member field in Game1 for you, and they are happy to initialize it in LoadContent as well, however I've removed it for these demos. When we get to the point we need sprites, I'll re-add it. Until then, no reason to have it around.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
// Allow users to resize the window, and handle the Projection Matrix on Resize
Window.Title = "JWalsh's XNA Demo";
Window.AllowUserResizing = true;
Window.ClientSizeChanged += OnClientSizeChanged;
}
After the fields is the constructor. Not much has changed here. I still create a GraphicsDevice in order to ensure the proper functioning of the ContentManager, and to make sure my Draw phase works correctly, and I also set the RootDirectory on the ContentManager as normal.
The following four lines are the unique elements of this demo. This ensures the mouse is visible, gives the app a friendly title, allows the window to be resized, and then captures any OnClientSizeChanged events in order to allow us to change the projection matrix used by our 3D math to accommodate the new aspect ratio. I won't be adding that in this demo, but it'll be there in the next.
After that I'm going to add a helper property and our event handler.
protected bool IsFullScreen
{
get { return graphics.IsFullScreen; }
set
{
if (value != graphics.IsFullScreen)
{
// Toggle FullScreen, and Mouse Display, then apply the changes
// on the DeviceManager
graphics.IsFullScreen = !graphics.IsFullScreen;
IsMouseVisible = !IsMouseVisible;
graphics.ApplyChanges();
}
}
}
protected void OnClientSizeChanged(object sender, EventArgs e)
{
}
This property just allows us to easily toggle between fullscreen and windowed mode. After that is the blank OnClientSizeChanged method. Nothing to see here folks.
Then we've got a few (more or less) empty methods.
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
}
protected override void UnloadContent()
{
}
As with previous versions of XNA, you must call base.Initialize() in the Initialize method if you want LoadContent to be called. In this demo it's not a huge necessity. But in all additional demos it'll be essential that LoadContent be called. After those functions we have the Update method. This method is one of the few methods that had any modifications to it for this project.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
KeyboardState keyboard = Keyboard.GetState();
if (keyboard.IsKeyDown(Keys.Escape) )
Exit();
if (keyboard.IsKeyDown(Keys.F11) && prevKeyboardState.IsKeyUp(Keys.F11))
IsFullScreen = !IsFullScreen;
prevKeyboardState = keyboard;
}
In Update, we capture the Back button of the Game Pad as usual, so we can close the application with the Game Pad. In addition, we get the keyboard state so we can check to see whether ESC is down, or whether the F11 key was pressed. Note: By checking to see if F11 is down NOW, but wasn't PREVIOUSLY, it avoids the nasty situation of changing back and forth between fullscreen and windowed mode a bunch of times because you held the F11 key down too long.
Another thing to note about Update is that while it still receives a GameTime object, the GameTime object itself has changed. Noticeably absent are the ElapsedRealtime and TotalRealTime properties. Presumably, the difference between game time and real time was confusing enough to developers that they did away with wall clock time. Now, holding the window by dragging the titlebar around, or resizing the window causes the game to stop processing. When you release the window, it continues updating as though no time at all has elapsed. For those who still want access to the wall-clock time, you've still got the DateTime class, which gives millisecond precise times.
And finally we come to the Draw method. In this demo the only thing the Draw method does is clear the back buffer. Again, nothing to see here folks!
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
}
}
}
If you enter in the above, and/or download the full project file below and compile/run it, you'll see the following, unexciting screen:

Hopefully this brief look at the starting project and some of the changes to the API have been interesting. In the next project we'll starting drawing some triangles to the screen.