Unity Shader 法线贴图跟切线空间有什么关系

法线贴图与切线空间的关系,是 "存储空间""参考系" 的关系:法线贴图中的颜色信息,本质上描述的是在切线空间下的扰动法线方向。

为了让你在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)来简化这些步骤,但理解底层原理对调试和优化至关重要。


总结

  1. Unity内置函数简化操作

    Unity提供了 UnpackNormal() 函数,会根据平台设置自动解压法线(如某些平台DXT5nm格式的贴图)。推荐使用:

    复制代码
    float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, uv));
  2. 副切线的自动计算

    如果顶点没有提供副切线,可以在Shader中用 cross(normal, tangent.xyz) * tangent.w 实时计算。Unity的模型导入时会自动生成切线数据。

  3. 法线贴图强度调节

    可以通过对采样得到的 tangentNormal 的 xy 分量乘以一个强度因子 (0,1],再重新归一化,来实现法线强度控制。

  4. 镜像UV的处理

    当模型的UV被镜像时,切线方向需要反转。Unity的 tangent.w 就是为此设计的:如果UV镜像,模型导出软件会设置 tangent.w = -1,确保副切线的朝向正确。

  5. 关系核心 :法线贴图存储了切线空间下的法线方向,而切线空间由顶点的切线、副切线和法线定义。

  6. Shader中的任务:构建TBN矩阵(或它的逆),将法线贴图采样结果转换到世界空间(或将世界空间的光照方向转换到切线空间),从而参与BRDF光照计算。

  7. 为什么要这么麻烦:因为只有使用切线空间,法线贴图才能被复用到不同姿势、不同模型,并正确响应动画和变形。

相关推荐
mxwin3 小时前
Unity Shader 贴图和采样的关系 如何保证贴图清晰
unity·游戏引擎·贴图·shader
cici158743 小时前
双目视觉定位 + 贴图(纹理映射)方案
贴图
心前阳光5 小时前
Unity之使用火山引擎实现文字提问流式回复
unity·游戏引擎·火山引擎
mxwin7 小时前
Unity Shader 什么是球谐光照 原理是什么
unity·游戏引擎·shader
心前阳光7 小时前
Unity之使用火山引擎实现语音识别
unity·语音识别·火山引擎
心前阳光7 小时前
Unity之使用火山引擎实现流式语音合成
unity·游戏引擎·火山引擎
心前阳光9 小时前
Unity之使用火山引擎实现音频剪辑提问,流式语音回复
unity·音视频·火山引擎
心前阳光9 小时前
Unity之音频剪辑提问,流式语音回复使用示例
unity·游戏引擎·音视频
小拉达不是臭老鼠21 小时前
Unity学习_ScriptableObject
学习·unity