Terrain Generation with a Heightmap

  CU_MDX_Terrain.zip (736.9 KiB, 4,361 hits)


  CUnitFramework.zip (101.1 KiB, 20,443 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

In this tutorial, we’ll learn how to generate terrain from a gray scale image known as a height map. We can use the color of each pixel in the height map to specify the height of the corresponding terrain vertex. A darker gray means lower terrain and a lighter gray means higher terrain. Each pixel is represented by a byte. Therefore, the height value of a vertex is represented by a byte in the height map.

A file format that has 1 byte per pixel is the RAW file format. A RAW file is just a linear array of bytes, no header junk, no extra information, just the data that we want to read. You can create_ your own height map in a paint program that exports to the RAW format or you can use a program that generates the file for you. I used Terragen to generate my RAW file.

RAW File and Terrain TextureHeight MapTerrain Texture

WireframeOnce we read in the height data, we can generate our vertex data. To generate the terrain, we will create_ a grid out of a single indexed triangle strip. If you read the texturing tutorial, you may remember me briefly mentioning a TriangleStripPlane class I wrote to generate a grid with a customizable number of vertices. If we apply this to our terrain, we can make the grid have a vertex for each pixel in the height map. The only difference this time is that instead of a flat grid, each vertex has a y-value (height) that is determined from the value read in from the height map.

The grid will be built with a single triangle strip in a snake-like motion. We’ll render the bottom row of quads (2 triangles put together) from left to right. Then we’ll move to the next row up and render right to left. We keep going back and forth until the grid is complete.

When we move up to the next row, we can’t just go straight up from the last vertex in the previous row. Take a look at the diagram below. The figure on the right side of the diagram depicts the triangle strip from a side view. If we were to go straight up and our target vertex wasn’t perfectly in line with the last 2 vertices, an extra triangle would be rendered as shown by the orange line. To prevent this extra triangle, we have to use what are called degenerate triangles, or triangles with no volume. A triangle with no volume will not be rendered. To create_ the degenerate triangle, we simply repeat the last vertex of each row. However, since each adjacent triangle has the opposite winding, we need to repeat the vertex once more or else the triangle would be considered back-facing.

Triangle strip

The easiest way to texture terrain generated from a height map is to just stretch one texture across the surface of the grid. You can painstakingly create_ your own in a paint program or you can do what I did and have one generated by a terrain program like Terragen. If your texture is too small, the terrain will lose a lot of the texture detail. This is because there just isn’t enough pixel information in the texture to expand to when viewed close up. So the bigger and more detailed the texture, the better the terrain will look. The cost of this is more memory consumption. My texture is 1024×1024. Even at this size, the terrain is a bit pixelated when viewed close up.

To implement our terrain, we will create_ a terrain class called Terrain. This class will encapsulate all the functionality and information needed to generate some simple terrain from a RAW file and a texture.

using System;
using System.IO;
using Microsoft.DirectX;
using Microsoft.DirectX.Generic;
using Microsoft.DirectX.Direct3D;
using Microsoft.DirectX.Direct3D.CustomVertex;

namespace CUnit
{

/// Terrain class
public class Terrain : WorldTransform
{

private int[] m_height;
private int m_numIndices;
private VertexBuffer m_vb = null;
private IndexBuffer m_ib = null;
private Texture m_textureBase = null;
private VertexFormats m_format;

///

Create a new Terrain object.
/// Direct3D Device
/// RAW file name
/// Texture file name
public Terrain( Device device, string rawFile, string terrainTexture ) Toggle

{

// Load texture
terrainTexture = Utility.GetMediaFile( terrainTexture );
m_textureBase = new Texture( device, terrainTexture );

m_format = PositionTextured.Format;

Generate( device, rawFile );

}

///

Generates the vertex and index data
/// D3D device
/// .RAW file name
private void Generate( Device device, string rawFile ) Toggle

{

// Load height map
rawFile = Utility.GetMediaFile( rawFile );
Stream stream = File.OpenRead( rawFile );
long length = stream.Seek( 0, SeekOrigin.End );
m_height = new int[length];
stream.Seek( 0, SeekOrigin.Begin );
for ( int i = 0; i < length; i++ )
{

m_height[i] = stream.ReadByte();

}
stream.Close();

// Generate vertices
// Sqrt limits terrain to square RAW files
int width = (int)Math.Sqrt( (double)length );

// Vertices with 1 set of texture coordinates
PositionTextured[] verts = TriangleStripPlane.GeneratePositionTexturedWithHeight( width, width, m_height );
m_vb = new VertexBuffer( device, verts.Length * PositionTextured.StrideSize, Usage.WriteOnly,
PositionTextured.Format, Pool.Managed, null );
GraphicsBuffer buffer = m_vb.Lock( 0, 0, LockFlags.None );
buffer.Write( verts );
m_vb.Unlock();
buffer.Dispose();

// Generate indices
int[] indices = TriangleStripPlane.GenerateIndices32( width, width );
m_numIndices = indices.Length;
m_ib = new IndexBuffer( device, indices.Length * sizeof( int ), Usage.WriteOnly, Pool.Managed, false, null );
GraphicsBuffer<int> indexBuffer = m_ib.Lock<int>( 0, 0, LockFlags.None );
indexBuffer.Write( indices );
m_ib.Unlock();
indexBuffer.Dispose();

}

///

Renders the terrain.
/// Direct3D device
public void Render( Device device ) Toggle

{

device.Transform.World = Transform;
device.SetTexture( 0, m_textureBase );
device.SetStreamSource( 0, m_vb, 0, PositionTextured.StrideSize );

device.Indices = m_ib;
device.VertexFormat = m_format;
device.DrawIndexedPrimitives( PrimitiveType.TriangleStrip, 0, 0, m_height.Length, 0, m_numIndices – 2);

}

///

Clean up resources
public void Dispose() Toggle

{

if ( m_vb != null )
{

m_vb.Dispose();
m_vb = null;

}
if ( m_ib != null )
{

m_ib.Dispose();
m_ib = null;

}
if ( m_textureBase != null )
{

m_textureBase.Dispose();
m_textureBase = null;

}
if ( m_textureDetail != null )
{

m_textureDetail.Dispose();
m_textureDetail = null;

}

}

}

}

To create_ the terrain, we first read in the height data from the raw file. We can do this with the normal C# file reading constructs. Since this isn’t a C# tutorial, I won’t go over that part. After we load the texture, we create_ the vertex and index data. Since this data is generated by the TriangleStripPlane class, our Terrain class looks really simple. After we have all the vertex and index data, we shove them into their corresponding buffers.

Since the terrain is located in vertex and index buffers, rendering it is a snap. First we set all of the terrains buffers, transforms, texture, etc., and then we call Device.DrawIndexedPrimitive. A nice thing about the TriangleStripPlane class is that the number of primitives in the grid is always equal to the number of indices – 2.

using System;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D.CustomVertex;

namespace CUnit
{

///
/// The TriangleStripPlane is used to create_ a grid from a single
/// indexed triangle strip. The grid will lie on the XZ-plane.
///

public class TriangleStripPlane
{

/// Generate PositionNormalTextured vertices.
/// Number of vertices along the width
/// Number of vertices along the length
/// Array of PositionNormalTextured vertices
public static PositionNormalTextured[] GeneratePositionNormalTextured( int verticesAlongWidth, int verticesAlongLength ) Toggle

{

if ( verticesAlongLength < 2 || verticesAlongWidth < 2 )
{

throw new Exception( “Can’t create_ a strip with the specified dimensions” );

}

PositionNormalTextured[] verts = new PositionNormalTextured[verticesAlongWidth * verticesAlongLength];
for ( int z = 0; z < verticesAlongLength; z++ )
{

for ( int x = 0; x < verticesAlongWidth; x++ )
{

// Center the grid in model space
float halfWidth = ((float)verticesAlongWidth – 1.0f) / 2.0f;
float halfLength = ((float)verticesAlongLength – 1.0f) / 2.0f;
PositionNormalTextured vertex = new PositionNormalTextured();
vertex.X = (float)x – halfWidth;
vertex.Y = 0.0f;
vertex.Z = (float)z – halfLength;
vertex.Nx = 0.0f;
vertex.Ny = 1.0f;
vertex.Nz = 0.0f;
vertex.U = (float)x / (verticesAlongWidth – 1);
vertex.V = (float)z / (verticesAlongLength – 1);
verts[z * verticesAlongLength + x] = vertex;

}

}
return verts;

}

///

Generate PositionTextured vertices with custom height values
/// Number of vertices along the width
/// Number of vertices along the length
/// Height values
/// Array of PositionTextured vertices
public static PositionTextured[] GeneratePositionTexturedWithHeight( int verticesAlongWidth, int verticesAlongLength, int[] height ) Toggle

{

if ( verticesAlongLength < 2 || verticesAlongWidth < 2 )
{

throw new Exception( “Can’t create_ a strip with the specified dimensions” );

}

PositionTextured[] verts = new PositionTextured[verticesAlongWidth * verticesAlongLength];
for ( int z = 0; z < verticesAlongLength; z++ )
{

for ( int x = 0; x < verticesAlongWidth; x++ )
{

// Center the grid in model space
float halfWidth = ((float)verticesAlongWidth – 1.0f) / 2.0f;
float halfLength = ((float)verticesAlongLength – 1.0f) / 2.0f;
PositionTextured vertex = new PositionTextured();
vertex.X = (float)x – halfWidth;
vertex.Y = (float)height[z * verticesAlongLength + x];
vertex.Z = (float)z – halfLength;
vertex.U = (float)x / (verticesAlongWidth – 1);
vertex.V = (float)z / (verticesAlongLength – 1);
verts[z * verticesAlongLength + x] = vertex;

}

}
return verts;

}

///

Generate PositionTextured vertices with custom height values
/// Number of vertices along the width
/// Number of vertices along the length
/// Height values
/// Array of PositionTextured vertices
public static Vertex.Position2Textured[] GeneratePosition2TexturedWithHeight( int verticesAlongWidth, int verticesAlongLength, int[] height ) Toggle

{

if ( verticesAlongLength < 2 || verticesAlongWidth < 2 )
{

throw new Exception( “Can’t create_ a strip with the specified dimensions” );

}

Vertex.Position2Textured[] verts = new Vertex.Position2Textured[verticesAlongWidth * verticesAlongLength];
for ( int z = 0; z < verticesAlongLength; z++ )
{

for ( int x = 0; x < verticesAlongWidth; x++ )
{

// Center the grid in model space
float halfWidth = ((float)verticesAlongWidth – 1.0f) / 2.0f;
float halfLength = ((float)verticesAlongLength – 1.0f) / 2.0f;
Vertex.Position2Textured vertex;
vertex.X = (float)x – halfWidth;
vertex.Y = (float)height[z * verticesAlongLength + x];
vertex.Z = (float)z – halfLength;
vertex.Tu1 = (float)x / (verticesAlongWidth – 1);
vertex.Tv1 = (float)z / (verticesAlongLength – 1);
vertex.Tu2 = (float)x / 10.0f;
vertex.Tv2 = (float)z / 10.0f;
verts[z * verticesAlongLength + x] = vertex;

}

}
return verts;

}

///

Generate the 32-bit indices for a plane.
/// Number of vertices along the width
/// Number of vertices along the length
/// 32-bit indices for an indexed triangle strip plane
public static int[] GenerateIndices32( int verticesAlongWidth, int verticesAlongLength ) Toggle

{

int numIndices = (verticesAlongWidth * 2) * (verticesAlongLength – 1) + (verticesAlongLength – 2);

int[] indices = new int[numIndices];
int index = 0;
for ( int z = 0; z < verticesAlongLength – 1; z++ )
{

// Even rows move left to right, odd rows move right to left.
if ( z % 2 == 0 )
{

// Even row
int x;
for ( x = 0; x < verticesAlongWidth; x++ )
{

indices[index++] = x + (z * verticesAlongWidth);
indices[index++] = x + (z * verticesAlongWidth) + verticesAlongWidth;

}
// Insert degenerate vertex if this isn’t the last row
if ( z != verticesAlongLength – 2)
{

indices[index++] = –x + (z * verticesAlongWidth);

}

}
else
{

// Odd row
int x;
for ( x = verticesAlongWidth – 1; x >= 0; x– )
{

indices[index++] = x + (z * verticesAlongWidth);
indices[index++] = x + (z * verticesAlongWidth) + verticesAlongWidth;

}
// Insert degenerate vertex if this isn’t the last row
if ( z != verticesAlongLength – 2)
{

indices[index++] = ++x + (z * verticesAlongWidth);

}

}

}
return indices;

}

///

Generate the 16-bit indices for a plane.
/// Number of vertices along the width
/// Number of vertices along the length
/// 16-bit indices for an indexed triangle strip plane
public static ushort[] GenerateIndices16( int verticesAlongWidth, int verticesAlongLength ) Toggle

{

int numIndices = ( verticesAlongWidth * 2 ) * ( verticesAlongLength – 1 ) + ( verticesAlongLength – 2 );

ushort[] indices = new ushort[numIndices];
int index = 0;
for ( int z = 0; z < verticesAlongLength – 1; z++ )
{

// Even rows move left to right, odd rows move right to left.
if ( z % 2 == 0 )
{

// Even row
int x;
for ( x = 0; x < verticesAlongWidth; x++ )
{

indices[index++] = (ushort)(x + ( z * verticesAlongWidth ));
indices[index++] = (ushort)(x + ( z * verticesAlongWidth ) + verticesAlongWidth);

}
// Insert degenerate vertex if this isn’t the last row
if ( z != verticesAlongLength – 2 )
{

indices[index++] = (ushort)(–x + ( z * verticesAlongWidth ));

}

}
else
{

// Odd row
int x;
for ( x = verticesAlongWidth – 1; x >= 0; x– )
{

indices[index++] = (ushort)(x + ( z * verticesAlongWidth ));
indices[index++] = (ushort)(x + ( z * verticesAlongWidth ) + verticesAlongWidth);

}
// Insert degenerate vertex if this isn’t the last row
if ( z != verticesAlongLength – 2 )
{

indices[index++] = (ushort)(++x + ( z * verticesAlongWidth ));

}

}

}
return indices;

}

}

}

This is the TriangleStripPlane class you’ve been reading about all this time. The algorithm is described at the start of this tutorial, but the best way to understand the algorithm is to work it out on paper. You can use the figure at the beginning for reference. Notice in GeneratePositionTexturedWithHeight, that we are setting the Y value of the vertex to the corresponding value of the height array. This is the height array that we filled up with the height map values.

Note: if all you see is the blue background and no terrain, your video card probably doesn’t support 32-bit index buffers, which is the implementation in this tutorial. If that is the case, you’ll have to modify the code to create_ an index buffer of a 16-bit data type like ushort instead of int. Once you render out the terrain, you’ll have something like the following:

Terrain