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.

Terrain

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)

Finished Terrain

Download the Code for this Chapter

Back to Game Engine Tutorial Series

17 Responses to “XNA Game Engine Tutorial Series #8: Basic Terrain” »

  1. Comment by Xan — December 17, 2008 @ 4:23 pm

    I don’t believe you included HeightMapObject, although I believe I figured it out on my own…

  2. Comment by Sean — December 17, 2008 @ 5:20 pm

    You’re right, its in there now.

  3. Comment by Ilya Ostrovskiy — December 19, 2008 @ 8:20 pm

    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!

  4. Comment by Sean — December 19, 2008 @ 11:16 pm

    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.

  5. Comment by Meclor — February 25, 2009 @ 1:56 am

    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.

  6. Comment by Binxalot — May 26, 2009 @ 9:10 am

    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.

  7. Comment by Binxalot — May 26, 2009 @ 9:23 am

    Nvermind its this code here:
    terrainIndices[i++] = (short)(x + (z + 1) * width);
    terrainIndices[i++] = (short)(x + z * width);

  8. Comment by Hellboytb — June 30, 2009 @ 2:27 am

    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 ><

  9. Comment by BDAnders — July 11, 2009 @ 7:18 pm

    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.

  10. Comment by Ash — July 20, 2009 @ 3:30 am

    @ 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.

  11. Comment by Sean — July 20, 2009 @ 11:03 am

    Most desktops do, but a lot of laptops still haven’t caught up.

  12. Comment by Sam Armstrong — August 9, 2009 @ 2:55 pm

    Is there a reason that the boxes are sliding down the hill instead of rolling? Why is there no pitch, yaw, or roll?

  13. Comment by Sam Armstrong — August 9, 2009 @ 6:52 pm

    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.

  14. Comment by Eric — October 30, 2009 @ 2:05 am

    Is it possible to download this example? i really need help in my project.

  15. Comment by Eric — November 8, 2009 @ 8:11 am

    I wanna add trees to this terrain. Anyone can help?

  16. Comment by Eldon Elledge — November 15, 2009 @ 11:15 am

    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.

  17. Comment by Jim — February 17, 2010 @ 4:23 pm

    I tried to build these two classes into my projects but I see some references to PhysicsObject…..where is that located?

RSS feed for comments on this post. TrackBack URI

Leave a comment