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

法线贴图呈现蓝紫色调(尤其以蓝色为主)是由其‌存储原理、切线空间坐标系设计及颜色编码规则共同决定的‌。

【从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解码‌:在着色器中逆向计算还原法线向量:此步骤是光照计算的基础‌。

    c 复制代码
    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

    c 复制代码
    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开始探索游戏渲染】专栏-直达

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

相关推荐
小贺儿开发1 天前
Unity3D 智慧城市管理平台
数据库·人工智能·unity·智慧城市·数据可视化
June bug2 天前
【领域知识】休闲游戏一次发版全流程:Google Play + Apple App Store
unity
星夜泊客2 天前
C# 基础:为什么类可以在静态方法中创建自己的实例?
开发语言·经验分享·笔记·unity·c#·游戏引擎
dzj20212 天前
PointerEnter、PointerExit、PointerDown、PointerUp——鼠标点击物体,则开始旋转,鼠标离开或者松开物体,则停止旋转
unity·pointerdown·pointerup
心前阳光2 天前
Unity 模拟父子关系
android·unity·游戏引擎
在路上看风景2 天前
26. Mipmap
unity
咸鱼永不翻身2 天前
Unity视频资源压缩详解
unity·游戏引擎·音视频
在路上看风景2 天前
4.2 OverDraw
unity
在路上看风景2 天前
1.10 CDN缓存
unity
ellis19703 天前
Unity插件SafeArea Helper适配异形屏详解
unity