Unity Shader URP:将法线可视化,便于调试

法线可视化是一种非常实用的 Shader 调试手段:把看不见的方向向量转成颜色,就能快速判断模型法线、法线贴图、TBN 矩阵、空间转换和背面显示是否出了问题。

1. 为什么要看见法线?

在 Unity URP 中,很多光照问题看起来像"材质不对""灯光不对"或者"阴影不对",但根源常常是法线方向异常。比如模型导入后局部面发黑、法线贴图左右反了、切线空间没有正确传入,或者世界空间法线没有归一化。

法线本质上是一个方向向量,常见范围是 -1 到 1。屏幕颜色范围却是 0 到 1。因此最经典的可视化方式是:

复制代码
float3 color = normal * 0.5 + 0.5;

这样 -1 会映射成 00 会映射成 0.51 会映射成 1。方向就变成了 RGB。

把法线的 XYZ 分量映射到 RGB,即可在屏幕上观察方向分布。

2. 最小 URP Shader:显示模型顶点法线

下面这个 Shader 使用 URP 的基础 HLSL include,把对象空间法线转到世界空间,然后输出为颜色。它适合检查模型导入后的法线方向是否正确。

cs 复制代码
Shader "Debug/URP_NormalVisualizer_World"
{
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType" = "Opaque"
        }

        Pass
        {
            Name "Normal Debug"

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float3 normalWS    : TEXCOORD0;
            };

            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                float3 n = normalize(input.normalWS);
                float3 color = n * 0.5 + 0.5;
                return half4(color, 1.0);
            }
            ENDHLSL
        }
    }
}

**观察要点:**正 X 方向更红,正 Y 方向更绿,正 Z 方向更蓝。颜色突变的位置往往对应硬边、法线断裂、UV seam 或模型本身的法线设置。

3. 世界空间、对象空间、切线空间看什么?

调试法线时,先要知道自己想检查哪一种空间。不同空间看到的问题不同。

对象空间法线

跟模型本地坐标相关。适合检查模型导入、镜像缩放、法线平滑组和硬边。

世界空间法线

跟场景方向相关。适合检查 Transform、非等比缩放、光照方向和最终参与光照的法线。

切线空间法线

跟法线贴图相关。适合检查 Normal Map 是否导入为法线类型、Y 通道是否翻转、TBN 是否正确。

同一个"法线"在不同空间里含义不同。调试前先明确输出的是哪一个空间。

4. 可视化法线贴图

如果要调试 Normal Map,重点不是顶点法线,而是采样贴图后得到的切线空间法线。URP 中可以用 UnpackNormal 解包法线贴图,再输出颜色。

cs 复制代码
Shader "Debug/URP_NormalMapVisualizer"
{
    Properties
    {
        [Normal] _BumpMap("Normal Map", 2D) = "bump" {}
        _BumpScale("Normal Scale", Range(0, 2)) = 1
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType" = "Opaque"
        }

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"

            TEXTURE2D(_BumpMap);
            SAMPLER(sampler_BumpMap);

            CBUFFER_START(UnityPerMaterial)
                float4 _BumpMap_ST;
                float _BumpScale;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
            };

            Varyings vert(Attributes input)
            {
                Varyings output;
                output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _BumpMap);
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                half4 packedNormal = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, input.uv);
                half3 normalTS = UnpackNormalScale(packedNormal, _BumpScale);
                half3 color = normalTS * 0.5h + 0.5h;
                return half4(color, 1);
            }
            ENDHLSL
        }
    }
}

普通的平坦法线贴图通常偏蓝紫色;红绿变化代表表面在切线空间内向左右或上下倾斜。

5. 如果要看最终世界空间法线贴图

很多时候我们不只想看 Normal Map 本身,还想确认它通过 TBN 转换到世界空间后是否正确。这就需要传入切线、法线和副切线符号,构造切线空间到世界空间的矩阵。

cs 复制代码
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;
};

