The blog is moving !

The blog is moving to nerdy-inverse.biz !

The content will be moved this month and new articles will be available soon ! :)

XNA - Application logic flow

Here’s basically how XNA applications are executing:

Initialization steps:

  1. The application (entry point) calls the game class constructor.
  2. The game class constructor creates the game components and call their constructors.
  3. The game class Initialize method is called by the XNA framework.
  4. The framework calls each game component’s Initialize methods.
  5. The framework calls each drawable game component’s LoadGraphicsContent methods.

Processing steps:

  1. The framework calls the game’s Update method.
  2. The framework calls each game component’s Update methods.
  3. The framework calls the game’s Draw method.
  4. The framework calls each drawable game component’s Draw methods.

Device events:

If the device is lost (the application window is moving to another monitor, the screen resolution is changed, the window is minimized, etc..), calls to the game’s and drawable game components’ UnloadGraphicsContent are made by the framework.

If the device is reset, then the framework calls the game’s LoadGraphicsContent methods as well as each drawable game component’s LoadGraphicsContent methods.

Shutdown steps:

  1. The game’s Exit() function is called.
  2. The framework calls the game’s Dispose method.
  3. The game’s Dispose method calls each of the game component’s Dispose methods.
  4. The framework calls the game’s UnloadGraphicsContent method.
  5. The framework calls each drawable game component’s UnloadGraphicsContent method.
  6. The game now exits.

Now that you are aware of the basic flow, there’s one particular thing in the processing steps that differs from what you can probably expect when using XNA for the first time. This particular thing is the game loop timing.

The game loop timing can be either fixed steps or variable steps. By default, XNA sets the game class to use a fixed time step. This determines how often the Update and Draw methods will be called.

When using the variable step setting, the game calls its Update method as soon as the previous frame is done drawing. This setting forces you to implement the game logic and animation based on elapsed time between frames to ensure a smooth game experience. Because the Update function is called immediately, the delay between calls can vary a lot. Without taking the elapsed time into considerations, you would see that the game is oftenly going faster or slower.

When using the fixed step setting, the game tries to call its Update method on the fixed interval specified in its TargetElapsedTime property (60 by default). This allows you to use the Update call as a basic unit of time and assume it will be called at the specified interval. When an update runs slower than expected, the game will skip the Draw calls and try to keep up the target elapsed time pace. This ensures that Update will have been called the expected number of times when the game loop catches up from a slow down. Also, the GameTime class provides a IsRunningSlowly boolean flag indicating if the game loop is taking longer that the target elapsed time. This allows you to adjust the amount of work you do in the Update function by yourself to meet the desired pace.

You can read more on game time in XNA on Shawn Hargreaves’ Blog here.

XNA - Focus on terrain - 306, heightmask revisited

In this part, we will add heightmask support as well as the multiply and add operations to the heightmap class. This new heightmask class works like a stencil. It has the same size than the heightmap and the mask values are all between 0.0f and 1.0f.

Since we also add the possibility of adding and multiplying heightmap together, we can make combination of different heightmap even more interesting. For example, you can use multiple terrain generation algorithms together and apply mask on them to blend the terrain from one type to another, like a volcanic area to an islands area. You might wonder what’s the difference between using a heightmap in the 0.0f - 1.0f range and a heightmask ? Well, the difference lies in the way we combine them:

Here’s the ApplyMask function of the Heightmap class:

  1. /// <summary>
  2. /// Apply a mask on the heightmap.
  3. /// </summary>
  4. /// <param name="heightmask"></param>
  5. public void ApplyMask(Heightmask heightmask)
  6. {
  7.     // Reject the heightmask if it doesn’t fit.
  8.     if ((_width != heightmask.Width) ||
  9.         (_depth != heightmask.Depth))
  10.     {
  11.         return;
  12.     }
  13.  
  14.     // Apply the heightmask.
  15.     for (int x = 0; x < _width; ++x)
  16.     {
  17.         for (int z = 0; z < _depth; ++z)
  18.         {
  19.             _heightValues[x + z * _width] = (_heightValues[x + z * _width] - _minimumHeight) * heightmask.GetMaskValue(x, z) + _minimumHeight;
  20.         }
  21.     }
  22. }

You can see that the ApplyMask function assure you that the values will stay between the minimum height and the maximum height of the heightmap. If you want to use a heightmap like a mask, you can only multiply it with a heightmap having also 0.0f and 1.0f as respectively minimum and maximum height.

Here’s the MultiplyHeightmap function and the AddHeightmap function:

  1. /// <summary>
  2. /// Multiply two heightmaps together.
  3. /// </summary>
  4. /// <param name="heightmap"></param>
  5. public void MultiplyHeightmap(Heightmap heightmap)
  6. {
  7.     // Reject the heightmap if it doesn’t fit.
  8.     if ((_width != heightmap.Width) ||
  9.         (_depth != heightmap.Depth) ||
  10.         (_minimumHeight != heightmap.MinimumHeight) ||
  11.         (_maximumHeight != heightmap.MaximumHeight))
  12.     {
  13.         return;
  14.     }
  15.  
  16.     // Multiply the heightmaps together.
  17.     for (int x = 0; x < _width; ++x)
  18.     {
  19.         for (int z = 0; z < _depth; ++z)
  20.         {
  21.             _heightValues[x + z * _width] = _heightValues[x + z * _width] * heightmap.GetHeightValue(x, z);
  22.         }
  23.     }
  24. }
  25.  
  26. /// <summary>
  27. /// Add two heightmaps together.
  28. /// </summary>
  29. /// <param name="heightmap"></param>
  30. public void AddHeightmap(Heightmap heightmap)
  31. {
  32.     // Reject the heightmap if it doesn’t fit.
  33.     if ((_width != heightmap.Width) ||
  34.         (_depth != heightmap.Depth) ||
  35.         (_minimumHeight != heightmap.MinimumHeight) ||
  36.         (_maximumHeight != heightmap.MaximumHeight))
  37.     {
  38.         return;
  39.     }
  40.  
  41.     // Add the heightmaps together.
  42.     for (int x = 0; x < _width; ++x)
  43.     {
  44.         for (int z = 0; z < _depth; ++z)
  45.         {
  46.             _heightValues[x + z * _width] = _heightValues[x + z * _width] + heightmap.GetHeightValue(x, z);
  47.         }
  48.     }
  49. }

Here’s a few screenshots of a simple heightmask in use:

The heightmask applied to a heightmap at maximum height.

The heightmask applied to a random heightmap.

The heightmask applied to a random heightmap.

You can download the sample application here.

XNA - Focus on terrain - 305, heightmap combination

In this part, we add the possibility to combine two heightmaps together. This is pretty easy to do. For each heights of the destination heightmap, we take it’s height multiplied by (1 minus the ratio) and we add the other heightmap’s height at the corresponding coodinates which we multiply by the ratio. The ratio is the percentage of the heightmap passed as a parameter and it’s in the [0.0f;1.0f] range. Also, I added the noise size parameter to the perlin noise generation as well as another NormalizeHeightmap function with a minimum and a maximum parameter.

Here’s some screenshots to illustrate heightmap combination:

The base heightmap.

The base heightmap at 90% and a random heightmap at 10%.

The base heightmap at 50% and a random fault formation at 50%.

The base heightmap at 50% and a random fault formation at 50%.

The base heightmap at 50% and a random fault formation at 50%.

The base heightmap at 50% and a random mid point displacement at 50%.

The base heightmap at 50% and a random mid point displacement at 50%.

The base heightmap at 50% and a random mid point displacement at 50%.

The base heightmap at 50% and a random particle deposition at 50%.

The base heightmap at 50% and a random particle deposition at 50%.

The base heightmap at 50% and a random particle deposition at 50%.

The base heightmap at 50% and a random particle deposition at 50%.

The base heightmap at 50% and a random perlin noise at 50%.

The base heightmap at 50% and a random perlin noise at 50%.

The base heightmap at 50% and a random perlin noise at 50%.

Here’s the CombineHeightmap function:

  1. /// <summary>
  2. /// Combine the heightmap with the passed heightmap.
  3. /// The amount variable is the ratio of the heightmap passed as a parameter.
  4. /// </summary>
  5. /// <param name="heightmap"></param>
  6. /// <param name="amount"></param>
  7. public void CombineHeightmap(Heightmap heightmap, float amount)
  8. {
  9.     // Reject the heightmap if it doesn’t fit.
  10.     if ((_width != heightmap.Width) ||
  11.         (_depth != heightmap.Depth) ||
  12.         (_minimumHeight != heightmap.MinimumHeight) ||
  13.         (_maximumHeight != heightmap.MaximumHeight))
  14.     {
  15.         return;
  16.     }
  17.  
  18.     // Combine the heightmaps.
  19.     // H1 = H1 * (1.0f - amount) + H2 * amount
  20.     for (int x = 0; x < _width; ++x)
  21.     {
  22.         for (int z = 0; z < _depth; ++z)
  23.         {
  24.             _heightValues[x + z * _width] = _heightValues[x + z * _width] * (1.0f - amount) + heightmap.GetHeightValue(x, z) * amount;
  25.         }
  26.     }
  27. }

Here’s an example on how we use it in the GameFOT class:

  1. // Generate a random heightmap using perlin noise.
  2. HeightmapPerlinNoiseSettings settings = new HeightmapPerlinNoiseSettings(1337, 0.75f, 10, 1.0f);
  3.  
  4. Heightmap heightmap = (Heightmap)_heightmap.Clone();
  5.  
  6. heightmap.PerlinNoiseSettings = settings;
  7. heightmap.GeneratePerlinNoiseHeightmap();
  8.  
  9. _terrain.Heightmap = (Heightmap)_heightmap.Clone();
  10.  
  11. _terrain.Heightmap.CombineHeightmap(heightmap, 0.5f);
  12.  
  13. _terrain.BuildTerrain();
  14.  
  15. _heightmapType = “Base 50% + perlin noise (1337, 0.75f, 10, 1.0f) 50%”;

You can download the source code here.

XNA - Focus on terrain - 304, perlin noise

Another way to create heightmaps is to generate them using perlin noise. Perlin noise was invented by Ken Perlin and is one of the most common ways of creating procedural noise. It works by adding together noise of different frequencies. To do this, we first need a number generator that returns the same random number for a specific seed. Here’s the one that we use:

  1. /// <summary>
  2. /// Return some noise for a specific seed.
  3. /// </summary>
  4. /// <param name="i"></param>
  5. /// <returns></returns>
  6. public static float Noise(int i)
  7. {
  8.     i = (i << 13) ^ i;
  9.  
  10.     return (1.0f - ((i * (i * i * 15731 + 789221) + 1376312589) & 0×7FFFFFFF) / 1073741824.0f);
  11. }

This function returns a float value in the range -1.0f to 1.0f. Next, we need to be able to interpolate between two random values, with a smooth effect. Linear interpolation isn’t really good for heightmaps, so we will use cubic interpolation since it’s available in XNA in the MathHelper class. While being more accurate, it’s also more expensive. If we generate a bunch of random numbers using the noise function and if we create a curve with them thanks to the cubic interpolation, we get some nice random curves.

