learnOpenGL 5.3 法线贴图

160 阅读3分钟

切线空间

要理解和应用法线贴图,首先须明白切线空间,这里贴一个知乎的文章,里面对切线空间的讲解非常细致切线空间(Tangent Space)完全解析

简单来说,切线空间就是把某个三角面片放到正对屏幕的位置,也就是其法线与屏幕空间z轴平行。其中最重要的一个公式
在这里插入图片描述
该公式描述的数学意义是,表示一个点在uv空间与三维空间的映射关系,其中TB作为基矢,以uv空间中的u和v的增长作为控制参数。假设三角形中存在一点P,则向量AP=u(p)*T+v(p)*B,只要知道P点的uv坐标值,即可得到P点的三维坐标值,反之亦然。

通过这个公式我们只要知道三角形的顶点坐标,就可以计算它的切线和副切线,代码如下:

	glm::vec3 pos1(-1.0f, 1.0f, 0);
    glm::vec3 pos2(-1.0f, -1.0f, 0);
    glm::vec3 pos3(1.0f, -1.0f, 0);
    glm::vec3 pos4(1.0f, 1.0f, 0);
    glm::vec2 uv1( 0 , 1.0f);
    glm::vec2 uv2(0, 0);
    glm::vec2 uv3(1.0f, 0);
    glm::vec2 uv4(1.0f, 1.0f);
    glm::vec3 nm(0, 0, 1.0f);

    glm::vec3 edge1 = pos2 - pos1;
    glm::vec3 edge2 = pos3 - pos1;
    glm::vec2 deltaUV1 = uv2 - uv1;
    glm::vec2 deltaUV2 = uv3 - uv1;

    float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
    glm::vec3 tangent1, bitangent1;
    glm::vec3 tangent2, bitangent2;
    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);

    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);

    edge1 = pos3 - pos1;
    edge2 = pos4 - pos1;
    deltaUV1 = uv3 - uv1;
    deltaUV2 = uv4 - uv1;

    f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

    tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
    tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
    tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
    tangent2 = glm::normalize(tangent2);


    bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
    bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
    bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
    bitangent2 = glm::normalize(bitangent2);

得到切线与副切线以后,经过一些变换,我们就可以得到TBN矩阵,他可以实现切线空间与世界空间之间的变换。

一般来说,我们使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间。

这样做的好处在于,所有向量的变换都在顶点着色器中进行,而顶点着色器只要对每个顶点运行,所以运算效率更高。

顶点着色器代码如下:

#version 330 core
layout (location=0) in vec3 Position;
layout (location=1) in vec3 Normal;
layout (location=2) in vec2 Texcoor;
layout (location=3) in vec3 Tangent;
layout (location=1) in vec3 Bitangent;

out vec3 fragpos;
out vec2 texcoor;
out vec3 lightpos;
out vec3 viewpos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform vec3 Lightpos;
uniform vec3 Viewpos;

void main()
{
	gl_Position=projection*view*model*vec4(Position,1.0f);
	fragpos=vec3(model*vec4(Position,1.0f));
	texcoor=Texcoor;

	mat3 normmat=transpose(inverse(mat3(model)));
	vec3 T=normalize(normmat*Tangent);
	vec3 B=normalize(normmat*Bitangent);
	vec3 N=normalize(normmat*Normal);
	mat3 TBN=transpose(mat3(T,B,N));

	lightpos=TBN*Lightpos;
	viewpos=TBN*Viewpos;
	fragpos=TBN*fragpos;
}

法线贴图

法线贴图的原理很简单,就是用贴图的RGB通道来存储每个片元的法线向量。

不过有一点要注意,XYZ每个维度的范围是[-1,1]。而RGB通道的范围是[0,1]。所以在存储时要先从[-1,1]转换到[0,1],读取时再从[0,1]转换到[-1,1]。

除此之外的工作都是光照模型,不再赘述,片元着色器如下:

#version 330 core

in vec3 fragpos;
in vec2 texcoor;
in vec3 lightpos;
in vec3 viewpos;

struct Material{
	sampler2D wall;
	sampler2D normwall;
	float shininess;
};
struct Light{
	float ambient;
	float diffuse;
	float specular;
};
uniform Material material;
uniform Light light;
out vec4 FragColor;

void main()
{
	vec3 normal=texture(material.normwall,texcoor).rgb;
	normal=normalize(normal*2.0-1.0);

	vec3 ambient=light.ambient*texture(material.wall,texcoor).rgb;

	vec3 lightdir=normalize(lightpos-fragpos);
	float diff=max(dot(lightdir,normal),0);
	vec3 diffuse=diff*light.diffuse*texture(material.wall,texcoor).rgb;

	vec3 viewdir=normalize(viewpos-fragpos);
	vec3 halfdir=normalize(lightdir+viewdir);
	float spec=pow(max(dot(halfdir,normal),0),material.shininess);
	vec3 specular=light.specular*spec*texture(material.wall,texcoor).rgb;

	vec3 result=ambient+diffuse+specular;
	FragColor=vec4(result,1.0f);
}

法线贴图模拟了三维图像的光照效果,但是凹凸情况下物体的自遮挡并没有表现出来,给一种虽立体却很扁的感觉。