GameTech/URP 渲染实战

案例 18.1噪声溶解:用 clip 挖洞入门

目标:给物体一个 0~1 的进度参数,进度增加时物体表面按噪声图案一块块消失,并且它投在地上的影子同步“溶掉”。

原理:溶解就是带阈值的 Alpha Test

给表面每个点分配一个“溶解值” v(最简单的就是从噪声贴图采样),再给一个全局进度 p。规则只有一条:v < p 的像素丢掉。进度从 0 涨到 1,越来越多的像素满足条件,物体就从“噪声值最小的地方”开始破洞,最后完全消失。HLSL 里丢弃像素用 clip(x):参数小于 0 时,这个片元不写颜色、不写深度、也不参与后续混合。

溶解后的物体仍然是“不透明”的——每个像素要么完整存在,要么完全不存在,没有半透明。所以它不需要透明队列,也不需要排序,用 "Queue"="AlphaTest"(2450)就行。把 Alpha Test 物体排在普通不透明物体之后,是为了让完全不透明的物体先把深度写好,更多被遮挡的溶解像素能被 Early-Z 提前剔除(带 clip 的 Shader 自己无法享受 Early-Z 的写深度优化,见第 29 章)。

这里有个容易忽略的细节:噪声贴图的取值很少真正达到 0 或 1。如果直接 clip(v - p),进度为 0 时可能已经有几个像素被裁掉,进度为 1 时可能还剩下几个“顽固像素”。解决办法是把进度区间稍微外扩:

Assets/Cookbook/Ch18/DissolveCore.hlsl(节选:clip)
// 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1)
half DissolveClip(half value, out half edge)
{
    half p = GetProgress() * (1.0h + _EdgeWidth + 0.02h) - _EdgeWidth - 0.01h;
    half d = value - p;          // d < 0:已经溶掉
    clip(d);
    // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0
    edge = 1.0h - saturate(d / max(_EdgeWidth, 1e-4h));
    return d;
}
#ifdef FRAGMENT   // discard 只能出现在片元着色器里(HLSL 的 clip 也是)
// 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1)
float DissolveClip(float value, out float edge)
{
    float p = GetProgress() * (1.0 + _EdgeWidth + 0.02) - _EdgeWidth - 0.01;
    float d = value - p;          // d < 0:已经溶掉
    if (d < 0.0) discard;         // HLSL 的 clip(d)
    // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0
    edge = 1.0 - saturate(d / max(_EdgeWidth, 1e-4));
    return d;
}
#endif

外扩量里还包含了下一个案例要用的边带宽度 _EdgeWidth:保证进度为 0 时连边带都不出现。函数同时输出 edge,它是“离溶解前沿有多近”的 0~1 值,案例 18.2 用它发光。

完整代码

溶解的材质参数与计算放在一个公共 HLSL 里,三个 Pass 都包含它:

Assets/Cookbook/Ch18/DissolveCore.hlsl
// 溶解效果的核心:材质参数 + “溶解值”计算。Forward、ShadowCaster、DepthOnly 三个 Pass 共用,
// 保证屏幕上的镂空、阴影里的镂空、深度图里的镂空完全一致。
#ifndef COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED
#define COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED

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

TEXTURE2D(_BaseMap);  SAMPLER(sampler_BaseMap);
TEXTURE2D(_NoiseMap); SAMPLER(sampler_NoiseMap);
TEXTURE2D(_EdgeRamp); SAMPLER(sampler_EdgeRamp);

// 所有 Pass 的 UnityPerMaterial 必须一模一样,SRP Batcher 才认
CBUFFER_START(UnityPerMaterial)
    float4 _BaseMap_ST;
    half4  _BaseColor;
    half   _Smoothness;
    float4 _NoiseMap_ST;
    half   _Progress;
    half   _AutoPlay;
    half   _EdgeWidth;
    half4  _EdgeColor;
    half   _UseRamp;
    half   _NoiseStrength;
    float4 _DissolveDir;     // xyz:对象空间方向
    float4 _DirRange;        // x/y:沿方向的最小/最大投影(对象空间)
    float4 _SphereCenter;    // xyz:世界空间球心,w:最大半径
CBUFFER_END

