Bootstrap

用三维模型的顶点法向量计算法线贴图

 

        法线贴图的核心概念是在不增加额外多边形数目的情况下,通过模拟细节来改善光照效果。具体流程包括:

  1. 法线的计算与存储:通过法线映射将三维法线向量转化为法线贴图的 RGB 值。
  2. 渲染中的使用:在片段着色器中使用法线贴图来替代原有的法线向量,进行光照计算。法线贴图通过模拟表面的细节,提升最终渲染的真实感。

        在法线贴图的实现中,需要将模型的三维法线信息转化为二维纹理(法线贴图)。不过,法线贴图通常是在纹理空间(2D 图像的每个像素)内进行操作,并不直接操作三维顶点的法线,而是利用法线贴图中的每个像素值表示一个表面的法向量。

法线贴图的核心原理

        法线贴图通常以 RGB 颜色存储法线信息,其中:

  • R(红色通道) 代表法线的 X 方向
  • G(绿色通道) 代表法线的 Y 方向
  • B(蓝色通道) 代表法线的 Z 方向

        这些 RGB 值通常经过归一化处理,确保其表示的法线是单位向量。法线贴图的 RGB 值范围通常是 [0, 1],但它们在纹理贴图中表示的是法线在三维空间中的偏移量。

法线贴图的流程

  1. 计算和创建法线贴图
    • 为每个顶点计算法线,并将其转化为法线贴图的 RGB 值。
    • 在3D建模软件(如 Blender)中,通常会将模型的法线信息烘焙到法线贴图上。烘焙过程会计算每个像素的法线,并把它们存储为 RGB 值。
  2. 使用法线贴图在渲染中替代原始法线
    • 在渲染时,顶点的原始法线不再用于光照计算,而是使用法线贴图中的法线(经过纹理坐标映射到模型表面的每个像素)。
    • 法线贴图中的 RGB 值将通过着色器计算得到一个新的法线向量,该法线向量与光照计算结合,影响最终的渲染效果。

法线贴图计算的步骤和代码

1. 计算法线贴图中的法线

        在模型的每个三角形上,根据顶点法线生成法线贴图。假设已经有了一个网格模型,并且每个顶点都有法线向量。

  1. 将法线向量转换为 RGB 值(该过程通常在法线贴图烘焙时由工具自动完成):

    • 需要将三维法线向量映射到 RGB 颜色空间。
    • 法线向量的 x, y, z 分别映射到 R, G, B 通道,通常通过以下公式转换:

    其中,Nx, Ny, Nz 是法线向量的三个分量,范围是 [-1, 1],通过上述公式映射到 [0, 1] 范围内的 RGB 值。

  2. 法线贴图在纹理中的存储方式:

    • 例如,对于一个法线 (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)

  1. 将法线从纹理映射到表面:在片段着色器中,normal 会从法线贴图中获取,经过映射后重新生成一个单位法线,代表表面的局部法向量。

  2. 光照计算:使用这个法线计算漫反射光照和高光,得到最终的表面颜色。由于法线是通过法线贴图获取的,表面看起来会有更丰富的细节,即使原始网格本身非常简单。

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

;