×
关于

现代 OpenGL 教程

第一课 创建一个窗口第二课 绘制一个点第三课 第一个三角形第四课 着色器基础第五课 一致变量第六课 平移变换第七课 旋转变换第八课 缩放变换第九课 插值第十课 索引绘制第11课 矩阵连乘第12课 透视投影变换第13课 相机空间第14课 相机控制(一)第15课 相机控制(二)第16课 基本的纹理贴图第17课 环境光第18课 漫反射光第19课 镜面反射光第20课 点光源第21课 聚光灯第22课 使用Assimp库导入模型第23课 Shadow Mapping 1第24课 Shadow Mapping 2第25课 天空盒第26课 法线纹理第27课 广告牌和几何着色器第28课 创建粒子系统第29课 3D拾取第30课 细分着色器基础第31课 基于PN三角形细分着色第32课 顶点数组对象第33课 实例渲染第34课 GLFX OpenGL特效库第35课 延迟渲染(一)第36课 延迟渲染(二)第37课 延迟渲染(三)第38课 Assimp导入骨骼动画第39课 轮廓检测第40课 模板阴影体第41课 运动模糊第42课 基于 PCF 的阴影优化第43课 点光源多通道阴影映射第44课 GLFW第45课 屏幕空间的环境光遮挡第46课 SSAO与深度重构

第21课 聚光灯


背景

聚光灯是第三个也是最后一个我们将要介绍的光源类型(至少在一段时间内)。他比平行光和点光源更加复杂,而本质上还是用到这二者的很多内容。我们需要设置聚光灯光源的位置,并且其光照强度随着和目标物距离的增加而减小(像点光源那样),而且他也要指向一个特定的方向(像平行光那样)。但是聚光灯新增了一个特性,它的光只分布在有限的圆锥形空间内并且不断减弱,而这个圆锥形空间随着随着与光源位置的增加,其而底部不断扩大。聚光灯的典型例子是手电筒,当你在开发的游戏中的角色在探索一个地牢或者是从监狱中逃跑时,聚光灯是非常有用的。 我们已经知道知道如何去计算物体表面的光照效果,现在还缺少的就是这种光类型的圆锥形效果。请看下面的图片:

聚光灯的方向被定义为垂直向下的黑色箭头所指向的方向。我们希望我们的光只在两条红线内有限区域内产生影响。点积运算在这里会再次派上用场。我们借助于每个红线和光线的方向之间的夹角(即红线之间夹角的一半)来定义我们的光锥。我们可以先算好这个夹角的 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];

GLSL 中有一个相似的聚光灯类型结构体。因为我们不能像在 C++ 代码中那样使用继承,所以我们把点光源结构体当做一个成员,接着添加新的属性。在这里最显著的不同之处是临界值在 C++ 代码中是角度本身,而在着色器中是角度的 cosine 值。着色器只关心 cosine 值,所以我们只计算 cosine 值一次,而不是为每个像素计算一次,这样会使代码更高效。我们还定义了一个聚光灯数组的数组,并且用一个叫做 '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;
}

计算点光源光照效果的函数有一个小的改动——他把 点光源结构体作为一个参数,而不是直接使用全局数组。这样我们在计算聚光灯光照效果的时候就可以直接调用这个函数。

(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);
}
...

用与点光源相似的方式,我们的 main 函数中有一个循环,用于累加每个聚光灯光源对最后的像素颜色的贡献。

(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 直接把点乘的结果和这个值相比较)。需要注意的是库函数 cosf() 以角度的弧度值为参数,所以我们使用方便的 ToRadian 宏指令来转化它。

操作结果


分类导航

关注微信下载离线手册

bootwiki移动版 bootwiki
(群号:472910771)