half GetProgress()
{
    // 运行时可以勾选 AutoPlay 让它自己循环;书中拍图时 _Time 不可控,用 _Progress 手动给进度
    return _AutoPlay > 0.5 ? frac(_Time.y * 0.25) : _Progress;
}

// 返回 0~1 的“溶解值”:值越小越早消失。进度 p 时,值 < p 的像素被裁掉。
half DissolveValue(float2 uv, float3 positionOS, float3 positionWS)
{
    half noise = SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r;
#if defined(_MODE_DIRECTION)
    // 方向溶解:沿对象空间某方向的投影,归一化到 0~1,再掺一点噪声让边缘不规则
    float h = dot(positionOS, normalize(_DissolveDir.xyz));
    half t = saturate((h - _DirRange.x) / (_DirRange.y - _DirRange.x));
    return lerp(t, noise, _NoiseStrength);
#elif defined(_MODE_SPHERE)
    // 球形溶解:离球心越近越早消失,像被一个不断变大的球“吞掉”
    half t = saturate(distance(positionWS, _SphereCenter.xyz) / _SphereCenter.w);
    return lerp(t, noise, _NoiseStrength);
#else
    return noise;
#endif
}

// 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1)
half DissolveClip(half value, out half edge)
{
    half p = GetProgress() * (1.0h + _EdgeWidth + 0.02h) - _EdgeWidth - 0.01h;
    half d = value - p;          // d < 0:已经溶掉
    clip(d);
    // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0
    edge = 1.0h - saturate(d / max(_EdgeWidth, 1e-4h));
    return d;
}

#endif
// 溶解效果的核心:材质参数 + “溶解值”计算。Forward、ShadowCaster、DepthOnly 三个 Pass 共用,
// 保证屏幕上的镂空、阴影里的镂空、深度图里的镂空完全一致。(GLSL 版,对应 DissolveCore.hlsl)
#ifndef COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED
#define COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED

uniform sampler2D _BaseMap;
uniform sampler2D _NoiseMap;
uniform sampler2D _EdgeRamp;

// HLSL 版要求所有 Pass 的 UnityPerMaterial 一模一样(SRP Batcher);GLSL 里就是普通 uniform
uniform vec4  _BaseMap_ST;
uniform vec4  _BaseColor;
uniform float _Smoothness;
uniform vec4  _NoiseMap_ST;
uniform float _Progress;
uniform float _AutoPlay;
uniform float _EdgeWidth;
uniform vec4  _EdgeColor;
uniform float _UseRamp;
uniform float _NoiseStrength;
uniform vec4  _DissolveDir;     // xyz:对象空间方向
uniform vec4  _DirRange;        // x/y:沿方向的最小/最大投影(对象空间)
uniform vec4  _SphereCenter;    // xyz:世界空间球心,w:最大半径

float GetProgress()
{
    // 运行时可以勾选 AutoPlay 让它自己循环;书中拍图时 _Time 不可控,用 _Progress 手动给进度
    return _AutoPlay > 0.5 ? fract(_Time.y * 0.25) : _Progress;
}

