发布

【URP】法线贴图为什么主要是蓝色的?

WAP站长网 2025-9-7 16:00
0 2

法线贴图呈现蓝紫色调(尤其以蓝色为主)是由其‌

存储原理、切线空间坐标系设计及颜色编码规则共同决定的

‌。

【从UnityURP开始探索游戏渲染】

专栏-直达

核心原因:法线向量的存储规则‌

‌法线向量的物理范围‌

法线是单位向量,每个分量(X, Y, Z)的取值范围为 ‌[-1, 1],分别代表切线空间中的方向:

  • ‌X(红色通道):左右偏移(左为负,右为正)
  • ‌Y(绿色通道):上下偏移(下为负,上为正)
  • ‌Z(蓝色通道):垂直表面的方向(指向外部为正)‌。

‌颜色空间的映射限制‌

图像颜色值范围是 ‌[0, 1](对应0~255),因此需要进行转换:

RGB=(Normalxyz+1)/2

  • 默认法线方向

    ‌:当表面完全垂直(无倾斜)时,法线向量为 ‌(0, 0, 1)。
  • 转换结果

    ‌:
    • R=20+1=0.5 (128)
    • G=20+1=0.5 (128)
    • B=21+1=1 (255)
    • 最终颜色为 ‌(128, 128, 255),即 ‌

      蓝紫色

      ‌(蓝色占主导)‌。

‌现实模型的主导方向

  • 大多数模型表面(如墙面、地面)以‌

    垂直方向为主

    ‌(Z≈1),因此蓝色通道值接近255,而XY通道接近128(中性灰),整体呈现蓝色基调‌。

‌颜色变化的场景解释‌

颜色表现

对应的法线方向

表面形态

‌深蓝色 (0,0,1) 完全垂直向外 平坦表面(如地板)
‌蓝紫色 (0.5,0.5,1) 轻微倾斜 缓坡、弧形表面
‌青色/绿色 (低R,高G,中B) 明显上/下倾斜(Y≠0) 边缘、陡坡
‌红色/粉色 (高R,中G,中B)‌ 明显左/右倾斜(X≠0) 侧壁、凹凸边缘

💎 ‌示例‌:墙面法线贴图中,砖缝凹陷处因法线指向侧方(X/Y增大),可能呈现红绿色调,但整体仍以蓝紫色为基底‌。


️ ‌技术实现验证‌

‌生成与解码逻辑‌

  • 生成法线贴图

    ‌:通过公式 color = (normal + 1) / 2 将高模法线烘焙为贴图‌。

  • Shader解码

    ‌:在着色器中逆向计算还原法线向量:此步骤是光照计算的基础‌。

    glsl vec3 normal = texture(normalMap, uv).rgb * 2.0 - 1.0; // [0,1] → [-1,1] 

‌切线空间的意义‌

法线贴图通常在‌ 切线空间(Tangent Space)中定义:

  • 以顶点法线为Z轴,切线为X轴,副切线为Y轴构建坐标系。
  • 优势

    ‌:无论模型如何旋转,法线方向始终相对于表面本地坐标,确保凹凸效果稳定‌。

常见误区澄清‌

  • 误区1

    ‌:蓝色是人为设定的美术风格。‌

    真相

    ‌:蓝色是数学映射的必然结果,由垂直方向(0,0,1)的编码规则决定‌。
  • 误区2

    ‌:法线贴图的颜色代表凹凸高度。‌

    真相

    ‌:它存储的是‌

    方向

    ‌而非高度,凹凸感通过光照模拟实现‌。

实际应用案例‌

  • Unity 工作流

    ‌:将法线贴图拖入材质球的 ‌

    Normal Map

    ‌ 插槽,通过 UnpackNormal() 函数解码(内置管线见 UnityCG.cginc,URP管线UnpackNormalScale()见Packing.hlsl)‌。
  • 效果增强

    ‌:调整 ‌

    Normal Scale

    ‌ 参数控制凹凸强度(值>1增强凸起,<1弱化)‌。

