Unity URP 法线贴图色彩空间、编码与解码

从切线空间到纹理像素,再到 Shader 中的法线重建 ------ 逐步拆解法线贴图的完整数据流

1. 法线贴图是什么

法线贴图(Normal Map)是一张存储了表面法线方向的纹理。它不存储颜色,而是将三维向量 (n⃗.x, n⃗.y, n⃗.z) 编码到 RGB 三个通道中,让低面数模型在光照计算时呈现出高面数的凹凸细节。

一张典型的法线贴图看起来是蓝紫色的 ------ 因为大多数表面法线指向"正上方"(+Z 方向),编码后对应 R=0.5, G=0.5, B=1.0,正是那种淡蓝紫颜色。

图 1:法线贴图的作用 ------ 让低面数模型获得高面数的光照效果

2. 切线空间与法线方向

法线贴图中的法线定义在切线空间(Tangent Space)中。切线空间由三个基向量构成:

图 2:切线空间的三个基向量 ------ 沿 UV 方向展开

关键性质:

  • 法线是单位向量,满足 x² + y² + z² = 1
  • 三个分量范围都是 -1, +1
  • "平坦"表面的法线 = (0, 0, 1),指向正 Z 方向
  • 切线空间跟随模型表面弯曲,所以法线贴图可以在不同模型上复用

3. 编码:从法线到颜色

纹理的 RGB 通道只能存储 0, 1 的值,而法线分量在 -1, +1 之间。编码的核心就是做一次线性映射

图 3:编码映射 ------ -1,+10,1 的线性变换

编码公式(法线 → 纹理颜色):

// 每个通道独立编码 color.r = normal.x * 0.5 + 0.5; color.g = normal.y * 0.5 + 0.5; color.b = normal.z * 0.5 + 0.5;

常见法线值的编码结果

法线方向 n (x, y, z) c (R, G, B) 视觉颜色
平坦表面(+Z) (0, 0, 1) (0.5, 0.5, 1.0) #8080FF
朝右倾斜 (1, 0, 0) (1.0, 0.5, 0.5) #FF8080
朝上倾斜 (0, 1, 0) (0.5, 1.0, 0.5) #80FF80
朝左倾斜 (-1, 0, 0) (0.0, 0.5, 0.5) #008080

直觉 :法线贴图中越接近蓝紫色 的区域,表面越平坦;越偏红/绿的区域,表面朝 X/Y 方向的倾斜越大。

4. 两种编码格式:RGB vs DXT5nm

Unity 支持两种法线贴图的存储方式,这是理解编码/解码的关键分歧点:

图 4:两种编码格式的通道布局与解码路径

为什么 DXT5nm 把 X 放到 Alpha 通道?

DXT5/BC3 压缩格式中,Alpha 通道有独立的 4-bit 插值,精度比 R/G/B 的共享 5-bit 更高。将 X 分量放到 Alpha 中可以在压缩后保留更多法线细节。而 Z 通道被丢弃是因为它可以由 X、Y 推导出来(单位向量约束),不存储反而节省了压缩空间。

DirectX vs OpenGL 绿通道方向

另一个常见困惑是绿通道(Y 分量)的方向:

在 Unity 的导入设置中可以通过 "Flip Green Channel" 选项来切换。两种格式互为绿通道取反关系。

5. 解码:UnpackNormal 的完整流程

Unity 内部通过两个关键函数完成法线贴图的解码。整个流程如下:

下面是 Unity 源码中两个关键解码函数的简化版:

cs 复制代码
// RGB 格式解码 ------ 三个通道完整存储 float3 UnpackNormalRGB(float4 packedNormal) { float3 normal; normal.xy = packedNormal.rg * 2.0 - 1.0; // [0,1] → [-1,+1]
normal.z = packedNormal.b * 2.0 - 1.0; // 直接读 Z
return normalize(normal); }
cs 复制代码
// DXT5nm 格式解码 ------ X 在 Alpha,Y 在 Green float3 UnpackNormalmapRGorAG(float4 packedNormal) { // 检测 R 通道是否被用来存 X(某些平台上 R=A) float2 rg_or_ag = packedNormal.rg; if (packedNormal.a >= 0.0) // Unity 通过编译宏决定路径 rg_or_ag = packedNormal.ag; // DXT5nm: 读 A 和 G float3 normal;
normal.xy = rg_or_ag * 2.0 - 1.0; // 解码 X 和 Y
normal.z = sqrt(1.0 - saturate( // 由单位向量约束推导 Z
dot(normal.xy, normal.xy)));
return normal; }

