OpenGL学习笔记12-Basic Lighting

Basic Lighting 基本采光技术

Lighting/Basic-Lighting

现实世界中的照明是极其复杂的,它依赖于太多的因素,我们无法用有限的处理能力来计算。因此,OpenGL中的照明是基于对现实的近似,使用简化的模型,更容易处理,看起来也相对相似。这些照明模型是基于我们所理解的光的物理原理。其中一个模型叫做Phong照明模型。Phong照明模型的主要构建模块包括3个组件:环境光、漫射光和高光。下面你可以看到这些照明组件看起来像他们自己和组合:

  • Ambient lighting: 即使是在黑暗的时候,通常在世界的某个地方仍然有一些光(月亮,一个遥远的光),所以物体几乎从不是完全黑暗的。为了模拟这一点,我们使用一个环境照明常数,它总是给物体一些颜色。
  • Diffuse lighting:模拟一个轻的物体对一个物体的方向影响。这是照明模型在视觉上最重要的组成部分。物体朝向光源的部分越多,物体就越亮。
  • Specular lighting: 模拟出现在闪亮物体上的光点。高光更倾向于光的颜色而不是物体的颜色。

 

要创建视觉上有趣的场景,我们至少要模拟这3个照明组件。我们将从最简单的一个开始:环境照明。

Ambient lighting

光通常不是来自一个单一的光源,而是来自分散在我们周围的许多光源,即使它们不是立即可见的。光的特性之一是它可以向多个方向散射和反弹,到达那些不直接可见的点;因此,光线可以反射到其他表面,并对物体的照明产生间接的影响。考虑到这一点的算法被称为全局光照算法,但是这些算法计算起来很复杂,而且成本很高。

因为我们不是复杂和昂贵的算法的粉丝,我们将开始使用一个非常简单的模型的全局照明,即环境照明。正如你在上一节看到的,我们使用了一个小的常量(光)颜色,我们添加到物体碎片的最终结果颜色,因此使它看起来总是有一些散射光,即使没有直接光源。

在场景中添加环境照明非常简单。我们取光的颜色,用一个小的恒定环境因子乘以它,再用它乘以物体的颜色,然后在立方体物体的着色器中使用它作为片段的颜色:


void main()
{
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}  

如果你现在运行这个程序,你会注意到照明的第一阶段现在已经成功地应用到对象上了。对象是相当暗的,但不完全,因为环境照明应用(注意,光立方体不受影响,因为我们使用了不同的着色器)。它应该是这样的:

 

Diffuse lighting

环境光本身不会产生最有趣的结果,但是漫射光将开始给对象一个显著的视觉影响。漫射光线使物体的碎片与光源的光线越接近,物体的亮度就越高。为了让你更好地理解漫射照明,看看下面的图片:

 

在左边,我们找到了一个光源,其中有一束光线瞄准了我们的物体的一个单一碎片。我们需要测量光线接触碎片的角度。如果光线垂直于物体表面,光线的影响最大。为了测量光线和碎片之间的角度,我们使用了一个叫做法向量的东西,这是一个垂直于碎片表面的向量(这里描绘为一个黄色箭头);我们待会再谈。这两个向量之间的夹角可以通过点积很容易地计算出来。

你们可能还记得变换transformations 那一章,两个单位向量的夹角越小,点积就越倾向于1。当两个向量的夹角是90度时,点积就是0。同样的道理也适用于“最后的结果”:“最后的结果”越大,光线对碎片颜色的影响就越小。

注意,为了(仅)得到两个向量之间夹角的余弦值,我们将使用单位向量(长度为1的向量),因此我们需要确保所有的向量都是标准化的,否则点积返回的不仅仅是余弦值(参见变换transformations )。

由此产生的点积返回一个标量,我们可以使用它来计算光线对片段颜色的影响,从而产生基于光线方向的不同光线片段。

那么,我们需要什么来计算漫射光:

  • Normal vector: 垂直于顶点表面的向量
  • The directed light ray: 一个方向矢量,它是光的位置和碎片的位置之间的差矢量。为了计算这个光线,我们需要光线的位置向量和碎片的位置向量。

 

