Unity URP 法线贴图色彩空间、编码与解码
从切线空间到纹理像素再到 Shader 中的法线重建 —— 逐步拆解法线贴图的完整数据流1. 法线贴图是什么法线贴图Normal Map是一张存储了表面法线方向的纹理。它不存储颜色而是将三维向量 (n⃗.x, n⃗.y, n⃗.z) 编码到 RGB 三个通道中让低面数模型在光照计算时呈现出高面数的凹凸细节。一张典型的法线贴图看起来是蓝紫色的 —— 因为大多数表面法线指向正上方Z 方向编码后对应 R0.5, G0.5, B1.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,1] → [0,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 DXT5nmUnity 支持两种法线贴图的存储方式这是理解编码/解码的关键分歧点图 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 源码中两个关键解码函数的简化版// 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); }// DXT5nm 格式解码 —— X 在 AlphaY 在 Green float3 UnpackNormalmapRGorAG(float4 packedNormal) { // 检测 R 通道是否被用来存 X某些平台上 RA 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 的伽马校正。图 6sRGB 解码对法线贴图的影响 —— 伽马校正确保暗部细节的设计初衷对法线数据是灾难性的核心规则属性漫反射贴图法线贴图数据性质颜色视觉感知数学向量sRGB 标记true需要伽马校正false原始数据采样后处理硬件自动 Linear 转换直接使用不做转换Unity 标记方式默认 sRGB true设为 Normal Map 类型Unity 如何处理当你在 Inspector 中将纹理类型设为Normal MapUnity 会1. 自动将 sRGB 标记设为 false2. 如果源纹理是 DXT5nm 格式在导入时重排通道R→A 或保持3. 在SAMPLE_TEXTURE2D采样时因为 sRGBfalseGPU 不会做伽马转换直接返回原始 [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解码后 Z1 而非正确值修复使用 UnpackNormal 或手动从 xy 推导 z4切线空间未正确传递到 Fragment结果法线贴图在模型不同部位方向不一致出现光照条纹修复确保 Vertex 中计算 TBN 并使用 correctTangentSpace 变体5BC5/BC7 平台差异不同平台压缩格式不同用 #pragma multi_compile_local 处理图 7五大常见陷阱与修复方案检查清单每次使用法线贴图时确认以下事项• 纹理类型 Normal Map自动设 sRGBfalse• 绿通道方向与项目约定一致DirectX / OpenGL• 使用UnpackNormal()而非手动解码• 切线向量在 Vertex Shader 中正确计算并传递• TBN 矩阵的三个向量都已归一化总结完整数据流