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.