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.

 OpenGL: Programming:

Copyright © 2002 Adrian Welbourn. All Rights Reserved.