目录
前记:学!!!!!---------------------------------------------------------------------------------------博主:mx
基本应用:
增强纹理细节。例如,砖块的表面。砖块的表面非常粗糙,显然不是完全平坦的:它包含着接缝处水泥凹痕,以及非常多的细小的空洞。
如果只用普通的纹理贴图,只有一件事决定物体的形状,这就是垂直于它的法线向量。砖块表面只有一个法线向量,表面完全根据这个法线向量被以一致的方式照亮。

如果我们使用法线贴图,每个fragment使用了自己的法线,我们就可以让光照相信一个表面由很多微小的(垂直于法线向量的)平面所组成,物体表面的细节将会得到极大提升。
法线纹理的存储:
我们需要将法线的xyz存储在纹理的rgb中,法线的范围在-1到1,颜色范围在0到1,所以我们需要先将法线映射回0到1
vec3 rgb_normal = normal * 0.5 + 0.5; // 从 [-1,1] 转换至 [0,1]
将法线向量变换为RGB颜色元素,我们就能把根据表面的形状的fragment的法线保存在2D纹理中。
这会是一种偏蓝色调的纹理(几乎所有法线贴图都是这样的)。这是因为所有法线的指向都偏向z轴(0, 0, 1)这是一种偏蓝的颜色。法线向量从z轴方向也向其他方向轻微偏移,颜色也就发生了轻微变化,这样看起来便有了一种深度。例如,你可以看到在每个砖块的顶部,颜色倾向于偏绿,这是因为砖块的顶部的法线偏向于指向正y轴方向(0, 1, 0),这样它就是绿色的了。
法线纹理的采样:
需要注意的是OpenGL读取的纹理的y(或V)坐标和纹理通常被创建的方式相反。是以1-y这种方式进行读取的。
shader中的采样:
我们需要将采样的法线颜色从0到1重新映射回-1到1,便能将RGB颜色重新处理成法线。
我们在shader中这样采样:
// 从法线贴图范围[0,1]获取法线
normal = texture(normalMap, fs_in.TexCoords).rgb;
// 将法线向量转换为范围[-1,1]
normal = normalize(normal * 2.0 - 1.0);
存在的问题:
如果我们是在常规的空间下对法线纹理进行采样,那么每个方向都必须要有一个单独的法线贴图,如果是一个立方体的话,我们就需要有6个法线贴图,如果模型表面有无数个不同的朝向,那么我们需要有无数个方向的法线贴图。
---------------------------------------------------------------------------------------博主:mx
解决方案:
在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对与这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间。
切线空间:
定义:
在切线空间,Z轴为法线方向,Y轴为切线方向,Z轴为副切线方向。
切线空间的转换矩阵:
我们先看view矩阵怎么求的:
对应的矩阵获取就是:
view = glm::lookAt(fragPos, fragPos+ N, T);
对应到shader中就是:
mat3 TBN = mat3(T, B, N);
那么我们需要关心的就是怎么计算切线和副切线。
计算切线和副切线:
我们先考虑他的数学特性,看下面这个图:
注意上图中边E2E2与纹理坐标的差ΔU2、ΔV2构成一个三角形。ΔU2与切线向量TT方向相同,而ΔV2与副切线向量B方向相同。这也就是说,所以我们可以将三角形的边E1与E2写成切线向量\T和副切线向量B的线性组合:
E1=ΔU1T+ΔV1B
E2=ΔU2T+ΔV2B
把它拆一下:
(E1x,E1y,E1z)=ΔU1(Tx,Ty,Tz)+ΔV1(Bx,By,Bz)
(E2x,E2y,E2z)=ΔU2(Tx,Ty,Tz)+ΔV2(Bx,By,Bz)
再转换成矩阵乘法的形式:

然后两边乘上ΔUΔV的逆矩阵,等式变换为:

