法线贴图

236 阅读5分钟

简要描述

在图形学中,法线贴图是十分重要的一个部分,通过更改法线的偏移量,进而控制光线和法线的相互作用,可以达到控制效果的生成。了解法线贴图就是比较的有必要的。

原理

首先需要了解的是法线贴图是坐标信息,贴图是一张图片,是保存的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矩阵