第10课:法线贴图与 TBN 矩阵

教材对照:The Unity Shaders Bible 第六章 6.0.1 — 6.0.3

10.1 法线贴图(Normal Maps)的原理

10.1.1 为什么需要法线贴图?

在实时渲染中,细节性能始终是一对矛盾。如果我们想要一个砖墙表面每一块砖都凹凸分明, 最直接的方法是让建模师制作一个包含数百万顶点的高精度模型。但这样的模型在游戏中几乎无法流畅运行——每一帧都需要 GPU 处理海量的顶点变换与光栅化。

法线贴图(Normal Map)正是为了解决这一矛盾而诞生的技术。它在不增加几何顶点数的前提下, 通过欺骗光照的方式让表面呈现出凹凸不平的细节。

核心思想 法线贴图不改变物体的几何形状(网格顶点不动),而是修改每个像素的法线方向

使光照计算的结果仿佛表面真的有凹凸起伏。

下图示意了法线贴图的工作原理:左侧是低模(低多边形)的几何体,右侧通过法线贴图”模拟”出高模(高多边形)的表面细节。 光照在计算漫反射和高光时,使用贴图中存储的扰动法线而非原始几何法线,从而产生立体感。

10.1.2 法线贴图的颜色含义

如果你打开一张法线贴图,会发现它整体呈现偏蓝紫色的色调。这并不是偶然的,而是由法线贴图的编码方式决定的。

法线贴图将法线向量的 XYZ 三个分量映射到纹理的 RGB 三个通道中:

法线到颜色的映射

法线向量范围:[−1, 1] → 纹理颜色范围:[0, 1]

R = X × 0.5 + 0.5

G = Y × 0.5 + 0.5

B = Z × 0.5 + 0.5

在**切线空间(Tangent Space)**中,默认法线方向是 (0, 0, 1),即垂直于表面指向外。 映射到 RGB 后得到 (0.5, 0.5, 1.0),这正是蓝紫色的来源。而扰动后的法线会在 XY 方向有偏移, 反映为贴图中的红绿色调变化——红色偏移表示 X 方向的变化,绿色偏移表示 Y 方向的变化。

直观理解 法线贴图中的"蓝色通道"是最重要的,因为它存储了法线的 Z 分量(垂直方向),

而 R 和 G 通道则记录了表面在水平方向的微小幅度的倾斜。这就是为什么法线贴图看起来”偏蓝”。

10.2 DXT 压缩与法线贴图优化

法线贴图相比普通纹理要”重”得多,因为它们存储的是三通道(RGB)的高精度向量数据。 如果不加压缩,会占用大量的 GPU 显存和带宽,尤其是在移动设备上还会导致发热和耗电。

10.2.1 DXT / BC 压缩格式

DXT 压缩(也称 BC 压缩,Block Compression)是 GPU 上最常用的纹理压缩技术之一。 其核心思想是将纹理分割为 4×4 像素的块,每个块只存储两个端点颜色和一组索引, 从而大幅减少数据量。

格式别名输入通道压缩比法线贴图适用性
DXT1BC1RGB(1-bit Alpha)6:1不推荐(质量损失大)
DXT5BC3RGBA4:1可用,但非最优
DXT5nmBC3(双通道优化)AG(R→A, G→G, B→重构)4:1Unity 默认法线贴图格式
BC5BC5(RG 双通道)RG(B 重构)4:1高质量法线贴图
BC7BC7RGBA(高质量)3:1最高质量,适合 PBR

10.2.2 DXT5nm 压缩原理

DXT5nm 是 Unity 对法线贴图的默认压缩格式。它的巧妙之处在于:法线贴图的 R 通道(X 分量) 和 B 通道(Z 分量)之间存在冗余——由于法线是归一化向量,知道 X 和 Y 就可以计算出 Z。

具体策略如下:

  • 将 R 通道的数据移动到 A 通道(Alpha)中——因为 DXT5 对 Alpha 通道的压缩质量更高
  • 保留 G 通道不变
  • 丢弃 B 通道(Z 分量),利用 勾股定理 在着色器中重建

Z 分量重建公式(DXT 解压缩)

R’ = A × 2 − 1 (从 Alpha 通道读取) G’ = G × 2 − 1 (从 Green 通道读取) B’ = √( 1 − (R’² + G’²) ) (勾股定理重建)

原理:归一化向量满足 R’² + G’² + B’² = 1,因此 B’ = √(1 − R’² − G’²)

10.2.3 解压缩的 shader 实现

以下是教材中自定义的 DXT 解压缩函数,与 Unity 内置的 UnpackNormal() 功能等价:

最佳实践 实际开发中直接使用UnpackNormal(normalMap) 即可,Unity 会自动处理平台差异。

上述代码是为了帮助理解底层原理。

10.3 切线空间(Tangent Space)vs 物体空间(Object Space)

法线贴图可以在两种不同的参考系中存储:

特性切线空间(Tangent Space)物体空间(Object Space)
参考系基于每个顶点的切平面基于物体的局部坐标轴
默认法线(0, 0, 1),即垂直于三角形表面物体自身的法线方向
贴图颜色偏蓝紫色(因为 Z 分量占主导)五颜六色(取决于物体形状)
可重用性——同一张贴图可用于不同物体低——每张贴图只能用于对应物体
变形适应性支持(法线随切线帧变换)不支持(变形后法线错误)
压缩效率高(偏蓝,各通道独立性强)较低
主要用途游戏渲染(最常用)静态物体、烘焙贴图

