XNA Game Engine Tutorial Series #8: Basic Terrain
In this tutorial we will be creating basic terrain from a heightmap. This is starting code for a more advanced terrain we’ll make in a later tutorial. This one has no efficiency algorithms or fancy drawing techniques like multitexturing and atmospheric scattering. The idea here is that you have a grayscale picture, and you generate vertices based on the width and height of the image, and set the heights of those vertices based on how light or dark the pixels in the image are. The terrain will also have a texture on it and basic lighting, since we are using BasicEffect for drawing.

Note: Some of the following was borrowed from Riemer Grootjans’ “XNA 2.0 Game Programming Recipes”, Chapter 5: “Getting the Most Out of Vertices”
First, the setup code:
using System;
using JigLibX.Geometry;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Innovation
{
public class Terrain : Component, I3DComponent
{
// Height representation
public float[,] heightData;
// Physics height representation
HeightMapInfo heightMapInfo;
// Physics object
HeightMapObject heightMapObject;
// Terrain texture
Texture2D texture;
// Vertex and index buffers
VertexDeclaration myVertexDeclaration;
VertexBuffer terrainVertexBuffer;
IndexBuffer terrainIndexBuffer;
// Effect
BasicEffect basicEffect;
// I3DComponent values
Vector3 position = Vector3.Zero;
Matrix rotation = Matrix.Identity;
Vector3 scale = new Vector3(1, 1, -1);
BoundingBox boundingBox = new BoundingBox(new Vector3(-1), new Vector3(1));
public Vector3 Position { get { return position; } set { position = value; } }
public Vector3 EulerRotation
{
get { return MathUtil.MatrixToVector3(Rotation); }
set { this.Rotation = MathUtil.Vector3ToMatrix(value); }
}
public Matrix Rotation { get { return rotation; } set { rotation = value; } }
public Vector3 Scale { get { return scale; } set { scale = value; } }
public BoundingBox BoundingBox { get { return boundingBox; } }
// Constructors
public Terrain(Texture2D HeightMap, Texture2D Texture)
: base()
{
Setup(HeightMap, Texture);
}
public Terrain(Texture2D HeightMap, Texture2D Texture, GameScreen Parent)
: base(Parent)
{
Setup(HeightMap, Texture);
}
void Setup(Texture2D Heightmap, Texture2D Texture)
{
// Load height data
heightData = CreateTerrain(Heightmap, Texture);
// Create vertex and index buffers
myVertexDeclaration = new VertexDeclaration(Engine.GraphicsDevice,
VertexPositionNormalTexture.VertexElements);
VertexPositionNormalTexture[] terrainVertices = CreateVertices();
int[] terrainIndices = CreateIndices();
terrainVertices = GenerateNormalsForTriangleStrip(terrainVertices,
terrainIndices);
CreateBuffers(terrainVertices, terrainIndices);
// Setup effect
basicEffect = new BasicEffect(Engine.GraphicsDevice, null);
SetupEffect();
}
// Sets up terrain, texture, etc
private float[,] CreateTerrain(Texture2D heightMap, Texture2D texture)
{
// Minimum and maximum heights for terrain
float minimumHeight = 0;
float maximumHeight = 255;
// Width and height of terrain (from heightmap)
int width = heightMap.Width;
int height = heightMap.Height;
// Setup bounding box with width and height
boundingBox = new BoundingBox(
new Vector3(-width / 2, maximumHeight - minimumHeight, -height / 2),
new Vector3(width / 2, maximumHeight - minimumHeight, height / 2));
this.texture = texture;
// Get data from heightmap
Color[] heightMapColors = new Color[width * height];
heightMap.GetData<Color>(heightMapColors);
// Setup height data from heightmap data
float[,] heightData = new float[width, height];
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
heightData[x, y] = heightMapColors[x + y * width].R;
if (heightData[x, y] < minimumHeight)
minimumHeight = heightData[x, y];
if (heightData[x, y] > maximumHeight)
maximumHeight = heightData[x, y];
}
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
heightData[x, y] = (heightData[x, y] - minimumHeight)
/ (maximumHeight - minimumHeight) * 30.0f;
// Setup physics
heightMapInfo = new HeightMapInfo(heightData, 1);
if (heightMapObject != null)
{
heightMapObject.DisableComponent();
heightMapObject = null;
}
heightMapObject = new HeightMapObject(heightMapInfo, new Vector2(
heightMapInfo.Width / 2,
-heightMapInfo.Height / 2 + heightMapInfo.Height));
return heightData;
}
The following code actually creates the vertices and indices in the terrain object:
// Set up vertices
private VertexPositionNormalTexture[] CreateVertices()
{
// Get width and height and create new vertex array
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);
VertexPositionNormalTexture[] terrainVertices =
new VertexPositionNormalTexture[width * height];
// Calculate position, normal, and texcoords for vertices
int i = 0;
for (int z = 0; z < height; z++)
for (int x = 0; x < width; x++)
{
Vector3 position = new Vector3(x, heightData[x, z], -z);
Vector3 normal = new Vector3(0, 0, 1);
Vector2 texCoord = new Vector2((float)x / 30.0f,
(float)z / 30.0f);
terrainVertices[i++] = new VertexPositionNormalTexture(
position, normal, texCoord);
}
return terrainVertices;
}
// Set up indices
private int[] CreateIndices()
{
// Get width and height and create new index array
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);
int[] terrainIndices = new int[(width) * 2 * (height - 1)];
// Calculate indices for triangle
int i = 0;
int z = 0;
while (z < height - 1)
{
for (int x = 0; x < width; x++)
{
terrainIndices[i++] = x + z * width;
terrainIndices[i++] = x + (z + 1) * width;
}
z++;
if (z < height - 1)
{
for (int x = width - 1; x >= 0; x--)
{
terrainIndices[i++] = x + (z + 1) * width;
terrainIndices[i++] = x + z * width;
}
}
z++;
}
return terrainIndices;
}
// Generates normals for a group of triangles
private VertexPositionNormalTexture[] GenerateNormalsForTriangleStrip(
VertexPositionNormalTexture[] vertices, int[] indices)
{
for (int i = 0; i < vertices.Length; i++)
vertices[i].Normal = new Vector3(0, 0, 0);
bool swappedWinding = false;
for (int i = 2; i < indices.Length; i++)
{
Vector3 firstVec = vertices[indices[i - 1]].Position
- vertices[indices[i]].Position;
Vector3 secondVec = vertices[indices[i - 2]].Position
- vertices[indices[i]].Position;
Vector3 normal = Vector3.Cross(firstVec, secondVec);
normal.Normalize();
if (swappedWinding)
normal *= -1;
if (!float.IsNaN(normal.X))
{
vertices[indices[i]].Normal += normal;
vertices[indices[i - 1]].Normal += normal;
vertices[indices[i - 2]].Normal += normal;
}
swappedWinding = !swappedWinding;
}
for (int i = 0; i < vertices.Length; i++)
vertices[i].Normal.Normalize();
return vertices;
}
The rest of the code is for setting up the BasicEffect and for drawing:
// Sets up vertex and index buffers used for drawing
private void CreateBuffers(VertexPositionNormalTexture[] vertices,
int[] indices)
{
terrainVertexBuffer = new VertexBuffer(Engine.GraphicsDevice,
VertexPositionNormalTexture.SizeInBytes * vertices.Length,
BufferUsage.WriteOnly);
terrainVertexBuffer.SetData(vertices);
terrainIndexBuffer = new IndexBuffer(Engine.GraphicsDevice,
typeof(int), indices.Length, BufferUsage.WriteOnly);
terrainIndexBuffer.SetData(indices);
}
// Setup BasicEffect
private void SetupEffect()
{
basicEffect.Texture = texture;
basicEffect.TextureEnabled = true;
basicEffect.EnableDefaultLighting();
basicEffect.DirectionalLight0.Direction = new Vector3(1, -1, 1);
basicEffect.DirectionalLight0.Enabled = true;
basicEffect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
basicEffect.DirectionalLight1.Enabled = false;
basicEffect.DirectionalLight2.Enabled = false;
basicEffect.SpecularColor = new Vector3(0, 0, 0);
}
// Draw the terrain
public override void Draw()
{
// Require the camera
Camera camera = Engine.Services.GetService<Camera>();
if (camera == null)
throw new Exception("The engine services does not contain a "
+ "camera service. The terrain requires a camera to draw.");
// Set effect values
basicEffect.World = MathUtil.CreateWorldMatrix(position,
rotation, scale);
basicEffect.View = camera.View;
basicEffect.Projection = camera.Projection;
// Get width and height
int width = heightData.GetLength(0);
int height = heightData.GetLength(1);
// Terrain uses different vertex winding than normal models,
// so set the new one
Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
// Start the effect
basicEffect.Begin();
// For each pass..
foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
{
// Begin the pass
pass.Begin();
// Draw the terrain vertices and indices
Engine.GraphicsDevice.Vertices[0].SetSource(terrainVertexBuffer, 0,
VertexPositionNormalTexture.SizeInBytes);
Engine.GraphicsDevice.Indices = terrainIndexBuffer;
Engine.GraphicsDevice.VertexDeclaration = myVertexDeclaration;
Engine.GraphicsDevice.DrawIndexedPrimitives(
Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleStrip,
0, 0, width * height, 0, width * 2 * (height - 1) - 2);
// End the pass
pass.End();
}
// End the effect
basicEffect.End();
// Set the vertex winding back
Engine.GraphicsDevice.RenderState.CullMode =
CullMode.CullCounterClockwiseFace;
}
}
}
Finally, we need to make the physics object called HeightmapObject, which is the object that simulates heightmap physics with JigLibX:
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using JigLibX.Collision;
using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Math;
using Microsoft.Xna.Framework.Graphics;
using JigLibX.Utils;
#endregion
namespace Innovation
{
public class HeightMapObject : PhysicsObject
{
public HeightMapInfo info;
public HeightMapObject(HeightMapInfo heightMapInfo, Vector2 shift)
{
Setup(heightMapInfo, shift);
InitializeComponent(Engine.DefaultScreen);
}
public HeightMapObject(HeightMapInfo heightMapInfo, Vector2 shift, GameScreen parent)
{
Setup(heightMapInfo, shift);
InitializeComponent(parent);
}
void Setup(HeightMapInfo heightMapInfo, Vector2 shift)
{
Body = new Body(); // just a dummy. The PhysicObject uses its position to get the draw pos
CollisionSkin = new CollisionSkin(null);
info = heightMapInfo;
Array2D field = new Array2D(heightMapInfo.Heights.GetUpperBound(0), heightMapInfo.Heights.GetUpperBound(1));
for (int x = 0; x < heightMapInfo.Heights.GetUpperBound(0); x++)
{
for (int z = 0; z < heightMapInfo.Heights.GetUpperBound(1); z++)
{
field.SetAt(x, z, heightMapInfo.Heights[x, z]);
}
}
// move the body. The body (because its not connected to the collision
// skin) is just a dummy. But the base class should know where to
// draw the model.
Body.MoveTo(new Vector3(shift.X, 0, shift.Y), Matrix.Identity);
CollisionSkin.AddPrimitive(new Heightmap(field, shift.X, shift.Y, 1, 1), (int)MaterialTable.MaterialID.UserDefined, new MaterialProperties(0.7f, 0.7f, 0.6f));
PhysicsSystem.CurrentPhysicsSystem.CollisionSystem.AddCollisionSkin(CollisionSkin);
}
}
}
And that’s it! Here’s how it is used (Download the content: Content.zip):
Terrain terrain = new Terrain(
Engine.Content.Load<Texture2D>("Content/heightmap"),
Engine.Content.Load<Texture2D>("Content/grass"));
Run it and you will see your terrain (you will have to move the camera around a bit if you cant see it)