‌URP中的法线贴图

法线贴图设置流程

  • 导入法线贴图

    • 纹理类型设为"Default/Normal map"
    • 压缩格式推荐BC5(DXT5nm)或BC7
    • 勾选"sRGB"选项确保正确色彩空间转换
  • 创建URP材质

    • 使用Shader路径:Universal Render Pipeline/Lit
    • 将法线贴图拖拽到Normal Map插槽
    • 调整Normal Scale参数(建议0.5-1.5)

完整Shader代码实现

  • NormalMapURP.shader

    Shader "Custom/URPNormalMap" { Properties { _BaseMap("Albedo", 2D) = "white" {} _BaseColor("Color", Color) = (1,1,1,1) _NormalMap("Normal Map", 2D) = "bump" {} _NormalScale("Normal Scale", Range(0,2)) = 1 _Metallic("Metallic", Range(0,1)) = 0 _Smoothness("Smoothness", Range(0,1)) = 0.5 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Metallic; half _Smoothness; half _NormalScale; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float4 tangentWS : TEXCOORD2; float3 positionWS : TEXCOORD3; }; ENDHLSL Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag Varyings vert(Attributes input) { Varyings output; VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); output.positionCS = vertexInput.positionCS; output.positionWS = vertexInput.positionWS; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.normalWS = normalInput.normalWS; output.tangentWS = float4(normalInput.tangentWS, input.tangentOS.w); return output; } half4 frag(Varyings input) : SV_Target { // 采样基础贴图 half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; // 采样和解压法线贴图 half4 normalSample = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv); half3 normalTS = UnpackNormalScale(normalSample, _NormalScale); // 构建TBN矩阵 half3 bitangentWS = cross(input.normalWS, input.tangentWS.xyz) * input.tangentWS.w; half3x3 TBN = half3x3(input.tangentWS.xyz, bitangentWS, input.normalWS); half3 normalWS = TransformTangentToWorld(normalTS, TBN); // 光照计算 Light mainLight = GetMainLight(); half3 lightDir = normalize(mainLight.direction); half NdotL = saturate(dot(normalWS, lightDir)); half3 diffuse = baseColor.rgb * NdotL * mainLight.color; // 高光计算 half3 viewDir = normalize(_WorldSpaceCameraPos - input.positionWS); half3 halfVec = normalize(lightDir + viewDir); half NdotH = saturate(dot(normalWS, halfVec)); half specular = pow(NdotH, _Smoothness * 256) * _Metallic; half3 finalColor = diffuse + specular * mainLight.color; return half4(finalColor, baseColor.a); } ENDHLSL } } } 

关键实现说明

  • 法线解压

    ‌:使用UnpackNormalScale函数处理法线贴图数据,支持强度调节
  • TBN矩阵

    ‌:通过切线、副切线和法线构建转换矩阵,将切线空间法线转到世界空间
  • 光照模型

    ‌:采用Blinn-Phong模型计算漫反射和高光
  • URP适配

    ‌:使用URP特有的GetVertexPositionInputs等函数替代传统Shader写法

常见问题解决方案

  • 法线效果异常

    ‌:检查切线空间计算是否正确,确保模型导入时勾选"Calculate Tangents"
  • 性能优化

    ‌:移动端可考虑在切线空间计算光照减少矩阵运算
  • 多光源支持

    ‌:需添加AdditionalLights Pass处理额外光源

总结

法线贴图的蓝色基调本质是‌

垂直方向向量(0,0,1)经归一化映射后的颜色表达

‌,这种方法平衡了存储效率与光照计算需求,是3D渲染中模拟表面细节的核心技术‌,直观的颜色样式只是数据可视化的一种直观显示。


【从UnityURP开始探索游戏渲染】

专栏-直达


(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)