教程 21
聚光灯光源
原文: http://ogldev.atspace.co.uk/www/tutorial21/tutorial21.html
CSDN完整版专栏: http://blog.csdn.net/column/details/13062.html
背景
聚光灯光源是眼下这里要介绍的第三种也是最后一种光源类型了,它比平行光和点光源要复杂,但聚光灯光源事实上是具有平行光和点光源核心特征的一种特殊光源。聚光灯光源也会随着距离衰减。但它不是像点光源照向四面八方的而是像平行光那样有一个聚光方向(相当于取点光源的一个锥形的一小部分),聚光灯光源呈锥形,因此有一个新的属性。就是离光源越远。照亮的圆形区域会越大(光源位于锥形体的尖端)。
聚光灯光源,顾名思义,相应于现实中的聚光灯,比如:手电筒。
在游戏中。聚光灯主要用于某些场景。比如:主角拿着手电筒在黑暗的地道里探索或者逃离监狱。
我们已经知道了创建聚光灯光源的全部技术,这里最后还要另外学一下怎样实现这个光源类型的锥形效果。例如以下图:
图中垂直指向地面的黑色尖头指的是光源方向,这里想实现让光源仅仅照亮两条红线夹角之间的区域。这里仍然能够使用点积来实现。我们能够定义光锥为光线方向L和红线之间的那个夹角(两条红线之间夹角的一半)。计算那个夹角的余弦值‘C’(点积计算得到)以及L和V夹角的余弦。当中V指的是光源到某个像素的向量,假设后者的值大于余弦值‘C’(夹角越小余弦越大),说明L和V之间的夹角偏小,该像素就位于被照亮的区域内。反之,像素位于区域外就不会被该光源照亮。
假设我们紧紧依照上面说的在照亮区域内就点亮像素,否则就不点亮。那样就会看上去非常假,由于照亮区域和未照亮区域之间的边界边缘会非常明显(没有一个自然的过渡),看上去会是一个清晰的圆形画在一个黑色区域(假设没有其它光源的话)。一个真实的聚光灯光源会从照亮区域的中心向圆形边缘慢慢衰减。这里我们能够利用上面计算得到的那些点积作为一个衰减的參数。
首先我们知道,当L和V两个向量相等重合时,点积为‘1’。可是用余弦来做衰减參数会有问题,由于聚光灯光源的夹角不能太大,否则范围太广就失去了聚光灯的效果,可是在夹角从0到一个比較小的角度范围内。cos值得变化是非常缓慢的,导致衰减不明显。比如:让聚光灯的夹角为20度,余弦值就为0.939,[0.939,1.0]这个变化范围就不好作为衰减參数了。在这个范围内进行插值的空间不足,造成的衰减程度不足以让眼睛察觉到。要想衰减效果明显这个參数范围应该是[0,1]。解决方法是将这个參数的小范围映射到[0,1]的范围。方法例如以下:
原理非常easy:计算大的范围和小的范围的比例,然后依据那个比例对小范围进行映射扩张就可以。
源码具体解释
(lighting_technique.h:68)
struct SpotLight : public PointLight
{
Vector3f Direction;
float Cutoff;
SpotLight()
{
Direction = Vector3f(0.0f, 0.0f, 0.0f);
Cutoff = 0.0f;
}
};
聚光灯光源的结构体继承自点光源的结构体。并加入了两个属性和点光源差别开:一个是光源的方向向量,还有一个是截断光源照亮范围的一个阈值。
阈值代表的是光源方向向量和光源到可照亮像素之间的最大夹角。
比这个阈值夹角大的像素是不会被该光源照亮的。这里还在LightingTechnique类中为shader加入了一个位置数组,用来获取shader中的聚光灯光源数组。
(lighting.fs:39)
struct SpotLight
{
struct PointLight Base;
vec3 Direction;
float Cutoff;
};
...
uniform int gNumSpotLights;
...
uniform SpotLight gSpotLights[MAX_SPOT_LIGHTS];
在GLSL中有一个聚光灯光源类型的相似的结构体。由于这里我们不能够在C++代码中进行继承,所以这里将一个点光源结构体对象作为一个成员对象变量,并在后面加入新的属性。
有一个不一样的地方是在C++代码中那个阈值是夹角本身。而在shader中这个阈值是那个夹角的余弦值。
shader着色器仅仅关心夹角的余弦值,因此计算一次并存储比为每个像素都又一次计算余弦值要高效得多。
这里还定义了一个聚关灯光源的数组,并使用一个叫做’gNumSpotLights’的计数器来限制同意应用去创建使用的聚光灯光源的数量。
(lighting.fs:85)
vec4 CalcPointLight(struct PointLight l, vec3 Normal)
{
vec3 LightDirection = WorldPos0 - l.Position;
float Distance = length(LightDirection);
LightDirection = normalize(LightDirection);
vec4 Color = CalcLightInternal(l.Base, LightDirection, Normal);
float Attenuation = l.Atten.Constant +
l.Atten.Linear * Distance +
l.Atten.Exp * Distance * Distance;
return Color / Attenuation;
}
点光源的函数有了轻微的修改:将一个点光源的结构体作为一个參数,而不是直接获取全局数组。这样更easy将它分享给聚光灯光源对象使用。其它的这里没有做修改。
(lighting.cpp:fs)
vec4 CalcSpotLight(struct SpotLight l, vec3 Normal)
{
vec3 LightToPixel = normalize(WorldPos0 - l.Base.Position);
float SpotFactor = dot(LightToPixel, l.Direction);
if (SpotFactor > l.Cutoff) {
vec4 Color = CalcPointLight(l.Base, Normal);
return Color * (1.0 - (1.0 - SpotFactor) * 1.0/(1.0 - l.Cutoff));
}
else {
return vec4(0,0,0,0);
}
}
这里这个函数是我们计算聚光灯光源效果的地方。首先得到光源到某个像素的向量,将向量单位化方便点积运算,然后和单位化了的光源方向向量进行点积运算得到他们之间夹角的余弦值。将得到的余弦值和光源的阈值(定义光源范围的最大夹角的余弦值)进行比較。假设余弦值比阈值小,说明夹角太大像素在照亮圆区域的外面,这样像素就不会被该光源点亮。这样那个阈值就能够将聚光灯光源的照亮范围限制在一个大的或者小的圆圈内。反之假设像素在照亮区域内,我们就先像点光源那样计算光源的基础颜色。然后将那个点积计算得到的參数’SpotFactor’放到上面的公式中,将这个參数线性插值到0到1的范围。最后和点光源颜色相乘计算得到终于的聚光灯颜色值。
(lighting.fs:122)
...
for (int i = 0 ; i < gNumSpotLights ; i++) {
TotalLight += CalcSpotLight(gSpotLights[i], Normal);
}
...
和点光源的计算模式一样我们在主函数通过循环遍历累加全部聚光灯光源的效果得到相应像素的终于颜色值。
(lighting_technique.cpp:367)
void LightingTechnique::SetSpotLights(unsigned int NumLights, const SpotLight* pLights)
{
glUniform1i(m_numSpotLightsLocation, NumLights);
for (unsigned int i = 0 ; i < NumLights ; i++) {
glUniform3f(m_spotLightsLocation[i].Color, pLights[i].Color.x, pLights[i].Color.y, pLights[i].Color.z);
glUniform1f(m_spotLightsLocation[i].AmbientIntensity, pLights[i].AmbientIntensity);
glUniform1f(m_spotLightsLocation[i].DiffuseIntensity, pLights[i].DiffuseIntensity);
glUniform3f(m_spotLightsLocation[i].Position, pLights[i].Position.x, pLights[i].Position.y, pLights[i].Position.z);
Vector3f Direction = pLights[i].Direction;
Direction.Normalize();
glUniform3f(m_spotLightsLocation[i].Direction, Direction.x, Direction.y, Direction.z);
glUniform1f(m_spotLightsLocation[i].Cutoff, cosf(ToRadian(pLights[i].Cutoff)));
glUniform1f(m_spotLightsLocation[i].Atten.Constant, pLights[i].Attenuation.Constant);
glUniform1f(m_spotLightsLocation[i].Atten.Linear, pLights[i].Attenuation.Linear);
glUniform1f(m_spotLightsLocation[i].Atten.Exp, pLights[i].Attenuation.Exp);
}
}
这个函数依据聚光灯光源的结构体数组来继续更新着色器程序,基本上和点光源中相应的这个函数一样,除了额外又加入了两个參数,光的方向单位化后也传给了shader,另外那个阈值夹角装换成它的余弦值之后也传给了shader(方便shader直接用它和点积运算的结果进行比較)。注意库函数cosf()使用的是弧度值參数,是先用ToRadian宏将角度转换成的弧度值。