【Modern OpenGL】光照类型 Light casters

说明:跟着learnopengl的内容学习,不是纯翻译,只是自己整理记录。
强烈推荐原文,无论是内容还是排版。 原文链接
本文地址:http://blog.csdn.net/aganlengzi/article/details/50578872

光照类型 Light casters

目前我们用到的光照都是由一个单一的光源发出的——一个在空间中的点。它的确产生了比较好的光照效果,但是在现实世界中我们有可以按照其特点分类的不同种类光源。把光投射到物体上的光源叫做光源投射器(A light source that casts light upon objects is called a light caster)。在本次教程中,我们将要讨论几种不同的光源投射器,学习它们可以让我们绘制的场景更加丰富多彩。

我们将会首先讨论平行光,然后谈论点光源(这和我们已经在使用中的光源相类似),最后我们将会讨论聚光灯。在下一个教程中,我们将会把这几种光源集合到一个场景中。

平行光 Directional Light

当光源在无限远的地方,传输过来的光线可以认为是平行光。平行光中,光线看上去是来自同一个方向的,它们彼此平行。所以在我们绘制场景的时候,如果一个光源被设置在无穷远的地方,那么这个光源被叫做平行光源。它的光线被认为是平行的,和具体的光源位置是无关的(只要知道光源被设置在无穷远的地方就可以了)。

最典型的例子就是太阳光。太阳实际上并不是离我们无穷远的,但是它离我们已经足够远以至于我们可以认为它是离我们无穷远的(因为这样的假设并不会对我们的计算效果产生较大的偏差,而且大大简化了计算)。所有从太阳发射到地球的光线都被认为是平行的,就像下面这张图片上展示的:

【Modern OpenGL】光照类型 Light casters_第1张图片

因为在平行光源的模型中,我们已经假设所有的光线都是平行的,所以在场景中的物体无论其处于什么样的位置,光线对其作用效果是一致的。因为所有光线的方向向量都是相同的,所以光照效果的计算对于场景中的每个对象都是十分类似的。

我们在代码中可以通过定义一个光线方向向量来对平行光源进行模拟,而不再像前面那样定义光源的位置而通过物体上的点与光源的位置计算各自的光线方向向量。实际的计算过程还是相同的,只是我们将通过点与光源的相对位置计算光线方向(lightDir)的步骤省去了:

struct Light {
    // vec3 position; // No longer necessery when using directional lights.
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-light.direction);
  ...
}

需要注意的是,和前面一样,我们将光线方向向量反向了。因为到目前为止,我们用于计算的光线方向都被OpenGL要求为从要被渲染的片段指向光源方向的,而我们定义的光线方向显然是从光源中发出的,所以要进行反向的操作。同时需要将这个方向向量进行标准化,记得前面讲过,标准化能够避免在缩放、旋转中的法向量的无意变动,而不同期望输入的光线方向向量都是标准化的。

经过上面两个处理的光线方向向量在后面的计算中和上次教程中将的漫反射和镜面反射映射的计算相似。

为了展示平行光对场景中的不同位置的物体对象产生的作用效果是一致的,我们使用之前在坐标系统那个教程中生成的一个场景中放置十个箱子的场景。你可能已经忘记的,不过没关系。我们首先定义10个箱子的不同位置并且定义每个箱子的模型矩阵(model matrix),每个模型矩阵中都需要包含从本地坐标系到世界坐标系的转换:

for(GLuint i = 0; i < 10; i++)
{
   model = glm::mat4();
   model = glm::translate(model, cubePositions[i]);
   GLfloat angle = 20.0f * i; 
   model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));
   glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

   glDrawArrays(GL_TRIANGLES, 0, 36);
}

还有,不要忘记指定光源的光照方向(像前面说过的,我们定义的光线方向是从光源指向物体的):

GLint lightDirPos = glGetUniformLocation(lightingShader.Program, "light.direction");
glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f);          

现在如果你编译并且执行程序,你看到的场景应该是和下面的相类似的样子:有一个类似于太阳光的光源发射平行光线照射在场景中的所有物体上(所有物体同一个方向被照亮)。我运行的效果:

【Modern OpenGL】光照类型 Light casters_第2张图片

这次是下载的原教程中的代码,打包在这儿了。

点光源 Point lights

平行光源可以作为全局光照亮整个场景,但是除了平行光之外,我们还想要另外的点光源来作为照亮整个场景的光源。点光源,顾名思义,就是在空间中的一个位置放置一个能向四面八方发出光线的光源。就像屋子里面的灯泡一样。

【Modern OpenGL】光照类型 Light casters_第3张图片