注意 :Unity 的 UnpackNormal() 函数会根据纹理的导入设置自动选择正确的解码路径。你通常不需要手动判断格式。只要法线贴图在 Inspector 中被设置为 "Normal Map" 类型,Unity 就会正确处理编码和色彩空间。

6. 色彩空间:sRGB vs Linear

这是法线贴图最容易出错的地方。法线贴图的数据是数学向量,不是颜色,因此它不应该经过 sRGB → Linear 的伽马校正。

图 6:sRGB 解码对法线贴图的影响 ------ 伽马校正确保暗部细节的设计初衷对法线数据是灾难性的

核心规则:

属性 漫反射贴图 法线贴图
数据性质 颜色(视觉感知) 数学向量
sRGB 标记 true(需要伽马校正) false(原始数据)
采样后处理 硬件自动 Linear 转换 直接使用,不做转换
Unity 标记方式 默认 sRGB = true 设为 "Normal Map" 类型

Unity 如何处理?

当你在 Inspector 中将纹理类型设为 "Normal Map" ,Unity 会:

  1. 自动将 sRGB 标记设为 false

  2. 如果源纹理是 DXT5nm 格式,在导入时重排通道(R→A 或保持)

  3. SAMPLE_TEXTURE2D 采样时,因为 sRGB=false,GPU 不会做伽马转换,直接返回原始 0,1 数据

7. 常见陷阱与检查清单

1纹理未标记为 Normal Map结果:sRGB 伽马校正被应用,法线方向偏移,表面光照异常修复:Inspector → Texture Type → Normal Map2绿通道方向错误结果:凹凸方向反转,凸起变凹陷(或反之)修复:Inspector → Flip Green Channel / 在 Shader 中 Y 取反3手动乘以 2 减 1 但忘了 Z 重建结果:DXT5nm 格式下 Z 分量来自 Blue 通道(值为 1),解码后 Z=1 而非正确值修复:使用 UnpackNormal 或手动从 xy 推导 z4切线空间未正确传递到 Fragment结果:法线贴图在模型不同部位方向不一致,出现光照条纹修复:确保 Vertex 中计算 TBN 并使用 correctTangentSpace 变体5BC5/BC7 平台差异不同平台压缩格式不同,用 #pragma multi_compile_local 处理

图 7:五大常见陷阱与修复方案

检查清单 :每次使用法线贴图时,确认以下事项:

• 纹理类型 = Normal Map(自动设 sRGB=false)

• 绿通道方向与项目约定一致(DirectX / OpenGL)

• 使用 UnpackNormal() 而非手动解码

• 切线向量在 Vertex Shader 中正确计算并传递

• TBN 矩阵的三个向量都已归一化

总结:完整数据流

相关推荐
玖玥拾4 小时前
Cocos学习笔记:项目框架搭建与异步加载进度
游戏引擎·cocos2d
mxwin4 小时前
Unity Shader URP:将法线可视化,便于调试
unity·游戏引擎·shader
蓝黑墨水5 小时前
unity相关链接
unity·游戏引擎
mxwin5 小时前
Unity Shader 法线贴图的七种错误用法
unity·游戏引擎·贴图·shader
mxwin8 小时前
Unity URP 切线空间详解
unity·游戏引擎·shader
caimouse14 小时前
Godot Engine 最新版官方文档(简体中文完整翻译 & 精简梳理)
游戏引擎·godot
huizhixue-IT16 小时前
Superpowers 游戏引擎从零开发实战指南
游戏引擎
做cv的小昊1 天前
计算机图形学:【Games101】学习笔记08——光线追踪(辐射度量学、渲染方程与全局光照、蒙特卡洛积分与路径追踪)
图像处理·笔记·学习·计算机视觉·游戏引擎·图形渲染·概率论
玖玥拾1 天前
Cocos学习笔记:序列化、配置文件与数据驱动
游戏引擎·cocos2d