I said before that perlin noise was about adding noise together right ? In our case, we will add curves made of noise together. The frequency is the number of random numbers that we use to generate the curve. For each curve that we use, we double the number of random numbers. Each doubled frequency is called an octave. As frequency of each curve increases, we want that the amplitude (or influence) of each curve to decrease. We control how much the amplitude decreases per octave using persistence.

The formula is quite simple: Amplitude = Persistance ^ octave

The cool thing about perlin noise, is that we can do that in 2D, 3D, or any other number of dimensions as well.

Here’s some screenshots to illustrate perlin noise:

Seed = 1337, persistence = 0.75, octaves = 10

Same as above, in wireframe.

Seed = 1669, persistence = 0.8, octaves = 4.

Same as above, in wireframe.

Seed = 12345, persistence = 0.7, octaves = 8.

Same as above, in wireframe.

As you can see, the results don’t look very good. That’s because perlin noise based heightmaps are way better then we combine them by using a small noise size with medium persistence multiplied by a large noise size with high persistence for example. We will see how to combine heightmaps in the next part as well as how to control noise size.

Like before, we create a HeightmapPerlinNoiseSettings class to store the settings, the seed value, the persistence value and the octaves count.

Here’s the meat of the heightmap class that deals with perlin noise:

  1. /// <summary>
  2. /// Generate a random heightmap using perlin noise and the passed parameters.
  3. /// </summary>
  4. /// <param name="width"></param>
  5. /// <param name="depth"></param>
  6. /// <param name="min"></param>
  7. /// <param name="max"></param>
  8. /// <param name="settings"></param>
  9. public void GeneratePerlinNoiseHeightmap(int width, int depth, float min, float max, HeightmapPerlinNoiseSettings settings)
  10. {
  11.     _width = width;
  12.     _depth = depth;
  13.  
  14.     _perlinNoiseSettings = settings;
  15.  
  16.     _heightValues = new float[_width * _depth];
  17.  
  18.     GeneratePerlinNoiseHeightmap();
  19. }
  20.  
  21. /// <summary>
  22. /// Generate a random heightmap using perlin noise and the default parameters.
  23. /// </summary>
  24. public void GeneratePerlinNoiseHeightmap()
  25. {
  26.     int txi, tzi;
  27.  
  28.     float freq, amp;
  29.  
  30.     float xf, tx, fracx;
  31.     float zf, tz, fracz;
  32.  
  33.     float v1, v2, v3, v4;
  34.     float i1, i2, total;
  35.  
  36.     // For each height..
  37.     for (int z = 0; z < _depth; ++z)
  38.     {
  39.         for (int x = 0; x < _width; ++x)
  40.         {
  41.             // Scale x and y to the range of 0.0f, 1.0f.
  42.             xf = (float)x / (float)_width;
  43.             zf = (float)z / (float)_depth;
  44.  
  45.             total = 0.0f;
  46.  
  47.             // For each octaves..
  48.             for (int i = 0; i < _perlinNoiseSettings.Octaves; ++i)
  49.             {
  50.                 // Calculate frequency and amplitude (different for each octave).
  51.                 freq = (float)Math.Pow(2.0, i);
  52.                 amp = (float)Math.Pow(_perlinNoiseSettings.Persistence, i);
  53.  
  54.                 // Calculate the x, z noise coodinates.
  55.                 tx = xf * freq;
  56.                 tz = zf * freq;
  57.  
  58.                 txi = (int)tx;
  59.                 tzi = (int)tz;
  60.  
  61.                 // Calculate the fractions of x and z.
  62.                 fracx = tx - txi;
  63.                 fracz = tz - tzi;
  64.  
  65.                 // Get noise per octave for these four points.
  66.                 v1 = RandomHelper.Noise(txi + tzi * 57 + _perlinNoiseSettings.Seed);
  67.                 v2 = RandomHelper.Noise(txi + 1 + tzi * 57 + _perlinNoiseSettings.Seed);
  68.                 v3 = RandomHelper.Noise(txi + (tzi + 1) * 57 + _perlinNoiseSettings.Seed);
  69.                 v4 = RandomHelper.Noise(txi + 1 + (tzi + 1) * 57 + _perlinNoiseSettings.Seed);
  70.  
  71.                 // Smooth noise in the x axis.
  72.                 i1 = MathHelper.SmoothStep(v1, v2, fracx);
  73.                 i2 = MathHelper.SmoothStep(v3, v4, fracx);
  74.  
  75.                 // Smooth in the z axis.
  76.                 total += MathHelper.SmoothStep(i1, i2, fracz) * amp;
  77.             }
  78.  
  79.             // Save to heightmap.
  80.             _heightValues[x + z * _width] = total;
  81.         }
  82.     }
  83.  
  84.     // Normalize the terrain.
  85.     NormalizeHeightmap();
  86. }

You can simply press the B button or the enter key to cycle through each heightmap types.

You can download the source code here.

XNA - Focus on terrain - 303, particle deposition

In nature, volcanic mountain ranges and island systems like the Pacific Rim’s are generated by lava flow. In this part, I use a particle system borrowed from the field of molecular beam epitaxy to simulate lava flow. The original idea comes from Jason Shankel.

The idea behind this algorithm is to drop sequences of particles and simulate their flow across a surface composed of previously dropped particles. Dropping a sufficient number of particles will produce structures that look like the flow patterns of viscous fluid (like lava). We start with an empty heightmap, and we drop one particle on it at a random location. After that, we drop a second particle on top of the first and we shake the heightmap until it comes to rest (until none of its neighbors is at a lower altitude). We continue to drop particles until we have a decent size pile. If we keep the drop point in a single place, we will have a large peak, moving the drop point periodically will create chains of multiple small peaks.

Volcanoes have very distinctive mountaintops. Once the lava flow stops, the lava cools and recedes back into the earth, creating some kind of bowl called a caldera. To generate a caldera, we invert the heightmap value above a certain altitude about the horizontal plane defined by that altitude. To make it more realistic, we apply some erosion using the low pass filter that we have created for the fault line deformation algorithm. We implement the caldera inversion using a flood fill technique to avoid interference with other peaks.

Here’s some screenshots to illustrate particle deposition:

Caldera = 0.7, 1024 jumps, peak walk = 4, minimum particle drop = 128, 1024 for the maximum.

Same as above in wireframe.

Caldera = 0.5, 1024 jumps, peak walk = 2, minimum particle drop = 128, 1024 for the maximum.

Same as above in wireframe.

Caldera = 0.7, 1024 jumps, peak walk = 8, minimum particle drop = 128, 1024 for the maximum.

Same as above in wireframe.

Caldera = 0.5, 128 jumps, peak walk = 2, minimum particle drop = 128, 1024 for the maximum.

Caldera = 0.5, 1024 jumps, peak walk = 2, minimum particle drop = 128, 1024 for the maximum.

You can get all sorts of results with this algorithm. It looks even better when you have water.

Like before, we create a HeightmapParticleDepositionSettings class to store the settings. We have the caldera height, the number of jumps, the peak walk value, the minimum and the maximum particle drops.

