- OpenGL的一种光照模型:冯氏光照模型(Phong lighting model),主要包含3个分量:环境光(ambient)、扩散光(diffuse)和镜面光(specular)。各分量对物体渲染效果的影响如下图所示:(图片取自书中)
- 环境光:现实世界中总会存在一些光源而不可能完全黑暗,OpenGL使用一个环境光常量来模拟这种情形,让物体总有一些颜色。
- 扩散光:模拟指定方向光源对物体颜色的影响,这是光照模型中最重要的分量。
-
镜面光:模拟出现在小物体上的一块明亮光斑。
冯氏光照模型效果
1. 环境光
- 在场景中添加环境光很容易,我们将光源颜色乘以一个环境因子(ambient factor) 小常量再与物体颜色点积,将结果作为物体片元的颜色。下面是物体顶点着色器主函数的修改:
void main()
{
float ambientStrength = 0.3;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
-
添加环境光渲染效果(与上一章节光线颜色对比,明显变暗许多)
环境光效果
2. 扩散光
- 物体片元与光线照射角度越小片元的颜色越亮,当光线垂直于物体表面对物体颜色影响最大。
- 要测量物体片元与光线的角度我们需要使用法向量(normal vector)。法向量就是垂直于片元表面的矢量。如下图中的黄色矢量:(图片取自书中)
法向量 - 计算扩散光所需的数据
- 法向量:垂直于顶点表面的一个矢量。
- 光线方向矢量:光源位置与片元位置之差,因此要计算光线的方向矢量我们需要光源的位置矢量和片元的位置矢量。
- 由于立方体相对简单,我们直接将法向量数据添加到顶点数据中
// 顶点数据(纹理坐标未使用)
float vertices[] = {
// positions // normal vector // texture
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
// 2
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
// 3
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// 4
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// 5
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
// 6
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};
- 更新顶点着色器,接受法向量数据
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
...
- 根据顶点位置数据和法向量数据修改顶点属性的设置
glBindVertexArray(objectVAO);
// 顶点位置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 顶点法向量
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
- 所有的光源计算都是在片元着色器中进行,因此我们需要将法向量数据传递到片元着色器
// 顶点着色器:添加法向量的输出
...
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0f);
Normal = aNormal;
}
// 片元着色器:添加法向量的输入
in vec3 Normal;
- 对应计算扩散光的第二部分,我们还需光源的位置和片元的位置,因此我们在片元着色器中添加一个表示光源位置的unfirom变量,并在代码设置:
// 片元着色器
uniform vec3 lightPos;
// 代码设置
objectShader.setVec3("lightPos", lightPos.x, lightPos.y, lightPos.z);
- 因为我们是在世界空间完成光源计算,因此对于片元位置,我们可以很容易通过模型矩阵从顶点位置转换得到。
// 顶点着色器:输出片元位置
out vec3 FragPos;
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0f);
FragPos = vec3(model * vec4(aPos, 1.0)); // 利用模型矩阵获取片元位置
Normal = aNormal;
}
// 片元着色器:接受片元位置
in vec3 FragPos;
- 最后一步,完成扩散光的计算
- 正则化片元位置矢量和光源方向矢量。
- 计算扩散光的对光源颜色的影响。
- 根据环境光和扩散光计算最终片元颜色。
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// 1
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
// 2
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// 3
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
}
-
渲染效果
环境光+扩散光效果 - 注意:在光源计算中我们一般不考虑矢量的大小,只关注矢量的方向。因此为了便于计算,所有的计算都是在单位矢量间进行。在光源计算中记得先将相关矢量正则化,确保是单位矢量再进行计算。
3. 正规矩阵(normal matrix)
-
法向量只是方向矢量不代表空间中的一个位置,同时法向量也没有齐次坐标。这意味着平移变换不会影响法向量。如果使用模型矩阵对法向量执行不成比例的缩放,可能导致法向量不再垂直于物体表面。如下图所示:(图片取自书中)
法向量不成比例缩放
-
要解决法向量变换出现不垂直与物体表面从而扭曲光线的问题,我们不能直接使用模型矩阵,而是应该使用特定于法向量变换的正规矩阵(normal matrix)。正规矩阵定义为模型矩阵左上角部分的逆矩阵的转置。
-
在顶点着色器中,我们可以使用
inverse
和transpose
函数来构造正规矩阵并转换法向量。
Normal = mat3(transpose(inverse(model))) * aNormal;
- 注意:逆矩阵操作对于着色器来说很耗时,因为操作需要在你场景中的每个顶点执行,因此应该尽量避免进行逆矩阵操作。实际中,我们最好在CPU侧计算正规矩阵,然后通过uniform变量传入着色器。
4. 镜面光
-
镜面光与扩散光相似,都与光源方向矢量和物体法向量有关,但是镜面光同时与视线方向矢量有关。如下图所示:
镜面光 - 我们通过绕法向量反射(镜像)光线方向矢量来计算反射矢量,然后计算反射矢量与视线矢量之间的角度。角度(图中的)越小镜面光的影响越大。视线方向矢量可以通过相机在世界空间的位置矢量和片元位置矢量计算得到。因此,要计算镜面光,我们需要相机位置矢量,片元位置矢量,反射光方向矢量和法向量。
- 注意:很多人选择在视空间计算光源,但本书选择使用世界空间来计算光源,是因为这样更容易理解。
- 要获得观察者在世界空间的位置,我们可以简单使用相机的位置矢量。因此,修改片元着色器添加一个uniform变量表示相机位置矢量。
// 片元着色器
uniform vec3 viewPos;
// 代码设置值
objectShader.setVec3("viewPos", camera.Position.x, camera.Position.y, camera.Position.z);
- 下面在片元着色器中完成镜面光的计算,其中我们将镜面光强度定义为0.5,以获得一个中等亮度,以免产生太大的影响。
float specularStrength = 0.5; // 镜面光强度
vec3 viewDir = normalize(viewPos - FragPos); // 视线方向矢量
vec3 reflectDir = reflect(-lightDir, norm); // 放射光方向矢量
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
`reflect`函数中对光线方向矢量取反,因为光线是指向片元位置,而我们进行反射需要的是指向光源位置的。
-
计算镜面光时的幂值32代表的是亮斑的亮度值。该值越高代表物体越是会将光进行反射,而不是扩散,这导致更小的亮斑。不同取值效果如下图所示:(图片取自书中)
不同亮度值效果
-
最终渲染效果
冯氏光照模型效果1 -
物体放大两倍的效果
冯氏光照模型效果2 -
早期开发者经常在顶点着色器实现冯氏光照模型。这样做的好处是,与片元着色器相比,顶点的数量要远远少于片元的数量,因此光源计算更快。但是最终计算出来的光的颜色只是顶点的,顶点之间的表面需要通过插值算法插入光的颜色,导致渲染效果不真实。如下图所示:
冯氏光照模型实现对比 -
在顶点着色器实现的冯氏光照模型称为Gouraud shading而不是Phong Shading。
网友评论