Normal vectors

法向量是垂直于顶点表面的(单位)向量。由于顶点本身没有表面(它只是空间中的一个点),我们通过使用它周围的顶点来获取法向量来计算出顶点的表面。我们可以使用一个小技巧,通过使用叉乘来计算立方体所有顶点的法向量,但由于三维立方体不是一个复杂的形状,我们可以简单地手动将它们添加到顶点数据中。更新后的顶点数据数组可以在这里here.找到。试着想象法线确实是垂直于每个平面表面的向量(一个立方体由6个平面组成)。

因为我们给顶点数组添加了额外的数据,我们应该更新立方体的顶点着色器:


#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...

现在我们为每个顶点添加了一个法向量并更新了顶点着色器,我们也应该更新顶点属性指针。请注意,光源的立方体使用相同的顶点数组作为顶点数据,但是灯光着色器没有使用新添加的法向量。我们不需要更新灯的着色器或属性配置,但我们至少需要修改顶点属性指针来反映新的顶点数组的大小:


glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

我们只需要使用每个顶点的前3个浮点数,忽略最后3个浮点数,所以我们只需要将stride参数更新为float的6倍大小,这样就完成了。

使用没有被lamp着色器完全使用的顶点数据可能看起来效率很低,但是顶点数据已经从容器对象存储在GPU的内存中,所以我们不需要将新的数据存储到GPU的内存中。这实际上比为lamp分配一个新的VBO更有效率。

所有的光照计算都是在片段着色器中完成的,所以我们需要将顶点着色器的法向量转发到片段着色器。让我们这样做:


out vec3 Normal;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
} 

接下来要做的就是在fragment shader中声明相应的输入变量:


in vec3 Normal;  

Calculating the diffuse color 计算漫射色

现在我们有了每个顶点的法向量,但是我们仍然需要光线的位置向量和片段的位置向量。因为光线的位置是一个单一的静态变量,我们可以在fragment shader中声明它为uniform :


uniform vec3 lightPos;  

然后在渲染循环中更新制服(或者在渲染循环之外,因为它不会每帧改变)。我们使用前一章中声明的光点矢量作为漫射光源的位置:


lightingShader.setVec3("lightPos", lightPos);  

最后我们需要的是片段的实际位置。我们会在世界空间中做所有的光照计算所以我们想要一个在世界空间中的顶点位置。我们可以通过将顶点位置属性与模型矩阵相乘(而不是视图和投影矩阵)来实现这一点,从而将其转换为世界空间坐标。这很容易在顶点着色器中完成,所以让我们声明一个输出变量并计算它的世界空间坐标:


out vec3 FragPos;  
out vec3 Normal;
  
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = aNormal;
}

最后在fragment shader中添加相应的输入变量:


in vec3 FragPos;  

这个in变量将从三角形的3个世界位置向量中得到插值,以形成FragPos向量,即每个片段的世界位置。现在所有必需的变量都设置好了,我们可以开始照明计算了。

首先我们需要计算的是光源和碎片位置之间的方向矢量。从上一节我们知道光的方向矢量是光的位置矢量和片段的位置矢量之间的差矢量。你可能还记得在变换这一章中,我们可以通过彼此减去两个向量来计算这个差值。我们还想确保所有相关的向量最终都是单位向量,所以我们对法向量和最终的方向向量进行标准化:


vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);  

在计算光照时,我们通常不关心矢量的大小或它们的位置;我们只关心它们的方向。因为我们只关心它们的方向,几乎所有的计算都是用单位向量完成的,因为它简化了大多数计算(比如点积)。所以在进行光照计算时,确保你总是对相关的向量进行规格化,以确保它们是实际的单位向量。忘记标准化向量是一个常见的错误。

接下来,我们需要计算光线在当前片段上的漫射效果,方法是取norm和lightDir向量之间的点积。得到的值与光的颜色相乘得到漫反射分量,结果是两个矢量之间的角度越大,漫反射分量就越暗:


float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

