GameTech/URP 渲染实战

案例 2.5同一个 Shader 的三个 Pass,谁会被画?进阶

目标:写一个有三个 Pass 的 Shader:UniversalForward 做正常着色、一个不写 LightMode 的 Pass 做外描边、一个 LightMode 为自定义名字 CookbookXRay 的 Pass 做“透过墙看到”的 X 光效果,然后验证 URP 会画哪些。

Assets/Cookbook/Ch02/LightModeDemo.shader(节选:passes)
Pass
{
    Name "Forward"
    Tags { "LightMode"="UniversalForward" }

    HLSLPROGRAM
    #pragma vertex VertDefault
    #pragma fragment Frag
    half4 Frag(Varyings input) : SV_Target
    {
        float3 n = normalize(input.normalWS);
        Light light = GetMainLight();
        half3 diffuse = light.color * saturate(dot(n, light.direction));
        half3 ambient = SampleSH(n);
        return half4(_BaseColor.rgb * (diffuse + ambient), 1);
    }
    ENDHLSL
}

Pass
{
    Name "Outline"
    // 没有 LightMode 标签 → URP 视为 "SRPDefaultUnlit"
    Cull Front

    HLSLPROGRAM
    #pragma vertex Vert
    #pragma fragment Frag
    Varyings Vert(Attributes input)
    {
        Varyings output;
        // 沿法线把顶点往外推,只画背面,就得到一圈“外壳”描边(第 14 章细讲)
        float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
        float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
        output.positionCS = TransformWorldToHClip(positionWS + normalWS * _OutlineWidth);
        output.normalWS = normalWS;
        return output;
    }
    half4 Frag(Varyings input) : SV_Target { return _OutlineColor; }
    ENDHLSL
}

Pass
{
    Name "XRay"
    Tags { "LightMode"="CookbookXRay" }
    // 只在“被挡住”的地方画:深度比已有深度更远才通过
    ZTest Greater
    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha

    HLSLPROGRAM
    #pragma vertex VertDefault
    #pragma fragment Frag
    half4 Frag(Varyings input) : SV_Target { return _XRayColor; }
    ENDHLSL
}
Pass
{
    Name "Forward"
    Tags { "LightMode"="UniversalForward" }

    GLSLPROGRAM
    #ifdef VERTEX
    void main() { VertDefault(); }
    #endif
    #ifdef FRAGMENT
    void main()
    {
        vec3 n = normalize(normalWS);
        Light light = GetMainLight();
        vec3 diffuse = light.color * saturate(dot(n, light.direction));
        vec3 ambient = SampleSH(n);
        fragColor = vec4(_BaseColor.rgb * (diffuse + ambient), 1);
    }
    #endif
    ENDGLSL
}

Pass
{
    Name "Outline"
    // 没有 LightMode 标签 → URP 视为 "SRPDefaultUnlit"
    Cull Front

    GLSLPROGRAM
    #ifdef VERTEX
    void main()
    {
        // 沿法线把顶点往外推,只画背面,就得到一圈“外壳”描边(第 14 章细讲)
        vec3 positionWS = TransformObjectToWorld(positionOS.xyz);
        vec3 nWS = TransformObjectToWorldNormal(normalOS);
        gl_Position = TransformWorldToHClip(positionWS + nWS * _OutlineWidth);
        normalWS = nWS;
    }
    #endif
    #ifdef FRAGMENT
    void main() { fragColor = _OutlineColor; }
    #endif
    ENDGLSL
}

Pass
{
    Name "XRay"
    Tags { "LightMode"="CookbookXRay" }
    // 只在“被挡住”的地方画:深度比已有深度更远才通过
    ZTest Greater
    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha

    GLSLPROGRAM
    #ifdef VERTEX
    void main() { VertDefault(); }
    #endif
    #ifdef FRAGMENT
    void main() { fragColor = _XRayColor; }
    #endif
    ENDGLSL
}

拍摄脚本分三格:第一格用 Material.SetShaderPassEnabled("SRPDefaultUnlit", false) 关掉描边 Pass(按 LightMode 关闭);第二格打开它;第三格再给 Renderer 加一个 URP 自带的 RenderObjects Feature,让它在 AfterRenderingOpaques 时点名绘制 LightMode 为 CookbookXRay 的 Pass。对应的编辑器操作是:在 Universal Renderer Data 上 Add Renderer Feature › Render Objects,Event 选 AfterRenderingOpaques,Filters 里 Queue 选 Opaque、Layer Mask 选 Everything,LightMode Tags 填 CookbookXRay。

图 2-12左:只有 UniversalForward;中:加上无 LightMode(SRPDefaultUnlit)的描边 Pass,URP 在同一次绘制中画了它;右:再加 RenderObjects Feature 点名 CookbookXRay,被墙挡住的部分以蓝色显示

右图的 X 光 Pass 用了 ZTest Greater:只有当它比深度缓冲里已有的深度“更远”(被挡住)时才通过,于是只有墙后的部分被染成蓝色。这个效果第 16 章还会用模板测试做得更精细。这里的重点是理解:LightMode 是 Shader 与管线之间的“接头暗号”。URP 认识的暗号,它会自动在对应的时机画;不认识的,就需要你用 Feature 去点名。