简要描述
在图形学中,法线贴图是十分重要的一个部分,通过更改法线的偏移量,进而控制光线和法线的相互作用,可以达到控制效果的生成。了解法线贴图就是比较的有必要的。
原理
首先需要了解的是法线贴图是坐标信息,贴图是一张图片,是保存的RGB的颜色值,所以需要对于其进行一定的变换,法线一般只用来表示方向,值的取值的范围是[-1,1],在图片的颜色的值是取值范围都被归一化到了[0,1],所以对于颜色需要先把范围控制到[-1,1]中,一般采用的方式是:
vec3 rgb_normal = normal * 0.5 + 0.5;
一般情况下,网上的所有的法线的贴图都是偏蓝色的图片,这是因为蓝色是RGB中的第3个通道,所有的法线都指向了Z轴,在附近发生了一些的偏移导致。切线空间
所有的法线都是指向Z的方向,场景的方向和摆放的位置密切相关,这代表一个模型需要多张的法线贴图,这是很不方便的,所以引入了切线空间,简单来说,就是需要把当前空间的变换到切线空间,这就需要一个TBN矩阵变化,这一过程的推理也不复杂,这里就不进行描述。一般也会有二种处理方式,在模型空间中进行计算,或者在切线空间中进行变化。
世界空间
这里还是以opengl的代码来例子:
顶点着色器
//顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aUV;
layout (location = 2) in vec3 aNormal; //法线
layout (location = 3) in vec3 aTangent; //切线
layout (location = 4) in vec3 aBitangent; //副切线
out VS_OUT {
vec3 normal;
vec2 texcoord;
vec3 FragPos;
mat3 TBN; //TBN矩阵
}vs_out;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0); //计算切线空间下的坐标
vs_out.FragPos = vec3(model * vec4(aPos, 1.0)); //模型空间下的坐标
vs_out.normal = mat3(transpose(inverse(model))) * aNormal;; //法线坐标进行变换
vs_out.texcoord = aUV; //纹理
vec3 T = normalize(vec3(model * vec4(aTangent, 0.0)));
vec3 B = normalize(vec3(model * vec4(aBitangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(aNormal, 0.0)));
vs_out.TBN = mat3(T, B, N); //构建TBN矩阵
}
片元着色器
#version 330 core
in mat3 f_TBN;
void main()
{
vec3 norm = normalize(f_normal);
norm = texture(material.normal_texture, f_texcoord).rgb;
norm = normalize(norm * 2.0 - 1.0); //法线变换到[-1,1]
norm = normalize(f_TBN * norm); //变换到世界空间之中
}
一般情况下,变换到模型空间是一种比较好理解的方式,因为后面可能需要对于灯光的位置,相机的位置不需要进行变换。
切线空间
使用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; //M变换之后的顶点信息
vec2 TexCoords;
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;
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()
{
// Obtain normal from normal map in range [0,1]
vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
// Transform normal vector to range [-1,1]
normal = normalize(normal * 2.0 - 1.0); // this normal is in tangent space
// Get diffuse color
vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;
// Ambient
vec3 ambient = 0.1 * color;
// Diffuse
vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// Specular
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);
}
光源的位置和顶点的位置不是计算每一个片元着色器都需要计算,减少一定的性能损耗,比如中间可能还有其他的着色器。
补充
在三角形中可能有很多的重复的顶点(这是必定的),但是需要控制自己的变换的信息,法线,切线,副切线不一定互相垂直的,为了保保证互相垂直,采用施密特正交法,副切线是垂直于法线和切线的平面,考虑有:
vec3 T = normalize(vec3(model * vec4(aTangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(aNormal, 0.0)));
T = normalize(T - dot(T, N) * N); //重新正交化切线T
vec3 B = cross(T, N); //计算副切线B
vs_out.TBN = mat3(T, B, N); //TBN矩阵