Here’s the meat of the heightmap class that deals with particle deposition:

  1. /// <summary>
  2. /// Generate a random heightmap using particle deposition and the passed parameters.
  3. /// </summary>
  4. /// <param name="width"></param>
  5. /// <param name="depth"></param>
  6. /// <param name="min"></param>
  7. /// <param name="max"></param>
  8. /// <param name="settings"></param>
  9. public void GenerateParticleDepositionHeightmap(int width, int depth, float min, float max, HeightmapParticleDepositionSettings settings)
  10. {
  11.     _width = width;
  12.     _depth = depth;
  13.  
  14.     _particleDepositionSettings = settings;
  15.  
  16.     _heightValues = new float[_width * _depth];
  17.  
  18.     GenerateParticleDepositionHeightmap();
  19. }
  20.  
  21. /// <summary>
  22. /// Generate a random heightmap using particle deposition and default parameters.
  23. /// </summary>
  24. public void GenerateParticleDepositionHeightmap()
  25. {
  26.     int i, j, m, p, particleCount;
  27.     int x, px, minx, maxx, sx, tx;
  28.     int z, pz, minz, maxz, sz, tz;
  29.  
  30.     bool done;
  31.  
  32.     int[] dx = { 0, 1, 0, _width - 1, 1, 1, _width - 1, _width - 1 };
  33.     int[] dz = { 1, 0, _depth - 1, 0, _depth - 1, 1, _depth - 1, 1 };
  34.  
  35.     float ch, ph;
  36.  
  37.     int[] calderaMap = new int[_width * _depth];
  38.  
  39.     // Clear the heightmap.
  40.     for (i = 0; i < _width * _depth; ++i)
  41.     {
  42.         _heightValues[i] = 0.0f;
  43.     }
  44.  
  45.     // For each jump ..
  46.     for (p = 0; p < _particleDepositionSettings.Jumps; ++p)
  47.     {
  48.         // Pick a random spot.
  49.         x = RandomHelper.Random.Next(_width);
  50.         z = RandomHelper.Random.Next(_depth);
  51.  
  52.         // px and pz track where the caldera is formed.
  53.         px = x;
  54.         pz = z;
  55.  
  56.         // Determine how many particles we are going to drop.
  57.         particleCount = RandomHelper.Random.Next(_particleDepositionSettings.MinParticlesPerJump, _particleDepositionSettings.MaxParticlesPerJump);
  58.  
  59.         // Drop particles.
  60.         for (i = 0; i < particleCount; ++i)
  61.         {
  62.             // If we have to move the drop point, agitate it in a random direction.
  63.             if ((_particleDepositionSettings.PeakWalk != 0) && ((i % _particleDepositionSettings.PeakWalk) == 0))
  64.             {
  65.                 m = RandomHelper.Random.Next(8);
  66.  
  67.                 x = (x + dx[m] + _width) % _width;
  68.                 z = (z + dz[m] + _depth) % _depth;
  69.             }
  70.  
  71.             // Drop it.
  72.             _heightValues[x + z * _width] += 1.0f;
  73.  
  74.             // Now agitate it until it settles.
  75.             sx = x;
  76.             sz = z;
  77.  
  78.             done = false;
  79.  
  80.             // While it’s not settled
  81.             while (!done)
  82.             {
  83.                 // Consider it is.
  84.                 done = true;
  85.  
  86.                 // Pick a random neighbor and start inspecting.
  87.                 m = RandomHelper.Random.Next();
  88.  
  89.                 for (j = 0; j < 8; ++j)
  90.                 {
  91.                     tx = (sx + dx[(j + m) % 8]) % _width;
  92.                     tz = (sz + dz[(j + m) % 8]) % _depth;
  93.  
  94.                     // If we can move to this neighbor, do it.
  95.                     if (_heightValues[tx + tz * _width] + 1.0f < _heightValues[sx + sz * _width])
  96.                     {
  97.                         _heightValues[tx + tz * _width] += 1.0f;
  98.                         _heightValues[sx + sz * _width] -= 1.0f;
  99.  
  100.                         sx = tx;
  101.                         sz = tz;
  102.  
  103.                         done = false;
  104.  
  105.                         break;
  106.                     }
  107.                 }
  108.             }
  109.  
  110.             // Check to see if the latest point is higher than the caldera point.
  111.             // If so, move the caldera point here.
  112.             if (_heightValues[sx + sz * _width] > _heightValues[px + pz * _width])
  113.             {
  114.                 px = sx;
  115.                 pz = sz;
  116.             }
  117.         }
  118.  
  119.         // Now that we are done with the peak, invert the caldera.
  120.         //
  121.         // ch is the caldera cutoff altitude.
  122.         // ph is the height at the caldera start point.
  123.         ph = _heightValues[px + pz * _width];
  124.         ch = ph * (1.0f - _particleDepositionSettings.Caldera);
  125.  
  126.         // We do a floodfill, so we use an array of integers to mark the visited locations.
  127.         minx = px;
  128.         maxx = px;
  129.         minz = pz;
  130.         maxz = pz;
  131.  
  132.         // Mark the start location for the caldera.
  133.         calderaMap[px + pz * _width] = 1;
  134.  
  135.         done = false;
  136.  
  137.         while (!done)
  138.         {
  139.             // Assume work is done.
  140.             done = true;
  141.  
  142.             sx = minx;
  143.             sz = minz;
  144.             tx = maxx;
  145.             tz = maxz;
  146.  
  147.             // Examine the bounding rectangle looking for unvisited neighbors.
  148.             for (x = sx; x <= tx; ++x)
  149.             {
  150.                 for (z = sz; z <= tz; ++z)
  151.                 {
  152.                     px = (x + _width) % _width;
  153.                     pz = (z + _depth) % _depth;
  154.  
  155.                     // If this cell is marked but unvisited, check it out.
  156.                     if (calderaMap[px + pz * _width] == 1)
  157.                     {
  158.                         // Mark cell as visited.
  159.                         calderaMap[px + pz * _width] = 2;
  160.  
  161.                         // If this cell should be inverted, invert it and inspect neighbors.
  162.                         // We mark any unmarked and unvisited neighbor.
  163.                         // We don’t invert any cells whose height exceeds the initial caldera height.
  164.                         // This prevents small peaks from destroying large ones.
  165.                         if ((_heightValues[px + pz * _width] > ch) && (_heightValues[px + pz * _width] <= ph))
  166.                         {
  167.                             done = false;
  168.  
  169.                             _heightValues[px + pz * _width] = 2 * ch - _heightValues[px + pz * _width];
  170.  
  171.                             // Left and right neighbors.
  172.                             px = (px + 1) % _width;
  173.  
  174.                             if (calderaMap[px + pz * _width] == 0)
  175.                             {
  176.                                 if (x + 1 > maxx)
  177.                                 {
  178.                                     maxx = x + 1;
  179.                                 }
  180.  
  181.                                 calderaMap[px + pz * _width] = 1;
  182.                             }
  183.  
  184.                             px = (px + _width - 2) % _width;
  185.  
  186.                             if (calderaMap[px + pz * _width] == 0)
  187.                             {
  188.                                 if (x - 1 < minx)
  189.                                 {
  190.                                     minx = x - 1;
  191.                                 }
  192.  
  193.                                 calderaMap[px + pz * _width] = 1;
  194.                             }
  195.  
  196.                             // Top and bottom neighbors.
  197.                             px = (x + _width) % _width;
  198.                             pz = (pz + 1) % _depth;
  199.  
  200.                             if (calderaMap[px + pz * _width] == 0)
  201.                             {
  202.                                 if (z + 1 > maxz)
  203.                                 {
  204.                                     maxz = z + 1;
  205.                                 }
  206.  
  207.                                 calderaMap[px + pz * _width] = 1;
  208.                             }
  209.  
  210.                             pz = (pz + _depth - 2) % _depth;
  211.  
  212.                             if (calderaMap[px + pz * _width] == 0)
  213.                             {
  214.                                 if (z - 1 < minz)
  215.                                 {
  216.                                     minz = z - 1;
  217.                                 }
  218.  
  219.                                 calderaMap[px + pz * _width] = 1;
  220.                             }
  221.                         }
  222.                     }
  223.                 }
  224.             }
  225.         }
  226.     }
  227.  
  228.     // Since calderas increase aliasing, we erode the terrain with a filter value proportional
  229.     // to the prominence of the caldera.
  230.     FilterHeightmap(_particleDepositionSettings.Caldera);
  231.  
  232.     // Normalize the heightmap.
  233.     NormalizeHeightmap();
  234. }

You can simply press the B button or the enter key to cycle through each heightmap types.

You can download the source code here.

XNA - Focus on terrain - 302, mid point displacement

Mountain ranges like the Himalayas are formed by a geological process called uplift. Lateral pressure from the movement of tectonic plates causes the surface of the Earth to wrinkle like fabric, pushing up mountain ranges. We can simulate this effect using the mid point displacement algorithm, also kown as the plasma fractal or the diamond square algorithm.

In one dimension, mid point displacement works like this. Take a line segment, pick the middle point and displace it by a random value in a small range (like -half length, +half length of the segment). After that, you reduce the range and you repeat the process for each new segments created this way.

In our algorithm, at each iteration, the offset range is multiplied by 2^-r, where r is the roughness constant.

When r > 1, the offset will decreases faster than the line segment length, so the terrain will look like a mountain or a valley, very smooth.

When r = 1, it’s where we have the best results, small regions will look like large regions.

When r < 1, we end up with a very chaotic result. The late iterations have a disproportionately large effect on the terrain.

In two dimension, instead of a line segment, we use a rectangle. Instead of one middle point, we now have five middle point, one for each side and the middle of the rectangle. In the diamond-square algorithm, the calculation of the rectangle’s mid point is called the diamond step, and the calculation of the side mid point is called the square step.

To do that, we start with a rectangle ABCD seeded with height values at the four corners. We calculate the height at the midpoint E by averaging the height of each corners A, B, C and D and we add a random value in the defined range (in our case, half the width length). This is the diamond step. Now, the square step, calculate the heights at the mid points of the line segments by averaging the corner values and the midpoints of the adjacent rectangles, and again, adding a random value in the defined range. You can repeat the process as much as you want.

Since the square step relies on the diamond values of neighbording square, we must first perform each diamond step before doing the square step.

Here’s some screenshots to illustrate mid point displacement:

Diamond step only with r = 1

Diamond step only with r = 0.25

Diamond step only with r = 4.0

Mid point displacement with r = 1.0

Mid point displacement with r = 1.0 in wireframe.

Mid point displacement with r = 0.25

Mid point displacement with r = 4.0

As you can see, with r close to 1.0, we get the best results.

Like what we did for the fault line deformation algorithm, we create a HeightmapMidPointSettings to store the algorithm settings (in my implementation, we only have one rough constant).

Here’s the meat of the heightmap class that deals with mid point displacement:

  1. /// &lt;summary&gt;
  2. /// Generate a random heightmap using mid point displacement and passed parameters.
  3. /// &lt;/summary&gt;
  4. /// &lt;param name="width"&gt;&lt;/param&gt;
  5. /// &lt;param name="depth"&gt;&lt;/param&gt;
  6. /// &lt;param name="min"&gt;&lt;/param&gt;
  7. /// &lt;param name="max"&gt;&lt;/param&gt;
  8. /// &lt;param name="settings"&gt;&lt;/param&gt;
  9. public void GenerateMidPointHeightmap(int width, int depth, float min, float max, HeightmapMidPointSettings settings)
  10. {
  11.     _width = width;
  12.     _depth = depth;
  13.  
  14.     _midPointSettings = settings;
  15.  
  16.     _heightValues = new float[_width * _depth];
  17.  
  18.     GenerateMidPointHeightmap();
  19. }
  20.  
  21. /// &lt;summary&gt;
  22. /// Generate a random heightmap using mid point displacement and default parameters.
  23. /// &lt;/summary&gt;
  24. public void GenerateMidPointHeightmap()
  25. {
  26.     int i, ni, mi, pmi;
  27.     int j, nj, mj, pmj;
  28.     int width = _width;
  29.     int depth = _depth;
  30.  
  31.     float deltaHeight = (float)width * 0.5f;
  32.  
  33.     float r = (float)Math.Pow(2.0f, -1 * _midPointSettings.Rough);
  34.  
  35.     // Since the terrain wraps, all four corners are represented by the value at 0, 0.
  36.     // So seeding the heightfield is very straightforward.
  37.     _heightValues[0] = 1337.0f;
  38.  
  39.     while(width &gt; 0)
  40.     {
  41.         // Diamond step.
  42.         //
  43.         // We find the values at the center of the rectangles by averaging the values at the
  44.         // corners and adding a random offset.
  45.         //
  46.         // a . . . b
  47.         // .       .
  48.         // .   e   .         e = ( a + b + c + d ) / 4 + random
  49.         // .       .
  50.         // c . . . d
  51.         //
  52.         // a = (i, j)
  53.         // b = (ni, j)
  54.         // c = (i, nj)
  55.         // d = (ni, nj)
  56.         // e = (mi, mj)
  57.  
  58.         for (i = 0; i &lt; _width; i += width)
  59.         {
  60.             for (j = 0; j &lt; _depth; j += depth)
  61.             {
  62.                 ni = (i + width) % _width;
  63.                 nj = (j + depth) % _depth;
  64.  
  65.                 mi = (i + width / 2);
  66.                 mj = (j + depth / 2);
  67.  
  68.                 _heightValues[mi + _width * mj] = (_heightValues[i + j * _width] + _heightValues[ni + j * _width] + _heightValues[i + nj * _width] + _heightValues[ni + nj * _width]) * 0.25f + RandomHelper.GetFloatInRange(-deltaHeight * 0.5f, deltaHeight * 0.5f);
  69.             }
  70.         }
  71.  
  72.         // Square step.
  73.         //
  74.         // We find the values on the left and top sides of each rectangle.
  75.         // The right and bottom sides are the left and top sides of the neighboring rectangles.
  76.         // So we don’t need to calculate them.
  77.         //
  78.         // Since the heightmap wraps, we are never left hanging. The right side of the last rectangle
  79.         // in a row is the left side of the first rectangle in the row. The bottom side of the last rectangle
  80.         // in a column is the top side of the first rectangle in the column.
  81.         //
  82.         // a . . . b
  83.         // . . . . .
  84.         // . . . . .
  85.         // . . . . .
  86.         // c . . . d
  87.         //
  88.         // . . . . .
  89.         // . . . . .
  90.         // . . e . .
  91.         // . . . . .
  92.         // . . . . .
  93.         //
  94.         // a . f . b
  95.         // . . . . .
  96.         // g . e . h
  97.         // . . . . .
  98.         // c . i . d
  99.         //
  100.         // a . f . b
  101.         // . j l k .                g = ( d + f + a + b ) / 4 + random
  102.         // g . e . h                h = ( a + c + e + f ) / 4 + random
  103.         // . . . . .
  104.         // c . i . d
  105.         //
  106.         // a = (i, j)
  107.         // b = (ni, j)
  108.         // c = (i, nj)
  109.         // d = (mi, pmj)
  110.         // e = (pmi, mj)
  111.         // f = (mi, mj)
  112.         // g = (mi, j)
  113.         // h = (i, mj)
  114.  
  115.         for (i = 0; i &lt; _width; i += width)
  116.         {
  117.             for (j = 0; j &lt; _depth; j += depth)
  118.             {
  119.                 ni = (i + width) % _width;
  120.                 nj = (j + depth) % _depth;
  121.  
  122.                 mi = (i + width / 2);
  123.                 mj = (j + depth / 2);
  124.  
  125.                 pmi = (i - width / 2 + _width) % _width;
  126.                 pmj = (j - depth / 2 + _depth) % _depth;
  127.  
  128.                 // Calculate the square value for the top side of the rectangle.
  129.                 _heightValues[mi + j * _width] = (_heightValues[i + j * _width] + _heightValues[ni + j * _width] + _heightValues[mi + pmj * _width] + _heightValues[mi + mj * _width]) * 0.25f + RandomHelper.GetFloatInRange(-deltaHeight * 0.5f, deltaHeight * 0.5f);
  130.  
  131.                 // Calculate the square value for the left side of the rectangle.
  132.                 _heightValues[i + mj * _width] = (_heightValues[i + j * _width] + _heightValues[i + nj * _width] + _heightValues[pmi + mj * _width] + _heightValues[mi + mj * _width]) * 0.25f + RandomHelper.GetFloatInRange(-deltaHeight * 0.5f, deltaHeight * 0.5f);
  133.             }
  134.         }
  135.  
  136.         // Set the values for the next iteration.
  137.         width /= 2;
  138.         depth /= 2;
  139.         deltaHeight *= r;
  140.     }
  141.  
  142.     // Normalize heightmap (height field values in the range _minimumHeight - _maximumHeight.
  143.     NormalizeHeightmap();
  144. }

