法线贴图的核心概念是在不增加额外多边形数目的情况下,通过模拟细节来改善光照效果。具体流程包括:
- 法线的计算与存储:通过法线映射将三维法线向量转化为法线贴图的 RGB 值。
- 渲染中的使用:在片段着色器中使用法线贴图来替代原有的法线向量,进行光照计算。法线贴图通过模拟表面的细节,提升最终渲染的真实感。
在法线贴图的实现中,需要将模型的三维法线信息转化为二维纹理(法线贴图)。不过,法线贴图通常是在纹理空间(2D 图像的每个像素)内进行操作,并不直接操作三维顶点的法线,而是利用法线贴图中的每个像素值表示一个表面的法向量。
法线贴图的核心原理
法线贴图通常以 RGB 颜色存储法线信息,其中:
- R(红色通道) 代表法线的 X 方向。
- G(绿色通道) 代表法线的 Y 方向。
- B(蓝色通道) 代表法线的 Z 方向。
这些 RGB 值通常经过归一化处理,确保其表示的法线是单位向量。法线贴图的 RGB 值范围通常是 [0, 1]
,但它们在纹理贴图中表示的是法线在三维空间中的偏移量。
法线贴图的流程
- 计算和创建法线贴图:
- 为每个顶点计算法线,并将其转化为法线贴图的 RGB 值。
- 在3D建模软件(如 Blender)中,通常会将模型的法线信息烘焙到法线贴图上。烘焙过程会计算每个像素的法线,并把它们存储为 RGB 值。
- 使用法线贴图在渲染中替代原始法线:
- 在渲染时,顶点的原始法线不再用于光照计算,而是使用法线贴图中的法线(经过纹理坐标映射到模型表面的每个像素)。
- 法线贴图中的 RGB 值将通过着色器计算得到一个新的法线向量,该法线向量与光照计算结合,影响最终的渲染效果。
法线贴图计算的步骤和代码
1. 计算法线贴图中的法线
在模型的每个三角形上,根据顶点法线生成法线贴图。假设已经有了一个网格模型,并且每个顶点都有法线向量。
-
将法线向量转换为 RGB 值(该过程通常在法线贴图烘焙时由工具自动完成):
- 需要将三维法线向量映射到 RGB 颜色空间。
- 法线向量的
x
,y
,z
分别映射到R
,G
,B
通道,通常通过以下公式转换:
其中,
Nx
,Ny
,Nz
是法线向量的三个分量,范围是[-1, 1]
,通过上述公式映射到[0, 1]
范围内的 RGB 值。 -
法线贴图在纹理中的存储方式:
- 例如,对于一个法线
(Nx, Ny, Nz)
,其对应的 RGB 值可以是:- R = (Nx + 1) / 2
- G = (Ny + 1) / 2
- B = (Nz + 1) / 2
- 例如,对于一个法线
2. 在渲染时使用法线贴图
在渲染时,法线贴图中的 RGB 值会被用来替代顶点的法线,计算最终的光照效果。
假设已经加载了法线贴图,并且传递给片段着色器,在着色器中,我们将法线贴图的 RGB 值重新映射为三维法线向量,进行光照计算。
顶点着色器(Vertex Shader)
#version 330 core
layout (location = 0) in vec3 aPos; // 顶点位置
layout (location = 1) in vec3 aNormal; // 顶点法向量
layout (location = 2) in vec2 aTexCoords; // 纹理坐标
out vec2 TexCoords; // 输出纹理坐标给片段着色器
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
// 计算最终的顶点位置
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoords = aTexCoords; // 将纹理坐标传递给片段着色器
}
片段着色器(Fragment Shader)
-
将法线从纹理映射到表面:在片段着色器中,
normal
会从法线贴图中获取,经过映射后重新生成一个单位法线,代表表面的局部法向量。 -
光照计算:使用这个法线计算漫反射光照和高光,得到最终的表面颜色。由于法线是通过法线贴图获取的,表面看起来会有更丰富的细节,即使原始网格本身非常简单。
#version 330 core
out vec4 FragColor;
in vec2 TexCoords; // 从顶点着色器传递的纹理坐标
uniform sampler2D texture1; // 法线贴图纹理
uniform vec3 lightPos; // 光源位置
uniform vec3 viewPos; // 观察者(相机)位置
void main() {
// 从法线贴图中读取 RGB 法线值
vec3 normal = texture(texture1, TexCoords).rgb;
normal = normalize(normal * 2.0 - 1.0); // 映射到 [-1, 1] 范围
// 简单的光照计算:漫反射 + 视角方向
vec3 lightDir = normalize(lightPos - FragCoord.xyz); // 光源方向
float diff = max(dot(normal, lightDir), 0.0); // 漫反射光照
vec3 viewDir = normalize(viewPos - FragCoord.xyz); // 视线方向
vec3 reflectDir = reflect(-lightDir, normal); // 反射方向
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); // 高光计算
// 颜色输出:光照与法线贴图相结合
FragColor = vec4(diff + spec, diff + spec, diff + spec, 1.0);
}