[OpengGL] 基础照明[12]

英文原文:https://learnopengl.com/Lighting/Basic-Lighting

  现实世界中的照明极其复杂,取决于太多因素,我们无法以有限的处理能力进行计算。 因此,OpenGL 中的照明是基于使用更容易处理且看起来相对相似的简化模型的现实近似值。 这些照明模型基于我们所理解的光的物理特性。 其中一个模型称为 Phong 光照模型。 Phong 照明模型的主要构建块由 3 个组件组成:环境照明、漫射照明和镜面照明。 您可以在下面看到这些照明组件单独使用和组合使用时的样子:

在这里插入图片描述

  • 环境光:即使天黑了,世界上的某个地方通常仍然有一些光(月亮,远处的光)所以物体几乎永远不会完全黑暗。 为了模拟这一点,我们使用了一个环境照明常数,它总是给物体一些颜色。
  • 漫射照明:模拟光对象对对象的定向影响。 这是光照模型中视觉上最重要的组成部分。 物体的一部分越朝向光源,它变得越亮。
  • 镜面照明:模拟出现在发光物体上的光的亮点。 镜面高光更倾向于光的颜色而不是物体的颜色。

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

环境照明(Ambient lighting)

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

  由于我们不是复杂和昂贵算法的忠实拥护者,我们将从使用非常简单的全局照明模型开始,即环境照明。 正如您在上一节中看到的那样,我们使用了一种小的常量(浅色)颜色,我们将其添加到对象片段的最终结果颜色中,因此即使没有直接光源,它看起来也总是有一些散射光 .

  向场景添加环境照明非常容易。 我们采用光的颜色,将它乘以一个小的常量环境因子,将其乘以对象的颜色,并将其用作立方体对象着色器中的片段颜色:

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

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

  如果您现在运行该程序,您会注意到第一阶段的光照现在已成功应用于该对象。 物体很暗,但由于应用了环境光照,所以不完全暗(请注意,光立方不受影响,因为我们使用了不同的着色器)。 它应该看起来像这样:
在这里插入图片描述

漫射照明(Diffuse lighting)

  环境光本身不会产生最有趣的结果,但是漫射光会开始对物体产生显着的视觉影响。 漫射照明为物体提供更多亮度,其碎片越接近光源发出的光线。 为了让您更好地了解漫射照明,请看下图:
在这里插入图片描述
  在左侧,我们发现一个光源,其光线瞄准了我们物体的单个片段。 我们需要测量光线以什么角度接触碎片。 如果光线垂直于物体表面,则光线的影响最大。 为了测量光线和片段之间的角度,我们使用了一种叫做法向量的东西,它是垂直于片段表面的向量(这里用黄色箭头表示); 我们稍后再谈。 然后可以使用点积轻松计算两个向量之间的角度。

  你可能还记得在变换章节中,两个单位向量之间的角度越小,点积越倾向于值 1。当两个向量之间的角度为 90 度时,点积变为 0。同样适用 to θ:θ 越大,光线对片段颜色的影响就越小。

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

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

  那么,我们需要计算漫反射照明的什么:

  • 法向量:垂直于顶点表面的向量。
  • 定向光线:一个方向向量,它是光的位置和片段的位置之间的差值向量。 要计算这条光线,我们需要光线的位置向量和片段的位置向量。

法向量(Normal vectors)

  法向量是垂直于顶点表面的(单位)向量。 由于顶点本身没有表面(它只是空间中的一个点),我们通过使用其周围的顶点来计算出顶点的表面来检索法向量。 我们可以使用一个小技巧,通过叉积计算立方体所有顶点的法向量,但由于 3D 立方体不是复杂的形状,我们可以简单地手动将它们添加到顶点数据中。 可以在此处找到更新的顶点数据数组。 尝试想象法线确实是垂直于每个平面表面的向量(一个立方体由 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 个浮点数,因此我们只需要将步幅参数更新为浮点数的 6 倍,就完成了。

使用灯着色器未完全使用的顶点数据可能看起来效率低下,但顶点数据已经从容器对象存储在 GPU 的内存中,因此我们不必将新数据存储到 GPU 的内存中。 这实际上比专门为灯分配一个新的 VBO 更有效。

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

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

  剩下要做的就是在片段着色器中声明相应的输入变量:

in vec3 Normal;  

计算漫反射颜色

  我们现在有了每个顶点的法线向量,但我们仍然需要光的位置向量和片段的位置向量。 由于光的位置是一个单一的静态变量,我们可以在片段着色器中将其声明为 uniform 变量:

uniform vec3 lightPos;  

  然后在渲染循环中更新制服(或外部,因为它不会每帧更改)。 我们使用上一章声明的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;
}

最后将相应的输入变量添加到片段着色器:

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);

如果您的应用程序(和着色器)编译成功,您应该看到如下内容:
在这里插入图片描述
  您可以看到,在漫射照明下,立方体再次开始看起来像一个真正的立方体。 尝试在脑海中想象法向量,并围绕立方体移动相机,以查看法向量与光的方向向量之间的角度越大,片段变得越暗。

如果您遇到困难,请随时将您的源代码与此处的完整源代码进行比较。