You can simply press the B button or the enter key to cycle through each heightmap types.

You can download the source code here.

XNA - Focus on terrain - 301, fault line deformation

In this part, we introduce the concept of fractal terrain generation. In this particular part, we will focus on fault line deformation. In nature, forces like tectonic plates, mass-wasting and shorelines erosion create terrain features like escarpments, mesas and seaside cliffs. Fault lines allow you to generate these kinds of terrain.

The idea behind this algorithm is to start with a flat heightfield. Then we compute a random line through it and add an offset value to each value on one side. After that, we decrease the offset value and we repeat the process until we are satisfied with the level of detail generated.

The formula used to decrease the offset value at each iteration is:

offset = maxOffset - ( ( maxOffset - minOffset )  * i ) / iterations

When we pick random lines to intersect with the heightfield, we pick two random points within the heightfield and we use them to determine the line.

Also, we add the concept of erosion by using a low pass filter on the heightmap. This filter reduce the noise (in this case, the variance of heights from one point to his neighbors). Because fault lines generate huge differences between neighboring heights, we provide a parameter to set the frequency of the filter as well as an erosion factor.

Here’s some screenshots to illustrate fault lines:

This heightmap is generated using random values.

This heightmap is generated using a few fault lines without filtering.

This heightmap is generated with more fault lines without filtering.

This heightmap is generated used a lot of fault lines without filtering.

This heightmap is generated used a lot of fault lines with average filtering.

The same heightmap as above in wireframe, notice the effect of the low pass filter.

This heightmap is generated with low filtering and few fault lines.

This heightmap is generated with more fault lines with heavy filtering.

As you can see, by playing with the fault generation settings, you can have a wide range of results.

Now let’s see how it works in the code:

First, I added a HeightmapFaultSettings class which is used as a container for the minimumDelta, maximumDelta (both used for the offset calculation), the number of iterations, the nomber of iterations per filtering, and the filter value (between 0.0 and 1.0).

Here’s the meat of the heightmap class that deals with fault generation:

  1. /// &lt;summary&gt;
  2. /// Generate a random heightmap using fault line deformation and the passed parameters.
  3. /// &lt;/summary&gt;
  4. /// &lt;param name="width"&gt;&lt;/param&gt;
  5. /// &lt;param name="depth"&gt;&lt;/param&gt;
  6. /// &lt;param name="min"&gt;&lt;/param&gt;
  7. /// &lt;param name="max"&gt;&lt;/param&gt;
  8. /// &lt;param name="settings"&gt;&lt;/param&gt;
  9. public void GenerateFaultHeightmap(int width, int depth, float min, float max, HeightmapFaultSettings settings)
  10. {
  11.     _width = width;
  12.     _depth = depth;
  13.  
  14.     _faultSettings = settings;
  15.  
  16.     _heightValues = new float[_width * _depth];
  17.  
  18.     GenerateFaultHeightmap();
  19. }
  20.  
  21. /// &lt;summary&gt;
  22. /// Generate a random heightmap using fault line deformation and the _faultSettings.
  23. /// &lt;/summary&gt;
  24. public void GenerateFaultHeightmap()
  25. {
  26.     int x1, z1, dx1, dz1;
  27.     int x2, z2, dx2, dz2;
  28.  
  29.     int deltaHeight;
  30.  
  31.     for (int i = 0; i &lt; _faultSettings.Iterations; ++i)
  32.     {
  33.         // Calculate the deltaHeight for this iteration.
  34.         // (linear interpolation from max delta to min delta).
  35.         deltaHeight = _faultSettings.MaximumDelta - ((_faultSettings.MaximumDelta - _faultSettings.MinimumDelta) * i) / _faultSettings.Iterations;
  36.  
  37.         // Pick two random points on the field for the line.
  38.         // (make sure they aren’t identical).
  39.         x1 = RandomHelper.Random.Next(_width);
  40.         z1 = RandomHelper.Random.Next(_depth);
  41.  
  42.         do
  43.         {
  44.             x2 = RandomHelper.Random.Next(_width);
  45.             z2 = RandomHelper.Random.Next(_depth);
  46.         } while (x1 == x2 &amp;&amp; z1 == z2);
  47.  
  48.         // dx1, dz1 is a vector in the direction of the line.
  49.         dx1 = x2 - x1;
  50.         dz1 = z2 - z1;
  51.  
  52.         for (x2 = 0; x2 &lt; _width; ++x2)
  53.         {
  54.             for (z2 = 0; z2 &lt; _depth; ++z2)
  55.             {
  56.                 // dx2, dz2 is a vector from x1, z1 to the candidate point.
  57.                 dx2 = x2 - x1;
  58.                 dz2 = z2 - z1;
  59.  
  60.                 // if y component of the cross product is ‘up’, then elevate this point.
  61.                 if (dx2 * dz1 - dx1 * dz2 &gt; 0)
  62.                 {
  63.                     _heightValues[x2 + _width * z2] += (float)deltaHeight;
  64.                 }
  65.             }
  66.         }
  67.  
  68.         // Erode the terrain.
  69.         if ((_faultSettings.IterationsPerFilter != 0) &amp;&amp; (i % _faultSettings.IterationsPerFilter) == 0)
  70.         {
  71.             FilterHeightmap(_faultSettings.FilterValue);
  72.         }
  73.     }
  74.  
  75.     // Normalize heightmap (height field values in the range _minimumHeight - _maximumHeight.
  76.     NormalizeHeightmap();
  77. }
  78.  
  79. /// &lt;summary&gt;
  80. /// Normalize the heightmap.
  81. /// &lt;/summary&gt;
  82. public void NormalizeHeightmap()
  83. {
  84.     float min = float.MaxValue;
  85.     float max = float.MinValue;
  86.  
  87.     // Get the lowest and the highest values.
  88.     for (int x = 0; x &lt; _width; ++x)
  89.     {
  90.         for (int z = 0; z &lt; _depth; ++z)
  91.         {
  92.             if (_heightValues[x + z * _width] &gt; max)
  93.             {
  94.                 max = _heightValues[x + z * _width];
  95.             }
  96.  
  97.             if (_heightValues[x + z * _width] &lt; min)
  98.             {
  99.                 min = _heightValues[x + z * _width];
  100.             }
  101.         }
  102.     }
  103.  
  104.     // If the heightmap is flat, we set it to the average between _minimumHeight and _maximumHeight.
  105.     if (max &lt;= min)
  106.     {
  107.         for (int x = 0; x &lt; _width; ++x)
  108.         {
  109.             for (int z = 0; z &lt; _depth; ++z)
  110.             {
  111.                 _heightValues[x + z * _width] = (_maximumHeight - _minimumHeight) * 0.5f;
  112.             }
  113.         }
  114.  
  115.         return;
  116.     }
  117.  
  118.     // Normalize the value between 0.0 and 1.0 then scale it between _minimumHeight and _maximumHeight.
  119.     float diff = max - min;
  120.     float scale = _maximumHeight - _minimumHeight;
  121.  
  122.     for (int x = 0; x &lt; _width; ++x)
  123.     {
  124.         for (int z = 0; z &lt; _depth; ++z)
  125.         {
  126.             _heightValues[x + z * _width] = (_heightValues[x + z * _width] - min ) / diff * scale + _minimumHeight;
  127.         }
  128.     }
  129. }
  130.  
  131. /// &lt;summary&gt;
  132. /// Filter the heightmap using a low pass filter in all four directions..
  133. /// &lt;/summary&gt;
  134. /// &lt;param name="filterValue"&gt;&lt;/param&gt;
  135. public void FilterHeightmap(float filterValue)
  136. {
  137.     // Erode rows left to right.
  138.     for (int j = 0; j &lt; _depth; ++j)
  139.     {
  140.         for (int i = 1; i &lt; _width; ++i)
  141.         {
  142.             _heightValues[i + j * _width] = filterValue * _heightValues[i - 1 + j * _width] + (1 - filterValue) * _heightValues[i + j * _width];
  143.         }
  144.     }
  145.  
  146.     // Erode rows right to left.
  147.     for (int j = 0; j &lt; _depth; ++j)
  148.     {
  149.         for (int i = 0; i &lt; _width - 1; ++i)
  150.         {
  151.             _heightValues[i + j * _width] = filterValue * _heightValues[i + 1 + j * _width] + (1 - filterValue) * _heightValues[i + j * _width];
  152.         }
  153.     }
  154.  
  155.     // Erode columns top to bottom.
  156.     for (int j = 1; j &lt; _depth; ++j)
  157.     {
  158.         for (int i = 0; i &lt; _width; ++i)
  159.         {
  160.             _heightValues[i + j * _width] = filterValue * _heightValues[i + (j - 1) * _width] + (1 - filterValue) * _heightValues[i + j * _width];
  161.         }
  162.     }
  163.  
  164.     // Erode columns bottom to top.
  165.     for (int j = 0; j &lt; _depth - 1; ++j)
  166.     {
  167.         for (int i = 0; i &lt; _width; ++i)
  168.         {
  169.             _heightValues[i + j * _width] = filterValue * _heightValues[i + (j + 1) * _width] + (1 - filterValue) * _heightValues[i + j * _width];
  170.         }
  171.     }
  172. }

