前言
我们可以用一张纹理贴图来表现物体表面的基础反射颜色,也可以用一张镜面反射贴图,来指派表面是否产生高光。除此之外,我们可以用贴图来存储表面的法线信息,以及高度信息,从而让渲染效果更加精细。
法线贴图
我们可以让每一个fragment采用自己的不同的法线,这样就可以获得一种表面看起来复杂得多的幻觉。
采用法线贴图有一个问题,如果物体位姿发生变化了,通过采样获得的法线如何变换?可以记录下物体的初始位姿,如果物体发生了旋转,再对采样获得的法线进行旋转。
也可以变换到切线空间。在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对于这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间。
对于一个三角形来说,它的切线空间可以这样计算,
N
N
N轴可以由三角形的两条边
E
1
,
E
2
E_1,E_2
E1,E2叉乘得出,
T
T
T轴和
B
B
B轴和
U
V
UV
UV的方向对齐,要求解
T
T
T和
B
B
B轴,我们可以将三角形的边
E
1
E_1
E1与
E
2
E_2
E2写成
T
T
T轴和
B
B
B轴的线性组合:
E
1
=
Δ
U
1
T
+
Δ
V
1
B
E
2
=
Δ
U
2
T
+
Δ
V
2
B
E_1 = \Delta U_1T + \Delta V_1B\\ E_2 = \Delta U_2T + \Delta V_2B
E1=ΔU1T+ΔV1BE2=ΔU2T+ΔV2B
其中
E
1
E_1
E1和
E
2
E_2
E2是这个三角形在本地坐标系中的坐标,可以参考这篇文章。
对上述公式进行进一步计算,我们可以求得切线空间的三个轴,以此作为切线空间的表示。
因为模型在uv上是平铺开的,因此能够利用uv坐标来计算切线和副切线,在其他地方也不好计算。
可以说,一个三角形(或者一个最小绘制单元,如四边形)中的所有点,位于同一个切线空间中。
现在我们有了TBN矩阵,如果来使用它呢?通常来说有两种方式使用它,我们会把这两种方式都说明一下:
- 我们直接使用TBN矩阵,这个矩阵可以把切线坐标空间的向量转换到世界坐标空间。因此我们把它传给片段着色器中,把通过采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。
- 我们也可以使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间,这样法线和其他光照变量再一次在一个坐标系中了。
第二种方法看似要做的更多,它还需要在像素着色器中进行更多的乘法操作,所以为何还用第二种方法呢?
将向量从世界空间转换到切线空间有个额外好处,**我们可以把所有相关向量在顶点着色器中转换到切线空间,不用在像素着色器中做这件事。**这是可行的,因为lightPos和viewPos不是每个fragment运行都要改变,对于fs_in.FragPos,我们也可以在顶点着色器计算它的切线空间位置。基本上,不需要把任何向量在像素着色器中进行变换,而第一种方法中就是必须的,因为采样出来的法线向量对于每个像素着色器都不一样。这是一个极佳的优化,因为顶点着色器通常比像素着色器运行的少。
一张法线贴图的渲染效果图如下:
视差贴图
视差贴图属于位移贴图(Displacement Mapping)技术的一种,它对根据储存在纹理中的几何信息对顶点进行位移或偏移。一种实现的方式是比如有1000个顶点,根据纹理中的数据对平面特定区域的顶点的高度进行位移。
视差贴图背后的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都根据观察方向和高度贴图。为了理解它如何工作,看看下面砖块表面的图片:
这里粗糙的红线代表高度贴图中的数值的立体表达,向量V¯代表观察方向。如果平面进行实际位移,观察者会在点B看到表面。然而我们的平面没有实际上进行位移,观察方向将在点A与平面接触。视差贴图的目的是,在A位置上的fragment不再使用点A的纹理坐标而是使用点B的。随后我们用点B的纹理坐标采样,观察者就像看到了点B一样。
这个技巧就是描述如何从点A得到点B的纹理坐标。视差贴图尝试通过对从fragment到观察者的方向向量V¯进行缩放的方式解决这个问题,缩放的大小是A处fragment的高度。所以我们将V¯的长度缩放为高度贴图在点A处H(A)采样得来的值。下图展示了经缩放得到的向量P¯:
我们随后选出P¯以及这个向量与平面对齐的坐标作为纹理坐标的偏移量。这样能行是因为向量P¯是使用从高度贴图得到的高度值计算出来的,所以一个fragment的高度越高位移的量越大。
视差贴图和法线贴图同理,应用于切线空间。
实际在采样的过程中,是反着来的,视差贴图中每个位置的值代表着深度,这是因为使用反色高度贴图(也叫深度贴图)去模拟深度比模拟高度更容易。
如上图所示,相机看向A这个位置,此时从视差贴图中采样,获得深度值H(A),并记录下此时视线方向在xy上的分量 V ′ V' V′。将深度值H(A)乘上一个系数,再乘以 V ′ V' V′,我们可以获得一个偏移量 Δ \Delta Δ,A加上这个偏移量 Δ \Delta Δ,得到位置C。我们将位置C的颜色返回。对应到实现上, 这一过程的核心代码如下:
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
float height = texture(depthMap, texCoords).r;
vec2 p = viewDir.xy / viewDir.z * (height * height_scale);
return texCoords - p;
}
下图便是视差贴图的渲染效果图:
陡峭视差贴图
陡峭视差映射(Steep Parallax Mapping)是视差映射的扩展,原则是一样的,但不是使用一个样本而是多个样本来确定向量P¯到B。即使在陡峭的高度变化的情况下,它也能得到更好的结果,原因在于该技术通过增加采样的数量提高了精确性。
陡峭视差映射的基本思想是将总深度范围划分为同一个深度/高度的多个层。从每个层中我们沿着P¯方向移动采样纹理坐标,直到我们找到一个采样低于当前层的深度值。看看下面的图片:
我们从上到下遍历深度层,我们把每个深度层和储存在深度贴图中的它的深度值进行对比。如果这个层的深度值小于深度贴图的值,就意味着这一层的P¯向量部分在表面之下。我们继续这个处理过程直到有一层的深度高于储存在深度贴图中的值:这个点就在(经过位移的)表面下方。
这个例子中我们可以看到第二层(D(2) = 0.73)的深度贴图的值仍低于第二层的深度值0.4,所以我们继续。下一次迭代,这一层的深度值0.6大于深度贴图中采样的深度值(D(3) = 0.37)。我们便可以假设第三层向量P¯是可用的位移几何位置。我们可以采用 T 3 T_3 T3这个位置作为偏移后的纹理坐标。
此时的结果如下图所示:
视差遮蔽映射
视差遮蔽映射(Parallax Occlusion Mapping)和陡峭视差映射的原则相同,但不是用触碰的第一个深度层的纹理坐标,而是在触碰之前和之后,在深度层之间进行线性插值。我们根据表面的高度距离啷个深度层的深度层值的距离来确定线性插值的大小。看看下面的图片就能了解它是如何工作的:
相比于陡峭视差映射,这一步加上了一个插值。
最终渲染出的效果图如下所示:
参考
https://learnopengl-cn.github.io/05%20Advanced%20Lighting/04%20Normal%20Mapping/
https://learnopengl-cn.github.io/05%20Advanced%20Lighting/05%20Parallax%20Mapping/
https://zhuanlan.zhihu.com/p/446957364