【URP】法线贴图为什么主要是蓝色的?
【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 { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } 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 { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } LOD 100 Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } 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 着色器代码的实战演示,我们看到了法线贴图的解码、通道可视化以及光照计算过程。理解这一原理有助于开发者更好地调试法线贴图效果,并在自定义着色器中正确处理法线数据。记住,法线贴图不是颜色贴图,它是存储方向数据的数学工具,蓝色只是这种数据编码的视觉表现。