You have probably noticed that I added a simple RandomHelper class to avoid rewriting the same code everywhere in the project.

In the GameFOT class, I have just added a few lines to display the current algorithm used for the terrain. You can press B or Enter to go from one heightmap to another.

You can download the source code here.

XNA - Focus on terrain - 203, terrain patches

In this part, we add the support of terrain patches. Terrain patches are small part of the terrain geometry. Patches are used to cull the terrain efficiently and avoid to draw the whole terrain geometry at each frame. By reducing the number of patches to draw, we reduce the number of draw calls to make.

Here’s two screenshots of the terrain patches clearly visible:

In the wireframe screenshot, you can see that each patch is composed of the same amount of geometry. In later tutorials, we will introduce the concept of level of detail at the terrain patch level. Also, you can deduce from the screenshots that the patch size matters a lot in the efficiency of the culling. The best way to balance the patch size vs batching draw calls is by profiling the performance of the application.

How are we culling the terrain patches ? Simply by computing the bounding frustum of the camera then testing for each patch, if its bounding box is contained or not inside of the frustum. If the terrain patch is inside of the frustum, we simply draw it.

Here’s two screenshots of the final sample:

In the first screenshot, you can see that the whole terrain is drawn, therefore all terrain patches are inside of the bounding frustum of the camera. In the second screenshot, you can see that only a subset of the terrain is inside of the frustum. In this case, 37 patches are visible or partially visible.

In the fly camera class, we add a bounding frustum variable and we expose it using a public property. In the update function, we compute the bounding frustum by using its constructor. We pass to the constructor the multiplication of the view matrix by the projection matrix.

Here’s the new TerrainPatch class:

  1. /// &lt;summary&gt;
  2. /// This is the terrain patch class.
  3. /// &lt;/summary&gt;
  4. public class TerrainPatch
  5. {
  6.     /// &lt;summary&gt;
  7.     /// Bounding box used for this patch.
  8.     /// &lt;/summary&gt;
  9.     BoundingBox _boundingBox;
  10.  
  11.     /// &lt;summary&gt;
  12.     /// Geometry of the terrain patch.
  13.     /// &lt;/summary&gt;
  14.     VertexPositionNormalTexture[] _geometry;
  15.  
  16.     /// &lt;summary&gt;
  17.     /// Indices of the terrain patch.
  18.     /// &lt;/summary&gt;
  19.     short[] _indices;
  20.  
  21.     /// &lt;summary&gt;
  22.     /// Vertex buffer of the terrain patch.
  23.     /// &lt;/summary&gt;
  24.     VertexBuffer _vertexBuffer;
  25.  
  26.     /// &lt;summary&gt;
  27.     /// Index buffer of the terrain patch.
  28.     /// &lt;/summary&gt;
  29.     IndexBuffer _indexBuffer;
  30.  
  31.     /// &lt;summary&gt;
  32.     /// Width of the terrain patch.
  33.     /// &lt;/summary&gt;
  34.     int _width;
  35.  
  36.     /// &lt;summary&gt;
  37.     /// Depth of the terrain patch.
  38.     /// &lt;/summary&gt;
  39.     int _depth;
  40.  
  41.     /// &lt;summary&gt;
  42.     /// X offset used when we retrieve the heigth values from the heightmap.
  43.     /// &lt;/summary&gt;
  44.     int _offsetX;
  45.  
  46.     /// &lt;summary&gt;
  47.     /// Y offset used when we retrieve the height values from the heightmap.
  48.     /// &lt;/summary&gt;
  49.     int _offsetZ;
  50.  
  51.     /// &lt;summary&gt;
  52.     /// Get the bounding box.
  53.     /// &lt;/summary&gt;
  54.     public BoundingBox BoundingBox
  55.     {
  56.         get
  57.         {
  58.             return _boundingBox;
  59.         }
  60.     }
  61.  
  62.     /// &lt;summary&gt;
  63.     /// Default constructor.
  64.     /// &lt;/summary&gt;
  65.     public TerrainPatch()
  66.     {
  67.         _boundingBox = new BoundingBox();
  68.     }
  69.  
  70.     /// &lt;summary&gt;
  71.     /// Build a terrain patch.
  72.     /// &lt;/summary&gt;
  73.     /// &lt;param name="heightmap"&gt;&lt;/param&gt;
  74.     /// &lt;param name="worldMatrix"&gt;&lt;/param&gt;
  75.     /// &lt;param name="width"&gt;&lt;/param&gt;
  76.     /// &lt;param name="depth"&gt;&lt;/param&gt;
  77.     /// &lt;param name="offsetX"&gt;&lt;/param&gt;
  78.     /// &lt;param name="offsetZ"&gt;&lt;/param&gt;
  79.     public void BuildPatch(Heightmap heightmap, Matrix worldMatrix, int width, int depth, int offsetX, int offsetZ)
  80.     {
  81.         _width = width;
  82.         _depth = depth;
  83.  
  84.         _offsetX = offsetX;
  85.         _offsetZ = offsetZ;
  86.  
  87.         _boundingBox.Min.X = offsetX;
  88.         _boundingBox.Min.Z = offsetZ;
  89.  
  90.         _boundingBox.Max.X = offsetX + width;
  91.         _boundingBox.Max.Z = offsetZ + depth;
  92.  
  93.         BuildVertexBuffer(heightmap);
  94.  
  95.         _vertexBuffer = new VertexBuffer(GameFOT.Instance.GraphicsDevice, VertexPositionNormalTexture.SizeInBytes * _geometry.Length, ResourceUsage.WriteOnly, ResourceManagementMode.Automatic);
  96.  
  97.         _vertexBuffer.SetData&lt;VertexPositionNormalTexture&gt;(_geometry);
  98.  
  99.         BuildIndexBuffer();
  100.  
  101.         _indexBuffer = new IndexBuffer(GameFOT.Instance.GraphicsDevice, sizeof(short) * _indices.Length, ResourceUsage.WriteOnly, IndexElementSize.SixteenBits);
  102.  
  103.         _indexBuffer.SetData&lt;short&gt;(_indices);
  104.  
  105.         // Apply the world matrix transformation to the bounding box.
  106.         _boundingBox.Min = Vector3.Transform(_boundingBox.Min, worldMatrix);
  107.         _boundingBox.Max = Vector3.Transform(_boundingBox.Max, worldMatrix);
  108.     }
  109.  
  110.     /// &lt;summary&gt;
  111.     /// Build the vertex buffer as well as the bounding box.
  112.     /// &lt;/summary&gt;
  113.     /// &lt;param name="heightmap"&gt;&lt;/param&gt;
  114.     private void BuildVertexBuffer(Heightmap heightmap)
  115.     {
  116.         int index = 0;
  117.  
  118.         Vector3 position;
  119.         Vector3 normal;
  120.  
  121.         _boundingBox.Min.Y = float.MaxValue;
  122.  
  123.         _boundingBox.Max.Y = float.MinValue;
  124.  
  125.         _geometry = new VertexPositionNormalTexture[_width * _depth];
  126.  
  127.         for (int z = _offsetZ; z &lt; _offsetZ + _depth; ++z)
  128.         {
  129.             for (int x = _offsetX; x &lt; _offsetX + _width; ++x)
  130.             {
  131.                 float height = heightmap.GetHeightValue(x, z);
  132.  
  133.                 if (height &lt; _boundingBox.Min.Y)
  134.                 {
  135.                     _boundingBox.Min.Y = height;
  136.                 }
  137.  
  138.                 if (height &gt; _boundingBox.Max.Y)
  139.                 {
  140.                     _boundingBox.Max.Y = height;
  141.                 }
  142.  
  143.                 position = new Vector3((float)x, height, (float)z);
  144.  
  145.                 ComputeVertexNormal(heightmap, x, z, out normal);
  146.  
  147.                 _geometry[index] = new VertexPositionNormalTexture(position, normal, new Vector2(x, z));
  148.  
  149.                 ++index;
  150.             }
  151.         }
  152.     }
  153.  
  154.     /// &lt;summary&gt;
  155.     /// Build the index buffer.
  156.     /// &lt;/summary&gt;
  157.     private void BuildIndexBuffer()
  158.     {
  159.         int stripLength = 4 + (_depth - 2) * 2;
  160.         int stripCount = _width - 1;
  161.  
  162.         _indices = new short[stripLength * stripCount];
  163.  
  164.         int index = 0;
  165.  
  166.         for (int s = 0; s &lt; stripCount; ++s)
  167.         {
  168.             for (int z = 0; z &lt; _depth; ++z)
  169.             {
  170.                 _indices[index] = (short)(s + _depth * z);
  171.  
  172.                 ++index;
  173.  
  174.                 _indices[index] = (short)(s + _depth * z + 1);
  175.  
  176.                 ++index;
  177.             }
  178.         }
  179.     }
  180.  
  181.     /// &lt;summary&gt;
  182.     /// Compute vertex normal at the given x,z coordinate.
  183.     /// &lt;/summary&gt;
  184.     /// &lt;param name="heightmap"&gt;&lt;/param&gt;
  185.     /// &lt;param name="x"&gt;&lt;/param&gt;
  186.     /// &lt;param name="z"&gt;&lt;/param&gt;
  187.     /// &lt;param name="normal"&gt;&lt;/param&gt;
  188.     private void ComputeVertexNormal(Heightmap heightmap, int x, int z, out Vector3 normal)
  189.     {
  190.         int width = heightmap.Width;
  191.         int depth = heightmap.Depth;
  192.  
  193.         Vector3 center;
  194.         Vector3 p1;
  195.         Vector3 p2;
  196.         Vector3 avgNormal = Vector3.Zero;
  197.  
  198.         int avgCount = 0;
  199.  
  200.         bool spaceAbove = false;
  201.         bool spaceBelow = false;
  202.         bool spaceLeft = false;
  203.         bool spaceRight = false;
  204.  
  205.         Vector3 tmpNormal;
  206.         Vector3 v1;
  207.         Vector3 v2;
  208.  
  209.         center = new Vector3((float)x, heightmap.GetHeightValue(x, z), (float)z);
  210.  
  211.         if (x &gt; 0)
  212.         {
  213.             spaceLeft = true;
  214.         }
  215.  
  216.         if (x &lt; width - 1)
  217.         {
  218.             spaceRight = true;
  219.         }
  220.  
  221.         if (z &gt; 0)
  222.         {
  223.             spaceAbove = true;
  224.         }
  225.  
  226.         if (z &lt; depth - 1)
  227.         {
  228.             spaceBelow = true;
  229.         }
  230.  
  231.         if (spaceAbove &amp;&amp; spaceLeft)
  232.         {
  233.             p1 = new Vector3(x - 1, heightmap.GetHeightValue(x - 1, z), z);
  234.             p2 = new Vector3(x - 1, heightmap.GetHeightValue(x - 1, z - 1), z - 1);
  235.  
  236.             v1 = p1 - center;
  237.             v2 = p2 - p1;
  238.  
  239.             tmpNormal = Vector3.Cross(v1, v2);
  240.             avgNormal += tmpNormal;
  241.  
  242.             ++avgCount;
  243.         }
  244.  
  245.         if (spaceAbove &amp;&amp; spaceRight)
  246.         {
  247.             p1 = new Vector3(x, heightmap.GetHeightValue(x, z - 1), z - 1);
  248.             p2 = new Vector3(x + 1, heightmap.GetHeightValue(x + 1, z - 1), z - 1);
  249.  
  250.             v1 = p1 - center;
  251.             v2 = p2 - p1;
  252.  
  253.             tmpNormal = Vector3.Cross(v1, v2);
  254.             avgNormal += tmpNormal;
  255.  
  256.             ++avgCount;
  257.         }
  258.  
  259.         if (spaceBelow &amp;&amp; spaceRight)
  260.         {
  261.             p1 = new Vector3(x + 1, heightmap.GetHeightValue(x + 1, z), z);
  262.             p2 = new Vector3(x + 1, heightmap.GetHeightValue(x + 1, z + 1), z + 1);
  263.  
  264.             v1 = p1 - center;
  265.             v2 = p2 - p1;
  266.  
  267.             tmpNormal = Vector3.Cross(v1, v2);
  268.             avgNormal += tmpNormal;
  269.  
  270.             ++avgCount;
  271.         }
  272.  
  273.         if (spaceBelow &amp;&amp; spaceLeft)
  274.         {
  275.             p1 = new Vector3(x, heightmap.GetHeightValue(x, z + 1), z + 1);
  276.             p2 = new Vector3(x - 1, heightmap.GetHeightValue(x - 1, z + 1), z + 1);
  277.  
  278.             v1 = p1 - center;
  279.             v2 = p2 - p1;
  280.  
  281.             tmpNormal = Vector3.Cross(v1, v2);
  282.             avgNormal += tmpNormal;
  283.  
  284.             ++avgCount;
  285.         }
  286.  
  287.         normal = avgNormal / avgCount;
  288.     }
  289.  
  290.     /// &lt;summary&gt;
  291.     /// Draw the terrain patch.
  292.     /// &lt;/summary&gt;
  293.     public void Draw()
  294.     {
  295.         int primitivePerStrip = (_depth - 1) * 2;
  296.         int stripCount = _width - 1;
  297.         int vertexPerStrip = _depth * 2;
  298.  
  299.         for (int s = 0; s &lt; stripCount; ++s)
  300.         {
  301.             GameFOT.Instance.GraphicsDevice.Vertices[0].SetSource(_vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
  302.  
  303.             GameFOT.Instance.GraphicsDevice.Indices = _indexBuffer;
  304.  
  305.             GameFOT.Instance.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, _geometry.Length, vertexPerStrip * s, primitivePerStrip);
  306.         }
  307.     }
  308. }

Most of the terrain patch code comes from the old terrain code.

Here’s the new terrain code:

  1. /// &lt;summary&gt;
  2. /// This is the terrain class.
  3. /// &lt;/summary&gt;
  4. public class Terrain
  5. {
  6.     /// &lt;summary&gt;
  7.     /// Heightmap used to generate the terrain mesh.
  8.     /// &lt;/summary&gt;
  9.     Heightmap _heightmap;
  10.  
  11.     /// &lt;summary&gt;
  12.     /// Texture used to represent the lowest heights of the terrain.
  13.     /// &lt;/summary&gt;
  14.     Texture2D _groundTexture;
  15.  
  16.     /// &lt;summary&gt;
  17.     /// Texture used to represent the heights above the ground.
  18.     /// &lt;/summary&gt;
  19.     Texture2D _mudTexture;
  20.  
  21.     /// &lt;summary&gt;
  22.     /// Texture used to represent the heights above the mud.
  23.     /// &lt;/summary&gt;
  24.     Texture2D _rockTexture;
  25.  
  26.     /// &lt;summary&gt;
  27.     /// Texture used to represent the highest heights of the terrain.
  28.     /// &lt;/summary&gt;
  29.     Texture2D _snowTexture;
  30.  
  31.     /// &lt;summary&gt;
  32.     /// Vertex declaration of the terrain mesh.
  33.     /// &lt;/summary&gt;
  34.     VertexDeclaration _vertexDeclaration;
  35.  
  36.     /// &lt;summary&gt;
  37.     /// Terrain effect shader.
  38.     /// &lt;/summary&gt;
  39.     Effect _effect;
  40.  
  41.     /// &lt;summary&gt;
  42.     /// World matrix.
  43.     /// &lt;/summary&gt;
  44.     Matrix _worldMatrix;
  45.  
  46.     /// &lt;summary&gt;
  47.     /// Wireframe fill mode flag.
  48.     /// &lt;/summary&gt;
  49.     bool _isWireframe;
  50.  
  51.     /// &lt;summary&gt;
  52.     /// List of the terrain patches.
  53.     /// &lt;/summary&gt;
  54.     List&lt;TerrainPatch&gt; _patches;
  55.  
  56.     /// &lt;summary&gt;
  57.     /// Patch count.
  58.     /// &lt;/summary&gt;
  59.     int _patchCount;
  60.  
  61.     /// &lt;summary&gt;
  62.     /// Drawn patch count.
  63.     /// &lt;/summary&gt;
  64.     int _drawnPatchCount;
  65.  
  66.     /// &lt;summary&gt;
  67.     /// Get or set the wireframe fill mode.
  68.     /// &lt;/summary&gt;
  69.     public bool IsWireframe
  70.     {
  71.         get
  72.         {
  73.             return _isWireframe;
  74.         }
  75.         set
  76.         {
  77.             _isWireframe = value;
  78.         }
  79.     }
  80.  
  81.     /// &lt;summary&gt;
  82.     /// The heightmap used by the terrain.
  83.     /// &lt;/summary&gt;
  84.     public Heightmap Heightmap
  85.     {
  86.         get
  87.         {
  88.             return _heightmap;
  89.         }
  90.         set
  91.         {
  92.             _heightmap = value;
  93.         }
  94.     }
  95.  
  96.     /// &lt;summary&gt;
  97.     /// Patch count.
  98.     /// &lt;/summary&gt;
  99.     public int PatchCount
  100.     {
  101.         get
  102.         {
  103.             return _patchCount;
  104.         }
  105.     }
  106.  
  107.     /// &lt;summary&gt;
  108.     /// Drawn patch count.
  109.     /// &lt;/summary&gt;
  110.     public int DrawnPatchCount
  111.     {
  112.         get
  113.         {
  114.             return _drawnPatchCount;
  115.         }
  116.     }
  117.  
  118.     /// &lt;summary&gt;
  119.     /// Default constructor.
  120.     /// &lt;/summary&gt;
  121.     /// &lt;param name="heightmap"&gt;&lt;/param&gt;
  122.     public Terrain(Heightmap heightmap)
  123.     {
  124.         _patches = new List&lt;TerrainPatch&gt;();
  125.  
  126.         _heightmap = heightmap;
  127.     }
  128.  
  129.     /// &lt;summary&gt;
  130.     /// Load the graphics content and also build the mesh.
  131.     /// &lt;/summary&gt;
  132.     public void LoadGraphicsContent()
  133.     {
  134.         _effect = GameFOT.Instance.ContentManager.Load&lt;Effect&gt;(“Content/Effects/TerrainEffect”);
  135.  
  136.         _effect.CurrentTechnique = _effect.Techniques[“DefaultTechnique”];
  137.  
  138.         _groundTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainGround”);
  139.         _mudTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainMud”);
  140.         _rockTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainRock”);
  141.         _snowTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainSnow”);
  142.  
  143.         BuildTerrain();
  144.     }
  145.  
  146.     /// &lt;summary&gt;
  147.     /// Draw the terrain.
  148.     /// &lt;/summary&gt;
  149.     /// &lt;param name="gameTime"&gt;&lt;/param&gt;
  150.     /// &lt;param name="viewMatrix"&gt;&lt;/param&gt;
  151.     /// &lt;param name="projectionMatrix"&gt;&lt;/param&gt;
  152.     /// &lt;param name="frustum"&gt;&lt;/param&gt;
  153.     public void Draw(GameTime gameTime, Matrix viewMatrix, Matrix projectionMatrix, BoundingFrustum frustum)
  154.     {
  155.         int width = _heightmap.Width;
  156.         int depth = _heightmap.Depth;
  157.  
  158.         _patchCount = _patches.Count;
  159.         _drawnPatchCount = 0;
  160.  
  161.         if (_isWireframe)
  162.         {
  163.             GameFOT.Instance.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
  164.         }
  165.         else
  166.         {
  167.             GameFOT.Instance.GraphicsDevice.RenderState.FillMode = FillMode.Solid;
  168.         }
  169.  
  170.         GameFOT.Instance.GraphicsDevice.RenderState.DepthBufferEnable = true;
  171.         GameFOT.Instance.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
  172.  
  173.         Matrix worldViewProjection = _worldMatrix * viewMatrix * projectionMatrix;
  174.  
  175.         Vector3 lightDirection = new Vector3(-20.0f * (float)Math.Sin(gameTime.TotalRealTime.TotalMilliseconds * 0.0001f), 0.0f, -20.0f * (float)Math.Cos(gameTime.TotalRealTime.TotalMilliseconds * 0.0001f));
  176.  
  177.         GameFOT.Instance.GraphicsDevice.VertexDeclaration = _vertexDeclaration;
  178.  
  179.         _effect.Parameters[“g_matWorldViewProjection”].SetValue(worldViewProjection);
  180.  
  181.         _effect.Parameters[“g_vecLightDirection”].SetValue(lightDirection);
  182.  
  183.         _effect.Parameters[“g_vecHeights”].SetValue(new Vector3(14.0f, 21.0f, 28.0f));
  184.  
  185.         _effect.Parameters[“g_texGround”].SetValue(_groundTexture);
  186.         _effect.Parameters[“g_texMud”].SetValue(_mudTexture);
  187.         _effect.Parameters[“g_texRock”].SetValue(_rockTexture);
  188.         _effect.Parameters[“g_texSnow”].SetValue(_snowTexture);
  189.  
  190.         _effect.Begin();
  191.        
  192.         foreach (EffectPass pass in _effect.CurrentTechnique.Passes)
  193.         {
  194.             pass.Begin();
  195.  
  196.             for (int i = 0; i &lt; _patches.Count; ++i)
  197.             {
  198.                 // Test the patch against frustum.
  199.                 if (frustum.Contains(_patches[i].BoundingBox) != ContainmentType.Disjoint)
  200.                 {
  201.                     _patches[i].Draw();
  202.  
  203.                     ++_drawnPatchCount;
  204.                 }
  205.             }
  206.  
  207.             pass.End();
  208.         }
  209.        
  210.         _effect.End();
  211.  
  212.         if (_isWireframe)
  213.         {
  214.             GameFOT.Instance.GraphicsDevice.RenderState.FillMode = FillMode.Solid;
  215.         }
  216.     }
  217.  
  218.     /// &lt;summary&gt;
  219.     /// Build the terrain.
  220.     /// &lt;/summary&gt;
  221.     public void BuildTerrain()
  222.     {
  223.         int width = _heightmap.Width;
  224.         int depth = _heightmap.Depth;
  225.  
  226.         // Clear the terrain patches.
  227.         _patches.Clear();
  228.  
  229.         // Compute the world matrix to place the terrain in the middle of the scene.
  230.         _worldMatrix = Matrix.CreateTranslation((float)width * -0.5f, 0.0f, (float)depth * -0.5f);
  231.  
  232.         // Create the terrain patches.
  233.         int patchWidth = 16;
  234.         int patchDepth = 16;
  235.         int patchCountX = width / patchWidth;
  236.         int patchCountZ = depth / patchDepth;
  237.         int patchCount = patchCountX * patchCountZ;
  238.  
  239.         for (int x = 0; x &lt; patchCountX; ++x)
  240.         {
  241.             for (int z = 0; z &lt; patchCountZ; ++z)
  242.             {
  243.                 TerrainPatch patch = new TerrainPatch();
  244.  
  245.                 patch.BuildPatch(_heightmap, _worldMatrix, patchWidth, patchDepth, (x + 1) * (patchWidth - 1), (z + 1) * (patchDepth - 1));
  246.  
  247.                 _patches.Add(patch);
  248.             }
  249.         }
  250.  
  251.         _vertexDeclaration = new VertexDeclaration(GameFOT.Instance.GraphicsDevice, VertexPositionNormalTexture.VertexElements);
  252.     }
  253.  
  254.     /// &lt;summary&gt;
  255.     /// Save the terrain to a file.
  256.     /// &lt;/summary&gt;
  257.     /// &lt;param name="filename"&gt;&lt;/param&gt;
  258.     public void SaveToFile(String filename)
  259.     {
  260.         _heightmap.SaveToFile(filename);
  261.     }
  262.  
  263.     /// &lt;summary&gt;
  264.     /// Load the terrain from a file and build the terrain.
  265.     /// &lt;/summary&gt;
  266.     /// &lt;param name="filename"&gt;&lt;/param&gt;
  267.     public void LoadFromFile(String filename)
  268.     {
  269.         _heightmap.LoadFromFile(filename);
  270.  
  271.         BuildTerrain();
  272.     }
  273. }