// 返回 0~1 的“溶解值”:值越小越早消失。进度 p 时,值 < p 的像素被裁掉。
float DissolveValue(vec2 uv, vec3 positionOS, vec3 positionWS)
{
    float noise = texture(_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r;
#if defined(_MODE_DIRECTION)
    // 方向溶解:沿对象空间某方向的投影,归一化到 0~1,再掺一点噪声让边缘不规则
    float h = dot(positionOS, normalize(_DissolveDir.xyz));
    float t = saturate((h - _DirRange.x) / (_DirRange.y - _DirRange.x));
    return mix(t, noise, _NoiseStrength);
#elif defined(_MODE_SPHERE)
    // 球形溶解:离球心越近越早消失,像被一个不断变大的球“吞掉”
    float t = saturate(distance(positionWS, _SphereCenter.xyz) / _SphereCenter.w);
    return mix(t, noise, _NoiseStrength);
#else
    return noise;
#endif
}

#ifdef FRAGMENT   // discard 只能出现在片元着色器里(HLSL 的 clip 也是)
// 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1)
float DissolveClip(float value, out float edge)
{
    float p = GetProgress() * (1.0 + _EdgeWidth + 0.02) - _EdgeWidth - 0.01;
    float d = value - p;          // d < 0:已经溶掉
    if (d < 0.0) discard;         // HLSL 的 clip(d)
    // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0
    edge = 1.0 - saturate(d / max(_EdgeWidth, 1e-4));
    return d;
}
#endif

#endif

Shader 主体如下。Forward Pass 负责着色;ShadowCaster 与 DepthOnly Pass 只做一件事——用同一个函数算溶解值然后 clip:

Assets/Cookbook/Ch18/Dissolve.shader
// 案例 18.1~18.3:溶解。噪声 clip + 边缘发光 + 三种溶解形状(噪声 / 方向 / 球形)。
Shader "Cookbook/Ch18/Dissolve"
{
    Properties
    {
        [MainTexture] _BaseMap ("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor ("Base Color", Color) = (1,1,1,1)
        _Smoothness ("Smoothness", Range(0,1)) = 0.4
        [Header(Dissolve)]
        [NoScaleOffset] _NoiseMap ("Noise (R)", 2D) = "gray" {}
        _NoiseMap_ST ("Noise Tiling/Offset", Vector) = (1,1,0,0)
        _Progress ("Progress", Range(0,1)) = 0.3
        [Toggle] _AutoPlay ("Auto Play (_Time)", Float) = 0
        [KeywordEnum(Noise, Direction, Sphere)] _Mode ("Mode", Float) = 0
        _NoiseStrength ("Noise Strength (Dir/Sphere)", Range(0,1)) = 0.25
        _DissolveDir ("Direction (Object)", Vector) = (0,1,0,0)
        _DirRange ("Direction Range (min,max)", Vector) = (-0.5,0.5,0,0)
        _SphereCenter ("Sphere Center(xyz) MaxRadius(w)", Vector) = (0,0,0,2)
        [Header(Edge)]
        _EdgeWidth ("Edge Width", Range(0,0.3)) = 0.08
        [HDR] _EdgeColor ("Edge Color", Color) = (4,1.6,0.4,1)
        [Toggle] _UseRamp ("Use Ramp", Float) = 1
        [NoScaleOffset] _EdgeRamp ("Edge Ramp", 2D) = "white" {}
        [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 0
    }

    SubShader
    {
        // 溶解本质是 Alpha Test:用 AlphaTest 队列,让 Early-Z 友好的不透明物体先画
        Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }
            Cull [_Cull]

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
            #pragma multi_compile _ _FORWARD_PLUS
            #pragma multi_compile_fog

            #include "Assets/Cookbook/Ch18/DissolveCore.hlsl"
            #include "Assets/Cookbook/Ch18/Ch18Common.hlsl"

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

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv         : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float3 positionOS : TEXCOORD2;
                half3  normalWS   : TEXCOORD3;
                half   fogFactor  : TEXCOORD4;
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                VertexPositionInputs p = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = p.positionCS;
                o.positionWS = p.positionWS;
                o.positionOS = v.positionOS.xyz;
                o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                o.uv = v.uv;
                o.fogFactor = ComputeFogFactor(p.positionCS.z);
                return o;
            }

            half4 frag(Varyings i, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
            {
                half edge;
                DissolveClip(DissolveValue(i.uv, i.positionOS, i.positionWS), edge);

                half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv * _BaseMap_ST.xy + _BaseMap_ST.zw).rgb * _BaseColor.rgb;
                // 双面渲染时,背面(物体内壁)把法线翻过来,再压暗一点,看起来像“空心的内部”
                half3 normalWS = normalize(i.normalWS) * IS_FRONT_VFACE(face, 1.0h, -1.0h);
                albedo *= IS_FRONT_VFACE(face, 1.0h, 0.35h);

                // 前沿附近先“烧焦”变暗,再在最前沿发光
                albedo *= 1.0h - 0.85h * smoothstep(0.0h, 0.6h, edge);
                half3 color = SimpleLighting(albedo, i.positionWS, normalWS, _Smoothness);

                half3 glow = _EdgeColor.rgb;
                if (_UseRamp > 0.5)
                    glow *= SAMPLE_TEXTURE2D(_EdgeRamp, sampler_EdgeRamp, float2(edge, 0.5)).rgb;
                // edge^2:让发光集中在最前沿,向内快速衰减
                color += glow * edge * edge;

                color = MixFog(color, i.fogFactor);
                return half4(color, 1);
            }
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            Cull [_Cull]

            HLSLPROGRAM
            #pragma vertex vertShadow
            #pragma fragment fragClip
            #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Assets/Cookbook/Ch18/DissolveCore.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

            float3 _LightDirection;   // 由 URP 的 ShadowUtils 在画阴影前设置
            float3 _LightPosition;

            struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; };
            struct Varyings  { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionOS : TEXCOORD1; float3 positionWS : TEXCOORD2; };

            Varyings vertShadow(Attributes v)
            {
                Varyings o;
                float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(v.normalOS);
            #if _CASTING_PUNCTUAL_LIGHT_SHADOW
                float3 lightDirWS = normalize(_LightPosition - positionWS);
            #else
                float3 lightDirWS = _LightDirection;
            #endif
                o.positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS));
            #if UNITY_REVERSED_Z
                o.positionCS.z = min(o.positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #else
                o.positionCS.z = max(o.positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #endif
                o.uv = v.uv; o.positionOS = v.positionOS.xyz; o.positionWS = positionWS;
                return o;
            }

            half4 fragClip(Varyings i) : SV_Target
            {
                half edge;
                DissolveClip(DissolveValue(i.uv, i.positionOS, i.positionWS), edge);
                return 0;
            }
            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            Cull [_Cull]

            HLSLPROGRAM
            #pragma vertex vertDepth
            #pragma fragment fragClip
            #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE
            #include "Assets/Cookbook/Ch18/DissolveCore.hlsl"

            struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; };
            struct Varyings  { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionOS : TEXCOORD1; float3 positionWS : TEXCOORD2; };

            Varyings vertDepth(Attributes v)
            {
                Varyings o;
                o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
                o.positionCS = TransformWorldToHClip(o.positionWS);
                o.uv = v.uv; o.positionOS = v.positionOS.xyz;
                return o;
            }

            half4 fragClip(Varyings i) : SV_Target
            {
                half edge;
                DissolveClip(DissolveValue(i.uv, i.positionOS, i.positionWS), edge);
                return i.positionCS.z;
            }
            ENDHLSL
        }
    }
}
// 案例 18.1~18.3:溶解。噪声 clip + 边缘发光 + 三种溶解形状(噪声 / 方向 / 球形)。(GLSL 版,对应 Ch18/Dissolve.shader)
Shader "Cookbook/Ch18/Dissolve"
{
    SubShader
    {
        // 溶解本质是 Alpha Test:用 AlphaTest 队列,让 Early-Z 友好的不透明物体先画
        Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "RenderPipeline"="UniversalPipeline" }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }
            Cull [_Cull]

            GLSLPROGRAM
            #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE
            #pragma multi_compile_fog

            #include "Ch18/DissolveCore.glsl"
            #include "Ch18/Ch18Common.glsl"

            VARYING vec2  uv;
            VARYING vec3  positionWS;
            VARYING vec3  posOS;        // 对象空间位置(HLSL 版的 positionOS 字段;GLSL 里这个名字已被顶点输入占用)
            VARYING vec3  normalWS;
            VARYING float fogFactor;

            #ifdef VERTEX
            void main()
            {
                VertexPositionInputs p = GetVertexPositionInputs(positionOS.xyz);
                gl_Position = p.positionCS;
                positionWS = p.positionWS;
                posOS = positionOS.xyz;
                normalWS = TransformObjectToWorldNormal(normalOS);
                uv = texcoord0.xy;
                fogFactor = ComputeFogFactor(p.positionCS.z);
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                float edge;
                DissolveClip(DissolveValue(uv, posOS, positionWS), edge);

                vec3 albedo = texture(_BaseMap, uv * _BaseMap_ST.xy + _BaseMap_ST.zw).rgb * _BaseColor.rgb;
                // 双面渲染时,背面(物体内壁)把法线翻过来,再压暗一点,看起来像“空心的内部”
                vec3 N = normalize(normalWS) * (gl_FrontFacing ? 1.0 : -1.0);
                albedo *= gl_FrontFacing ? 1.0 : 0.35;

                // 前沿附近先“烧焦”变暗,再在最前沿发光
                albedo *= 1.0 - 0.85 * smoothstep(0.0, 0.6, edge);
                vec3 color = SimpleLighting(albedo, positionWS, N, _Smoothness);

                vec3 glow = _EdgeColor.rgb;
                if (_UseRamp > 0.5)
                    glow *= texture(_EdgeRamp, vec2(edge, 0.5)).rgb;
                // edge^2:让发光集中在最前沿,向内快速衰减
                color += glow * edge * edge;

                color = MixFog(color, fogFactor);
                fragColor = vec4(color, 1.0);
            }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            Cull [_Cull]

            GLSLPROGRAM
            #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE
            #include "Ch18/DissolveCore.glsl"
            // _LightDirection / _LightPosition 由引擎在画阴影前设置(Lighting.glsl 里已声明)

            VARYING vec2 uv;
            VARYING vec3 posOS;
            VARYING vec3 positionWS;

            #ifdef VERTEX
            void main()
            {
                positionWS = TransformObjectToWorld(positionOS.xyz);
                vec3 normalWS = TransformObjectToWorldNormal(normalOS);
                vec3 lightDirWS = _CastingPunctualLightShadow > 0.5 ? normalize(_LightPosition - positionWS) : _LightDirection;
                vec4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS));
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE * positionCS.w);
                gl_Position = positionCS;
                uv = texcoord0.xy; posOS = positionOS.xyz;
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                float edge;
                DissolveClip(DissolveValue(uv, posOS, positionWS), edge);
                fragColor = vec4(0.0);
            }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            Cull [_Cull]

            GLSLPROGRAM
            #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE
            #include "Ch18/DissolveCore.glsl"

            VARYING vec2 uv;
            VARYING vec3 posOS;
            VARYING vec3 positionWS;

            #ifdef VERTEX
            void main()
            {
                positionWS = TransformObjectToWorld(positionOS.xyz);
                gl_Position = TransformWorldToHClip(positionWS);
                uv = texcoord0.xy; posOS = positionOS.xyz;
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                float edge;
                DissolveClip(DissolveValue(uv, posOS, positionWS), edge);
                fragColor = vec4(gl_FragCoord.z);
            }
            #endif
            ENDGLSL
        }
    }
}