在前面的教程中,我们实际已经在使用简化版本的点光源了。我们为设定的光源指定了一个位置,在计算光源的作用效果的时候,我们通过计算需要渲染的点与这个光源的相对位置决定光线的方向向量。但是其中有一点是和真正的点光源是不一致的,那就是我们之前设定的光源发出恒定强度的光线,虽然我们通过设置参数将这个值设定为不是100%,但是在现实世界中,这显然也是不对的。因为点光源发出的光一般形成的是一个以光源为中心的球体形状(意思是在一定的范围内的,光线的强度会随着距离光源的位置而变化)。

具体的效果可以将上面绘制的10个箱子逐渐远离光源,得到的光亮程度是一致的。

衰减 Attenuation

光线强度随着距离的增加而减弱的现象叫做光强度衰减。光线强度衰减的一种方法是定义一个光强与距离的线性等式。这能够达到上述的大致效果,但是这种线性的函数看上去并不是那么真实。在真实的世界中,光强与距离之间的关系显然不是线性相关的。

幸运的是,之前就有很聪明的人为我们找出了光强和距离之间的关系。下面的等式:

其中I代表的是当前片段的光强度,d代表当前片段距离光源的距离。等式左边的衰减值由这个等式得出,其中要定义三个参数,分别是Kc,KlKq,分别是常数项,线性距离影响系数和二次项距离影响系数。

这三个系数是在我们具体使用的时候选择和配置的,在后面会讲到具体的参数选取方法。某种配置得到的效果如下:

【Modern OpenGL】光照类型 Light casters_第4张图片

上面这张图显示的是光强度随着距离的改变而改变的情况,在距离较近的情况下,距离稍有增减,光强度会很快下降。这比较符合显示世界中的情况。

选择正确的参数值

上文中,我们已经得到光强度随着距离改变的公式,但是公式中的三个参数(Kc,KlKq)应该怎样根据具体使用的环境合适选择呢?这当然取决于很多因素:环境、设定的光源覆盖范围和光照类型等等。在大多数情况下,它取决于我们的经验和调试。下面的表格中显示了一些模拟现实世界中情况的一些配置参数:

【Modern OpenGL】光照类型 Light casters_第5张图片

如你所见,常数值Kc一般都是设置为1.0,线性参数Kl比常数值要小,二次参数Kq更小。

实现光强度衰减效果

为了实现光强度衰减效果,需要在片段处理程序中引入额外的三个参数,即上面公式中的Kc,KlKq。这些值最好是定义在我们之前的Light结构体中。注意这一版是在上一版的基础之上进行修改,而不是在本次教程前面部分的基础之上进行修改,所以Light结构体中有position而没有direction。

struct Light {
    vec3 position;  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
}; 

然后,我们在OpenGL程序中来设定这三个值,数值设定按照上面表中距离是50的来设定。

glUniform1f(glGetUniformLocation(lightingShader.Program, "light.constant"), 1.0f);
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.linear"), 0.09);
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.quadratic"), 0.032);     

在片段处理程序中实现衰减是比较直观的。我们只需要按照上面的公式计算出衰减值,并且和环境光、漫反射光和镜面反射光分量相乘就可以了。

其中距离d的计算应该是比较简单的,就是片段到光源位置的向量长度。如下所示:

float distance    = length(light.position - FragPos); float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance));    

在具体衰减值的使用中,我们可以不对环境光进行衰减作用,这意味着环境光不会随着距离的改变而改变。但是,当我们在一个场景中使用多个环境光源的时候,它们的光亮度会叠加。所以,环境光也应该加上衰减效果。这个可以根据自己实现的场景进行灵活调整。

ambient  *= attenuation; 
diffuse  *= attenuation;
specular *= attenuation;   

加上光的衰减后,运行效果大致是这个样子:

【Modern OpenGL】光照类型 Light casters_第6张图片

从效果图中,你应该可以看到只有距离光源很近的箱子的表面显得很亮,而距离比较远的箱子并没有光照效果。

所以一个点光源是具有可配置的位置和衰减效果参数设置的光源。是我们常用光源的一种。

聚光灯 Spotlight

我们要讨论的最后一种光源是聚光灯。聚光灯实际上就是场景中所有的光线往一个特定方向上发射的光源。它作用的效果是在其光线方向上特定半径内的物体被照亮而其它方向或者位置上的物体没有相应的作用效果。一个很好的例子是手电筒照射出的光或者是探照灯。