In the GameFOT class, we simply add a sprite batch and a sprite font to display the patch count and the drawn patch count of the terrain.

That’s it !

Here you can download the source code.

XNA - Focus on terrain: 202, textured terrain

A picture is worth a thousand words:

That’s what we’re making in this part. The terrain is textured using multiple textures, based on the height at each vertex. Also, we compute the normal at each vertex and use it to light the terrain with a directional source. Here’s the changes since part 105.

In the heightmap class, we remove all code related to the heightmask, and we add the following constructor:

  1. /// &lt;summary&gt;
  2. /// Default constructor for a prebuilt heightmap.
  3. /// &lt;/summary&gt;
  4. /// &lt;param name="width"&gt;&lt;/param&gt;
  5. /// &lt;param name="depth"&gt;&lt;/param&gt;
  6. /// &lt;param name="min"&gt;&lt;/param&gt;
  7. /// &lt;param name="max"&gt;&lt;/param&gt;
  8. /// &lt;param name="data"&gt;&lt;/param&gt;
  9. public Heightmap(int width, int depth, float min, float max, float[] data)
  10. {
  11.     _width = width;
  12.     _depth = depth;
  13.  
  14.     _minimumHeight = min;
  15.     _maximumHeight = max;
  16.  
  17.     _heightValues = (float[])data.Clone();
  18. }

The fly camera class is slightly updated to move faster because of the increased terrain size. The input manager is kept as before.

Most of the changes happen in the terrain class. First, we add four textures, one for the ground level, one for the mud level, one for the rocky level and one for the snow level. The ground is the lowest height and the snow the highest one. We change the old VertexPositionColor format used for the geometry to VertexPositionNormalTexture everywhere in the code.

The textures are loaded in the LoadGraphicsContent method:

  1. /// &lt;summary&gt;
  2. /// Load the graphics content and also build the mesh.
  3. /// &lt;/summary&gt;
  4. public void LoadGraphicsContent()
  5. {
  6.     _effect = GameFOT.Instance.ContentManager.Load&lt;Effect&gt;(“Content/Effects/TerrainEffect”);
  7.  
  8.     _effect.CurrentTechnique = _effect.Techniques[“DefaultTechnique”];
  9.  
  10.     _groundTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainGround”);
  11.     _mudTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainMud”);
  12.     _rockTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainRock”);
  13.     _snowTexture = GameFOT.Instance.ContentManager.Load&lt;Texture2D&gt;(“Content/Textures/TerrainSnow”);
  14.  
  15.     BuildTerrainMesh();
  16. }

