Commit e12cae2e authored by Nathan H. Bean's avatar Nathan H. Bean

Added heightmap terrain section

parent ae9d7c6e
---
title: "Introduction"
pre: "1. "
weight: 1
date: 2020-03-24T10:00:00-05:00
---
Now that we understand how 3D worlds are built from triangle meshes, and how we can use cameras to explore those worlds, let's start putting those ideas to work. In this section, we'll focus on creating terrain from a heightmap - a grayscale bitmap representing the changing elevation of the ground.
Like our earlier examples, we'll start from a starter project with our assets pre-loaded. In addition, we'll include the `ICamera` interface and the `FPSCamera` we created in the lesson on Lighting and Cameras. It is also preloaded with public-domain content assets, including a heightmap from Wikimedia and a grass texture from Para on OpenGameArt's [Synthetic Grass Texture Pack](https://opengameart.org/content/synthetic-grass-texture-pack).
You can find the starter project here: [https://github.com/ksu-cis/heightmap-terrain-starter](https://github.com/ksu-cis/heightmap-terrain-starter)
\ No newline at end of file
---
title: "Heightmaps"
pre: "2. "
weight: 2
date: 2020-03-24T10:00:00-05:00
---
You might be wondering just what a _heightmap_ is. If you've ever used a [topographic map](https://en.wikipedia.org/wiki/Topographic_map), you've seen a similar idea. Countour maps include _countour_ lines_, lines that trace when the ground reaches a certain altitude. Inside the line is higher than that altitude, and outside of the line is lower (or visa-versa). The countours themselves are typically marked with the altitude they represent.
A heightmap is similar, but instead of using lines, each _pixel_ in the map represents a square section of land, and the color value at that point indicates the average altitude of that square. Since there is only one value to represent, heightmaps are typically created in grayscale. And, to optimize space, they may also be saved in a monochrome format (where each pixel is stored as a single 8-bit value, instead of the 32-bits typical for storing RGB values).
![Heightmap Example]({{<static "images/Heightmap.png">}})
You can obtain heightmaps in a number of ways. You can draw a heightmap with any raster graphics program, though it takes a lot of skill and patience to make one that mimicks natrual terrain. You can also get real-world heightmaps directly from organizations like the [USGS](http://earthexplorer.usgs.gov/) or [NASA's Viewfinder Project](http://viewfinderpanoramas.org/Coverage%20map%20viewfinderpanoramas_org3.htm). Or you can generate one using Perlin Noise and algorithms that mimic the results of plate tetonics. There also exist many height-map generation programs, both open-source and commercial.
Along with the height map, you also need to know the sampling resolution (how large each terrain square should be), and the scale that should be applied to the heights (as the pixel values of the heightmap will be in values between 0 and 255).
Now, let's turn our attention to creating a Terrain class that will use a heightmap.
\ No newline at end of file
This diff is collapsed.
---
title: "Using the Terrain"
pre: "4. "
weight: 4
date: 2020-03-24T10:00:00-05:00
---
Let's see our terrain in action. First we'll need to make some changes in our `Game1` class. We'll add a `Terrain` field:
```csharp
// The terrain
Terrain terrain;
```
In our `Game1.LoadContent()`, we'll load the heihtmap and construct our terrain:
```csharp
// Build the terrain
Texture2D heightmap = Content.Load<Texture2D>("heightmap");
terrain = new Terrain(this, heightmap, 10f, Matrix.Identity);
```
And in our `Game1.Draw()` we'll render it with the existing camera:
```csharp
// Draw the terrain
terrain.Draw(camera);
```
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]({{<static "images/heightmap-terrain-4.1.png">}})
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`:
```csharp
/// <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 coordiate - 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`:
```csharp
/// <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`:
```csharp
/// <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:
```csharp
// Adjust camera height to heightmap
if(HeightMap != null)
{
position.Y = HeightMap.GetHeightAt(position.X, position.Z) + HeightOffset;
}
```
Notice that we wrap this in a `null` check. If there is no heightmap, we want to keep our defalt behavior.
## Refactoring Game1
Since the `HeightMap` is a property of the `FPSCamera`, we'll need to set it to our terrain in the `Game1.LoadContent()` method after both the camera and terrain have been created:
```csharp
camera.HeightMap = Terrain;
```
## Refactoring Terrain
Now we need to implement the `IHeightMap` interface in our `Terrain` class. Add it to the class definition:
```csharp
public class Terrain : IHeightMap {
...
}
```
And add the method it calls for:
```csharp
/// <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. Specficially, multiplying world coordinates by the _inverse of the world matrix_ should transform them into model coordinates.
The [Matrix.Invert()](https://www.monogame.net/documentation/?page=M_Microsoft_Xna_Framework_Matrix_Invert_1) method can create this inverse matrix:
```csharp
Matrix inverseWorld = Matrix.Invert(effect.World);
```
We'll also need the world coordinates as a `Vector3` to transform:
```csharp
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()](https://www.monogame.net/documentation/?page=M_Microsoft_Xna_Framework_Vector3_Transform_7):
```csharp
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 terrrain 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:
```csharp
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:
```csharp
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:
```csharp
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!
---
title: "Interpolating Heights"
pre: "5. "
weight: 5
date: 2020-03-24T10:00:00-05:00
---
While you can now walk over your terrain, you probably notice that the camera seems really jittery. Why isn't it smooth?
Think about how we render our terrain. The diagram below shows the terrain in one dimension. At each integral step, we have a height value. The terrain (represented by green lines) is interpolated between these heights.
![The terrain as rendered]({{<static "images/heightmap-terrain-5.1.png">}})
Now think about what our function transforming world coordinates to heights is doing. It casts `tx` to an `int` to throw away the fractional part of the coordinate in order to get an array index. Thus, it is a step-like function, as indicated by the red lines in the diagram below:
![The current height function]({{<static "images/heightmap-terrain-5.2.png">}})
No wonder our movement is jerky!
Instead, we need to _interpolate_ the height between the two coordinates, so we match up with the visual representation.
## Linear Interpolation
We could use a method like [MathHelper.Lerp](https://www.monogame.net/documentation/?page=M_Microsoft_Xna_Framework_MathHelper_Lerp) to interpolate between two height values:
```csharp
var height1 = height[(int)x]
var height2 = height[(int)x + 1]
var fraction = x - (int)x;
MathHelper.Lerp(fraction, height1, height2);
```
What does linear interpolation actually do? Mathematically it's quite simple:
1. Start with the first value at point A (`height1`)
2. Calculate the difference between the value at point A and point B (`height2 - height1`)
3. Calculate the fraction of the distance between point A and B that our point of interest lies (`x - floor(x)`)
4. Multiply the difference by the fraction, and add it to the height at point A.
If we were to write our own linear interpolation implemenation, it might look like:
```csharp
public float Lerp(float fraction, float value1, float value2)
{
return value1 + fraction * (value2 - value1);
}
```
However, we aren't working with just _one_ dimension, we need to consider _two_. In other words, we need to use _bilinear interpolation_. But XNA does not define a method for this, so we'll have to do it ourselves.
## Implementing Bilinear Interpolation
Bilinear interpolation is the extension of linear interpolation into two dimensions. Instead of interpolating a point on a line (as is the case with linear interpolation), in bilinear interpolation we are interpolating a point on a _plane_. But with our terrain, we have _two_ planes per grid cell:
![Terrain triangles]({{<static "images/heightmap-terrain-5.3.png">}})
In this diagram, _n_ and _m_ are coordinates in our `heights` array, corresponding to the vertex making up the grid cell. So if our `(x, y)` point is in this grid cell, `n < x < n+1` and `m < y < m+1`.
Remember, a triangle defines a plane, and we used _two_ triangles to define each grid cell in our terrain. So we need to know which triangle our point falls on.
This is why we wanted our diagonals to both face the same way, and also why we wanted them facing the way they do. If the fractional distance along either the x or y axis is greater than halfway (0.5 in our model coordinates), then we are on the upper-right triangle. The inverse is also true; if both coordinates are less than halfway, we're in the lower left triangle. Any coordinate falling on line between the two triangles is shared by both.
Let's return to our `Terrain.GetHeightAt()` method, and start refactoring it. First, we'll want to change our out-of-bounds test to be slightly more exclusive, as we'll be getting both the height values at both the lower-left corner (tx, ty) and the upper-right corner (tx + 1, ty + 1):
```csharp
if (tx < 0 || ty < 0 || tx > width - 2 || ty > height - 2) return 0;
```
We can then delete the line `return heights[(int)tx, (int)ty];`, and replace it with our test to determine which triangle we are in:
```csharp
// Determine which triangle our coordinate is in
if(tx - (int)tx < 0.5 && ty - (int)ty < 0.5)
{
// In the lower-left triangle
}
else
{
// In the upper-right triangle
}
```
Let's finish the lower-left triangle case first. We'll start with the height at (tx, ty), and add the amount of change along the x-axis as we approach (tx + 1, ty), and the amount of change along the y-axis as we approach (tx, ty + 1).
```csharp
// In the lower-left triangle
float xFraction = tx - (int)tx;
float yFraction = ty - (int)ty;
float xDifference = heights[(int)tx + 1, (int)ty] - heights[(int)tx, (int)ty];
float yDifference = heights[(int)tx, (int)ty + 1] - heights[(int)tx, (int)ty];
return heights[(int)tx, (int)ty]
+ xFraction * xDifference
+ yFraction * yDifference;
```
The upper-right triangle is similar, only we'll start with the height at (tx + 1, ty + 1) and subtract the amount of change along the x-axis as we approach (tx, ty + 1), and the amount of change along the y-axis as we approach (tx + 1, ty).
```csharp
// In the upper-right triangle
float xFraction = (int)tx + 1 - tx;
float yFraction = (int)ty + 1 - ty;
float xDifference = heights[(int)tx + 1, (int)ty + 1] - heights[(int)tx, (int)ty + 1];
float yDifference = heights[(int)tx + 1, (int)ty + 1] - heights[(int)tx + 1, (int)ty];
return heights[(int)tx + 1, (int)ty + 1]
- xFraction * xDifference
- yFraction * yDifference;
```
Now if you run your code, your camera should smootly glide over the terrain!
This `GetHeightAt()` method can be used for other purposes as well. For example, we could scatter instances of the crates we developed previously across the terrain, using it to determine what thier Y-position should be.
\ No newline at end of file
---
title: "Summary"
pre: "6. "
weight: 6
date: 2020-03-24T10:00:00-05:00
---
Now you've seen the basics of creating a terrain from a heightmap. Armed with this knowledge, you can create an outdoor game world. You can find or create additional heightmaps to add new terrains to your game. You can swap the textures to create different kinds of environments as well.
But you could also create an even _larger_ worlds by using multiple terrains and stitching them together at the edges - a technique often called _terrain patches_. With enough of them, you could create an infinite world by looping back to a prior terrain. Or you could rotate a terrain sideways to create a rugged cliff face, or upside down to create a cavern roof.
And you could also change out the `BasicEffect` for a custom effect that could blend textures based on height changes, or provide a detail texture. You could also light the terrain realistically if you adjusted the surface normals to be perpendicular to the slope at each vertex.
\ No newline at end of file
+++
title = "Heightmap Terrain"
date = 2020-03-20T10:53:05-05:00
weight = 10
chapter = true
+++
### Game Development Techniques
# Heightmap Terrain
Keep your feet on the ground!
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment