GameTech/URP 渲染实战
源码 / Ch20/Grass.shader
Ch20/Grass.shader
// 案例 20.7–20.9:GPU Instancing 草——每实例颜色变化、交互压弯、距离淡出
Shader "Cookbook/Ch20/Grass"
{
    Properties
    {
        _RootColor ("Root Color", Color) = (0.05, 0.12, 0.03, 1)
        _TipColor ("Tip Color", Color) = (0.35, 0.55, 0.12, 1)
        _DryColor ("Dry Color", Color) = (0.55, 0.5, 0.2, 1)
        _DryAmount ("Dry Amount", Range(0, 1)) = 0.35
        _Translucency ("Translucency", Range(0, 2)) = 0.6
        _NormalUp ("Normal Towards Up", Range(0, 1)) = 0.5
        _WindDir ("Wind Dir (XZ)", Vector) = (1, 0, 0.3, 0)
        _WindStrength ("Wind Strength", Range(0, 0.5)) = 0.1
        _WindSpeed ("Wind Speed", Float) = 1.5
        _WindPhase ("Wind Phase (for stills)", Float) = 0
        _BendStrength ("Interact Bend", Range(0, 2)) = 0.9
        _PressDown ("Interact Press Down", Range(0, 1)) = 0.3
        _FadeStart ("Fade Start (m)", Float) = 18
        _FadeEnd ("Fade End (m)", Float) = 24
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
        Cull Off    // 草叶是单层面片,两面都要画

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

        CBUFFER_START(UnityPerMaterial)
            half4 _RootColor, _TipColor, _DryColor;
            half _DryAmount, _Translucency, _NormalUp;
            float4 _WindDir;
            float _WindStrength, _WindSpeed, _WindPhase;
            float _BendStrength, _PressDown;
            float _FadeStart, _FadeEnd;
        CBUFFER_END

        // 每实例属性:C# 用 MaterialPropertyBlock.SetFloatArray("_Variation", ...) 按批传入
        UNITY_INSTANCING_BUFFER_START(GrassProps)
            UNITY_DEFINE_INSTANCED_PROP(float, _Variation)
        UNITY_INSTANCING_BUFFER_END(GrassProps)

        // 交互体:全局数组,xyz = 世界坐标,w = 影响半径。不放进 UnityPerMaterial,由 Shader.SetGlobal* 设置
        float4 _Ch20Interactors[8];
        float  _Ch20InteractorCount;

        struct GrassAttributes
        {
            float4 positionOS : POSITION;
            float3 normalOS   : NORMAL;
            float4 color      : COLOR;
            float2 uv         : TEXCOORD0;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

        // 返回变形后的世界坐标;fade 为距离淡出系数(1 = 完全可见)
        float3 GrassDeform(GrassAttributes v, out float fade)
        {
            float3 pivotWS = TransformObjectToWorld(float3(0, 0, 0));     // 这一丛草的根部中心
            // 1) 距离淡出:先把整丛草“压矮”,再配合片元里的抖动裁剪
            float camDist = distance(pivotWS, GetCameraPositionWS());
            fade = saturate((_FadeEnd - camDist) / max(_FadeEnd - _FadeStart, 1e-3));
            float3 posOS = v.positionOS.xyz;
            posOS.y *= lerp(0.3, 1.0, fade);
            float3 posWS = TransformObjectToWorld(posOS);

            float h = v.uv.y;            // 0 根部 → 1 叶尖
            float bendW = h * h;         // 根部钉死、越往上越软(平方让弯曲更像弧线)

            // 2) 风:简化的行波(完整的风场做法见第 17 章)。_WindPhase 供静帧拍摄用
            float phase = _WindPhase + _Time.y * _WindSpeed;
            float wave = sin(dot(pivotWS.xz, float2(0.35, 0.22)) + phase) * 0.5 + 0.5;
            wave += sin(dot(pivotWS.xz, float2(-1.3, 0.9)) + phase * 2.3) * 0.15;
            posWS.xz += normalize(_WindDir.xz) * (wave * _WindStrength * bendW);

            // 3) 交互:每个交互体把附近的草沿水平方向推开,并往下压
            float2 push = 0;
            float press = 0;
            int count = (int)_Ch20InteractorCount;
            for (int k = 0; k < count; k++)
            {
                float4 it = _Ch20Interactors[k];
                float2 d = posWS.xz - it.xz;
                float dist = length(d);
                float f = saturate(1.0 - dist / it.w);
                f = f * f * (3.0 - 2.0 * f);                         // smoothstep 形状,边缘不突变
                f *= saturate(1.0 - (pivotWS.y - it.y) / it.w);      // 交互体离地太高就不影响
                push += d / max(dist, 1e-3) * f;
                press = max(press, f);
            }
            // 水平推开量不超过顶点离地高度;再按“到根部的距离不变”把顶点压低:y' = √(h² − |o|²)
            float hy = max(posWS.y - pivotWS.y, 0.0);
            float2 o = push * (_BendStrength * bendW);
            float ol = length(o);
            float maxO = hy * 0.95;
            o *= min(1.0, maxO / max(ol, 1e-4));
            ol = min(ol, maxO);
            posWS.xz += o;
            posWS.y = pivotWS.y + sqrt(max(hy * hy - ol * ol, 0.0)) * (1.0 - press * _PressDown);
            return posWS;
        }
        ENDHLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }   // 自定义表面混合,没有 GBuffer Pass:Deferred 下也走前向
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
            #pragma multi_compile _ _FORWARD_PLUS
            #pragma multi_compile_fog
            #pragma multi_compile_instancing

            #include "Assets/Cookbook/Ch20/TerrainCommon.hlsl"

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;
                half3  normalWS   : TEXCOORD1;
                float4 data       : TEXCOORD2;   // x = 高度 uv.y, y = 颜色随机值, z = 距离淡出, w = 雾
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            Varyings vert(GrassAttributes v)
            {
                Varyings o = (Varyings)0;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                float fade;
                float3 posWS = GrassDeform(v, fade);
                o.positionWS = posWS;
                o.positionCS = TransformWorldToHClip(posWS);
                o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                // 每实例随机值 + 每片叶子的随机值,组合出颜色变化
                float variation = UNITY_ACCESS_INSTANCED_PROP(GrassProps, _Variation);
                o.data = float4(v.uv.y, frac(variation + v.color.r * 0.5), fade, ComputeFogFactor(o.positionCS.z));
                return o;
            }

            // 4×4 Bayer 抖动阈值(0..1):按屏幕像素位置给出不同阈值,用 clip 做“半透明”
            float Bayer4x4(float2 pixel)
            {
                const float m[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
                uint2 p = uint2(pixel) % 4;
                return (m[p.y * 4 + p.x] + 0.5) / 16.0;
            }

            half4 frag(Varyings i, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);
                // 距离淡出的后半段:抖动裁剪,避免草在某个距离“啪”地整片消失
                clip(i.data.z - Bayer4x4(i.positionCS.xy));

                float h = i.data.x;
                half3 albedo = lerp(_RootColor.rgb, _TipColor.rgb, h);
                albedo = lerp(albedo, _DryColor.rgb * lerp(0.6, 1.0, h), smoothstep(1.0 - _DryAmount, 1.0, i.data.y));
                albedo *= lerp(0.85, 1.15, frac(i.data.y * 7.31));

                // 双面:背面翻转法线;再往“上”方向拉,让整片草地的明暗更柔和统一
                half3 n = normalize(i.normalWS) * (IS_FRONT_VFACE(face, 1, -1));
                n = normalize(lerp(n, half3(0, 1, 0), _NormalUp));
                half occlusion = lerp(0.35, 1.0, h);    // 根部互相遮挡,压暗
                half4 color = Ch20ShadePBR(i.positionWS, n, i.positionCS, albedo, 0, 0.2, occlusion, i.data.w);

                // 逆光透射:视线与光线方向接近相反时,叶尖透出亮绿色
                Light mainLight = GetMainLight(TransformWorldToShadowCoord(i.positionWS));
                half3 V = GetWorldSpaceNormalizeViewDir(i.positionWS);
                half back = pow(saturate(dot(V, -mainLight.direction)), 4.0);
                color.rgb += albedo * mainLight.color * (mainLight.shadowAttenuation * back * _Translucency * h);
                return color;
            }
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            HLSLPROGRAM
            #pragma vertex shadowVert
            #pragma fragment shadowFrag
            #pragma multi_compile_instancing
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"   // Shadows.hlsl 用到其中的 LerpWhiteTo
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
            float3 _LightDirection;
            float3 _LightPosition;

            // 阴影 Pass 必须做同样的顶点变形,否则影子不跟着草动
            float4 shadowVert(GrassAttributes v) : SV_POSITION
            {
                UNITY_SETUP_INSTANCE_ID(v);
                float fade;
                float3 posWS = GrassDeform(v, fade);
                float3 normalWS = TransformObjectToWorldNormal(v.normalOS);
            #if _CASTING_PUNCTUAL_LIGHT_SHADOW
                float3 lightDir = normalize(_LightPosition - posWS);
            #else
                float3 lightDir = _LightDirection;
            #endif
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(posWS, normalWS, lightDir));
            #if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #else
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #endif
                return positionCS;
            }
            half4 shadowFrag() : SV_Target { return 0; }
            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            HLSLPROGRAM
            #pragma vertex depthVert
            #pragma fragment depthFrag
            #pragma multi_compile_instancing
            float4 depthVert(GrassAttributes v) : SV_POSITION
            {
                UNITY_SETUP_INSTANCE_ID(v);
                float fade;
                return TransformWorldToHClip(GrassDeform(v, fade));
            }
            half4 depthFrag() : SV_Target { return 0; }
            ENDHLSL
        }
    }
}