什么是凹凸贴图?
凹凸贴图也称法线贴图(Normal Map)
当一个物体表面是凹凸不平的时候,为了使表面在光照下显示更真实有二种方法:1,把模型划分更多的三角形;2:采用法线贴图。更多的三角形代表需要消耗更多算力。法线贴图是,配合低精确的网格(节省算力)也能保证物体表面的质感的一种手段:
精度网格.png
上图中左边有4M个三角形,右边减少到500个三角形配合法线贴图,展现效果差不多。
法线贴图样例
样列.png
法线贴图一般都是像上例这样偏蓝的颜色,因为法线贴图定义为:纹理空中的法线方向的扰动,所有法线的指向都偏向z轴(0, 0, 1),只有砖块的边缘发生偏移比较多。
法线贴图使用
Phong光照计算(https://learnopengl-cn.github.io/02%20Lighting/02%20Basic%20Lighting/)需要使用3个向量(光照方向、视角方向、表面法线),三角形表面的法线是平坦的(三角形顶点线的插值),所以采用法线贴图的法线向量来计算光线,提升效果。
法线.png
切线空间
法线贴图中的法线是定义在纹理空间(切线空间)中的向量,而光照、视角 是在世界坐标系统中,所以需要一个叫TBN的矩阵转化为同一个坐标系中。
TBN矩阵(Tangent-Bitangent-Normal) 是将法向量从贴图变换到物体本地坐标空间的变换矩阵
面它的转置矩阵(称为NBT)就是从物体本地坐标空间转换为纹理坐标系的矩阵。
三角形的每个顶点都有物体空间坐标(x,y,z) 纹理坐标(st,sy),它们之间定义了转换关系,TBN矩阵推导过程参考(https://zhuanlan.zhihu.com/p/150570773)
凹凸贴图实现过程(部份代码)
一、计算切线向量
从模型文件加载后,生成了顶点缓冲、顶点索引缓冲
// 遍历顶点索引,每3个索引是一个角形
for (let i = group.indicesOffset; i < group.indicesLength; i += 3) {
// 取到三个顶点
let ver0 = scene.verts[scene.indices[i]];
let ver1 = scene.verts[scene.indices[i + 1]];
let ver2 = scene.verts[scene.indices[i + 2]];
// 计算二条边
let Edge1 = new Vec3(ver1.Position.clone().subtract(ver0.Position))
let Edge2 = new Vec3(ver2.Position.clone().subtract(ver0.Position))
let DeltaU1 = ver1.TextureCoord.x - ver0.TextureCoord.x
let DeltaV1 = ver1.TextureCoord.y - ver0.TextureCoord.y
let DeltaU2 = ver2.TextureCoord.x - ver0.TextureCoord.x
let DeltaV2 = ver2.TextureCoord.y - ver0.TextureCoord.y
let f = 1.0 / (DeltaU1 * DeltaV2 - DeltaU2 * DeltaV1)
let Tagent: Vec3 = new Vec3([0, 0, 0])
Tagent.x = f * (DeltaV2 * Edge1.x - DeltaV1 * Edge2.x)
Tagent.y = f * (DeltaV2 * Edge1.y - DeltaV1 * Edge2.y)
Tagent.z = f * (DeltaV2 * Edge1.z - DeltaV1 * Edge2.z)
ver0.Tangent = <Vec3>ver0.Tangent.addition(Tagent)
ver1.Tangent = <Vec3>ver1.Tangent.addition(Tagent)
ver2.Tangent = <Vec3>ver2.Tangent.addition(Tagent)
}
// 切线向量单位化(因为公用顶点重复加了切线,为样实现平均化)
for (let i = group.indicesOffset; i < group.indicesLength; i++) {
let ver0 = scene.verts[scene.indices[i]];
ver0.Tangent = <Vec3>ver0.Tangent.normalize()
}
顶点着色器中计算TBN,关转换相关向量到纹理空间
shader_vs: `# version 300 es
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec2 aTexCoord;
layout (location = 2) in vec3 aNormal;
layout (location = 3) in vec3 aTangent; //切线
uniform mat4 uModelMatrix;
uniform mat3 uNormalMatrix; //法线矩阵 [模型矩阵左上角3x3部分的逆矩阵的转置矩阵]
uniform mat4 lightSpaceMatrix; //光线矩阵
out vec2 vTexCoord;
out vec3 vTangentFragPos; //切线空间的顶点坐标
out vec3 vTangentViewPos; //切线空间的观察位置
out vec3 vTangentDirLightDirection; //平行光方向
out vec3 vTangentPointLightPos; //点光线位置
out vec3 vTangentFlashLightPos; // 聚光源位置
out vec3 vTangentFlashLightDirection; //聚光源方向
void main(void) {
gl_Position = project * view * uModelMatrix * vec4(aPosition, 1.0);
vTexCoord = aTexCoord;
vec3 N = normalize(uNormalMatrix * aNormal);
vec3 T = normalize(uNormalMatrix * aTangent);
T = normalize(T - dot(T, N) * N); //Gram-Schmidt正交化
vec3 B = normalize(cross(T, N));
mat3 NTB = transpose(mat3(T, B, N)); //世界坐标,变换到切线坐标
vTangentFragPos = NTB * vec3(uModelMatrix * vec4(aPosition, 1.0));
vTangentViewPos = NTB * viewPos;
vTangentDirLightDirection = NTB * uDirLight.direction;
vTangentPointLightPos = NTB * uPointLights.position;
vTangentFlashLightPos = NTB * uFlashLight.position;
vTangentFlashLightDirection = NTB * uFlashLight.direction;
}
片元着色器中采样法线向量
shader_fs_blinn: `# version 300 es
#ifdef GL_ES
precision highp float;
#endif
// 材质
struct Material {
sampler2D diffuseSampler; // 纹理采样1(漫反射)
sampler2D specularSampler; // 纹理采样2(镜面反射)
sampler2D normalSampler; // 纹理采样3(法线贴图)
float shininess; // 镜面指数 (越大,越完美反射)
vec3 ambient; // 材质阴影色,
vec3 diffuse; // 材质颜色本色
vec3 specular; // 材质光泽度(高光)
bool useDiffuseTexture; // 是否使用(漫反射)贴图
bool useSpecularTexture; // 是否使用(镜面反射)贴图
bool useNormalTexture; // 是否使用(法线)贴图
};
in vec2 vTexCoord;
in vec3 vTangentFragPos; //切线空间的顶点坐标
in vec3 vTangentViewPos; //切线空间的观察位置
in vec3 vTangentDirLightDirection; //
in vec3 vTangentPointLightPos;
in vec3 vTangentFlashLightPos;
in vec3 vTangentFlashLightDirection; //
// 物体材质
uniform Material uMaterial;
out vec4 FragColor;
// function declare
vec3 affactDirLight(DirLight dirLight, vec3 normal, vec3 viewDir, vec2 texCoord);
vec3 affactPointLight(PointLight pointLight, vec3 normal, vec3 fragPos, vec3 viewDir, vec2 texCoord);
vec3 affactFlashLight(FlashLight flashLight, vec3 normal, vec3 fragPos, vec3 viewDir, vec2 texCoord);
void main(void) {
// 观察者方向 顶点->观察者
vec3 viewDir = normalize(vTangentViewPos - vTangentFragPos );
float alpha = 1.0;
if (uMaterial.useDiffuseTexture)
alpha = texture(uMaterial.diffuseSampler, vTexCoord).w;
// 法线贴图-采样法线
vec3 normal = texture(uMaterial.normalSampler, vTexCoord).rgb;
// 变换范围到[-1,1]
normal = normalize( normal * 2.0 - 1.0);
// step-0 计算环境光
vec3 result = uGlobalAmbient * uMaterial.diffuse;
if (uMaterial.useDiffuseTexture)
result = uGlobalAmbient * vec3(texture(uMaterial.diffuseSampler, vTexCoord));
// step1 平行光
if (uHasDirLight)
result += affactDirLight(uDirLight, normal, viewDir, vTexCoord);
FragColor = vec4(result, alpha);
}
// 计算平行光
vec3 affactDirLight(DirLight dirLight, vec3 normal, vec3 viewDir, vec2 texCoord) {
// 指向光源方向
vec3 lightDir = - vTangentDirLightDirection;
vec3 ambient = dirLight.ambient * uMaterial.diffuse;
if (uMaterial.useDiffuseTexture)
{
vec4 text = texture(uMaterial.diffuseSampler, texCoord);
ambient = dirLight.ambient * vec3(text);
}
// 计算漫反射
float diff = max(dot(lightDir, normal), 0.0);
//return vec3(lightDir);
vec3 diffUse = dirLight.diffuse * diff * uMaterial.diffuse;
if (uMaterial.useDiffuseTexture)
{
vec4 text = texture(uMaterial.diffuseSampler, texCoord);
diffUse = dirLight.diffuse * diff * vec3(text);
}
// 计算镜面反射
// 半程向量
vec3 halfwayDir = normalize(lightDir + viewDir);
// 计算反射光
float spec = pow(max(dot(normal, halfwayDir), 0.0), uMaterial.shininess * 4.0); //Blinng 选择冯氏着色时反光度分量的2到4倍
vec3 specular = dirLight.specular * spec * uMaterial.specular;
if (uMaterial.useSpecularTexture)
specular = dirLight.specular * spec * vec3(texture(uMaterial.specularSampler, texCoord));
return ambient + diffUse + specular;
}
实现效果对比
如下面二张图,使用法线贴图光泽度更好、细节更加逼真。
使用法线贴图
未使用法线贴图