Heightmap Terrain Smoothing

XNA Lesson 7 -- Difficulty: Moderate

September 27th, 2007 -- Heightmap Terrain Smoothing

As you may or may not know I do not currently have a tutorial/lesson for setting up a heightmap or even terrain. I've partly based my heightmap terrain off of the lessons of Riemer, if you'd like to first learn how to setup your own terrain I suggest starting there before you worry about smoothing it :)

Anyhow, on with the lesson....

Once your terrain has been read in from the heightmap file and the initial vertices are created, it is time to smooth them out. The process is really quite simple. You simple choose a vertex, and average the heights of all its adjacent vertices, which you then average against the height of your chosen vertex. Then you do this for all the vertices on your heightmap. Easier said than done? Maybe, so here are a couple illustrations, and then the code.

If we took a simple 4x4 map for example, with two very high points on the map, we might have something like this:

The numbers represent elevation. Now in the real world elevations do not simply go from nothing to a high number unless you're talking about a cliff. The numbers should fade from 0 to 20 gently to represent hills. So let us choose a vertex and apply the smoothing algorithm to it.

We'll choose the top-left grid that has a 20 value in it:

Then we simply add up the height totals of all of the red squares, which in this case is 8 squares, and divide that total by 8. This gives us 20 / 8, which is 2.5. Now we average 2.5 and 20, and we get (20 + 2.5) / 2 = 11.25.

This leaves us with this:

Now there is still work to be done. As you can see the peak is now lower, but the surrounding land isn't. The trick is to do this with every grid space, one by one. You create another grid to hold your new values, so that you do not corrupt the originals that you need, then when you're finished you overwrite the original with your smoothed version.

If we were to do this with every space we'd have this:

You can see how the surrounding terrain is now elevating slightly to reach the peaks, while the peaks aren't nearly as steep or tall as before. This represents the terrain after a single pass. However, with the algorithm below, you'll see that you can define the number of passes to take. The more passes the smoother the terrain will be.

public void SmoothTerrain(int Passes)
{
   float[,] newHeightData;

   while (Passes > 0)
   {
       Passes--;

       // Note: MapWidth and MapHeight should be equal and power-of-two values
       newHeightData = new float[MapWidth, MapHeight];

       for (int x = 0; x < MapWidth; x++)
       {
          for (int y = 0; y < MapHeight; y++)
          {
              int adjacentSections = 0;
              float sectionsTotal = 0.0f;

              if ((x - 1) > 0) // Check to left
              {
                 sectionsTotal += HeightData[x - 1, y];
                 adjacentSections++;

                 if ((y - 1) > 0) // Check up and to the left
                 {
                    sectionsTotal += HeightData[x - 1, y - 1];
                    adjacentSections++;
                 }

                 if ((y + 1) < MapHeight) // Check down and to the left
                 {
                    sectionsTotal += HeightData[x - 1, y + 1];
                    adjacentSections++;
                 }
              }

              if ((x + 1) < MapWidth) // Check to right
              {
                 sectionsTotal += HeightData[x + 1, y];
                 adjacentSections++;

                 if ((y - 1) > 0) // Check up and to the right
                 {
                     sectionsTotal += HeightData[x + 1, y - 1];
                     adjacentSections++;
                 }

                 if ((y + 1) < MapHeight) // Check down and to the right
                 {
                     sectionsTotal += HeightData[x + 1, y + 1];
                     adjacentSections++;
                 }
              }

              if ((y - 1) > 0) // Check above
              {
                 sectionsTotal += HeightData[x, y - 1];
                 adjacentSections++;
              }

              if ((y + 1) < MapHeight) // Check below
              {
                 sectionsTotal += HeightData[x, y + 1];
                 adjacentSections++;
              }

              newHeightData[x, y] = (HeightData[x, y] + (sectionsTotal / adjacentSections)) * 0.5f;
           }
       }

      // Overwrite the HeightData info with our new smoothed info
      for (int x = 0; x < MapWidth; x++)
      {
          for (int y = 0; y < MapHeight; y++)
          {
              HeightData[x, y] = newHeightData[x, y];
          }
      }
   }
}

Now for some examples with terrains....

Tall mountain terrain, high elevations.
NO smoothing passes:


1 Smoothing pass:


5 Smoothing passes:


50 Smoothing passes (slight overkill, 25 would've been fine):

Next