切线空间
要理解和应用法线贴图,首先须明白切线空间,这里贴一个知乎的文章,里面对切线空间的讲解非常细致切线空间(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);
}
法线贴图模拟了三维图像的光照效果,但是凹凸情况下物体的自遮挡并没有表现出来,给一种虽立体却很扁的感觉。