法线贴图与切线空间的关系,是 "存储空间" 与 "参考系" 的关系:法线贴图中的颜色信息,本质上描述的是在切线空间下的扰动法线方向。
为了让你在Shader中正确使用法线贴图,下面从"为什么需要切线空间"到"实际转换方法"逐步说明。
一、法线贴图存储的是什么?
一张法线贴图中的每个像素(通常RGB通道)对应一个单位向量 的(x, y, z)分量。这个向量经过从[-1,1]到[0,1]的映射存储:
normal = (color.r * 2 - 1, color.g * 2 - 1, color.b * 2 - 1)
关键点: 这个 (x,y,z) 向量所在的坐标系并不是世界空间,也不是模型空间,而是切线空间 (Tangent Space)。
在切线空间中:
-
Z 轴 (通常是蓝色通道):表面的原始法线方向 (Normal)
-
X 轴 (红色通道):表面的切线方向 (Tangent),即沿UV坐标U增加的方向
-
Y 轴 (绿色通道):表面的副切线方向 (Bitangent / Binormal),由 Normal × Tangent 得出
因此,当一个法线贴图看起来"蓝紫色"时,表示大部分像素指向(0,0,1) → 即没有扰动,垂直于表面。
二、为什么要用切线空间?不能直接用模型空间法线贴图吗?
可以用模型空间的法线贴图,但切线空间有以下不可替代的优势:
| 特性 | 切线空间法线贴图 | 模型空间法线贴图 |
|---|---|---|
| 可复用性 | 可贴在任意相同UV布局的模型上(如不同姿势的角色、平铺贴花) | 只能用于特定模型的静止姿势 |
| 可压缩性 | Z值可推导 (sqrt(1 - x² - y²)),可只存储两通道 | 需要完整三通道 |
| 骨骼动画/变形 | 法线随表面变形自动正确(因为切线空间相对表面不变) | 必须随顶点变形重新计算,无法直接使用 |
| 镜像纹理 | 支持(通过调整副切线方向) | 不支持,镜像后法线朝向错误 |
因此,绝大多数现代游戏引擎(包括Unity)都默认使用切线空间法线贴图。
三、在Unity Shader中:如何从切线空间转换到世界空间?
因为光照计算需要在同一坐标系下进行(通常是世界空间 ),我们需要将法线贴图采样得到的方向,转换到世界空间。这需要用到 TBN 矩阵。
1. 建立切线空间的基向量
顶点着色器接收每个顶点的信息:
-
normal(模型空间法线) -
tangent(模型空间切线,Unity中tangent.w 用于决定副切线的方向)
副切线 binormal 可以通过叉积计算,并统一方向:
float3 binormal = cross(normal, tangent.xyz) * tangent.w;
tangent.w 通常为 ±1,确保副切线的朝向与UV坐标的V方向一致。
2. 构建 TBN 矩阵
将这三个向量转换到世界空间(或视图空间,取决于你的光照模型)。最典型的做法:
// 在顶点着色器中计算世界空间的 TBN 矩阵的每一行(或者列,取决于乘法顺序)
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// 构建 3x3 矩阵,用于将切线空间向量转换到世界空间
float3x3 TBN = float3x3(worldTangent, worldBinormal, worldNormal);
3. 采样并转换法线
// 采样法线贴图(注意必须标记为 "UnpackNormal" 或使用 UnpackNormal函数)
float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, uv));
// 转换到世界空间
float3 worldNormal = normalize(mul(TBN, tangentNormal));
4. 也可以反过来:将光照方向转换到切线空间
这样做可以在顶点着色器完成矩阵乘法,减少像素着色器中的计算量。但通用性稍差,因为若涉及多光源、视线向量等,需要转换多个向量到切线空间,代码复杂度增加。现代Shader多用前一种方法。
四、特殊情况与注意事项
如果你在编写一个完整的Unity PBR Shader,通常会使用Unity的内置宏(如 TANGENT_SPACE_ROTATION)来简化这些步骤,但理解底层原理对调试和优化至关重要。
总结
-
Unity内置函数简化操作
Unity提供了
UnpackNormal()函数,会根据平台设置自动解压法线(如某些平台DXT5nm格式的贴图)。推荐使用:float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, uv)); -
副切线的自动计算
如果顶点没有提供副切线,可以在Shader中用
cross(normal, tangent.xyz) * tangent.w实时计算。Unity的模型导入时会自动生成切线数据。 -
法线贴图强度调节
可以通过对采样得到的
tangentNormal的 xy 分量乘以一个强度因子(0,1],再重新归一化,来实现法线强度控制。 -
镜像UV的处理
当模型的UV被镜像时,切线方向需要反转。Unity的
tangent.w就是为此设计的:如果UV镜像,模型导出软件会设置tangent.w = -1,确保副切线的朝向正确。 -
关系核心 :法线贴图存储了切线空间下的法线方向,而切线空间由顶点的切线、副切线和法线定义。
-
Shader中的任务:构建TBN矩阵(或它的逆),将法线贴图采样结果转换到世界空间(或将世界空间的光照方向转换到切线空间),从而参与BRDF光照计算。
-
为什么要这么麻烦:因为只有使用切线空间,法线贴图才能被复用到不同姿势、不同模型,并正确响应动画和变形。