Varyings vert(Attributes input)
{
    Varyings output;
    output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
    output.uv = TRANSFORM_TEX(input.uv, _BumpMap);

    output.normalWS = TransformObjectToWorldNormal(input.normalOS);
    output.tangentWS = TransformObjectToWorldDir(input.tangentOS.xyz);

    float sign = input.tangentOS.w * GetOddNegativeScale();
    output.bitangentWS = cross(output.normalWS, output.tangentWS) * sign;
    return output;
}

half4 frag(Varyings input) : SV_Target
{
    half3 normalTS = UnpackNormalScale(
        SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, input.uv),
        _BumpScale
    );

    float3x3 tangentToWorld = float3x3(
        normalize(input.tangentWS),
        normalize(input.bitangentWS),
        normalize(input.normalWS)
    );

    float3 normalWS = normalize(mul(normalTS, tangentToWorld));
    return half4(normalWS * 0.5 + 0.5, 1);
}

这个版本可以暴露更隐蔽的问题:比如模型缺少 tangents、镜像 UV 导致副切线方向错误、非等比缩放后没有重新归一化,或者 Normal Map 的绿色通道方向不符合项目约定。

6. Shader Graph 做法

如果项目主要使用 Shader Graph,思路也一样:取 Normal Vector 节点,选择需要的空间,再做 * 0.5 + 0.5,最后接到 Base Color 或 Emission。

Shader Graph 中的核心链路就是 Normal Vector → Multiply 0.5 → Add 0.5 → 输出颜色。

7. 常见现象速查

看到的现象 可能原因 排查方向
某些面突然变成完全不同的颜色 硬边、法线断裂、平滑组设置不同 检查模型导入设置、法线模式、DCC 软件中的法线/平滑组
法线贴图看起来左右反了 切线方向、UV 镜像或贴图通道约定不一致 检查 tangent.w、副切线符号、Normal Map 导入类型
绿色方向上下颠倒 DirectX/OpenGL 法线贴图 Y 通道约定不同 尝试翻转 G 通道,或在导入/生成流程中统一规范
旋转模型后颜色变化不符合预期 输出空间理解错了 对象空间颜色随模型本地坐标,世界空间颜色随场景坐标
颜色发灰、不稳定或有奇怪条纹 法线没有归一化、插值后长度变化、缺少 tangent 在片元阶段使用 normalize,检查 mesh tangent 数据

8. 实战建议

  • 先看顶点法线,再看法线贴图,最后看经过 TBN 转换后的世界空间法线。
  • 调试 Shader 最好使用 Unlit 或直接输出颜色,避免光照、阴影和后处理干扰判断。
  • 输出前总是 normalize,尤其是从顶点传到片元后的法线。
  • 项目内统一 Normal Map 的 Y 通道约定,避免不同工具导出的贴图混用。
  • 遇到镜像 UV、负缩放、双面材质时,优先怀疑 tangent sign 和正反面法线处理。

**一句话总结:**法线可视化就是把方向翻译成颜色。只要明确"现在看的是什么空间",URP 中的许多光照和法线贴图问题都会变得非常直观。

相关推荐
蓝黑墨水2 小时前
unity相关链接
unity·游戏引擎
mxwin2 小时前
Unity Shader 法线贴图的七种错误用法
unity·游戏引擎·贴图·shader
mxwin5 小时前
Unity URP 切线空间详解
unity·游戏引擎·shader
caimouse10 小时前
Godot Engine 最新版官方文档(简体中文完整翻译 & 精简梳理)
游戏引擎·godot
huizhixue-IT13 小时前
Superpowers 游戏引擎从零开发实战指南
游戏引擎
做cv的小昊1 天前
计算机图形学:【Games101】学习笔记08——光线追踪(辐射度量学、渲染方程与全局光照、蒙特卡洛积分与路径追踪)
图像处理·笔记·学习·计算机视觉·游戏引擎·图形渲染·概率论
玖玥拾1 天前
Cocos学习笔记:序列化、配置文件与数据驱动
游戏引擎·cocos2d
RReality1 天前
【Unity UGUI】血条 / 进度条(HP Bar)
ui·unity·游戏引擎·图形渲染
mxwin1 天前
Unity Shader URP:法线如何进行光照计算
unity·游戏引擎·shader