We’ll continue our exploration of the rendering pipeline with another shape - a cube. And as before, we’ll introduce another concept - vertex and index buffers.
For our triangle and quad, we drew our shapes using GraphicsDevice.DrawUserPrimitives<T>()
and GraphicsDevice.DrawUserIndexedPrimitives<T>()
. Our vertices
and indices
were simply arrays we declared normally, that we passed to the graphics card using the aforementioned methods. As with most variables we deal with in C#, the memory used by the vertices
and indices
arrays was allocated from the computer’s RAM. When we invoked the methods, one of the tasks they do is stream the data to the GPU, so that it can be rendered.
This process can be significantly sped up if, instead of storing the vertex and index data in RAM, we store it in Video Ram (VRAM). VRAM is the memory that is a part of our graphics card. Not surprisingly, the GPU has direct access to VRAM and can stream data from it quite quickly - more quickly than it can from RAM. Especially if we are drawing the same shape using the same vertex and index data every frame.
So how do we get this data into the VRAM? We have to create a VertexBuffer and an IndexBuffer. Let’s create a Cube
class with instances of these classes.
Defining the Cube Class
We’ll declare a Cube
class much like our Triangle
and Quad
:
/// <summary>
/// A class for rendering a cube
/// </summary>
public class Cube
{
/// <summary>
/// The vertices of the cube
/// </summary>
VertexBuffer vertices;
/// <summary>
/// The vertex indices of the cube
/// </summary>
IndexBuffer indices;
/// <summary>
/// The effect to use rendering the cube
/// </summary>
BasicEffect effect;
/// <summary>
/// The game this cube belongs to
/// </summary>
Game game;
}
The only real difference at this point is the use of a VertexBuffer
and IndexBuffer
as fields.
Creating the Vertex Buffer
We need to create the vertex data in much the same way we did with our other shapes - we start with a collection of vertices (we’ll use an array again, and give our vertices colors), but then we’ll copy that data into the VertexBuffer
, which effectively copies it into VRAM (if space is available). Once again we’ll wrap this in an initialization method:
/// <summary>
/// Initialize the vertex buffer
/// </summary>
public void InitializeVertices()
{
var vertexData = new VertexPositionColor[] {
new VertexPositionColor() { Position = new Vector3(-3, 3, -3), Color = Color.Blue },
new VertexPositionColor() { Position = new Vector3( 3, 3, -3), Color = Color.Green },
new VertexPositionColor() { Position = new Vector3(-3, -3, -3), Color = Color.Red },
new VertexPositionColor() { Position = new Vector3( 3, -3, -3), Color = Color.Cyan },
new VertexPositionColor() { Position = new Vector3(-3, 3, 3), Color = Color.Blue },
new VertexPositionColor() { Position = new Vector3( 3, 3, 3), Color = Color.Red },
new VertexPositionColor() { Position = new Vector3(-3, -3, 3), Color = Color.Green },
new VertexPositionColor() { Position = new Vector3( 3, -3, 3), Color = Color.Cyan }
};
vertices = new VertexBuffer(
game.GraphicsDevice, // The graphics device to load the buffer on
typeof(VertexPositionColor), // The type of the vertex data
8, // The count of the vertices
BufferUsage.None // How the buffer will be used
);
vertices.SetData<VertexPositionColor>(vertexData);
}
We declare our vertexData
as an instance variable, which means once this method returns, its memory will be reclaimed. The VertexBuffer
constructor allocates the space needed for the data in VRAM, while the VertexBuffer.SetData()
method what actually copies it into that location, managing the process for us.
If we were writing DirectX code in C++, we would need to:
- Allocate memory in VRAM for the buffer
- Lock that memory location (remember, both the GPU and CPU can access VRAM at a given time)
- Copy the bytes of the buffer into that location, and
- Release the lock
If you are interested in seeing what the equivalent code in C++ looks like, visit www.directxtutorial.com. Our vertex and index data is adapted from the cube presented there.
Creating the Index Buffer
The index buffer is initialized and the data copied in the same way:
/// <summary>
/// Initializes the index buffer
/// </summary>
public void InitializeIndices()
{
var indexData = new short[]
{
0, 1, 2, // Side 0
2, 1, 3,
4, 0, 6, // Side 1
6, 0, 2,
7, 5, 6, // Side 2
6, 5, 4,
3, 1, 7, // Side 3
7, 1, 5,
4, 5, 0, // Side 4
0, 5, 1,
3, 7, 2, // Side 5
2, 7, 6
};
indices = new IndexBuffer(
game.GraphicsDevice, // The graphics device to use
IndexElementSize.SixteenBits, // The size of the index
36, // The count of the indices
BufferUsage.None // How the buffer will be used
);
indices.SetData<short>(indexData);
}
Initializing the Effect
And our BasicEffect
is configured identically to how we di so for our Triangle
class:
/// <summary>
/// Initializes the BasicEffect to render our cube
/// </summary>
void InitializeEffect()
{
effect = new BasicEffect(game.GraphicsDevice);
effect.World = Matrix.Identity;
effect.View = Matrix.CreateLookAt(
new Vector3(0, 0, 4), // The camera position
new Vector3(0, 0, 0), // The camera target,
Vector3.Up // The camera up vector
);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4, // The field-of-view
game.GraphicsDevice.Viewport.AspectRatio, // The aspect ratio
0.1f, // The near plane distance
100.0f // The far plane distance
);
effect.VertexColorEnabled = true;
}
Drawing the Cube
Our cube.Draw()
method will be a bit different though:
/// <summary>
/// Draws the Cube
/// </summary>
public void Draw()
{
// apply the effect
effect.CurrentTechnique.Passes[0].Apply();
// set the vertex buffer
game.GraphicsDevice.SetVertexBuffer(vertices);
// set the index buffer
game.GraphicsDevice.Indices = indices;
// Draw the triangles
game.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList, // Tye type to draw
0, // The first vertex to use
0, // The first index to use
12 // the number of triangles to draw
);
}
Before we can use GraphicsDevice.DrawIndexedPrimitives(), we need to tell the GraphicsDevice
which VertexBuffer
and IndexBuffer
to use. The first is set with a method, the second through assignment. With both set, we can invoke GraphicsDevice.DrawIndexedPrimitives()
and it will draw the contents of the buffers.
Constructing the Cube
The Cube
constructor is back in familiar territory:
/// <summary>
/// Constructs a cube instance
/// </summary>
/// <param name="game">The game that is creating the cube</param>
public Cube(Game1 game)
{
this.game = game;
InitializeVertices();
InitializeIndices();
InitializeEffect();
}
Adding the Cube to Game1
To render our cube, we must go back to the Game1
class and add a reference to one:
// The cube to draw
Cube cube;
Construct it in the Game1.LoadContent()
method:
// Create the cube
cube = new Cube(this);
And draw it in the Game1.Draw()
method:
// draw the cube
cube.Draw();
If you run your code now, the cube is so big that the front face takes up the whole screen!
Changing the View Matrix
We could scale our cube down with a world transform - but rather than doing that, let’s look at it from farther away by changing the view transform. Let’s add an Update()
method to our Cube
class:
/// <summary>
/// Updates the Cube
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
// Look at the cube from farther away
effect.View = Matrix.CreateLookAt(
new Vector3(0, 5, -10),
Vector3.Zero,
Vector3.Up
);
}
And invoke this method in the Game1.Update()
method:
// update the cube
cube.Update(gameTime);
Now if you run the program, you should see the cube, and be able to see that its edges are distorted to simulate depth:
Let’s set our viewpoint rotating around the cube by refactoring our Cube.Update()
method:
/// <summary>
/// Updates the Cube
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
float angle = (float)gameTime.TotalGameTime.TotalSeconds;
// Look at the cube from farther away while spinning around it
effect.View = Matrix.CreateRotationY(angle) * Matrix.CreateLookAt(
new Vector3(0, 5, -10),
Vector3.Zero,
Vector3.Up
);
}
Now our cube appears to be spinning! But actually, it is our vantage point that is circling the cube, unlike the triangle, which is actually spinning in place. The final effect is the same, but one spin is applied to the world transform, and the other to the view transform. We’ll speak about both in more detail later.