Introduction

There are various ways to make your terrain look realistic in respect to lighting. But because of the huge amount of data to be processed, dynamic lighting for instance isn't a good choice if your lightsource, the sun, never moves because it is on a static skybox. And even if the sun would change its position from time to time, it wouldn't make great sense to slow down your engine with calculating lights and shadows per frame. Also, the calculation of drop shadows is even more a burden to your FPS.

The Trick

Our approach to the problem is to pregenerate a shadowmap that will be used as a texture. To generate this map, we need two things: the heightmap and the sun's position. Given the position of the sun, the calculation is very simple: Draw a straight line from the sun to the point of interest in the heightmap. If at any point the line's height is lower than the current height of the map, the sun ray won't reach the point, which therefore will be black. In other terms, as long as the sun ray isn't blocked by something in the terrain (like a hill), it will directly hit the point we are examining, which in this case will be white. Figure 1 will clear things up.

Figure 1: Sunray and covered pixels.
Figure 1: Sunray and covered pixels.

Some explanation for the image: The sunray v is a vector, its height at point p is v(p). h(p) is the height of the terrain at point p. To process the whole landscape, we simply iterate throug all pixels in the heightmap and find out if they will be influenced by the light or not. This is done with the steps in the following list:

  • Fetch point of interest (POI) p
  • Calculate vector v from p to sun
  • Iterate through all pixels covered by v
  • If v(p) ≤ h(p) at any pixel, stop iteration and mark it as black
Implementation Issues

As easy as it seems at first glance, there are some obstacles to overcome. Like the sun, all scene elements in this calculation will be represented through vectors. So, the sun's positon will be stored in a vector, as well as the ray. The problem now is that the heightmap itself is a discrete representation of the terrain, which doesn't fit too well into our floating-point vector-model. So, to evaluate which "pixel" is covered by our sun ray vector, we need to interpolate the float values of the vector into int values of the heightmap. To minimize the error, the best solution is to use Lerp, but I chose to just round the values to the next int (this will result in some small numerical errors). Warning: Do not simply cast floats to int! It'll result in horrible errors, mostly recognizable in the areas where the vectors have a negative sign in one of their x or z components. Another question is how to iterate along the vector v: Because we don't know the direction of v (it constantly changes from one POI to another), we can't simply "move to the next pixel", so we follow the vector v back to the sun step by step. But how long is a step? The normalized v should be enough! Illustration 2 is the final result for the heightmap shown below with the sun's position at the top-center.

Figure 2: Turning a heightmap into a shadowmap.
Figure 2: Turning a heightmap into a shadowmap.
Code

The code itself isn't a big issue. The comments should help you out!

int round(float n)
{
  if (n-((int)n) >= 0.5)
    return (int)n+1;
  else
    return (int)n;
}

int main()
{
  Vector3 CurrentPos;
  Vector3 LightDir;
  Vector3 Sun(128.0f, 512.0f, 256.0f);
  
  Heightmap hmap("hmap.bmp");
  Heightmap ShadowMap;
  
  int LerpX, LerpZ;
  
  const int MapWidth = hmap.GetWidth(), MapHeight = hmap.GetHeight();
  
  //Initialize new shadow map
  ShadowMap.Create(MapWidth, MapHeight);
  
  std::cout << "Status = ";
  
  //For every pixel on the map
  for (size_t z=0; z<MapHeight; ++z)
  {
    for (size_t x=0; x<MapWidth; ++x)
    {
      //Set current position in terrain
      CurrentPos.Set((float)x, hmap.Get(x, z), (float)z);
  
      //Calc new direction of lightray
      LightDir = Sun - CurrentPos;
      LightDir.Normalize();
  
      ShadowMap.Set(x, z, 255);
  
      //Start the test
      while ( CurrentPos.x() >= 0 &&
          CurrentPos.x() < MapWidth && 
          CurrentPos.z() >= 0 && 
          CurrentPos.z() < MapHeight && 
          CurrentPos != Sun && CurrentPos.y() < 255 )
      {
        CurrentPos+=LightDir;
    
        LerpX = round(CurrentPos.x());
        LerpZ = round(CurrentPos.z());
  
        //Hit?
        if(CurrentPos.y() <= hmap.Get(LerpX, LerpZ))
        { 
          ShadowMap.Set(x, z, 0);
          break;
        }
      }
    }
    if (!(z%32)) std::cout << "0";
  }
  
  std::cout << std::endl;
  
  ShadowMap.Save("result.bmp");
  
  return 0;
}
Final Words

The final result is a shadowmap in respect to the sun's position. There are two options to use it: Either by multitexturing, or by multiplication with the terrain texture. This of course is dependend on your texturing method. The pictures below are an example of a terrain texture multiplied with the shadowmap.

Figure 3: Multiplied terrain- and shadowmap.
Figure 3: Multiplied terrain- and shadowmap.

Another implementation idea would be to fade out the shadow at the edges, but in most cases the image will blur out anyway. For different times of daylight, one could generate 12 or 24 different lightmaps and change them on the fly when the skybox changes it's texture (I haven't yet tried this myself) [1].

Acknowledgements And Additional Sources

Sebastian Wagner for "debugging" this text.

[1] http://www.flipcode.com/tutorials/tut_advlightmaps.shtml