0 前言
本节笔记对应的内容为基础光照,部分参考自傅老師/OpenGL教學 第二章。
这一节我们学习一下一些简单的光照模型Phong model。
1 概述
Blinn-Phong光照模型只是是对Phong光照模型,在镜面反射上的改进。Blinn-Phong光照模型相关概念请参照GAMES101-学习笔记3-Shading着色一节的内容。
2 代码实现
Ambient lighting
把环境光照添加到场景里非常简单。
uniform vec3 ambientColor;
}
Diffuse lighting
Normal vectors
首先我们需要用到模型的法线信息,修改顶点数组:
//顶点 uv 法向
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,
...
};
更新光照的顶点着色器:
#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);
所有光照的计算都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器。我们这么做:
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
接下来,在片段着色器中定义相应的输入变量:
in vec3 Normal;
Calculating the diffuse color
我们现在对每个顶点都有了法向量,但是我们仍然需要光源的位置向量和片段的位置向量。由于光源的位置是一个静态变量,我们可以简单地在片段着色器中把它声明为uniform:
uniform vec3 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;
现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了。
我们需要做的第一件事是计算光的方向向量。前面提到,光的方向向量是光源位置向量与片段位置向量之间的向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和最终的方向向量都进行标准化:
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);
One last thing(法向量处理)
现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们是不是应该把法向量也转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。
首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。
其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。
The Normal Matrix(参考https://zhuanlan.zhihu.com/p/72734738)
假设两点\(P_1\)、\(P_2\)是三角形边上的顶点:\(T=P_2-P_1\),考虑到该向量仅表示方向,齐次坐标最后一位为0,可以将上式两边同时乘上模型矩阵\(Modelview\)
\[T * Modelview=(P_2-P_1) * Modelview\hspace{50cm}\] 因此: \[T * Modelview=P_2 * Modelview-P_1 * Modelview\hspace{50cm}\] \[T^{\prime}=P_2^{\prime}-P_1^{\prime}\hspace{50cm}\] 其中,\(P_1\)和\(P_2\)为变换后顶点,\(T^{\prime}\)仍表示三角形边的切线。因此,\(Modelview\)仅保留切线,不保留法线。
假设存在一个 3×3 的矩阵\(G\),让我们看看如何计算该矩阵以正确变换法向量。
我们知道,首先 \(T\cdot N = 0\) ,因为向量根据定义是垂直的。我们还知道,在变换后 \(N’\cdot T’\) 必须保持为零,因为它们必须保持相互垂直。
\(T\) 可以安全地乘以模型视图左上角的 3×3 子矩阵(\(T\) 是一个向量,因此 w 分量为零),我们称这个子矩阵为 \(M\)。
因此,有下式:
\[N^{\prime}\cdot T^{\prime}=(GN)\cdot (MT)\]
点积可以转化为向量的乘积,因此:
\[(GN)\cdot (MT)=(GN)^T * (MT)\]
又因为\(N^{\prime}\cdot T^{\prime}=N \cdot T=0\),所以:
\[G^TM=I \iff G=(M^{-1})^T\]
即法线变换矩阵等于观察变换矩阵逆的转置(模型矩阵左上角的逆矩阵的转置矩阵):
Normal = mat3(transpose(inverse(model))) * aNormal;
即使是对于着色器来说,逆矩阵也是一个开销比较大的运算,因此,只要可能就应该避免在着色器中进行逆矩阵运算,它们必须为你场景中的每个顶点都进行这样的处理。用作学习目这样做是可以的,但是对于一个对效率有要求的应用来说,在绘制之前你最好用CPU计算出法线矩阵,然后通过uniform把值传递给着色器(像模型矩阵一样)。
Specular Lighting
为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它当然就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器:
uniform vec3 viewPos;
lightingShader.setVec3("viewPos", camera.Position);
下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量:
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
需要注意的是我们对lightDir
向量进行了取反。reflect
函数要求第一个向量是从光源指向片段位置的向量,但是lightDir
当前正好相反,是从片段指向光源(由先前我们计算lightDir
向量时,减法的顺序决定)。为了保证我们得到正确的reflect向量,我们通过对lightDir向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。
剩下要做的是计算镜面分量。下面的代码完成了这件事:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = spec * lightColor;
我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。我们不希望镜面成分过于显眼,所以我们把指数保持为32。剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
我们现在为Phong shading计算了全部的光照分量。根据你的视角,你可以看到类似下面的画面:
「如果这篇文章对你有用,请随意打赏」
如果这篇文章对你有用,请随意打赏
使用微信扫描二维码完成支付

comments powered by Disqus