如果两个向量的夹角大于90度那么点积的结果就是负的最终得到负的漫射分量。出于这个原因,我们使用max函数来返回两个参数的最大值,以确保漫反射成分(因此颜色)不会变成负的。负色光并没有真正定义,所以最好远离它,除非你是那些古怪的艺术家之一。

现在我们已经有了环境和漫反射组件,我们将两种颜色相加,然后将结果与物体的颜色相乘,得到结果片段的输出颜色:


vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

如果你的应用程序(和着色器)成功编译,你应该看到这样的东西:

 

你可以看到,在漫射照明下,立方体再次看起来像一个真实的立方体。试着在你的脑海中想象法向量,在立方体中移动相机,看法向量和光线方向向量之间的角度越大,碎片就会变得越暗。

如果您卡住了,请随意将您的源代码与完整的源代码here进行比较。

One last thing

在上一节中,我们直接从顶点着色器传递了法向量到片段着色器。然而,碎片着色器中的计算都是在世界空间中完成的,所以我们不应该同样将法向量转换为世界空间坐标吗?基本上是可以的,但这并不像简单地用一个模型矩阵乘以它那么简单。

首先,法向量只是方向向量,并不代表空间中的特定位置。第二,法向量没有齐次坐标(顶点位置的w分量)。这意味着平动不应该对法向量有任何影响。所以如果我们想把这个法向量与矩阵模型我们想要删除的翻译部分矩阵通过左上方的3 x3矩阵模型的矩阵(注意,我们还可以设置的法向量的w分量0乘以4 x4矩阵)。

其次,如果模型矩阵将执行非均匀尺度,顶点将以这样一种方式改变,即法向量不再垂直于表面。下图显示了这样的模型矩阵(非均匀缩放)对法向量的影响:

 

当我们应用一个非均匀的比例(注意:均匀的比例只改变法线的大小,而不是它的方向,这很容易通过标准化来固定),法向量不再垂直于相应的表面,这会使光线失真。

修正这种行为的技巧是使用一个专门针对法向量定制的不同的模型矩阵。该矩阵称为正规矩阵,并使用一些线性代数运算来消除法向量缩放错误的影响。如果你想知道这个矩阵是如何计算的,我建议下面的文章article. 。

正规矩阵被定义为“模型矩阵左上角3x3部分的逆的转置”。唷,这很拗口,如果你不明白这是什么意思,不用担心;我们还没有讨论逆矩阵和转置矩阵。注意,大多数资源定义的标准矩阵是从模型-视图矩阵派生出来的,但是由于我们是在世界空间(而不是视图空间)中工作,我们将从模型矩阵派生它。

在顶点着色器中,我们可以通过使用在任何矩阵类型上工作的顶点着色器的逆和转置函数来生成标准矩阵。注意,我们将矩阵转换为一个3x3的矩阵,以确保它失去平移属性,并且它可以与vec3法向量相乘:


Normal = mat3(transpose(inverse(model))) * aNormal;  

对于着色器来说,反转矩阵是一项昂贵的操作,所以尽可能避免做反转操作,因为它们必须在场景的每个顶点上进行。为了学习的目的,这是好的,但对于一个有效的应用程序,你可能想要计算在CPU上的标准矩阵和发送它到着色器通过统一在绘图之前(就像模型矩阵)。

在漫射光线部分,光线很好,因为我们没有对物体做任何缩放,所以真的不需要使用法线矩阵,我们可以将法线与模型矩阵相乘。但是如果你在做一个非均匀尺度,你必须将法向量和法矩阵相乘。

Specular Lighting 镜面光照

如果你还没有被所有的照明谈话所累,我们可以开始完成Phong照明模型添加高光

与漫射光类似,高光也基于光的方向向量和物体的法向量,但是这次它也基于视图的方向,比如玩家从哪个方向看片段。高光照明是基于表面的反射属性。如果我们把物体的表面想象成一面镜子,高光是最强烈的地方,我们会看到光线在表面上反射。你可以在下面的图片中看到这种效果:

 

我们通过在法向量周围反射光的方向来计算反射向量。然后我们计算这个反射向量和视图方向之间的角距离。它们之间的角度越近,反射光的影响就越大。最终的效果是,当我们看着光线通过表面反射的方向时,我们看到了一点高光。