I don’t believe you included HeightMapObject, although I believe I figured it out on my own…
You’re right, its in there now.
Hmmm, I also believe that you left out the code for CreateBuffers. And the code you uploaded doesn’t even have the Terrain class and such…
Great tutorial nonetheless, thanks!
The CreateBuffers thing, you’re right. But the upload definitely does I’m looking right at it.. Its in the InnovationEngine project under Components>Environment. HeightmapObject is under Physics>HeightmapObject.
Great series of tutorials!
for those of you with old video cards that only support 16bit index buffers just change the CreateIndices() function to use “short” instead of “int”. Also change any other functions that reference the index array that is returned from CreateIndices().
you may still run into problems if you use a heightmap.bmp that is too large. so just scale the bitmap down. to 100×100 or 200×200. or whatever you want. just experiment around to find what works best for you.
Well, I can’t get the conversion to take, it says that it cant convert Int to short and I need to cast it, but when I do that the error never goes away.
Nvermind its this code here:
terrainIndices[i++] = (short)(x + (z + 1) * width);
terrainIndices[i++] = (short)(x + z * width);
Is there any way to run in from my laptop. I keep getting the same errors and even if i try fixing the errors it still gives problems ><
Following along in your tutorials, and I’ve run into a couple problems, where I’d have to get the file or function that went unexplained from your downloaded source, or had to make it myself. however I’ve run into something I’m not all sure what to do with.
I cannot find the HeightMapInfo class.
@ Meclor:
The minimum requirements for XNA games are as follows:
To run XNA Framework games on a computer running a Windows operating system, you need a graphics card that supports, at a minimum, Shader Model 1.1, and DirectX 9.0c. We recommend using a graphics card that supports Shader Model 2.0 because some samples and starter kits may require it.
I’m no expert but I would have thought that most, if not all, cards that support PS 1.1 and DX9.0c, as a minimum, have 32-bit index buffers anyway.
Most desktops do, but a lot of laptops still haven’t caught up.
Is there a reason that the boxes are sliding down the hill instead of rolling? Why is there no pitch, yaw, or roll?
Sorry, it turns out I screwed up the physics actor, and just didn’t notice. On a side note, if you want your object to have collision without rotation, get rid of the Matrix rotation member in the Physics Actor class.
Is it possible to download this example? i really need help in my project.
I wanna add trees to this terrain. Anyone can help?
This is a great tutorial, “Thank You” for taking the time to do this.
I would like to ask about the HeightMapInfo class. I do not see it anywhere and noticed at lease one other post about the same issue. There are serveral references to the class but I do not see the class itself.
Thanks in advance.
I tried to build these two classes into my projects but I see some references to PhysicsObject…..where is that located?