聚光灯是第三个也是最后一个我们将要介绍的光源类型(至少在一段时间内)。他比平行光和点光源更加复杂,而本质上还是用到这二者的很多内容。我们需要设置聚光灯光源的位置,并且其光照强度随着和目标物距离的增加而减小(像点光源那样),而且他也要指向一个特定的方向(像平行光那样)。但是聚光灯新增了一个特性,它的光只分布在有限的圆锥形空间内并且不断减弱,而这个圆锥形空间随着随着与光源位置的增加,其而底部不断扩大。
聚光灯的典型例子是手电筒,当你在开发的游戏中的角色在探索一个地牢或者是从监狱中逃跑时,聚光灯是非常有用的。 我们已经知道知道如何去计算物体表面的光照效果,现在还缺少的就是这种光类型的圆锥形效果。请看下面的图片:
聚光灯的方向被定义为垂直向下的黑色箭头所指向的方向。我们希望我们的光只在两条红线内有限区域内产生影响。点积运算在这里会再次派上用场。我们借助于每个红线和光线的方向之间的夹角(即红线之间夹角的一半)来定义我们的光锥。我们可以先算好这个夹角的 cosine 值 “C” ,然后计算光线方向 “L” 和从光源到像素点的矢量 “V” 之间的点积。如果点乘的结果比 “C” 大(记住 cosine 值随角度变小而增大),那么 “L” 和 “V” 之间的夹角就比 “L” 和两条红线之间的角度小。在这种情况下像素点应该接收到光照。如果结果较大,那么像素点将不能接收任何来自聚光灯的光线。在上面图中的例子 “L” 和 “V” 之间的点乘结果比 “L” 和任一条红线间点乘的结果都小(很明显 “L” 和 “V” 之间的夹角要比 “L“ 和红线之间的角度大),因此这个像素点在光锥之外,不能被照亮。
如果我们采用“接收/不接收光“的方法,我们得到的结果将是一个非常虚假的聚光灯效果,它照亮的地方与黑暗的地方有非常明显的界限。看起来就像一个在全黑世界里(假设没有其他的光源)的一个整圆。对于聚光灯来说,一个比较好的效果是离中心点越远的地方,光照效果越弱。我们可以将我们计算的点乘结果(用来确定像素是否被照亮)作为一个因素。我们已经知道 “L” 和 “V” 相等时点乘结果最大是 1(即最强光)。但是现在我们遇到了余弦函数的一些令人讨厌的地方,聚光灯的光锥的角度不能太大,否则光分布得太广,我们也就失去了聚光灯的效果。如果我们将角度设置为 20 度,20 度的 cosine 值是 0.939 ,但是 [0.939, 1.0] 的变化范围太小,不能作为因子,因为没有足够的空间进行眼睛能够注意到的插值。[0, 1]的范围能提供更好的效果。
我们要用的方法是把聚光灯角度所定义的小范围映射到[0, 1]这个较大的范围中。下面看是我们如何做到这一点的:
原理非常简单——计算小范围和大范围之间的比例,并且用这个比例来对你的范围进行缩放。
(lighting_technique.h:68) |
struct SpotLight : public PointLight |
{ |
Vector3f Direction; |
float Cutoff; |
SpotLight() |
{ |
Direction = Vector3f(0.0f, 0.0f, 0.0f); |
Cutoff = 0.0f; |
} |
}; |
(lighting.fs:39) |
struct SpotLight |
{ |
struct PointLight Base; |
vec3 Direction; |
float Cutoff; |
}; |
... |
uniform int gNumSpotLights; |
... |
uniform SpotLight gSpotLights[MAX_SPOT_LIGHTS]; |
(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; |
} |
(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); |
} |
} |
我们在这里计算聚光灯的光照效果。首先我们需要计算从光源到当前片元的向量。按照惯例,先提前标准化这个向量为点乘做好准备。之后再在这个向量和光的方向之间进行点乘(光的方向在应用程序中已经被标准化),以得到二者之间角度的 cosine 值,然后和光的临界值相比较(临界值是光的方向和定义光照射的圆区域的向量之间夹角的余弦值)。
比较后如果这个值小于临界值,当前像素处于聚光灯的光照区域之外。在这种情况下聚光灯的光照效果为 0。 通过这个临界值我们可以对聚光灯的光照效果限制在一个可控的区域内。之后我们就可以和点光源一样计算光照效果了,然后将刚刚计算的点乘结果('SpotFactor')带入到我们之前介绍过的公式中。那个提供将 '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); |
} |
} |