逆矩阵的求法:求出伴随矩阵,伴随矩阵的求法就是求出伴随余子式的矩阵然后转置。然后用伴随矩阵除以他原本矩阵的行列式的值。这样我们的等式就变成了:
然后我们可以根据三角形的两条边以及纹理坐标计算出切线向量T和副切线B。
ok,我们现在可以再cpu侧预计算出切线和副切线了,那么我们再shader里面具体要怎么用呢?
切线空间的法线贴图:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;
voidmain()
{
[...]
vec3 T = normalize(vec3(model * vec4(tangent, 0.0)));
vec3 B = normalize(vec3(model * vec4(bitangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(normal, 0.0)));
mat3 TBN = mat3(T, B, N)
}
我们把T/B/N转换到世界空间,这个其实是有个问题的,我们直接乘以model矩阵的话,如果是等比缩放这样是没用问题的,但是如果是非等比的缩放,这个就会出现问题
这个时候我们一般使用法线变换矩阵:
这个法线变换矩阵一般是原本矩阵的逆再转置
transpose( inverse( model ) ) * vec4(aNormal,0.0);
---------------------------------------------------------------------------------------博主:mx
现在我们得到了TBN矩阵,我们来看这么用啊。
TBN矩阵的使用:
按照LearnOpengl里面的说法是有以下两个使用:
- 我们直接使用TBN矩阵,这个矩阵可以把切线坐标空间的向量转换到世界坐标空间。因此我们把它传给片段着色器中,把通过采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。
- 我们也可以使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间,这样法线和其他光照变量再一次在一个坐标系中了。
根据这些用法在shader里面进行使用应该不难
小疑问(思考):
但是其实还说有点小疑问的,在LearnOpengl摄像机那一节说的是,我们可以使用lookat矩阵,来将坐标变换到该坐标空间。
而我们TBN矩阵与这个类似,为什么这个矩阵乘坐标的作用反而是把切线坐标空间的向量转换到世界坐标空间?
这里给一下OpenGL SuperBible里面的说法:

OpenGL SuperBible里面写的是给定一个TBN矩阵能够将一个向量从笛卡尔坐标系转换到局部坐标系
问题出现的原因:

OpenGLApi构建的矩阵是列矩阵,但是glm构建的矩阵是行矩阵,在shader中使用mat(T,B,N),构建出来的矩阵相当于有了转置,而TBN矩阵是正交矩阵,矩阵的转置等于矩阵的逆,所以作用就是把切线坐标空间的向量转换到世界坐标空间。
复杂物体的处理:
在模型加载器中一般都会内置了计算切线副切线的功能。比如Assimp中,当我们加载模型的时候调用aiProcess_CalcTangentSpace。当aiProcess_CalcTangentSpace应用到Assimp的ReadFile函数时,Assimp会为每个加载的顶点计算出柔和的切线和副切线向量。
const aiScene* scene = importer.ReadFile( path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace );
然后我们可以这样获取计算出来的切线空间:
vector.x = mesh->mTangents[i].x;
vector.y = mesh->mTangents[i].y;
vector.z = mesh->mTangents[i].z;
vertex.Tangent = vector;
然后再更新模型加载器,用以从带纹理模型中加载法线贴图。wavefront的模型格式(.obj)导出的法线贴图有点不一样,Assimp的aiTextureType_NORMAL并不会加载它的法线贴图,而aiTextureType_HEIGHT却能,所以我们经常这样加载它们:
vector<Texture> specularMaps = this->loadMaterialTextures( material, aiTextureType_HEIGHT, "texture_normal" );
法线贴图的另一个很直观的用处就是:
建模的时候高模烘焙法线贴图,然后转低模,能够保留住高模的细节。
施密特正交化:
当在更大的网格上计算切线向量的时候,它们往往有很大数量的共享顶点,当法向贴图应用到这些表面时将切线向量平均化通常能获得更好更平滑的结果。这样做有个问题,就是TBN向量可能会不能互相垂直,这意味着TBN矩阵不再是正交矩阵了。法线贴图可能会稍稍偏移,但这仍然可以改进。施密特正交化就可以对TBN向量进行重正交化,这样每个向量就又会重新垂直了。
vec3 T = normalize(vec3(model * vec4(tangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(normal, 0.0)));
// re-orthogonalize T with respect to N
T = normalize(T - dot(T, N) * N);
// then retrieve perpendicular vector B with the cross product of T and N
vec3 B = cross(T, N);
mat3 TBN = mat3(T, B, N);
---------------------------------------------------------------------------------------博主:mx
学习资料:
---------------------------------------------------------------------------------------博主:mx