TBN矩阵
T:切线向量
B:副切线向量
N:法线向量
法线向量不用我们计算,我们的主要任务是根据三角形的坐标计算出T和B向量。
公式推导
如图,E2向量与纹理坐标的差ΔU2、ΔV2构成一个三角形。ΔU2
与切线向量T方向相同,而ΔV2与副切线向量B方向相同。这也就是说,所以我们可以将三角形的边E1与E2写成如下两个等式,其中ΔU2、ΔV2是两个差值是数,而B、T、E1、E2是向量,B、T还是方向向量:
E1=ΔU1T+ΔV1B
E2=ΔU2T+ΔV2B
将E1、E2和T、B写成向量的形式得到下面的等式:
(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了。大致是把它变化为,1除以矩阵的行列式,再乘以它的伴随矩阵(Adjugate Matrix)。
在代码中实现TBN矩阵的计算
// 矩形的坐标
glm::vec3 pos1(-1.0, 1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
// 纹理坐标
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);
// 发现向量
glm::vec3 nm(0.0, 0.0, 1.0);
// 三角形的切线和副切线
glm::vec3 tangent1, bitangent1;
//计算下面计算所需的向量,包括E1、E2、两个纹理坐标向量,deltaUV1.x是ΔU1,deltaUV1.y是ΔV1,deltaUV2.x是ΔU2,deltaUV2.y是ΔV2。
glm::vec3 edge1 = pos2 - pos1;//E1
glm::vec3 edge2 = pos3 - pos1;//E2
glm::vec2 deltaUV1 = uv2 - uv1;//纹理坐标向量
glm::vec2 deltaUV2 = uv3 - uv1;//纹理坐标向量
//计算行列式
GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
//切线向量T的xyz坐标,根据矩阵乘法求得
tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
tangent1 = glm::normalize(tangent1);
//副切线向量B的xyz坐标,根据矩阵乘法求得
bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
bitangent1 = glm::normalize(bitangent1);
TBN矩阵的应用
使用TBN矩阵解决从法线贴图中采样得到的法线所处坐标空间与其他向量所处的坐标空间不同的问题:
从法线贴图中采样得到的法线处于切线空间下,而其他的向量是处于世界坐标下的。而TBN矩阵可以将切线空间的坐标变换到世界空间中,有两种方法来解决:
- 方法一:在片段着色器中,对采样得到的法线向量左乘TBN矩阵,将其变换到世界坐标中,需要将TBN矩阵从顶点着色器传给片段着色器。
- 方法二:在顶点着色器中,对光线向量和视线向量左乘TBN矩阵的逆矩阵将向量变换到切线空间中,不需要将TBN矩阵从顶点着色器传给片段着色器。
方法二的着色器
//顶点着色器
#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;
out VS_OUT {
vec3 FragPos;
vec2 TexCoords;
//切线空间下的光源坐标和视角坐标以及fragment坐标
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform vec3 lightPos;
uniform vec3 viewPos;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
vs_out.FragPos = vec3(model * vec4(position, 1.0));
vs_out.TexCoords = texCoords;
//将TBN矩阵的各个分量先变换到我们现在的坐标空间中,也就是世界空间
mat3 normalMatrix = transpose(inverse(mat3(model)));
vec3 T = normalize(normalMatrix * tangent);
vec3 B = normalize(normalMatrix * bitangent);
vec3 N = normalize(normalMatrix * normal);
//将坐标乘以变换矩阵的逆矩阵,将世界坐标中的坐标变换到切线空间,这样坐标就与在片段着色器中对法线贴图采样后得到的法线坐标在同一坐标系下
mat3 TBN = transpose(mat3(T, B, N));
vs_out.TangentLightPos = TBN * lightPos;
vs_out.TangentViewPos = TBN * viewPos;
vs_out.TangentFragPos = TBN * vs_out.FragPos;
}
//片段着色器
#version 330 core
out vec4 FragColor;
in VS_OUT {
vec3 FragPos;
vec2 TexCoords;
vec3 TangentLightPos;
vec3 TangentViewPos;
vec3 TangentFragPos;
} fs_in;
//漫反射贴图和法线贴图
uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform bool normalMapping;
void main()
{
//因为贴图中本来是存储颜色的,我们是运用了颜色的rgb值存储了法线坐标,由于法线坐标是-1到1,而颜色是0到1,所以在法线存储进贴图的时候我们需要进行一次变换,从贴图采样出来的法线坐标也需要一次变换
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
// 法线的变换,法线是在切线空间下的
normal = normalize(normal * 2.0 - 1.0);
// 获得漫反射光线
vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;
// 环境光
vec3 ambient = 0.1 * color;
// 漫反射光
vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// 镜面发射光
vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
vec3 reflectDir = reflect(-lightDir, normal);
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
vec3 specular = vec3(0.2) * spec;
//最终的颜色
FragColor = vec4(ambient + diffuse + specular, 1.0f);
}