最后一件事

  在上一节中,我们将法线向量直接从顶点着色器传递到片段着色器。 但是,片段着色器中的计算都是在世界空间中进行的,难道我们不应该将法向量也转换为世界空间坐标吗? 基本上是的,但它不像简单地将它与模型矩阵相乘那么简单。

  首先,法向量只是方向向量,并不代表空间中的具体位置。 其次,法向量没有齐次坐标(顶点位置的 w 分量)。 这意味着平移不应该对法向量有任何影响。 因此,如果我们想将法向量与模型矩阵相乘,我们希望通过采用模型矩阵的左上角 3x3 矩阵来移除矩阵的平移部分(请注意,我们还可以将法向量的 w 分量设置为 0 并乘以 4x4 矩阵)。

  其次,如果模型矩阵执行非均匀缩放,顶点将以法向量不再垂直于表面的方式改变。 下图显示了这种模型矩阵(具有非均匀缩放)对法向量的影响:
在这里插入图片描述
  每当我们应用非均匀比例(注意:均匀比例只改变法线的大小,而不是它的方向,这很容易通过归一化它来固定)法向量不再垂直于相应的表面,这会扭曲光照。

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

  法线矩阵定义为“模型矩阵左上 3x3 部分的逆矩阵的转置”。 呸,那是一口话,如果你真的不明白那是什么意思,别担心; 我们还没有讨论逆矩阵和转置矩阵。 请注意,大多数资源将法线矩阵定义为从模型-视图矩阵导出,但由于我们在世界空间(而不是视图空间)中工作,我们将从模型矩阵导出它。

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

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

  逆向矩阵对于着色器来说是一项代价高昂的操作,因此尽可能避免进行逆向操作,因为它们必须在场景的每个顶点上完成。 出于学习目的,这很好,但对于高效的应用程序,您可能希望在 CPU 上计算法线矩阵,并在绘制之前通过 uniform 将其发送到着色器(就像模型矩阵一样)。

  在漫反射照明部分,照明很好,因为我们没有对物体进行任何缩放,所以实际上不需要使用法线矩阵,我们可以将法线与模型矩阵相乘。 但是,如果您正在进行非均匀缩放,则必须将法线向量与法线矩阵相乘。

镜面照明(Specular Lighting)

  如果您还没有被所有的光照讨论弄得筋疲力尽,我们可以开始通过添加镜面高光来完成 Phong 光照模型。

  与漫反射照明类似,镜面反射照明基于光的方向向量和对象的法线向量,但这次它也基于视图方向,例如 玩家从哪个方向看片段。 镜面照明基于表面的反射特性。 如果我们把物体的表面想象成一面镜子,那么只要我们能看到表面反射的光,镜面照明就是最强的。 您可以在下图中看到此效果:

在这里插入图片描述
  我们通过围绕法向量反射光线方向来计算反射向量。 然后我们计算这个反射向量和观察方向之间的角距离。 它们之间的角度越近,镜面光的影响就越大。 由此产生的效果是,当我们查看通过表面反射的光的方向时,我们会看到一点高光。

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

我们选择在世界空间中进行光照计算,但大多数人倾向于在视图空间中进行光照计算。 视图空间的一个优点是观察者的位置始终在 (0,0,0),因此您已经免费获得了观察者的位置。 但是,我发现出于学习目的计算世界空间中的光照更加直观。 如果您仍想计算视图空间中的照明,您还想使用视图矩阵转换所有相关向量(不要忘记也更改法线矩阵)。

  为了获得观察者的世界空间坐标,我们只需获取相机对象(当然是观察者)的位置向量。 因此,让我们向片段着色器添加另一个 uniform 并将相机位置向量传递给着色器:

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 向量取反。 reflect 函数期望第一个向量从光源指向片段的位置,但 lightDir 向量目前指向相反的方向:从片段指向光源(这取决于我们之前计算时的减法顺序 lightDir 向量)。 为了确保我们得到正确的反射向量,我们首先通过负 lightDir 向量来反转它的方向。 第二个参数需要一个法向量,所以我们提供了归一化的norm 向量。

  然后剩下要做的就是实际计算镜面反射分量。 这是通过以下公式完成的:

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 照明模型的所有照明组件。 根据您的观点,您应该看到如下内容:

在这里插入图片描述
您可以在此处找到该应用程序的完整源代码

  在光照着色器的早期,开发人员习惯于在顶点着色器中实现 Phong 光照模型。 在顶点着色器中进行光照的优点是效率更高,因为与片段相比,顶点通常要少得多,因此(昂贵的)光照计算的执行频率较低。 但是,顶点着色器中的结果颜色值仅是该顶点的结果光照颜色,而周围片段的颜色值则是插值光照颜色的结果。 结果是除非使用大量顶点,否则照明不是很逼真:
在这里插入图片描述
  当 Phong 光照模型在顶点着色器中实现时,它被称为 Gouraud 着色而不是 Phong 着色。 请注意,由于插值,照明看起来有些偏离。 Phong 着色提供更平滑的照明效果。

  现在您应该开始了解着色器的强大之处了。 使用很少的信息,着色器能够计算出光照如何影响我们所有对象的片段颜色。 在接下来的章节中,我们将更深入地研究我们可以用光照模型做什么。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值