Landscape Lighting and Texturing

The landscape is loaded as a square heightmap with each vertex encoded as an unsigned char, so height values range from 0 to 255. Lighting and texturing are calculated at startup when the map is loaded, as follows.

1. Calculate a normal map. If you think of a heightmap, each point in the heightmap is surrounded by eight others, i.e. each point in the height map is surrounded by eight triangles:

  x--x--x
|\ | /|
| \|/ | x--X--x
| /|\ |
|/ | \|
x--x--x

The normal for each triangle is just the cross product of any two of its sides. This gives eight normals which are then averaged (by adding and renormalising the result) to a single normal for the center vertex. This is repeated for each point in the heightmap, et voila.

2. Calculate a light map. This is just the dot product of the normal map with the light vector plus an ambient component.

3. Calculate a shadow map. I use the following algorithm to calculate landscape shadows


For each vertex
  If the vertex is already in shadow
    Ignore the vertex
  Else
    Step along a ray passing through the vertex in the
    direction of the light vector and for each vertex
    intersected in x, y
      If the vertex height is <= ray height
        Write into the shadow map for this vertex
      End If
    End For
  End If
End For

Shadows must also be calculated for any BSP models insterted into the heightmap. This is done by projecting the faces of the BSP model along the light vector onto the plane z=0 and drawing the resulting projected polygons into the shadow map. This works fine as long as the heightmap around the embedded BSP model is relatively flat.

4. Calculate a texture map for the entire heightmap. The texture map is built by blending together a set of base textures according to the steepness of the heightmap, i.e. the z component of the normal map. The texture map intensity is then adjusted by applying the light and shadow maps. The texture map may then be chopped up into 1 or more chunks, to accomodate hardware limitations on maximum texture object size, and each chunk is used to create an OpenGL texture object with internal format GL_RGB.

5. Calculate alpha maps for the entire heightmap. Alpha maps are created for each splat texture based on the steepness of the heightmap, i.e. the z component of the normal map. A lighting alpha map is also created from the light and shadow maps. The alpha maps may then be chopped up into 1 or more chunks, to accomodate hardware limitations on maximum texture object size, and each chunk is used to create an OpenGL texture object with internal format GL_ALPHA.

6. The splat texture maps are loaded as OpenGL texture objects with internal format GL_RGB.

7. At render time the heightmap is rendered in patches, with multiple rendering passes per patch:


IF the patch is too distant for splatting
    Render the patch using the texture map calculated
    in step 4.
ELSE
    FOR each splat texture
        Load the splat texture into one texture unit
        using texture env mode GL_REPLACE (provides R,
        G and B components).
        Load the corresponding alpha map into another
        texture unit using texture env mode GL_REPLACE
        (provides A component).
        Render the patch using GL_SRC_ALPHA blending.
    END FOR
    Render the patch again, blending in the light map.
    Render the patch again, blending in the texture map
    calculated in step 4, with a blend factor determined
    by the distance between the patch and the camera.
END IF

 

Choosing the Mip Level for a Patch

The method used by lScape for choosing the mip level for a patch is different to that described in the original GeoMipMapping paper. In lScape, when the height map is loaded, an error array is calculated for each patch. The error array just contains the maximum error for each mip level for the patch (i.e. the maximum absolute difference between each interpolated heightmap point and each real heightmap point for each mip level). The error for miplevel zero is obviously always zero.

At run time, to get the mip level for a patch:

  • Get the perpendicular distance d between the viewpoint and the patch mid-point (can be calculated using dot products, no need for square roots).
  • Get the mip level Md based on a logarithmic distance scale e.g. something like
    0 <= d < f / 2 gives mip level 0
    f / 2 <= d < 3f / 4 gives mip level 1
    3f / 4 <= d < 7f / 8 gives mip level 2
    etc, where f is the far plane distance.
  • Get the mip level Ms based on screen space error - i.e. for each mip level for the patch, get the error from the pre-calculated error array and use d to project the error from world space to screen space, stop when the screen space error is <= some pre-defined acceptable limit (this is guaranteed to stop at mip level 0 since mip level 0 is guaranteed to have screen space error 0).
  • Set the patch mip level Mp = max(Md, Ms) i.e. lowest level of detail allowed by log distance scale and screen space error.

It sounds complicated but it can be done pretty quickly, and the end result gives very little popping. The patch size is currently 16 x 16 heightmap samples, but this is fairly arbitrary. I haven't really done much investigation into optimum patch size yet.

 

Last Update: 13th February 2002.

 
    Home
    About
    Projects
    Screenshots
    Downloads
    Contact
 
OpenGL:
    OpenGL.org
    NeHe Tutorials
    nVIDIA Developer
    GLUT
    GLUT for Win32
    GLUT for LCC-Win32
 
Programming:
    Flipcode
    LCC-Win32
    LCC-Win32 Meeting Point
    OO Programming In C
 

Copyright 2002 Adrian Welbourn. All Rights Reserved.