OpenGL中的聚光灯由世界坐标系中的某个位置、照射方向和一个截止角(用于决定聚光灯的作用半径)定义。对于场景中的每个片段,我们判断其是否在聚光灯照射的圆锥体中。如下图所示,它显示了聚光灯作用效果:

【Modern OpenGL】光照类型 Light casters_第7张图片

其中:
LightDir: 代表从片段指向光源的向量
SpotDir: 代表聚光灯指向的方向
Phi Φ: 截光角,用于指定聚光灯作用的半径,在这个范围外的物体都不会被作用。
Theta θ: 表示LightDir和SpotDir之间的角度,它应该比Φ要小。

所以我们需要做的就是计算出LightDir和SpotDir向量之间的点乘结果,将得到的角度θ和Φ相比较以决定是否对这个片段进行聚光灯效果作用。下面就让我们来试着创建一个。

手电筒 Flashlight

手电筒是在观察者位置上的聚光灯。它从观察者指向正前方。为了在我们的场景中创建一个手电筒,我们需要在片段处理程序中添加所需参数:位置向量,方向向量和截光角。我们还是将这几个参数放在Light结构体中:

struct Light {
    vec3  position;
    vec3  direction;
    float cutOff;
    ...
}; 

然后,我们将合适的参数值传递到处理程序中:

glUniform3f(lightPosLoc,        camera.Position.x, camera.Position.y, camera.Position.z); 
glUniform3f(lightSpotdirLoc,    camera.Front.x, camera.Front.y, camera.Front.z);
glUniform1f(lightSpotCutOffLoc, glm::cos(glm::radians(12.5f))); 

如你所见,我们并没有设置截光角的值,而是根据一个设定的角度值计算其余弦值传递给片段处理程序,在片段处理程序中实际上我们就是利用余弦值来比较需要渲染的片段是否在聚光灯的作用范围内。这样有助于提高性能(减少将余弦值转换成角度的计算)。

现在我们需要做的是计算θ值并且和截光角的余弦值进行比较以确定是否在聚光灯的作用范围之外:

float theta = dot(lightDir, normalize(-light.direction)); if(theta > light.cutOff) { // Do lighting calculations } else // else, use ambient light so scene isn't completely dark outside the spotlight. color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0f);

我们首先计算出lightDir向量和反向方向向量的余弦值,需要注意的是要将这两个向量都进行标准化操作。

或许你会说,为什么这里用的是大于号(>),这是因为根据余弦函数的性质,在那个区间(0.0,90.0)中,角度越大,余弦值越小。如下图所示:

【Modern OpenGL】光照类型 Light casters_第8张图片

运行上面的程序,场景中会出现聚光灯的样子,它看上去是这个样子的:

【Modern OpenGL】光照类型 Light casters_第9张图片

完整的代码在这儿(包括fragment shader和OpenGL code)。

它看上去还是有点假,主要是因为聚光灯的边缘,边缘太明显了。

边缘平滑 Smooth/Soft edges

为了达到边缘平滑的效果,我们采用的方式是在之前的圆锥外边再加上一个圆锥,现在我们有两个圆锥体,一个是上面我们定义的那个,称作内部的圆锥体,一个是在这个外面的,称作外部的圆锥体。我们将内部的圆锥体的边缘到外部圆锥体边缘的这段距离的光逐渐弱化。

为了创建一个外部的圆锥体,我们简单地定义另一个余弦值(像上面定义的方式一样)。如果某个片段在内部圆锥体和外部圆锥体之间,我们按照它距离内部圆锥体边缘的距离将它的光强度逐渐弱化,从1.0到0.0.

我们可以通过下面这个公式完成光强度的计算:

其中,ϵ是内部角度(ϕ)和外部角度(γ)余弦值的插值(ϵ=ϕ−γ)。计算结果I是当前聚光灯的光照强度。

这个公式从直观上比较难理解,我们可以参考一下样本值:

【Modern OpenGL】光照类型 Light casters_第10张图片

从表中可以看出,我们只是在做插值计算,如果还是不理解的话,就直接拿来用就可以了。

我们通过下面的方式完成上述计算:

float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
...
// We'll leave ambient unaffected so we always have a little light. diffuse *= intensity; specular *= intensity; ... 

注意上面用到的clamp函数保证其第一个参数值不会超出[0, 1]的范围。

保证你在Light结构体中定义了outerCutOff变量,并且通过OpenGL代码将这个值进行赋值。在下面这张图中,内部的截光角为12.5f,外部的截光角是17.5f(都是弧度表示):

这样是不是好多了。程序源码和fragment shader。

在下一个教程中,我们在场景中会将所有学到的灯光组合起来使用。

你可能感兴趣的:(Class,OpenGL)