【URP】法线贴图为什么主要是蓝色的在游戏开发中法线贴图Normal Map是一种常见的纹理技术用于模拟表面细节的光照效果。细心的开发者可能会注意到大多数法线贴图呈现出一种蓝紫色调尤其是直接观察纹理时蓝色通道占据主导。本文将深入解析这一现象并通过实战代码演示揭示法线贴图在Unity URP通用渲染管线中的工作原理。## 法线贴图的基本原理法线贴图存储的是表面法线向量而不是颜色信息。每个像素对应一个三维向量(X, Y, Z)表示该点相对于表面的朝向。在纹理中这些向量被编码为 RGB 颜色值其中- R 通道对应 X 轴切线方向- G 通道对应 Y 轴副法线方向- B 通道对应 Z 轴法线方向在标准坐标系中法线向量的 Z 分量通常指向表面外侧即垂直于表面的方向。由于大多数表面在局部坐标系中是平坦的法线向量的 Z 分量接近 1而 X 和 Y 分量接近 0。当这些向量被映射到 [0, 255] 的颜色范围时Z 分量对应蓝色通道因此法线贴图整体呈现蓝色。## 法线贴图编码与颜色映射法线贴图的编码方式决定了其颜色外观。在 Unity 中法线贴图通常使用Tangent Space切线空间存储其中- 向量分量范围从 [-1, 1] 映射到 [0, 1] 的颜色值- 映射公式color (vector 1) / 2对于平坦表面法线向量为(0, 0, 1)经过映射后- R (0 1) / 2 0.5 → 128灰色- G (0 1) / 2 0.5 → 128灰色- B (1 1) / 2 1.0 → 255蓝色因此法线贴图的主色调是蓝色而 R 和 G 通道呈现中性灰。当表面有凹凸细节时X 和 Y 分量会偏离 0导致颜色变化但蓝色始终占据主导。## 实战在 URP 中创建自定义法线贴图着色器为了深入理解法线贴图的颜色编码我们将编写一个 URP 自定义着色器直接显示法线贴图的各个通道。### 示例 1显示法线贴图通道的着色器hlsl// 文件名NormalMapDebug.shaderShader Custom/NormalMapDebug{ Properties { _NormalMap (法线贴图, 2D) bump {} } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } LOD 100 Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); Varyings vert (Attributes input) { Varyings output; output.positionHCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } half4 frag (Varyings input) : SV_Target { // 采样法线贴图并解压到 [-1, 1] 范围 half4 normalColor SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv); half3 normal normalColor.xyz * 2.0 - 1.0; // 解压 // 为了调试直接输出各个通道的颜色 // R 通道显示法线的 X 分量红色 // G 通道显示法线的 Y 分量绿色 // B 通道显示法线的 Z 分量蓝色 half4 debugColor half4(normal.x, normal.y, normal.z, 1.0); return debugColor; } ENDHLSL } }}代码解释- 该着色器采样法线贴图并解压到 [-1, 1] 范围- 直接输出法线向量的 X、Y、Z 分量作为 RGB 颜色- 如果法线贴图是标准的蓝色调你会看到红色和绿色通道接近中性灰而蓝色通道明亮- 这验证了法线向量 Z 分量占主导地位## 法线贴图与光照计算在 URP 的光照计算中法线贴图用于扰动表面法线从而改变光照效果。下面是一个完整的 URP 自定义光照着色器示例。### 示例 2带法线贴图的 URP 光照着色器hlsl// 文件名NormalMapLighting.shaderShader Custom/NormalMapLighting{ Properties { _BaseMap (基础贴图, 2D) white {} _NormalMap (法线贴图, 2D) bump {} _Color (颜色, Color) (1,1,1,1) _NormalStrength (法线强度, Range(0, 2)) 1.0 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } LOD 100 Pass { Name ForwardLit Tags { LightModeUniversalForward } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 tangentWS : TEXCOORD2; float3 bitangentWS : TEXCOORD3; float3 positionWS : TEXCOORD4; }; TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); half4 _Color; half _NormalStrength; Varyings vert (Attributes input) { Varyings output; // 顶点位置转换 VertexPositionInputs positionInputs GetVertexPositionInputs(input.positionOS.xyz); output.positionHCS positionInputs.positionCS; output.positionWS positionInputs.positionWS; // 法线、切线、副法线转换到世界空间 VertexNormalInputs normalInputs GetVertexNormalInputs(input.normalOS, input.tangentOS); output.normalWS normalInputs.normalWS; output.tangentWS normalInputs.tangentWS; output.bitangentWS normalInputs.bitangentWS; output.uv input.uv; return output; } half4 frag (Varyings input) : SV_Target { // 采样法线贴图并解压 half4 normalColor SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv); half3 normalTS normalColor.xyz * 2.0 - 1.0; // 切线空间法线 normalTS.xy * _NormalStrength; // 法线强度控制 normalTS.z sqrt(1.0 - saturate(dot(normalTS.xy, normalTS.xy))); // 重新归一化 // 构建 TBN 矩阵将切线空间法线转换到世界空间 float3x3 tangentToWorld float3x3(input.tangentWS, input.bitangentWS, input.normalWS); float3 normalWS normalize(mul(normalTS, tangentToWorld)); // 采样基础贴图 half4 baseColor SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _Color; // 获取主光源数据 Light mainLight GetMainLight(); float3 lightDir mainLight.direction; float3 viewDir GetWorldSpaceNormalizeViewDir(input.positionWS); half NdotL saturate(dot(normalWS, lightDir)); // 简单漫反射光照 half3 diffuse mainLight.color * NdotL * baseColor.rgb; // 环境光 half3 ambient SampleSH(normalWS) * baseColor.rgb; return half4(diffuse ambient, baseColor.a); } ENDHLSL } }}代码解释- 该着色器实现了完整的法线贴图光照计算- 从法线贴图采样切线空间法线使用_NormalStrength控制凹凸强度- 通过 TBN 矩阵将法线转换到世界空间- 计算漫反射光照和环境光- 运行此着色器你会发现法线贴图的蓝色通道直接影响光照效果## 法线贴图为何是蓝色深度解析从技术角度看法线贴图的蓝色是由编码机制决定的。但为什么选择这种编码方式主要有以下原因1.归一化约束法线向量必须为单位向量即X² Y² Z² 1。当 Z 接近 1 时X 和 Y 必须接近 0这导致 R 和 G 通道接近 128而 B 通道接近 255。2.切线空间的优势在切线空间中大部分法线指向表面外侧即 Z 轴正方向。这保证了蓝色通道的高值。3.压缩效率由于 Z 通道存储了大部分信息许多纹理压缩格式如 DXT5nm会专门优化蓝色通道的精度。4.视觉直观性蓝色调让开发者能快速识别法线贴图区别于普通颜色贴图。## 总结法线贴图呈现蓝色是因为其编码方式将法线向量的 Z 分量垂直于表面映射到蓝色通道而平坦表面的 Z 分量接近 1。通过 URP 着色器代码的实战演示我们看到了法线贴图的解码、通道可视化以及光照计算过程。理解这一原理有助于开发者更好地调试法线贴图效果并在自定义着色器中正确处理法线数据。记住法线贴图不是颜色贴图它是存储方向数据的数学工具蓝色只是这种数据编码的视觉表现。