在此前的博客中,使用的一直是预定义好的固定颜色或者纹理来渲染图形的。接下来,将进一步改造片段着色器部分,为其添加光照效果。
颜色的构成
在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的颜色。换句话说,那些不能被物体所吸收的颜色才是我们能够感知到的物体的颜色。例如,如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有子颜色,不被吸收的蓝色光被反射到眼中,让这个玩具看起来是蓝色的。
在计算机中有几种表示颜色的系统,不过一如既往的,这里使用RGB向量来表示颜色。
这些颜色反射的定律被直接地运用在图形领域。在OpenGL中创建一个光源时,会给光源一个颜色。光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。
冯氏光照模型
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,并且效果上也比较近似。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
- 环境光照:可以认为是背景光照,很小的一个亮度值。物体几乎永远不会是完全黑暗的,为了模拟这个,会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照:模拟光源对物体的方向性影响,它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照:模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
环境光照
环境光照的实现非常简单,直接通过一个环境光照因子乘上光的颜色即可
这里使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光。
在片段着色器中对颜色进行简单的额外处理即可。注意,要将光的颜色和物体颜色通过uniform
传进去。为了简单起见,这里使用的是白光光源,即颜色向量为(1.0f, 1.0f, 1.0f)。
......
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
漫反射光照
环境光照本身提供的只是很微弱的影响,但是漫反射光照就能开始对物体产生显著的视觉影响了。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。
为了计算漫反射光照,需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化(也就是更亮)。而为了测量光线和片段的角度,需要使用一个叫做法向量的东西,它是垂直于片段表面的一个向量。当两个向量的夹角为90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响就应该越小。
注意,为了(只)得到两个向量夹角的余弦值,使用的是单位向量(长度为1的向量),所以需要确保所有的向量都是标准化的,否则点乘返回的就不仅仅是余弦值了。
具体而言,计算漫反射光照需要法向量和光线方向。将这两个向量进行点乘,用于计算光源对当前片段实际的漫反射影响。结果值再乘以光的颜色,就得到了漫反射分量。
- 法向量:一个垂直于顶点表面的向量。
- 光线方向:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,需要光的位置向量和片段的位置向量,对其进行相减即可。
简单起见,由于使用的是正方体,这里将法线向量直接定义在顶点属性中。更改顶点数组,并设置对应的uniform。
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
然后更改顶点着色器和片段着色器:
#shader vertex
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 FragPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = mat3(transpose(inverse(view * model))) * aNormal;
FragPos = vec3(view * model * vec4(aPos, 1.0));
}
#shader fragment
#version 330 core
in vec3 FragPos;
in vec3 Normal;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
}
首先,一般而言,都是在观察空间进行光照计算。观察空间计算的优势是,观察者的位置总是在(0, 0, 0),所以已经零成本地拿到了观察者的位置。所以需要需要将所有相关的向量也用观察矩阵进行变换(不要忘记也修改法线矩阵),即全部乘上view
矩阵(当然,在此之前需要先变换到世界矩阵,即乘model
矩阵)。
法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w
分量)。这意味着,位移不应该影响到法向量。对于法向量,只希望对它实施缩放和旋转变换。
如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,不能直接用模型矩阵来变换法向量。每当应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。
因此,需要使用专门的法线矩阵,它通过一些线性代数的操作移除了对法向量错误缩放的影响。
在顶点着色器中,可以使用inverse
和transpose
函数自己生成这个法线矩阵。注意还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3
的法向量。
因为计算需要一个顶点位置,可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标,接着再变换到观察空间。将这个坐标输出到片段着色器中。
在片段着色器中,需要做的第一件事是计算光源和片段位置之间的方向向量。光的方向向量是光源位置向量与片段位置向量之间的向量差。为了确保所有相关向量最后都转换为单位向量,所以把法线和最终的方向向量都进行了标准化。
接着,对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫反射影响。结果值再乘以光的颜色,得到漫反射分量。
最后把这个值加到结果上即可。
镜面光照
和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,但是它也决定于观察方向,例如玩家是从什么方向看向这个片段的。镜面光照决定于表面的反射特性。如果把物体表面设想为一面镜子,那么镜面光照最强的地方就是看到表面上反射光的地方。
可以通过根据法向量翻折入射光的方向来计算反射向量。然后计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大。由此产生的效果就是,在看向在入射光在表面的反射方向时,会看到一点高光。
观察向量是计算镜面光照时需要的一个额外变量,可以使用观察者的位置和片段的位置来计算它,将其直接相减即可。
由于之前已经变换到观察空间进行光照计算,因此,观察者的坐标直接为(0,0,0)。
对应的,在片段着色器中添加代码计算镜面光照。
float specularStrength = 0.5;
vec3 viewDir = normalize(-FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
specularStrength
代表镜面强度,这里给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
下一步,计算视线方向向量,和对应的沿着法线轴的反射向量。需要注意的是这里对lightDir
向量进行了取反。reflect
函数要求第一个向量是从光源指向片段位置的向量,但是lightDir
当前正好相反,是从片段指向光源的,所以需要取反。第二个参数要求是一个法向量,所以提供的是已标准化的norm
向量。
最后,计算镜面反射分量并加到最终结果中。先计算视线方向与反射方向的点乘,并确保它不是负值,然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小,并且越亮。
最终效果
首先,创建一个光源,为了方便起见,直接使用立方体作为光源,然后将其创建在原立方体的上方位置。
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
glm::mat4 lightmodel;
lightmodel = glm::mat4();
lightmodel = glm::translate(lightmodel, lightPos);
lightmodel = glm::scale(lightmodel, glm::vec3(0.2f));
unsigned int VBO, VAO, lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
注意,还需在game循环中绑定并绘制光源:
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
简单的对图形的model矩阵进行改动,使其不断移动从而方便观察光照效果。
最终效果如下:
参考资料
[1] https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/03%20Hello%20Window/