视图向量是一个额外的变量,我们需要高光,我们可以计算使用观察者的世界空间位置和片段的位置。然后我们计算高光的强度,将其与光的颜色相乘,并将其添加到环境和漫反射组件中。

我们选择在世界空间中进行照明计算,但大多数人倾向于在视图空间中进行照明。视图空间的一个优点是,查看者的位置总是在(0,0,0),所以您已经免费获得了查看者的位置。然而,我发现计算世界空间中的照明对于学习来说更加直观。如果你仍然想在视图空间中计算光照,你也需要用视图矩阵变换所有相关的向量(别忘了也要改变法线矩阵)。

要获得观察者的世界空间坐标,我们只需取摄像机对象(当然是观察者)的位置向量。所以让我们添加另一个统一到片段着色器,并传递摄像机位置矢量到着色器:


uniform vec3 viewPos;

lightingShader.setVec3("viewPos", camera.Position); 

现在我们有了所有必需的变量,我们可以计算镜面强度了。首先我们定义一个高光强度值,给高光一个中等亮度的颜色,这样它不会有太多的影响:


float specularStrength = 0.5;

如果我们将其设置为1。0f,我们会得到一个非常明亮的镜面组件,这对于一个珊瑚立方体来说有点太多了。在下一章中,我们将讨论如何正确设置所有这些照明强度以及它们如何影响物体。接下来我们计算视图方向向量和相应的沿法向轴的反射向量:


vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);  

注意,我们否定了lightDir向量。反射函数期望第一个向量从光源指向片段的位置,但是lightDir向量当前的方向是相反的:从片段指向光源(这取决于之前计算lightDir向量时的减法顺序)。为了确保得到正确的反射向量,我们首先通过否定lightDir向量来逆转它的方向。第二个参数需要一个法向量,所以我们提供标准化的范数向量。

接下来要做的就是计算镜面成分。这可以用下面的公式来完成:


float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;  

我们首先计算视图方向和反射方向之间的点积(确保它不是负的),然后取32次方。这个32值是高光的亮度值。一个物体的亮度值越高,它就越能正确地反射光线,而不是将光线散射到周围,因此高光就变得越小。下面你可以看到一个图片,显示了不同亮度值的视觉影响:

我们不想让镜面组件太分散注意力,所以我们保持指数为32。唯一要做的就是将它添加到环境和漫反射组件中,并将合成结果与物体的颜色相乘:


vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

现在我们计算了Phong照明模型的所有照明组件。基于你的观点,你应该看到这样的事情:

您可以在这里找到应用程序的完整源代码。here.

在照明着色器的早期,开发人员使用顶点着色器来实现Phong照明模型。在顶点着色器中进行光照的好处是它效率更高,因为与片段相比,顶点通常要少得多,所以(昂贵的)光照计算就不那么频繁了。然而,在顶点着色器中产生的颜色值仅仅是那个顶点的照明颜色,而周围的片段的颜色值则是插值照明颜色的结果。结果是,照明不是非常现实,除非使用了大量的顶点:

当Phong光照模型在顶点着色器中实现时,它被称为Gouraud阴影而不是Phong阴影。注意,由于插值,照明看起来有些off. Phong阴影提供了更平滑的照明结果。

现在你应该开始看到着色器是多么强大了。只有很少的信息,着色器能够计算光线如何影响片段的颜色,为我们所有的对象。在接下来的章节中,我们将更深入地研究我们可以用灯光模型做什么。

Exercises

  • Right now the light source is a boring static light source that doesn't move. Try to move the light source around the scene over time using either sin or cos. Watching the lighting change over time gives you a good understanding of Phong's lighting model: solution.
  • Play around with different ambient, diffuse and specular strengths and see how they impact the result. Also experiment with the shininess factor. Try to comprehend why certain values have a certain visual output.
  • Do Phong shading in view space instead of world space: solution.
  • Implement Gouraud shading instead of Phong shading. If you did things right the lighting should look a bit off (especially the specular highlights) with the cube object. Try to reason why it looks so weird: solution.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值