GameTech/URP 渲染实战

案例 3.4透明度裁剪 Alpha Clip入门

目标:让贴图 alpha 低于阈值的像素“消失”,做出镂空的树叶、栅栏、铁丝网。

Assets/Cookbook/Ch03/C03_04_AlphaClip.shader
// 第 4 步:透明度裁剪(Alpha Clip / Alpha Test)。alpha 低于阈值的像素直接丢弃。
Shader "Cookbook/Ch03/AlphaClip"
{
    Properties
    {
        [MainTexture] _BaseMap ("Base Map", 2D) = "white" {}
        [MainColor]   _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
        // 剪掉的地方能看到背面,所以通常关掉背面剔除:0=Off 1=Front 2=Back
        [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 0
    }

    SubShader
    {
        // AlphaTest 队列(2450)排在普通不透明物体之后,RenderType 让替换着色/工具识别它
        Tags { "RenderType"="TransparentCutout" "RenderPipeline"="UniversalPipeline" "Queue"="AlphaTest" }

        Pass
        {
            Name "Unlit"
            Tags { "LightMode"="UniversalForward" }
            Cull [_Cull]                         // 渲染状态可以引用属性,用方括号

            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;
                half4  color      : COLOR;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv         : TEXCOORD0;
                half4  color      : COLOR;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            float4 _BaseMap_ST;
            half4  _BaseColor;
            half   _Cutoff;

            Varyings vert (Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                output.color = input.color;
                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor * input.color;
                // clip(x):x < 0 时丢弃当前片元,不写颜色也不写深度
                clip(col.a - _Cutoff);
                return half4(col.rgb, 1);        // 不透明物体输出 alpha=1
            }
            ENDHLSL
        }
    }
}
// 第 4 步:透明度裁剪(Alpha Clip / Alpha Test)。alpha 低于阈值的像素直接丢弃。
Shader "Cookbook/Ch03/AlphaClip"
{
    SubShader
    {
        // AlphaTest 队列(2450)排在普通不透明物体之后,RenderType 让替换着色/工具识别它
        Tags { "RenderType"="TransparentCutout" "RenderPipeline"="UniversalPipeline" "Queue"="AlphaTest" }

        Pass
        {
            Name "Unlit"
            Tags { "LightMode"="UniversalForward" }
            Cull [_Cull]                         // 渲染状态可以引用属性,用方括号

            GLSLPROGRAM
            VARYING vec2 uv;
            VARYING vec4 vColor;

            uniform sampler2D _BaseMap;
            uniform vec4  _BaseMap_ST;
            uniform vec4  _BaseColor;
            uniform float _Cutoff;

            #ifdef VERTEX
            void main()
            {
                gl_Position = TransformObjectToHClip(positionOS.xyz);
                uv = TransformTex(texcoord0.xy, _BaseMap_ST);
                vColor = color;
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                vec4 col = texture(_BaseMap, uv) * _BaseColor * vColor;
                // clip(x):x < 0 时丢弃当前片元,不写颜色也不写深度。GLSL 里写成 discard
                if (col.a - _Cutoff < 0.0) discard;
                fragColor = vec4(col.rgb, 1);        // 不透明物体输出 alpha=1
            }
            #endif
            ENDGLSL
        }
    }
}

核心只有一行 clip(col.a - _Cutoff)。clip(x) 是 HLSL 内置函数:参数的任一分量小于 0 时,丢弃(discard)当前片元——不写颜色,也不写深度,就像这个像素从没被画过。其余几处配套改动同样重要:

图 3-7贴图的 alpha 是每格中心为 1、向外递减到 0 的圆形渐变。左:Cutoff 0.15,留下的圆很大;中:0.5;右:0.8,只剩中心的小点。球体的背面透过孔可见,因为关闭了背面剔除

URP 自带 Shader 并不直接写 clip,而是用 ShaderVariablesFunctions.hlsl 里的 AlphaDiscard,并且只有在 _ALPHATEST_ON 关键字开启时才裁剪:

// 摘自 URP 14.0.12 ShaderLibrary/ShaderVariablesFunctions.hlsl
real AlphaDiscard(real alpha, real cutoff, real offset = real(0.0))
{
#if defined(_ALPHATEST_ON)
    if (IsAlphaDiscardEnabled())
        alpha = AlphaClip(alpha, cutoff + offset);
#endif
    return alpha;
}

用关键字包起来的原因是:只要着色器里出现了 discard,哪怕运行时从不触发,也可能影响 Early-Z 和移动端 TBDR 架构上的隐藏面剔除效率。所以不需要镂空的材质,最好连这条指令都不要编译进去。我们会在案例 3.8 用关键字实现同样的开关;它对性能的具体影响在第 29 章讨论,Alpha to Coverage 等让镂空边缘更柔和的技巧在第 16 章。