为什么游戏几乎都使用切线空间法线贴图?

  1. 可复用性:一张砖墙法线贴图可以贴在任意形状的物体上(平面、圆柱、球),法线方向的”上下左右”始终相对于表面。
  2. 支持变形:Skinned Mesh 在动画中顶点位置变化时,法线贴图仍能正确工作。
  3. 压缩友好:切线空间法线贴图的各通道独立性更强,DXT 压缩的质量损失更小。
  4. 镜像对称:左右对称的模型可以共用 UV 镜像的法线贴图。

当然,切线空间也有代价——每次采样法线贴图后,都需要通过 TBN 矩阵 将法线从切线空间变换到世界空间(或者将光照向量变换到切线空间)。 这正是下一节的核心内容。

10.4 TBN 矩阵的原理与构建

10.4.1 TBN 是什么?

TBN 是 Tangent(切线)、Bitangent(副切线/双切线,旧称 Binormal)、 Normal(法线)三个正交向量的缩写。它们共同构成一个正交基, 即切线空间的三个坐标轴。

TBN 正交基

N(法线):垂直于表面,由模型提供 T(切线):沿 UV 的 U 方向,与表面相切,由模型提供 B(副切线):沿 UV 的 V 方向,通过与 N 和 T 的**叉积(Cross Product)**计算得到

B = N × T × T.w

其中 T.w 表示切线的方向性(1 或 -1),用于处理 UV 镜像。

10.4.2 为什么需要 TBN 矩阵?

法线贴图采样得到的法线位于切线空间,而我们的光照计算 (漫反射、高光等)需要在**世界空间(World Space)**中进行。因此需要一种方式将法线 从切线空间变换到世界空间——这就是 TBN 矩阵的职责。

两种等价思路 思路 A(常用):将法线贴图的法线从切线空间 → 世界空间(用 TBN 矩阵) 思路 B(少见):将光照方向和视角方向从世界空间 → 切线空间(用 TBN 的逆矩阵) 两种思路数学上等价,但思路 A 更直观,因为光照和视角方向只需计算一次。

10.4.3 TBN 矩阵的数学形式

TBN 矩阵是一个 3×3 的矩阵,其列向量分别是 T、B、N 在世界空间的坐标:

对于一个切线空间的法线向量 ntangent = (x, y, z),变换到世界空间就是:

nworld = normalize(TBN_matrix × ntangent)

10.5 在 Shader 中实现法线贴图

下面我们逐步实现一个完整的法线贴图 Shader,涵盖从顶点输入到 Fragment Shader 的全流程。

10.5.1 第一步:Shader 属性声明

10.5.2 第二步:顶点输入与输出结构体

在顶点输入 appdata 中使用 NORMALTANGENT 语义获取法线和切线。 注意切线是 float4 类型,其 w 分量表示镜像方向。

重要 Fragment Shader 中不能使用 NORMAL 或 TANGENT 语义——这些语义仅用于顶点输入。

在 v2f 中必须使用TEXCOORD[n] 来传递法线和切线数据。 同时每个 TEXCOORD 的索引 n 必须唯一,否则会导致数据覆盖。

10.5.3 第三步:顶点 Shader 中的空间变换

10.5.4 第四步:Fragment Shader 中的法线贴图采样与 TBN 变换

10.6 关键概念总结与对比

概念说明要点
法线贴图用纹理存储法线扰动,模拟表面凹凸不增加顶点,骗过光照
切线空间以顶点切平面为参考系,Z 轴朝外偏蓝色、可复用、支持变形
DXT5nm法线贴图专用压缩格式A=R, G=G, B 重构
BC5/BC7更高质量的压缩格式BC5 双通道, BC7 高保真
TBN 矩阵切线→世界空间的变换矩阵T:切线 B:副切线 N:法线
UnpackNormalUnity 内置解压缩函数等价于自定义 DXTCompression
物体空间法线贴图以物体自身坐标系为参考不可复用,不常用

10.7 跨引擎通用原理

Note

法线贴图、切线空间、TBN 矩阵这些概念并非 Unity 独有。在任何现代渲染引擎中——

  • Unreal Engine:使用切线空间法线贴图,材质编辑器中 Normal 输入即是。
  • Godot:SpatialMaterial 同样支持法线贴图,TBN 自动计算。
  • Blender / Maya:烘焙法线贴图时选择”Tangent Space”。
  • DirectX / Vulkan:底层 API 层面不直接处理法线贴图,TBN 完全由开发者实现。 TBN 矩阵的数学定义在所有引擎中是一致的——T、B、N 三个正交向量构成的变换矩阵。

唯一需要注意的是坐标系差异

  • Direct3D 使用左手系,UV 原点在左上角
  • OpenGL 使用右手系,UV 原点在左下角
  • Unity 的 _ProjectionParams.x 就是为处理这种差异而存在

练习与回顾

  1. 概念理解:为什么法线贴图通常呈现蓝紫色?如果一张法线贴图偏红或偏绿,说明什么?
  2. 数学推导:证明在 DXT5nm 压缩中,给定 R 和 G 通道的值,Z 分量可以通过 √(1 − R² − G²) 精确重建。这个推导成立的前提条件是什么?
  3. 代码分析:在 TBN 矩阵的构建中,如果忘记乘以 v.tangent.w,会对渲染结果产生什么影响?什么情况下即使不乘也不会出错?
  4. 对比思考:分别列出切线空间法线贴图和物体空间法线贴图的一个适用场景和一个不适用场景。
  5. 性能优化:假设你需要在移动平台上使用法线贴图,你会选择 BC5 还是 DXT5nm?为什么?
  6. 工程实践:在 Unity 中将一个普通纹理设置为 Normal Map 类型时,Inspector 中会出现哪些选项?Create from Grayscale 功能是如何工作的?

教材对照The Unity Shaders Bible Chapter II, Section 6.0.1—6.0.3 (Pages 188-203)