Lightmapping Tutorial
By Alan Baylis 19/12/2001

Lightmapping is still the preferred method of lighting in most games, namely because
it is fast regardless of how many lights are in the scene, if you've ever shot
at a light bulb that didn't break or it broke but the light around it remained
then you have seen lightmaps in action (or lack of). They are not suitable for
dynamic lighting, though slight variations to the light mapping theme can be applied
to produce fake dynamic lights such as either on/off or flickering lights (normally
by using multiple lightmaps.)
A lightmap is basically just a texture that contains luminance information rather
than an image. The elements of the lightmap are referred to as lumels as they
represent elements of luminosity. After the lightmap has been generated, the texture
to be lit and the lightmap are blended together when applied to the polygon to
produce the final effect. The blending can be pre-calculated before runtime to
speed up the program, though the trend now is to use hardware multi-texturing.
Lightmaps are almost essential for any game engine these days, but I found that
there is a lack of tutorials or clear source on the subject, so this tutorial
and demo are provided to fill in the some of the gaps. This is my first attempt
at creating lightmaps so there may be mistakes in the source or other ways of
doing it.
Before we begin, note that the example code is compatible with the OpenGL API
and deals only with triangles (I will still refer to them as polygons.)
The first step is to calculate the lightmap's UV coordinates. In some cases you
could just use the texture's UV coordinates but this does not work in all cases,
so to do this we use a process called planar mapping. In brief this means that
we project the polygon's coordinates onto a primary axis plane and then convert
the new coordinates to 2D texture space, leaving us with orthogonal UV coordinates
that range between 0 and 1.
To project the polygon's coordinates onto a primary axis plane we must first determine
which of the three planes to use. This is done by checking which component of
the polygon's normal is the largest (using the absolute value), if the x component
is largest we map onto the YZ plane, if the y component is largest we map onto
the XZ plane, or if the z component is largest we map onto the XY plane.
Now that we know which plane we want to map onto, we project the polygon's coordinates
onto the plane by using the two relevant components of each vertex and dropping
the third. In other words, if we are mapping onto the YZ plane we use the y and
z components from each of the polygon's vertices and drop the x component.
In this example code, assume that we are making a lightmap for a single polygon
with three vertices and have a lightmap struct that has UV coordinates for each
vertex. I also set a flag to indicate which plane we are mapping onto which will
be used later.
poly_normal = polygon.GetNormal();
poly_normal.Normalize();
if (fabs(poly_normal.x) > fabs(poly_normal.y) &&
fabs(poly_normal.x) > fabs(poly_normal.z))
{
flag = 1;
lightmap.Vertex[0].u = polygon.Vertex[0].y;
lightmap.Vertex[0].v = polygon.Vertex[0].z;
lightmap.Vertex[1].u = polygon.Vertex[1].y;
lightmap.Vertex[1].v = polygon.Vertex[1].z;
lightmap.Vertex[2].u = polygon.Vertex[2].y;
lightmap.Vertex[2].v = polygon.Vertex[2].z;
}
else if (fabs(poly_normal.y) > fabs(poly_normal.x) &&
fabs(poly_normal.y) > fabs(poly_normal.z))
{
flag = 2;
lightmap.Vertex[0].u = polygon.Vertex[0].x;
lightmap.Vertex[0].v = polygon.Vertex[0].z;
lightmap.Vertex[1].u = polygon.Vertex[1].x;
lightmap.Vertex[1].v = polygon.Vertex[1].z;
lightmap.Vertex[2].u = polygon.Vertex[2].x;
lightmap.Vertex[2].v = polygon.Vertex[2].z;
}
else
{
flag = 3;
lightmap.Vertex[0].u = polygon.Vertex[0].x;
lightmap.Vertex[0].v = polygon.Vertex[0].y;
lightmap.Vertex[1].u = polygon.Vertex[1].x;
lightmap.Vertex[1].v = polygon.Vertex[1].y;
lightmap.Vertex[2].u = polygon.Vertex[2].x;
lightmap.Vertex[2].v = polygon.Vertex[2].y;
}
We then convert these lightmap UV coordinates to 2D texture space. To do this
we must firstly find the bounding box of these coordinates (by using the
minimum and maximum values) and also find the difference (delta) between these
minimum and maximum values. Having done this, we can then make all the lightmap's
UV coordinates relative to the origin by subtracting the minimum UV values from
the UV coordinates and then scale by dividing by the delta values.
Min_U = lightmap.Vertex[0].u;
Min_V = lightmap.Vertex[0].v;
Max_U = lightmap.Vertex[0].u;
Max_V = lightmap.Vertex[0].v;
for (int i = 0; i < 3; i++)
{
if (lightmap.Vertex[i].u < Min_U )
Min_U = lightmap.Vertex[i].u;
if (lightmap.Vertex[i].v < Min_V )
Min_V = lightmap.Vertex[i].v;
if (lightmap.Vertex[i].u > Max_U )
Max_U = lightmap.Vertex[i].u;
if (lightmap.Vertex[i].v > Max_V )
Max_V = lightmap.Vertex[i].v;
}
Delta_U = Max_U - Min_U;
Delta_V = Max_V - Min_V;
for (int i = 0; i < 3; i++)
{
lightmap.Vertex[i].u -= Min_U;
lightmap.Vertex[i].v -= Min_V;
lightmap.Vertex[i].u /= Delta_U;
lightmap.Vertex[i].v /= Delta_V;
}
So now we have the lightmaps UV coordinates we can move onto calculating the lumels.
We need two edges to interpolate with, and we can make these using
the minimum and maximum UV coordinates we calculated, but project them back to
the polygon's plane by using the plane equation Ax + By + Cz + D = 0.
Distance = - (poly_normal.x * pointonplane.x + poly_normal.y
* pointonplane.y + poly_normal.z * pointonplane.z);
switch (flag)
{
case 1: //YZ Plane
X = - ( poly_normal.y * Min_U + poly_normal.z * Min_V + Distance )
/ poly_normal.x;
UVVector.x = X;
UVVector.y = Min_U;
UVVector.z = Min_V;
X = - ( poly_normal.y * Max_U + poly_normal.z * Min_V + Distance )
/ poly_normal.x;
Vect1.x = X;
Vect1.y = Max_U;
Vect1.z = Min_V;
X = - ( poly_normal.y * Min_U + poly_normal.z * Max_V + Distance )
/ poly_normal.x;
Vect2.x = X;
Vect2.y = Min_U;
Vect2.z = Max_V;
break;
case 2: //XZ Plane
Y = - ( poly_normal.x * Min_U + poly_normal.z * Min_V + Distance )
/ poly_normal.y;
UVVector.x = Min_U;
UVVector.y = Y;
UVVector.z = Min_V;
Y = - ( poly_normal.x * Max_U + poly_normal.z * Min_V + Distance )
/ poly_normal.y;
Vect1.x = Max_U;
Vect1.y = Y;
Vect1.z = Min_V;
Y = - ( poly_normal.x * Min_U + poly_normal.z * Max_V + Distance )
/ poly_normal.y;
Vect2.x = Min_U;
Vect2.y = Y;
Vect2.z = Max_V;
break;
case 3: //XY Plane
Z = - ( poly_normal.x * Min_U + poly_normal.y * Min_V + Distance )
/ poly_normal.z;
UVVector.x = Min_U;
UVVector.y = Min_V;
UVVector.z = Z;
Z = - ( poly_normal.x * Max_U + poly_normal.y * Min_V + Distance )
/ poly_normal.z;
Vect1.x = Max_U;
Vect1.y = Min_V;
Vect1.z = Z;
Z = - ( poly_normal.x * Min_U + poly_normal.y * Max_V + Distance )
/ poly_normal.z;
Vect2.x = Min_U;
Vect2.y = Max_V;
Vect2.z = Z;
break;
}
edge1.x = Vect1.x - UVVector.x;
edge1.y = Vect1.y - UVVector.y;
edge1.z = Vect1.z - UVVector.z;
edge2.x = Vect2.x - UVVector.x;
edge2.y = Vect2.y - UVVector.y;
edge2.z = Vect2.z - UVVector.z;
Now that we have the two edge vectors, we can find the lumel positions in world
space by interpolating along these edges using the width and height of the lightmap.
The method I use to calculate the color of each lumel was found through trial
and error but basically uses the Lambert formula to scale the rgb intensities,
as soon I can find some info on this subject it will be improved. I also do a
check of which side of the polygon the lights are on so that only the front facing
polygons are lit.
for(int iX = 0; iX < Width; iX++)
{
for(int iY = 0; iY < Height; iY++)
{
ufactor = (iX / (GLfloat)Width);
vfactor = (iY / (GLfloat)Height);
newedge1.x = edge1.x * ufactor;
newedge1.y = edge1.y * ufactor;
newedge1.z = edge1.z * ufactor;
newedge2.x = edge2.x * vfactor;
newedge2.y = edge2.y * vfactor;
newedge2.z = edge2.z * vfactor;
lumels[iX][iY].x = UVVector.x + newedge2.x + newedge1.x;
lumels[iX][iY].y = UVVector.y + newedge2.y + newedge1.y;
lumels[iX][iY].z = UVVector.z + newedge2.z + newedge1.z;
combinedred = 0.0;
combinedgreen = 0.0;
combinedblue = 0.0;
for (int i = 0; i < numStaticLights; i++)
{
if (ClassifyPoint(staticlight[i].Position,
pointonplane, poly_normal) == 1)
{
lightvector.x = staticlight[i].Position.x - lumels[iX][iY].x;
lightvector.y = staticlight[i].Position.y - lumels[iX][iY].y;
lightvector.z = staticlight[i].Position.z - lumels[iX][iY].z;
lightdistance = lightvector.GetMagnitude();
lightvector.Normalize();
cosAngle = DotProduct(poly_normal, lightvector);
if (lightdistance < staticlight[i].Radius)
{
intensity = (staticlight[i].Brightness * cosAngle)
/ lightdistance;
combinedred += staticlight[i].Red * intensity;
combinedgreen += staticlight[i].Green * intensity;
combinedblue += staticlight[i].Blue * intensity;
}
}
}
if (combinedred > 255.0)
combinedred = 255.0;
if (combinedgreen > 255.0)
combinedgreen = 255.0;
if (combinedblue > 255.0)
combinedblue = 255.0;
lumelcolor[iX][iY][0] = combinedred;
lumelcolor[iX][iY][1] = combinedgreen;
lumelcolor[iX][iY][2] = combinedblue;
}
}
This next part takes the lumelcolor array and puts it into a RGBA format suitable
for saving as your preferred image type.
for(int iX = 0; iX < (Width * 4); iX += 4)
{
for(int iY = 0; iY < Height; iY += 1)
{
lightmap[iX + iY * Height * 4] = (char)lumelcolor[iX / 4][iY][0];
lightmap[iX + iY * Height * 4 + 1] = (char)lumelcolor[iX / 4][iY][1];
lightmap[iX + iY * Height * 4 + 2] = (char)lumelcolor[iX / 4][iY][2];
lightmap[iX + iY * Height * 4 + 3] = 255;
}
}
In closing, a few things to note are:
The static lights contain red, green and blue values ranging from 0-255 as well
as brightness and radius values. Balancing the brightness and radius is a manual
task that can produce different effects; experiment with these depending on the
light effect you want.
Contrary to intuition, a smaller lightmap makes for a better effect. If the lightmap
is too large you will end up with very obvious rings of light rather than a smooth
blend. A 16x16 sized lightmap does seem to be the best size.
When two polygons of different sizes are placed edge to edge with a light shining
on both of them, the light values either side of the edge do not match as we want.
I am pretty sure that this occurs because the lightmap is scaled differently for
each polygon, it isn't a problem when the lightmaps are larger and have more sample
points (but we run into the above problem with rings of light.) One solution would
be to make polygons that are touching have the same edge lengths but that would
be tedious. I'm still working on a solution to this and would appreciate any thoughts
on the subject.
References:
Tobias Johansson
Tomasz Zaniewski
Kurt Miller
Above Content Copyright © 1998 - 2005 Alan Baylis, All Rights Reserved