Using the Terrain

Let’s see our terrain in action. First we’ll need to make some changes in our ExampleGame class. We’ll add a Terrain field:

    // The terrain 
    Terrain terrain;

In our ExampleGame.LoadContent(), we’ll load the heightmap and construct our terrain:

    // Build the terrain
    Texture2D heightmap = Content.Load<Texture2D>("heightmap");
    terrain = new Terrain(this, heightmap, 10f, Matrix.Identity);

And in our ExampleGame.Draw() we’ll render it with the existing camera:

    // Draw the terrain

Now if you run the game, you should see your terrain, and even be able to move around it using the camera controls (WASD + Mouse).

The rendered terrain The rendered terrain

You’ll probably notice that your camera does not change position as you move over the terrain - in fact, in some parts of the map you can actually end up looking up from underneath!

Clearly we need to do a bit more work. We need a way to tell the camera what its Y-value should be, based on what part of the terrain it is over.

The IHeightMap Interface

Rather than linking our camera directly to our terrain implementation, let’s define an interface that could be used for any surface the player might be walking on. For lack of a better name, I’m calling this interface IHeightMap:

    /// <summary>
    /// An interface providing methods for determining the 
    /// height at a point in a height map
    /// </summary>
    public interface IHeightMap
        /// <summary>
        /// Gets the height of the map at the specified position
        /// </summary>
        /// <param name="x">The x coordinate in the world</param>
        /// <param name="z">The z coordinate in the world</param>
        /// <returns>The height at the specified position</returns>
        float GetHeightAt(float x, float z);

The interface defines a single method, GetHeightAt(). Note that we take the X and Z coordinate - these are world coordinates in the game. The return value is the Y world coordinate corresponding to the elevation of the terrain at x and z.

Refactoring FPSCamera

We can then use this interface within our FPSCamera class to change its height based on its X and Z. We’ll start by adding a property of type ICamera:

    /// <summary>
    /// Gets or sets the heightmap this camera is interacting with
    /// </summary>
    public IHeightMap HeightMap { get; set; }

We also might want to add a property to say how far above any heightmap we want the camera to be. Let’s call this HeightOffset:

    /// <summary>
    /// Gets or sets how high above the heightmap the camera should be
    /// </summary>
    public float HeightOffset { get; set; } = 5;

And we’ll modify our FPSCamera.Update() to use the HeightMap and HeightOffset to determine the camera’s Y position:

    // Adjust camera height to heightmap 
    if(HeightMap != null)
        position.Y = HeightMap.GetHeightAt(position.X, position.Z) + HeightOffset;

This should be done before we set the updated View matrix.

Notice that we wrap this in a null check. If there is no heightmap, we want to keep our default behavior.

Refactoring ExampleGame

Since the HeightMap is a property of the FPSCamera, we’ll need to set it to our terrain in the ExampleGame.LoadContent() method after both the camera and terrain have been created:

    camera.HeightMap = Terrain;

Refactoring Terrain

Now we need to implement the IHeightMap interface in our Terrain class. Add it to the class definition:

public class Terrain : IHeightMap {

And add the method it calls for:

    /// <summary>
    /// Gets the height of the terrain at
    /// the supplied world coordinates
    /// </summary>
    /// <param name="x">The x world coordinate</param>
    /// <param name="z">The z world coordinate</param>
    /// <returns></returns>
    public float GetHeightAt(float x, float z)

Now, let’s talk through the process of finding the height. As our comments suggest, we’re using world coordinates, not model coordinates. As long as the world matrix remains the identity matrix, these are the same. But as soon as that changes, the world coordinates no longer line up. So the first thing we need to do is transform them from world coordinates to model coordinates.

Since multiplying a vector in model coordinates by the world matrix transforms them into world coordinates, the inverse should be true. Specifically, multiplying world coordinates by the inverse of the world matrix should transform them into model coordinates.

The Matrix.Invert() method can create this inverse matrix:

    Matrix inverseWorld = Matrix.Invert(effect.World);

We’ll also need the world coordinates as a Vector3 to transform:

    Vector3 worldCoordinates = new Vector3(x, 0, z);

Here we don’t care about the y value, so we’ll set it to 0.

Then we can apply the transformation with Vector3.Transform():

    Vector3 modelCoordinates = Vector3.Transform(worldCoordinates, inverseWorld);

At this point, modelCoordinates.X and modelCoordinates.Z correspond to the x and -y indices of our heights array, respectively. The y coordinate needs to be inverted, because our terrain was defined along the negative z-axis (as the positive z-axis is towards the screen). Let’s save them in float variables so we don’t have to remember to invert the z as our y coordinate:

    float tx = modelCoordinates.X;
    float ty = -modelCoordinates.Z;

These should correspond to the x and y indices in the heights array, but it is also possible that they are out-of-bounds. It’s a good idea to check:

    if (tx < 0 || ty < 0 || tx >= width || ty >= height) return 0;

If we’re out-of-bounds, we’ll just return a height of 0. Otherwise, we’ll return the value in our heights array:

    return heights[(int)tx, (int)ty];

Now try running the game and exploring your terrain. The camera should now move vertically according to the elevation!