A cube is a nice start; it shows you how you
can create entire 3D objects out of simple triangles. In this section,
you’re going to take this manual creation a step further with the
creation of terrain. If you’ve ever seen games that take place in an
outside area, like a flight simulator, then you’ve been exposed to
terrain rendering. Terrain rendering is the generation of a landscape,
normally based on a grid, which includes features like hills, valleys,
and mountains. Generating an outdoor environment might seem to be an
outlandish leap from a spinning cube, but it really isn’t that
different.
The entire terrain is going to be based on a
two-dimensional grid with a fixed number of columns and rows. The grid
is laid out as a series of quads made up of triangles; like I said, it’s
actually very similar to the cube you just created. Initially, because
the grid is flat, your terrain will appear as a large single-colored
quad unless you view it in wireframe mode. When viewing the wireframe,
you’ll be able to see all the triangles that make up the object.
Because it would be difficult and boring to
generate every triangle in a huge landscape by hand, I’m going to show
you how to dynamically generate any size terrain. Because the terrain is
going to be based on a grid, the extents of this grid need to be
defined first. Just to keep things even, I’m setting up the grid to be
sixteen columns and sixteen rows.
// Grid Information
#define NUM_COLS 16
#define NUM_ROWS 16
The cells that are created by the rows and columns
will need to have a bit of space between them so the vertices that will
make up the terrain aren’t bunched right next to each other. As you can
see in the following definitions, each of the cells is going to be
32×32 in size.
// The width and height of each cell in the grid
#define CELL_WIDTH 32
#define CELL_HEIGHT 32
The grid itself will have 16 columns but it takes
one more vertex to create the final cell. The following definitions
allow for this extra vertex and are used in the generation of the
terrain.
#define NUM_VERTSX (NUM_COLS + 1)
#define NUM_VERTSY (NUM_ROWS + 1)
Generating the Grid
Like the triangle and cube objects you created
before, the first step is the filling of a buffer with all the vertices
in the object. Instead of hardcoding all the vertices required, they’re
going to be generated using the extents of the grid. To do this I use a
series of nested for loops. The outside
loop goes through the rows with the inside loop representing the
columns. When creating the grid, it is being set up so that the grid
will extend away from the viewer in the positive Z direction. This
minimizes the amount of movement needed to position the virtual camera.
Each of the vertex positions is created by simply
multiplying the current X and Z values by the cell width and heights.
You’ll notice that the Y value for each vertex is being set to 0. This
will be changed later, but for now just understand that this keeps the
grid completely flat.
Take a look at the following code. This shows how
all the vertices for this grid are generated. The vertices that make up
the grid use the VertexPosColorStruct created earlier.
// create the vertices array large enough to hold all those needed
VertexPosColorStruct vertices[NUM_VERTSX * NUM_VERTSY];
// Fill the vertices array with the terrain values
for(int z=0; z < NUM_VERTSY; ++z)
{
for(int x=0; x < NUM_VERTSX; ++x)
{
vertices[x + z * NUM_VERTSX].Pos.x = (float)x * CELL_WIDTH;
vertices[x + z * NUM_VERTSX].Pos.z = (float)z * CELL_HEIGHT;
// Restrict the height to 0
vertices[x + z * NUM_VERTSX].Pos.y = 0.0f;
// Create the default color
vertices[x + z * NUM_VERTSX].Color = D3DXVECTOR4(1.0, 0.0f, 0.0f, 0.0f);
}
}
After the vertex array is full, you need to
create and fill the array of indices for the index buffer. To keep the
index array simple, it is going to be filled with a series of triangles
to create a triangle list. The first step is the sizing of the indices
array. Each cell you create will require two triangles for a total of
six vertices. The index array is sized to allow for all six vertices per
cell.
Again, the indices are created using a series of
nested loops. This keeps you from having to manually define all the
indices for the array. Each cell is made up of a quad, so the inside of
the loops will need to create two triangles. The triangles themselves
are going to be laid out in a counterclockwise manner, making sure that
all the triangles have the same winding order. Because the grid is being
generated with triangle lists, there are going to be some vertices that
are duplicated. Figure 1 shows how the triangles in the grid will be laid out.
You’ll see that each of the triangles uses
vertices from both the surrounding columns and rows. Look at the
following code example that uses this layout to define the indices.
// Create the indices array, six vertices for each cell
DWORD indices[NUM_VERTSX * NUM_VERTSY * 6];
// The index counter
int curIndex = 0;
// Fill the indices array to create the triangles needed for the terrain
// The triangles are created in a counterclockwise direction
for (int z=0; z < NUM_ROWS; z++)
{
for (int x=0; x < NUM_COLS; x++)
{
// The current vertex to build off of
int curVertex = x + (z * NUM_VERTSX);
// Create the indices for the first triangle
indices[curIndex] = curVertex;
indices[curIndex+1] = curVertex + NUM_VERTSX;
indices[curIndex+2] = curVertex + 1;
// Create the indices for the second triangle
indices[curIndex+3] = curVertex + 1;
indices[curIndex+4] = curVertex + NUM_VERTSX;
indices[curIndex+5] = curVertex + NUM_VERTSX + 1;
// increment curIndex by the number of vertices for the two triangles
curIndex += 6;
}
}
Once the index array is full, you can use it to create the index buffer you’ll need when drawing the grid. Figure 2 shows what the grid looks like when rendered in wireframe mode.
Generating Terrain
The only difference between the grid you just
created and an outdoor environment is height. As I mentioned before,
outdoor environments have hills and valleys, so how do you add these
features to the flat grid you have now? The key to adding height is in
the code that generates the vertices. Remember how the Y value for all
the vertices was being set to 0? By altering the value stored in Y, the
height of that cell in the grid is altered as well. This value can be
either positive, generating a hill or a negative value, allowing for a
dip in the grid.
To allow for more of a dynamic variation, the following code uses the rand function to create the Y value and then uses the modulus (%) operator to restrict the height within the range of CELL_HEIGHT. This will keep the heights from being huge numbers.
// create the vertices array large enough to hold all those needed
VertexPosColorStruct vertices[NUM_VERTSX * NUM_VERTSY];
// Fill the vertices array with the terrain values
for(int z=0; z < NUM_VERTSY; ++z)
{
for(int x=0; x < NUM_VERTSX; ++x)
{
vertices[x + z * NUM_VERTSX].Pos.x = (float)x * CELL_WIDTH;
vertices[x + z * NUM_VERTSX].Pos.z = (float)z * CELL_HEIGHT;
// Allow the height of the cell to be randomly decided
vertices[x + z * NUM_VERTSX].Pos.y = (float)(rand() % CELL_HEIGHT);
// Create the default color
vertices[x + z * NUM_VERTSX].Color = D3DXVECTOR4(1.0, 0.0f, 0.0f, 0.0f);
}
}
Note
Terrain height is commonly generated using a
heightmap. The heightmap is either an input file with a series of height
values for each part of the grid or a grayscale image file where the
brightness of each pixel determines the height in the grid.
If you look at Figure 3
you’ll see the difference that the height makes.