《OGL dev》Etay Meiri Tutorial 26 - Normal Mapping 笔记

三角形表面的顶点法线插值使得表面光滑。Normal Mapping,又称为Bump Mapping是一种技术,使用从特殊的纹理中采样得到的法线替代顶点法线插值,实现三角形表面的凹凸感。这称特殊的纹理称为normal map。

因为一张normal map支持对应多个朝向不同方向的表面,所以它无法存储局部空间的法线。因此,OpenGL使用tangent space(切线空间),切线空间是定义在表面上的空间,会随表面的朝向改变空间的朝向。

切线空间定义有四种:

第一种切线空间,定义为以三角形表面法线为z轴,纹理空间的u、v轴分别为x和y轴的空间,其中x、y、z轴的单位向称分别称为tangent(切线)、bitangent(副切线)、normal(法线)。
使用纹理空间的u、v轴分别为x和y轴,可以这样理解:虽然随着纹理在一个模型上展开,u、v轴在局部空间中可能是一条曲线或折线,但在组成模型的任意一个三角形中,u、v轴必然是直线,虽然它们之间可能不再垂直。
三角形三个顶点在局部空间中的坐标是已知的,假设为E0E1E2E_0、E_1、E_2。顶点在纹理空间中的坐标是已知的假设为U0U1U2U_0、U_1、U_2。假设局部空间中的tangent和bitangent为TTBB,因为TTBB分别为u、v轴上的单位向量,可得:

E1E0=(U1U0)T+(V1V0)BE2E0=(U2U0)T+(V2V0)BE_1 - E_0 = (U_1 - U_0) T + (V_1 - V_0) B \\ E_2 - E_0 = (U_2 - U_0) T + (V_2 - V_0) B

假设:

E1=E1E0E2=E2E0U1=U1U0U2=U2U0V1=V1V0V2=V2V0\triangle E_1 = E_1 - E_0 \quad \triangle E_2 = E_2 - E_0 \\ \triangle U_1 = U_1 - U_0 \quad \triangle U_2 = U_2 - U_0 \\ \triangle V_1 = V_1 - V_0 \quad \triangle V_2 = V_2 - V_0

代入上式可得:

E1=U1T+V1BE2=U2T+V2B\triangle E_1= \triangle U_1 T + \triangle V_1 B \\ \triangle E_2 = \triangle U_2 T + \triangle V_2 B

上式可以转换为矩阵形式:

[E1E2]=[U1V1U2V2][TB]\begin{bmatrix} E_1 \\ E_2 \end{bmatrix} = \begin{bmatrix} \triangle U_1 & \triangle V_1 \\ \triangle U_2 & \triangle V_2 \end{bmatrix} \begin{bmatrix} T \\ B \end{bmatrix}

将两边都左乘[U1V1U2V2]1\begin{bmatrix} \triangle U_1 & \triangle V_1 \\ \triangle U_2 & \triangle V_2 \end{bmatrix}^{-1},并交换位置可得:

[TB]=[U1V1U2V2]1[E1E2]\begin{bmatrix} T \\ B \end{bmatrix} = \begin{bmatrix} \triangle U_1 & \triangle V_1 \\ \triangle U_2 & \triangle V_2 \end{bmatrix}^{-1} \begin{bmatrix} E_1 \\ E_2 \end{bmatrix}

矩阵的逆如果存在,可以通过伴随矩阵求得,所以:

[TB]=1U1V2U2V1[V2V1U2U1][E1E2]\begin{bmatrix} T \\ B \end{bmatrix} = \frac{1}{\triangle U_1 \triangle V_2 - \triangle U_2 \triangle V_1} \begin{bmatrix} \triangle V_2 & - \triangle V_1 \\ - \triangle U_2 & \triangle U_1 \end{bmatrix} \begin{bmatrix} E_1 \\ E_2 \end{bmatrix}

在介绍第二种切换空间之间,先介绍gram-schmidt process(施密特正交化):假设一个N维的坐标系,基向量分别为N1N2N3...N_1、N_2、N_3...

  1. N1N_1归一化。
  2. (N1N˙2)N1(N_1 \dot N_2) N_1得到N2N_2N1N_1上投影,N2(N1N˙2)N1N_2 - (N_1 \dot N_2) N_1得到垂直N1N_1的向量替换原来的N2N_2,将N2N_2归一化。
  3. N3N_3再分别与N1N2N_1、N_2点乘得到N3N_3在这两个向量上的投影,N3N_3再减去这两个投影得到垂直于N1N2N_1、N_2的向量替换N3N_3,将N3N_3归一化。
  4. 以此类推,对更新每一个基向量,最终得到一组标准正交基。

第二种切线空间,是第一种切线空间通过施密特正交化得到的空间。其中基向量计算的顺序是固定的:

  1. 首先,如果表面法线NN未归一化,将NN归一化。
  2. 然后,tangent向量TTNN作投影,并从NN中减去,归一化,用于更新TT
  3. 最后,因为是三维空间,bitangent向量B=N×TB = N \times T

第三种切线空间的z轴为顶点法线,tangent向量为共享这个顶点的所有三角形的tangent向量的平均值。bitangent向量通常不被需要,如果存在的话,与tangent向量类似。三角形内非顶点的像素,对应的此空间都是插值的结果。这个切线空间的作用是使得三角形之间过渡平滑。

第四种切线空间是第三种切线空间通过施密特正交化得到的空间。normal map中采样得到正是这个空间的法线,这个空间的基向量组成将法线变换到局部空间的TBN矩阵。

因为法线的xyz分量的取值区间为[-1, 1],而normal map中的通道的取值区间为[0, 1],两者之间转换需要进行映射。
因为大多数法线通常都不会偏离切线空间的Z轴太远,所以xy分量通常在0附近,z分量通常在1附近,映射到normal map中对应颜色在(0.5,0.5,1)附近,所以法线贴图看起来都偏蓝。
如果一张法线贴图的颜色全为(0.5,0.5,1),那意味着它的法线与切线空间的Z轴重合,它的效果等同于不使用法线贴图。

Assimp库,通过Importer.ReadFile加载模型时,使用aiProcess_CalcTangentSpace标记,可以为我们计算tangent向量,保存在aiMesh对象的mTangents数组中。