关键代码讲解

① 三个 Pass 必须一起裁剪。只在 Forward Pass 里 clip,屏幕上的物体会破洞,但地上的影子还是完整的——因为阴影贴图是 ShadowCaster Pass 画的;如果项目开了 SSAO 或者用到了深度图(比如本章后面的护盾),深度图里也会留下一个完整的“幽灵”。所以溶解值的计算必须写成公共函数,所有写深度的 Pass 都调用它。这也是为什么 CBUFFER 要放在公共文件里:同一个 Shader 所有 Pass 的 UnityPerMaterial 布局必须完全一致,否则材质会被 SRP Batcher 判为不兼容。

② ShadowCaster 的写法。阴影 Pass 的顶点函数和 URP 的 ShadowCasterPass.hlsl 一样:用 ApplyShadowBias 沿法线和光方向偏移,_LightDirection/_LightPosition 由 URP 在绘制阴影前设置,_CASTING_PUNCTUAL_LIGHT_SHADOW 区分方向光和点光/聚光(第 6 章)。有一个坑:URP 的 Shadows.hlsl 用到了 CommonMaterial.hlsl 里的 LerpWhiteTo,而 Lit 是通过 LitInput.hlsl 间接包含它的。自己写阴影 Pass 只包含 Shadows.hlsl 会报 “undeclared identifier 'LerpWhiteTo'”,需要先包含 Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl。

③ 双面渲染。物体被挖出洞后,你会透过洞看到它的内壁。默认 Cull Back 会把内壁剔掉,看起来像一张纸壳;本例把 _Cull 做成属性,配图里设为 Off,并在片元里用 FRONT_FACE_SEMANTIC 判断正反面:背面翻转法线、压暗颜色,看起来像实心物体被掏空。

图 18-2噪声溶解,进度从左到右依次为 0.15、0.4、0.6、0.8(此图关闭了边缘发光)。注意地面上的阴影与物体同步镂空,背面内壁被压暗。

常见坑与调参