The draw method is now also responsible of moving the directional light using a circle pattern. Also, we pass to the effect shader a vector3 called “g_vecHeights”. This vector contains the heights of each texture levels.

  1. /// &lt;summary&gt;
  2. /// Draw the terrain.
  3. /// &lt;/summary&gt;
  4. /// &lt;param name="gameTime"&gt;&lt;/param&gt;
  5. /// &lt;param name="viewMatrix"&gt;&lt;/param&gt;
  6. /// &lt;param name="projectionMatrix"&gt;&lt;/param&gt;
  7. public void Draw(GameTime gameTime, Matrix viewMatrix, Matrix projectionMatrix)
  8. {
  9.     int width = _heightmap.Width;
  10.     int depth = _heightmap.Depth;
  11.  
  12.     int primitivePerStrip = (depth - 1) * 2;
  13.     int stripCount = width - 1;
  14.     int vertexPerStrip = depth * 2;
  15.  
  16.     if (_isWireframe)
  17.     {
  18.         GameFOT.Instance.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;
  19.     }
  20.     else
  21.     {
  22.         GameFOT.Instance.GraphicsDevice.RenderState.FillMode = FillMode.Solid;
  23.     }
  24.  
  25.     GameFOT.Instance.GraphicsDevice.RenderState.DepthBufferEnable = true;
  26.     GameFOT.Instance.GraphicsDevice.RenderState.DepthBufferWriteEnable = true;
  27.  
  28.     Matrix worldViewProjection = _worldMatrix * viewMatrix * projectionMatrix;
  29.  
  30.     Vector3 lightDirection = new Vector3(-20.0f * (float)Math.Sin(gameTime.TotalRealTime.TotalMilliseconds * 0.0001f), 0.0f, -20.0f * (float)Math.Cos(gameTime.TotalRealTime.TotalMilliseconds * 0.0001f));
  31.  
  32.     GameFOT.Instance.GraphicsDevice.VertexDeclaration = _vertexDeclaration;
  33.  
  34.     _effect.Parameters[“g_matWorldViewProjection”].SetValue(worldViewProjection);
  35.  
  36.     _effect.Parameters[“g_vecLightDirection”].SetValue(lightDirection);
  37.  
  38.     _effect.Parameters[“g_vecHeights”].SetValue(new Vector3(14.0f, 21.0f, 28.0f));
  39.  
  40.     _effect.Parameters[“g_texGround”].SetValue(_groundTexture);
  41.     _effect.Parameters[“g_texMud”].SetValue(_mudTexture);
  42.     _effect.Parameters[“g_texRock”].SetValue(_rockTexture);
  43.     _effect.Parameters[“g_texSnow”].SetValue(_snowTexture);
  44.  
  45.     _effect.Begin();
  46.  
  47.     for (int s = 0; s &lt; stripCount; ++s)
  48.     {
  49.         foreach (EffectPass pass in _effect.CurrentTechnique.Passes)
  50.         {
  51.             pass.Begin();
  52.  
  53.             GameFOT.Instance.GraphicsDevice.Vertices[0].SetSource(_vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
  54.  
  55.             GameFOT.Instance.GraphicsDevice.Indices = _indexBuffer;
  56.  
  57.             GameFOT.Instance.GraphicsDevice.DrawIndexedPrimitives(_primitiveType, 0, 0, _geometry.Length, vertexPerStrip * s, primitivePerStrip);
  58.  
  59.             pass.End();
  60.         }
  61.     }
  62.    
  63.     _effect.End();
  64. }

The BuildVertexBuffer method is updated to compute the vertex normal. We compute each vertex normal using the ComputeVertexNormal method.

Here’s the new function:

  1. /// &lt;summary&gt;
  2. /// Compute normal for the given x,z coordinate.
  3. /// &lt;/summary&gt;
  4. /// &lt;param name="x"&gt;&lt;/param&gt;
  5. /// &lt;param name="z"&gt;&lt;/param&gt;
  6. /// &lt;param name="normal"&gt;&lt;/param&gt;
  7. private void ComputeVertexNormal(int x, int z, out Vector3 normal)
  8. {
  9.     int width = _heightmap.Width;
  10.     int depth = _heightmap.Depth;
  11.  
  12.     Vector3 center;
  13.     Vector3 p1;
  14.     Vector3 p2;
  15.     Vector3 avgNormal = Vector3.Zero;
  16.  
  17.     int avgCount = 0;
  18.  
  19.     bool spaceAbove = false;
  20.     bool spaceBelow = false;
  21.     bool spaceLeft = false;
  22.     bool spaceRight = false;
  23.  
  24.     Vector3 tmpNormal;
  25.     Vector3 v1;
  26.     Vector3 v2;
  27.  
  28.     center = new Vector3((float)x, _heightmap.GetHeightValue(x, z), (float)z);
  29.  
  30.     if (x &gt; 0)
  31.     {
  32.         spaceLeft = true;
  33.     }
  34.  
  35.     if (x &lt; width - 1)
  36.     {
  37.         spaceRight = true;
  38.     }
  39.  
  40.     if (z &gt; 0)
  41.     {
  42.         spaceAbove = true;
  43.     }
  44.  
  45.     if (z &lt; depth - 1)
  46.     {
  47.         spaceBelow = true;
  48.     }
  49.  
  50.     if (spaceAbove &amp;&amp; spaceLeft)
  51.     {
  52.         p1 = new Vector3(x - 1, _heightmap.GetHeightValue(x - 1, z), z);
  53.         p2 = new Vector3(x - 1, _heightmap.GetHeightValue(x - 1, z - 1), z - 1);
  54.  
  55.         v1 = p1 - center;
  56.         v2 = p2 - p1;
  57.  
  58.         tmpNormal = Vector3.Cross(v1, v2);
  59.         avgNormal += tmpNormal;
  60.  
  61.         ++avgCount;
  62.     }
  63.  
  64.     if (spaceAbove &amp;&amp; spaceRight)
  65.     {
  66.         p1 = new Vector3(x, _heightmap.GetHeightValue(x, z - 1), z - 1);
  67.         p2 = new Vector3(x + 1, _heightmap.GetHeightValue(x + 1, z - 1), z - 1);
  68.  
  69.         v1 = p1 - center;
  70.         v2 = p2 - p1;
  71.  
  72.         tmpNormal = Vector3.Cross(v1, v2);
  73.         avgNormal += tmpNormal;
  74.  
  75.         ++avgCount;
  76.     }
  77.  
  78.     if (spaceBelow &amp;&amp; spaceRight)
  79.     {
  80.         p1 = new Vector3(x + 1, _heightmap.GetHeightValue(x + 1, z), z);
  81.         p2 = new Vector3(x + 1, _heightmap.GetHeightValue(x + 1, z + 1), z + 1);
  82.  
  83.         v1 = p1 - center;
  84.         v2 = p2 - p1;
  85.  
  86.         tmpNormal = Vector3.Cross(v1, v2);
  87.         avgNormal += tmpNormal;
  88.  
  89.         ++avgCount;
  90.     }
  91.  
  92.     if (spaceBelow &amp;&amp; spaceLeft)
  93.     {
  94.         p1 = new Vector3(x, _heightmap.GetHeightValue(x, z + 1), z + 1);
  95.         p2 = new Vector3(x - 1, _heightmap.GetHeightValue(x - 1, z + 1), z + 1);
  96.  
  97.         v1 = p1 - center;
  98.         v2 = p2 - p1;
  99.  
  100.         tmpNormal = Vector3.Cross(v1, v2);
  101.         avgNormal += tmpNormal;
  102.  
  103.         ++avgCount;
  104.     }
  105.  
  106.     normal = avgNormal / avgCount;
  107. }

The TerrainEffect.fx shader have new uniform extern variables for the light direction, the terrain level heights, the four textures their samplers. The vertex shader transform the vertices, compute the diffuse color (which is uniform and used to light the terrain), the specular color for the snowy level. Also, it computes the ratio of each texture for each vertex and multiply it by the lighting value and it scales down the texture coordinates. The pixel shader simply apply the textures.

Here’s the effect shader:

  1. // World view projection matrix.
  2. uniform extern float4×4 g_matWorldViewProjection;
  3.  
  4. // A directional light.
  5. uniform extern float3 g_vecLightDirection;
  6.  
  7. // A vector containing the different heights used to divide the terrain in ground, mud, rock and snow areas.
  8. // Values are stored from the lowest to the highest heights.
  9. uniform extern float3 g_vecHeights;
  10.  
  11. // Textures used for the terrain.
  12. uniform extern texture g_texGround;
  13. uniform extern texture g_texMud;
  14. uniform extern texture g_texRock;
  15. uniform extern texture g_texSnow;
  16.  
  17. // Texture samplers.
  18. sampler2D g_smpGround = sampler_state
  19. {
  20.     texture  = &lt;g_texGround&gt;;
  21.     MinFilter = LINEAR;
  22.     MagFilter = LINEAR;
  23.     MipFilter = LINEAR;
  24.     AddressU = WRAP;
  25.     AddressV = WRAP;
  26. };
  27.  
  28. sampler2D g_smpMud = sampler_state
  29. {
  30.     texture  = &lt;g_texMud&gt;;
  31.     MinFilter = LINEAR;
  32.     MagFilter = LINEAR;
  33.     MipFilter = LINEAR;
  34.     AddressU = WRAP;
  35.     AddressV = WRAP;
  36. };
  37.  
  38. sampler2D g_smpRock = sampler_state
  39. {
  40.     texture  = &lt;g_texRock&gt;;
  41.     MinFilter = LINEAR;
  42.     MagFilter = LINEAR;
  43.     MipFilter = LINEAR;
  44.     AddressU = WRAP;
  45.     AddressV = WRAP;
  46. };
  47.  
  48. sampler2D g_smpSnow = sampler_state
  49. {
  50.     texture  = &lt;g_texSnow&gt;;
  51.     MinFilter = LINEAR;
  52.     MagFilter = LINEAR;
  53.     MipFilter = LINEAR;
  54.     AddressU = WRAP;
  55.     AddressV = WRAP;
  56. };
  57.  
  58. // Vertex shader input structure.
  59. struct VS_INPUT
  60. {
  61.  float4 vPosition : POSITION;
  62.  float3 vNormal  : NORMAL;
  63.  float2 vTexCoord : TEXCOORD0;
  64. };
  65.  
  66. // Vertex shader output structure.
  67. struct VS_OUTPUT
  68. {
  69.  float4 vPosition : POSITION;
  70.     float2 vTexCoord0 : TEXCOORD0;
  71.     float2 vTexCoord1 : TEXCOORD1;
  72.     float2 vTexCoord2 : TEXCOORD2;
  73.     float2 vTexCoord3 : TEXCOORD3;
  74.     float4 vDiffuse  : COLOR0;
  75. };
  76.  
  77. // Pixel shader input structure.
  78. struct PS_INPUT
  79. {
  80.  float2 vTexCoord0 : TEXCOORD0;
  81.     float2 vTexCoord1 : TEXCOORD1;
  82.     float2 vTexCoord2 : TEXCOORD2;
  83.     float2 vTexCoord3 : TEXCOORD3;
  84.     float4 vDiffuse  : COLOR0;
  85. };
  86.  
  87. VS_OUTPUT VS_Terrain(VS_INPUT suInput)
  88. {
  89.  VS_OUTPUT suOutput = (VS_OUTPUT)0;
  90.  
  91.  // Transform the vertices.
  92.  suOutput.vPosition = mul(suInput.vPosition, g_matWorldViewProjection);
  93.  
  94.  // Compute the diffuse color.
  95.  float diffuse = 0.3f * saturate(dot(g_vecLightDirection, suInput.vNormal)) + 0.7f;
  96.  
  97.  // Compute the specular color.
  98.  float specular = saturate(3.0f * dot(g_vecLightDirection, suInput.vNormal));
  99.  
  100.  // Get the height from the input position y component.
  101.  float height = suInput.vPosition.y;
  102.  
  103.  // Compute the ratio of each texture for this vertex and multiply it by the lighting.
  104.  suOutput.vDiffuse.x = diffuse * saturate(100.0f * (g_vecHeights.x - height));
  105.  suOutput.vDiffuse.y = diffuse * saturate(100.0f * (g_vecHeights.y - height)) - saturate(100.0f * (g_vecHeights.x - height));
  106.  suOutput.vDiffuse.z = diffuse * saturate(100.0f * (g_vecHeights.z - height)) - saturate(100.0f * (g_vecHeights.y - height));
  107.  suOutput.vDiffuse.w = (diffuse + specular) * 0.95f * saturate(100.0f * (height - g_vecHeights.z)) - saturate(100.0f * (g_vecHeights.z - height));
  108.  
  109.  // Scale down the texture coordinates and pass them to the pixel shader.
  110.  float2 coordinates = suInput.vTexCoord * 0.2f;
  111.  
  112.  suOutput.vTexCoord0 = coordinates;
  113.  suOutput.vTexCoord1 = coordinates;
  114.  suOutput.vTexCoord2 = coordinates;
  115.  suOutput.vTexCoord3 = coordinates;
  116.   
  117.  return suOutput;
  118. }
  119.  
  120. float4 PS_Terrain(PS_INPUT suInput) : COLOR
  121. {
  122.  // Sample the textures.
  123.  float4 ground = tex2D(g_smpGround, suInput.vTexCoord0);
  124.  float4 mud = tex2D(g_smpMud, suInput.vTexCoord1);
  125.  float4 rock = tex2D(g_smpRock, suInput.vTexCoord2);
  126.  float4 snow = tex2D(g_smpSnow, suInput.vTexCoord3);
  127.  
  128.  // Compute the output color.
  129.  float4 color = ground * suInput.vDiffuse.x + mud * suInput.vDiffuse.y + rock * suInput.vDiffuse.z + snow * suInput.vDiffuse.w;
  130.  
  131.  return color;
  132. }
  133.  
  134. technique DefaultTechnique
  135. {
  136.  pass P0
  137.  {
  138.   VertexShader = compile vs_2_0 VS_Terrain();
  139.   PixelShader = compile ps_2_0 PS_Terrain();
  140.  }
  141. }

In the GameFOT class, we add a simple flag to check if we just started the application in the LoadGraphicsContent method. We do that because we load the terrain geometry from a greyscale bitmap texture. The scene is initialized like this:

  1. /// &lt;summary&gt;
  2. /// Initialize scene.
  3. /// &lt;/summary&gt;
  4. public void InitializeScene()
  5. {
  6.     // Create the fly camera.
  7.     _flyCamera = new FlyCamera();
  8.  
  9.     // We set the camera above the terrain.
  10.     _flyCamera.Position = new Vector3(30.0f, 40.0f, 30.0f);
  11.     _flyCamera.Rotation = new Vector3(0.5f, 0.0f, 0.0f);
  12.  
  13.     // Load the heightmap data in a color array..
  14.     Texture2D heightmapTexture = _contentManager.Load&lt;Texture2D&gt;(“Content/Heightmaps/TerrainHeightmap”);
  15.  
  16.     Color[] textureData = new Color[heightmapTexture.Width * heightmapTexture.Height];
  17.  
  18.     heightmapTexture.GetData&lt;Color&gt;(textureData);
  19.  
  20.     // Convert the data into a float array.
  21.     int width = heightmapTexture.Width;
  22.     int depth = heightmapTexture.Height;
  23.  
  24.     float minimumHeight = 0.0f;
  25.     float maximumHeight = 20.0f;
  26.  
  27.     float scale = 50.0f;
  28.  
  29.     float[] data = new float[width * depth];
  30.  
  31.     // When using a greyscale, Texture2D.GetData&lt;&gt;() will set the same value for all R, G and B components
  32.     // with an alpha component set to 255. The scale variable
  33.     for (int x = 0; x &lt; width; ++x)
  34.     {
  35.         for (int z = 0; z &lt; depth; ++z)
  36.         {
  37.             data[x + width * z] = (float)textureData[x + width * z].R / 255.0f * scale;
  38.         }
  39.     }
  40.  
  41.     // Create the heightmap.
  42.     Heightmap heightmap = new Heightmap(width, depth, minimumHeight, maximumHeight, data);
  43.  
  44.     // Create the terrain.
  45.     _terrain = new Terrain(heightmap);
  46. }

Here’s the source code of this part.