法线贴图
引入法线贴图
- 对于是一个平面的物体,比如一块桌面或者一块地砖的表面,只使用纹理就可以表现出很好的效果;但对于凹凸不平的物体表面,比如由很多砖块砌成的墙面,只使用纹理就不能表现出凹凸不平的细节,原因是:在片段着色器中计算光照时,每个片段使用的法线是插值的顶点法线,而这些法线几乎相同,所以表现不出凹凸不平的视觉
- 如果我们能通过一种方式获取物体表面不同的朝向,再根据不同的朝向来计算光照,那么就可以表现出良好的凹凸不平的视觉效果
- 物体的表面朝向是通过法向量来表达的,如果我们提前把每个片段的法向量都计算出来,并记录到某个文件中,在计算光照的时候从这个文件中读取法向,就可以得出好的效果了。这个文件就叫做法线贴图
法线贴图
- 法线贴图是一个2D纹理
- 要把一个法向量记录到一个2D纹理中,需要做一些变换。因为法向量的xyz都是在[-1.0, 1.0]之间,而纹理中的rgb是在[0.0, 1.0]之间
- 法线贴图一般都是一张偏蓝色的纹理,因为大部分片段的法向都是(0,0,1)
- 法线贴图,一般由制作模型的人,在导出模型文件时,一并生成法线贴图;也可以通过ps等工具,将一张普通纹理生成对应的法线贴图
引入切线空间
- 当法线贴图中记录的法向量与场景中物体的实际法线方向一致时,效果很好;当他们不一致时,就又没有了凹凸不平的细节
- 原因是,物体的实际法线方向变了,而从法线贴图中读取的数据却没有变,导致光照计算错误
- 假设有这样一个的坐标空间:法线贴图中的法向量在这个坐标空间中总是指向正Z方向。在计算光照时,所有的光照向量都相对这个正Z方向进行变换,然后再计算光照,最后得到正确的计算结果;或者将这个正Z方向变换到与真实物体表面法线对齐,然后再计算光照,最后得到正确结果。这个坐标空间就叫做切线空间(tangent space)。
切线空间
- 法线贴图中的法线向量定义在切线空间中
- 在切线空间中,法线永远指着正z方向
- 切线空间是位于三角形表面之上的空间
- 我们可以计算出一个矩阵,把切线空间的z方向和表面的法线方向对齐。这就是切线空间的好处
- 这种矩阵叫做TBN矩阵,tangent(切线)、bitangent(副切线)和normal(法线)向量
计算TBN矩阵
-
要建构这样一个把切线空间转变为不同空间的矩阵,我们需要三个相互垂直的向量,它们沿一个表面的法线贴图对齐于:右、前、上;已知上是物体表面的法向量,求右和前的T、B
-
由于TBN相互垂直,且沿表面的法线贴图对齐于右、前、上,因此T、B对齐与纹理坐标的U、V方向
-
顶点P1、P2、P3是三角形的三个顶点,UV分别是他们的纹理坐标
Δ U 1 = U 2 − U 1 , Δ V 1 = V 2 − V 1 ΔU_1 = U_2 - U_1, ΔV_1 = V_2 - V_1 ΔU1=U2−U1,ΔV1=V2−V1
Δ U 2 = U 3 − U 2 , Δ V 2 = V 3 − V 2 ΔU_2 = U_3 - U_2, ΔV_2 = V_3 - V_2 ΔU2=U3−U2,ΔV2=V3−V2
E 1 = Δ U 1 T + Δ V 1 B E_1 = ΔU_1T + ΔV_1B E1=ΔU1T+ΔV1B
E 2 = Δ U 2 T + Δ V 2 B E_2 = ΔU_2T + ΔV_2B E2=ΔU2T+ΔV2B
( E 1 X , E 1 Y , E 1 Z ) = Δ U 1 ( T X , T Y , T Z ) + Δ V 1 ( B X , B Y , B Z ) (E_{1X},E_{1Y},E_{1Z}) = ΔU_1(T_X,T_Y,T_Z) + ΔV_1(B_X,B_Y,B_Z) (E1X,E1Y,E1Z)=ΔU1(TX,TY,TZ)+ΔV1(BX,BY,BZ)
( E 2 X , E 2 Y , E 2 Z ) = Δ U 2 ( T X , T Y , T Z ) + Δ V 2 ( B X , B Y , B Z ) (E_{2X},E_{2Y},E_{2Z}) = ΔU_2(T_X,T_Y,T_Z) + ΔV_2(B_X,B_Y,B_Z) (E2X,E2Y,E2Z)=ΔU2(TX,TY,TZ)+ΔV2(BX,BY,BZ)
[ E 1 X E 1 Y E 1 Z E 2 X E 2 Y E 2 Z ] = [ Δ U 1 Δ V 1 Δ U 2 Δ V 2 ] ∗ [ T X T Y T Z B X B Y B Z ] \left[ \begin{matrix} E_{1X} & E_{1Y} & E_{1Z} \\ E_{2X} & E_{2Y} & E_{2Z} \end{matrix} \right] = \left[ \begin{matrix} ΔU_1 & ΔV_1 \\ ΔU_2 & ΔV_2 \end{matrix} \right] * \left[ \begin{matrix} T_X & T_Y & T_Z \\ B_X & B_Y & B_Z \end{matrix} \right] [E1XE2XE1YE2YE1ZE2Z]=[ΔU1ΔU2ΔV1ΔV2]∗[TXBXTYBYTZBZ]
[ Δ U 1 Δ V 1 Δ U 2 Δ V 2 ] − 1 ∗ [ E 1 X E 1 Y E 1 Z E 2 X E 2 Y E 2 Z ] = [ T X T Y T Z B X B Y B Z ] \left[ \begin{matrix} ΔU_1 & ΔV_1 \\ ΔU_2 & ΔV_2 \end{matrix} \right]^{-1} * \left[ \begin{matrix} E_{1X} & E_{1Y} & E_{1Z} \\ E_{2X} & E_{2Y} & E_{2Z} \end{matrix} \right] = \left[ \begin{matrix} T_X & T_Y & T_Z \\ B_X & B_Y & B_Z \end{matrix} \right] [ΔU1ΔU2ΔV1ΔV2]−1∗[E1XE2XE1YE2YE1ZE2Z]=[TXBXTYBYTZBZ]
[ T X T Y T Z B X B Y B Z ] = 1 Δ U 1 Δ V 2 − Δ U 2 Δ V 1 ∗ [ Δ V 2 − Δ V 1 − Δ U 2 Δ U 1 ] ∗ [ E 1 X E 1 Y E 1 Z E 2 X E 2 Y E 2 Z ] \left[ \begin{matrix} T_X & T_Y & T_Z \\ B_X & B_Y & B_Z \end{matrix} \right] = \frac{1}{ΔU_1ΔV_2 - ΔU_2ΔV_1} * \left[ \begin{matrix} ΔV_2 & -ΔV_1 \\ -ΔU_2 & ΔU_1 \end{matrix} \right] * \left[ \begin{matrix} E_{1X} & E_{1Y} & E_{1Z} \\ E_{2X} & E_{2Y} & E_{2Z} \end{matrix} \right] [TXBXTYBYTZBZ]=ΔU1ΔV2−ΔU2ΔV11∗[ΔV2−ΔU2−ΔV1ΔU1]∗[E1XE2XE1YE2YE1ZE2Z]
- 由上面的推导可知,通过三角形的两条边、纹理坐标可以计算出切向量T、副切向量B
- 因为一个三角形永远是平坦的形状,我们只需为每个三角形计算一个切线/副切线,它们对于每个三角形上的顶点都是一样的
使用法线贴图
- 为了使用法线贴图,需要在顶点着色器中创建TBN矩阵,首先使用上面的方法计算出模型每个顶点的切线向量和副切线向量,并把他们和法向量一起传入顶点着色器,来创建TBN矩阵
- 需要注意的是,现在创建出来的TBN矩阵是在法线空间中,我们需要把他变换到世界空间中,方法就是乘以模型矩阵逆矩阵的转置矩阵
- 如果在将TBN矩阵变换到世界空间中时,只关心方向,不关心缩放和平移,可以直接乘以模型矩阵进行变换
- 从理论上说,顶点着色器中不需要传入副切线向量,因为可以通过切线向量和法线向量叉乘得出
方法一
-
直接使用TBN矩阵,把切线坐标空间的法向量转换到世界坐标空间
-
把TBN矩阵传到片段着色器中,把采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中,之后按照Blinn-Phong模型计算光照就可以了
-
顶点着色器
...
out VS_OUT
{
vec3 FragPos;
vec2 TexCoords;
mat3 TBN;
} vs_out;
uniform mat4 mormalMat;
void main()
{
vec3 T = normalize(vec3(modelMat * vec4(tangent, 0.0)));
vec3 N = normalize(vec3(modelMat * vec4(normal, 0.0)));
T = normalize(T - dot(T, N) * N);//修正
vec3 B = cross(N, T);
vs_out.TBN = mat3(T, B, N);
...
}
- 片段着色器
...
in VS_OUT
{
vec3 FragPos;
vec2 TexCoords;
mat3 TBN;
} fs_in;
void main()
{
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
normal = normalize(normal * 2.0 - 1.0);
normal = normalize(fs_in.TBN * normal);
...
}
方法二
-
使用TBN矩阵的逆矩阵,把世界坐标空间的向量转换到切线坐标空间
-
因为TBN矩阵是正交矩阵,在求逆矩阵时,可以使用转置矩阵代替
-
把lightDir,viewDir都变换到切线空间,这样他们就和法线贴图中采样到的法线就在同一个坐标空间了,之后就可以放肆的计算光照
-
顶点着色器
...
void main()
{
...
mat3 TBN = transpose(mat3(T, B, N));
vs_out.TangentLightPos = TBN * lightPos;
vs_out.TangentViewPos = TBN * viewPos;
vs_out.TangentFragPos = TBN * vec3(modelMat * vec4(position, 0.0));
}
- 片段着色器
...
in VS_OUT
{
vec3 FragPos;
vec2 TexCoords;
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} fs_in;
void main()
{
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
normal = normalize(normal * 2.0 - 1.0);
...
}
两种方法比较
- 方法二可以把所有有关向量的变换控制在顶点着色器中进行
- 方法二将切线空间的光源位置、观察位置以及顶点位置发送给片段着色器。这样我们就不用在片段着色器中进行矩阵乘法。这是一个极佳的优化,因为顶点着色器通常比片段着色器运行的次数少
使用法线贴图与不使用的对比
- 不使用法线贴图
- 使用法线贴图