文章目录
- 一、说明
- 二、颜色综述
- 三、灯光场景
- 四、光源位置
一、说明
光源和颜色模型也是OpenGL的重要模型之一,我们将光源也看成是一个物体,这个物体特点是,不仅可以自己移动位置,而且要和其它物体颜色进行反射运算,从而发出最后颜色结果。请看详细内容。
二、颜色综述
我们在前几章中简要使用和操作了颜色,但从未正确定义它们。在这里,我们将讨论什么是颜色,并开始为即将到来的照明章节构建场景。
在现实世界中,颜色可以采用任何已知的颜色值,每个对象都有自己的颜色。在数字世界中,我们需要将(无限的)真实颜色映射到(有限的)数字值,因此并非所有现实世界的颜色都可以用数字表示。颜色使用 和 分量(通常缩写为 )以数字形式表示red。green在blue的RGB范围内,仅使用这 3 个值的不同组合,[0,1]我们就可以表示几乎任何颜色。例如,要获得珊瑚色,我们将颜色向量定义为:
glm::vec3 coral(1.0f, 0.5f, 0.31f);
或者(python版):
import pyrr
v = pyrr.Vector3([1.,0.5.,0.31])
我们在现实生活中看到的物体的颜色并不是它实际具有的颜色,而是它的颜色反映来自物体的颜色。物体不吸收(拒绝)的颜色就是我们感知到的颜色。例如,太阳光被认为是白光,是许多不同颜色的总和(如您在图片中看到的那样)。如果我们将白光照在一个蓝色玩具上,它会吸收除蓝色之外的所有白色子颜色。由于玩具不吸收蓝色部分,所以它会被反射。反射光进入我们的眼睛,使玩具看起来像是蓝色的。下图显示了珊瑚色玩具反射的几种不同强度的颜色:
您可以看到,白色阳光是所有可见颜色的集合,物体吸收了这些颜色的很大一部分。它只反射那些代表物体颜色的颜色,这些颜色的组合就是我们所看到的(在本例中是珊瑚色)。
从技术上来说,这有点复杂,但我们将在 PBR 章节中讨论这个问题。
这些颜色反射规则直接适用于图形领域。当我们在 OpenGL 中定义光源时,我们希望赋予该光源一种颜色。在上一段中,我们有一个白色,因此我们也将赋予光源白色。如果我们将光源的颜色与物体的颜色值相乘,则得到的颜色将是物体的反射颜色(因此是其感知颜色)。让我们重新审视我们的玩具(这次使用珊瑚色值),看看我们如何在图形领域计算其感知颜色。我们通过在光和物体颜色向量之间进行分量相乘来获得结果颜色向量:
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
或者(python版):
import pyrr
lightColor=pyrr.Vector3([1.,1.,1]) #光源颜色
toyColor=pyrr.Vector3([1.0 , 0.5 , 0.31 ])
result = lightColor * toyColor; # = (1.0f, 0.5f, 0.31f);
我们可以看到,玩具的颜色吸收了大部分白光,但根据其自身的颜色值反射了几个红色、绿色和蓝色值。这表示颜色在现实生活中如何发挥作用。因此,我们可以将物体的颜色定义为它从光源反射的每个颜色成分的数量。
现在,如果我们使用绿光会发生什么?
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
或者(python版):
import pyrr
lightColor=pyrr.Vector3([0.,1.,0]) #光源颜色
toyColor=pyrr.Vector3([1.0 , 0.5 , 0.31 ])
result = lightColor * toyColor; # = (0, 0.5f, 0);
我们可以看到,玩具没有红光和蓝光可以吸收和/或反射。玩具还吸收了一半的绿光,但也反射了一半的绿光。我们感知到的玩具颜色将是深绿色。我们可以看到,如果我们使用绿光,只有绿色成分可以被反射,从而被感知到;没有红色和蓝色被感知到。结果,珊瑚物体突然变成了深绿色物体。让我们再举一个深橄榄绿色光的例子:
glm::vec3 lightColor(0.33f, 0.42f, 0.18f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);
三、灯光场景
在接下来的章节中,我们将通过大量使用颜色来模拟真实世界的照明,从而创建有趣的视觉效果。由于现在我们将使用光源,我们希望将它们显示为场景中的视觉对象,并添加至少一个对象来模拟照明。
我们首先需要一个物体来投射光线,我们将使用前几章中著名的立方体容器。我们还需要一个灯光物体来显示光源在 3D 场景中的位置。为了简单起见,我们也将用立方体来表示光源(我们已经有了顶点数据,对吧?)。
因此,填充顶点缓冲区对象、设置顶点属性指针以及所有这些步骤现在对您来说应该很熟悉了,所以我们不会带您完成这些步骤。如果您仍然不知道这些步骤是怎么回事,我建议您在继续之前先查看前面的章节,并尽可能完成练习。
因此,我们首先需要的是顶点着色器来绘制容器。容器的顶点位置保持不变(尽管这次我们不需要纹理坐标),因此代码应该没有什么新东西。
我们将使用上一章中精简版的顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
确保更新顶点数据和属性指针以匹配新的顶点着色器(如果您愿意,您实际上可以保持纹理数据和属性指针处于活动状态;我们现在只是不使用它们)。
因为我们还要渲染一个光源立方体,所以我们想为光源生成一个新的 VAO。我们可以使用相同的 VAO 渲染光源,然后在模型矩阵上进行一些光位置变换,但在接下来的章节中,我们将经常更改容器对象的顶点数据和属性指针,我们不希望这些更改传播到光源对象(我们只关心光立方体的顶点位置),所以我们将创建一个新的 VAO:
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// we only need to bind to the VBO, the container's VBO's data already contains the data.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// set the vertex attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
代码应该相对简单。现在我们创建了容器和光源立方体,还剩下一件事需要定义,那就是容器和光源的片段着色器:
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
FragColor = vec4(lightColor * objectColor, 1.0);
}
片段着色器从统一变量中接受对象颜色和光颜色。在这里,我们将光的颜色与对象的(反射)颜色相乘,就像我们在本章开头讨论的那样。同样,这个着色器应该很容易理解。让我们用白光将对象的颜色设置为上一节的珊瑚色:
// don't forget to use the corresponding shader program first (to set the uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
还有一点需要注意,当我们在下一章开始更新这些照明着色器时,光源立方体也会受到影响,而这并不是我们想要的。我们不希望光源物体的颜色受到照明计算的影响,而是将光源与其他物体隔离开来。我们希望光源具有恒定的明亮颜色,不受其他颜色变化的影响(这让它看起来就像光源立方体真的是光源)。
为了实现这一点,我们需要创建第二组着色器,用于绘制光源立方体,这样可以避免照明着色器的任何更改。顶点着色器与照明顶点着色器相同,因此您只需复制源代码即可。光源立方体的片段着色器通过在灯上定义恒定的白色来确保立方体的颜色保持明亮:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // set all 4 vector values to 1.0
}
当我们想要渲染时,我们希望使用刚刚定义的照明着色器来渲染容器对象(或可能是许多其他对象),而当我们想要绘制光源时,我们会使用光源的着色器。在照明章节中,我们将逐步更新照明着色器,以慢慢实现更逼真的效果。
四、光源位置
光源立方体的主要用途是显示光的来源。我们通常会在场景中的某个位置定义光源的位置,但这只是没有视觉意义的位置。为了显示光源的实际位置,我们会在光源的同一位置渲染一个立方体。我们使用光源立方体着色器渲染此立方体,以确保无论场景的光照条件如何,立方体始终保持白色。
因此让我们声明一个全局vec3变量来表示光源在世界空间坐标中的位置:
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
然后,我们将光源立方体平移到光源的位置,并在渲染之前将其缩小:
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));
最终光源立方体的渲染代码看起来应该像这样:
lightCubeShader.use();
// set the model, view and projection matrix uniforms
[...]
// draw the light cube object
glBindVertexArray(lightCubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
将所有代码片段注入到适当的位置,即可生成一个干净的 OpenGL 应用程序,该应用程序已正确配置,可用于进行照明实验。如果一切顺利,它应该如下所示:
现在真的没什么可看的,但我保证在接下来的章节中它会变得更有趣。
如果您难以找出应用程序整体中所有代码片段的组合位置,请检查此处的源代码并仔细研究代码/注释。
现在我们对颜色有了相当多的了解,并创建了一个用于实验照明的基本场景,我们可以跳到下一章,真正的魔法从那里开始。