Latest ArticlesEngine Architecture #1
Static Shadows #1
Paint on Mesh
A Tale of the Internet
Ambient Occlusion Oct 08, 2006
One way to improve static lighting in a game engine is by using an ambient occlusion map. The map itself stores the unoccluded percentage from 0 to 1 at each texel, or at each vertex if per texel isn't needed.
The idea is that ambient illumination is coming in equally from all directions, so the more directions from a sample point that are occluded, the less lighting hitting the point. Since there's really no such thing as ambient light, there's no proper way to do it, we just want to do whatever looks good.
The main limitation is that an ambient map is only for static meshes. If any parts of the mesh move relative to each other, it will need to be recomputed, which will be slow to do in realtime. Another limitation for per texel maps is the need for unique uv coordinates, although if a model is completely symmetric, then the halves can share coordinates. This article just discusses the basics since I haven't played much with ambient occlusion yet, there are fancier ways to use and compute ambient occlusion than those discussed here.
Creating the MapThe ambient occlusion map is created by casting a number of rays into the hemisphere above a sample point (ie. the rays that are within 90 degrees of the normal.) We test whether each ray hits the mesh. The more rays that intersect other mesh geometry, the darker the sample point. You can also take distance to occluder into account, darkening more the closer the occluder is. The more rays we use, the higher the quality.
If the mesh was just a box or a sphere, or anything completely convex, then none of the rays would hit anything. However if a mesh has a concave shape, or multiple convex shapes, then it should benefit from this technique since the ambient occlusion map will contain darker areas.
RenderingThe ambient occlusion map can be used with the base ambient render pass, and direct illumination added later with additive blending.
In addition to creating the ambient occlusion map, the rays can also be used to compute bent normals. This simply means perturbing the direction of the normals used with direct illumination an arbitrary amount away from the directions of occluded rays. Then the surface will be brighter when lit from a direction where it's likely less of the light will be occluded.
You don't have to assume ambient light coming equally from all directions. You can use a cube map to store an average brightness and color of light coming from each direction, perhaps based on the environment. Such as sky color from above, and a darker ground color to represent reflected light from below.
UV to World SpaceIn creating maps like an ambient occlusion map, or light map, or normal map, you want to be able to find the world space position from a uv coordinate. We can loop through the triangle list and do the calculation per triangle. As an intermediate step, we want 3d vectors that represent the world space position delta from u=0 to u=1 and v=0 to v=1. For code see the tangent space article, but make sure to remove the two lines of code that normalize the vectors. I'm assuming the UVs used are from 0 to 1, so you'll want want to divide the vectors by the width or height of the map. The code below should clarify:
// Compute the world space position delta along u and v ComputeTangentVecs( VertPos0, VertPos1, VertPos2, UV0, UV1, UV2, DeltaU, DeltaV ); DeltaU /= (float)TexWidth; DeltaV /= (float)TexHeight; // Compute the world space origin from the position and uv of vertex 0 Vec3 OriginWS = VertPos0; OriginWS -= DeltaU * (UV0.x * TexWidth - 0.5f); OriginWS -= DeltaV * (UV0.y * TexHeight - 0.5f); // This is how you compute the world space position for any uv on this triangle Vec3 PointWS = OriginWS + DeltaU * (float)u + DeltaV * (float)v;The origin is offset by 0.5 because we want to use the center of each texel. This exact code might not work correctly in a different program, for instance I had to flip the DeltaV vector from my usual computation.
For each triangle we loop through valid uv coordinates on that triangle, and cast rays from the 3d position. One way to do this is to compute an enclosing square using the min (u,v) and max (u,v), then for each point in the square test if it's in the triangle. Or you could use scan-converted horizontal lines.