http://www.alsprogrammingresource.com/lightmapping_tutorial.html
光照贴图教程
作者:Alan Baylis 19/12/2001
在大多数游戏中,光照贴图仍然是首选的照明方法,也就是因为无论场景中有多少灯光,它都是快速的,如果你曾经拍过一个没有破裂或者没有破坏的灯泡而是周围的灯光然后,你已经看到了行动(或缺乏)的光照贴图。它们不适合动态照明,但可以应用光照贴图主题的轻微变化来产生假动态光,例如开/关或闪烁的灯光(通常使用多个光照贴图)。
光照贴图基本上只是包含亮度信息而不是图像的纹理。光照贴图的元素称为亮度,因为它们代表光度的元素。生成光照贴图后,应用于多边形时,要点亮的纹理和光照贴图会混合在一起,以产生最终效果。可以在运行之前预先计算混合以加速程序,尽管现在的趋势是使用硬件多纹理。
光照贴图对于任何游戏引擎来说几乎都是必不可少的,但我发现这个主题缺乏教程或明确的来源,所以提供本教程和演示来填补一些空白。这是我创建光照贴图的第一次尝试,因此可能存在错误的来源或其他方式。
在开始之前,请注意示例代码与OpenGL API兼容,并且只处理三角形(我仍然将它们称为多边形。)
第一步是计算光照贴图的UV坐标。在某些情况下,您可以使用纹理的UV坐标,但这并不适用于所有情况,因此为此,我们使用称为平面映射的过程。简而言之,这意味着我们将多边形的坐标投影到主轴平面上,然后将新坐标转换为2D纹理空间,为我们留下0到1之间的正交UV坐标。
要将多边形的坐标投影到主轴平面上,我们必须首先确定要使用的三个平面中的哪一个。这是通过检查多边形法线的哪个分量最大(使用绝对值),如果x分量最大,我们映射到YZ平面,如果y分量最大,我们映射到XZ平面,或者如果z分量是我们映射到XY平面上的最大值。现在我们知道要映射到哪个平面,我们通过使用每个顶点的两个相关组件并丢弃第三个,将多边形的坐标投影到平面上。换句话说,如果我们映射到YZ平面,我们使用每个多边形顶点的y和z分量并删除x分量。
在此示例代码中,假设我们为具有三个顶点的单个多边形制作光照贴图,并且具有每个顶点具有UV坐标的光照贴图结构。我还设置了一个标志,指示我们要映射到哪个平面,稍后将使用该平面。
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;
}
否则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;
}
其他
{
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;
}
然后,我们将这些光照贴图UV坐标转换为2D纹理空间。要做到这一点,我们必须首先找到这些坐标的边界框(通过使用
最小值和最大值)并找出这些最小值和最大值之间的差值(delta)。完成此操作后,我们可以通过从UV坐标中减去最小UV值然后除以delta值来缩放相对于原点的所有光照贴图的UV坐标。
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;
}
所以现在我们可以使用光照贴图UV坐标来计算lumels。我们需要使用两个边进行插值,我们可以使用我们计算的最小和最大UV坐标来制作这些边,但是使用平面方程Ax + By + Cz + D = 0将它们投影回多边形平面。
距离= - (poly_normal.x * pointonplane.x + poly_normal.y
* pointonplane.y + poly_normal.z * pointonplane.z);
开关(旗帜)
{
案例1:// YZ飞机
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;
打破;
案例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;
打破;
情况3:// XY平面
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;
打破;
}
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;
现在我们有两个边矢量,我们可以通过使用光照贴图的宽度和高度沿这些边插值来找到世界空间中的lumel位置。我用来计算每个lumel颜色的方法是通过反复试验找到的,但基本上使用Lambert公式来缩放RGB强度,我很快就能找到关于这个主题的一些信息,它会得到改进。我还检查了灯光所在的多边形的哪一侧,以便只有前面的多边形被点亮。
for(int iX = 0; iX <Width; iX ++)
{
for(int iY = 0; iY <Height; iY ++)
{
ufactor =(iX /(GLfloat)Width);
vfactor =(iY /(GLfloat)高度);
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;
}
}
下一部分采用lumelcolor数组并将其放入RGBA格式,适合保存为首选图像类型。
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];
光照贴图[iX + iY *高度* 4 + 1] =(字符)lumelcolor [iX / 4] [iY] [1];
光照贴图[iX + iY *高度* 4 + 2] =(字符)lumelcolor [iX / 4] [iY] [2];
光照贴图[iX + iY *高度* 4 + 3] = 255;
}
}
最后,需要注意的一些事项是:
静态灯包含红色,绿色和蓝色值,范围为0-255以及亮度和半径值。平衡亮度和半径是一项手动任务,可以产生不同的效果; 根据您想要的光效实验这些。
与直觉相反,较小的光照贴图可以产生更好的效果。如果光照贴图太大,您将得到非常明显的光环而不是光滑的混合。16x16大小的光照贴图似乎确实是最佳尺寸
当两个不同大小的多边形从一个边缘到另一个边缘放置,并且两个光线都照射在它们上面时,边缘的任何一侧的光值都不符合我们的要求。我很确定这是因为光照贴图对每个多边形的缩放比例不同,当光照贴图较大并且有更多的采样点时它不是问题(但是我们遇到上述光环问题)。要使触摸的多边形具有相同的边长,但这将是乏味的。我仍在努力解决这个问题,并希望对此问题有任何想法
参考文献: