GameTech/URP 渲染实战
源码 / shaders/Ch20/Grass.glsl
shaders/Ch20/Grass.glsl
// 案例 20.7–20.9:GPU Instancing 草——每实例颜色变化、交互压弯、距离淡出(GLSL 版,对应 Ch20/Grass.shader)
// WebGL2 版的实例化:GrassRenderer.js 用 drawElementsInstanced 一次画完所有草丛,
// 每实例的矩阵和 _Variation 打包进一张浮点贴图 _GrassInstanceData,顶点着色器按 gl_InstanceID 取出来。
Shader "Cookbook/Ch20/Grass"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
        Cull Off    // 草叶是单层面片,两面都要画

        GLSLINCLUDE
        uniform vec4 _RootColor, _TipColor, _DryColor;
        uniform float _DryAmount, _Translucency, _NormalUp;
        uniform vec4 _WindDir;
        uniform float _WindStrength, _WindSpeed, _WindPhase;
        uniform float _BendStrength, _PressDown;
        uniform float _FadeStart, _FadeEnd;

        // 每实例属性:C# 用 MaterialPropertyBlock.SetFloatArray("_Variation", ...) 按批传入 UNITY_INSTANCING_BUFFER;
        // WebGL 没有这套宏,每实例占贴图里连续 4 个像素:矩阵的前三行 + (_Variation, 0, 0, 0)
        uniform highp sampler2D _GrassInstanceData;
        #define GRASS_DATA_WIDTH 1024

        // 交互体:全局数组,xyz = 世界坐标,w = 影响半径。不放进材质参数,由 GrassRenderer.js 按全局变量设置(同 Shader.SetGlobal*)
        uniform vec4  _Ch20Interactors[8];
        uniform float _Ch20InteractorCount;

        #ifdef VERTEX
        // 对应 UNITY_SETUP_INSTANCE_ID:取出这一实例的 unity_ObjectToWorld 和 _Variation
        mat4 instanceObjectToWorld;
        float instanceVariation;
        vec4 FetchInstanceTexel(int i) { return texelFetch(_GrassInstanceData, ivec2(i % GRASS_DATA_WIDTH, i / GRASS_DATA_WIDTH), 0); }
        void SetupGrassInstance()
        {
            int base = gl_InstanceID * 4;
            // 贴图里存的是矩阵的行,GLSL 的 mat4 按列构造,所以转置一下
            instanceObjectToWorld = transpose(mat4(FetchInstanceTexel(base), FetchInstanceTexel(base + 1), FetchInstanceTexel(base + 2), vec4(0, 0, 0, 1)));
            instanceVariation = FetchInstanceTexel(base + 3).x;
        }
        // 实例版的 TransformObjectToWorld / TransformObjectToWorldNormal(草丛只有旋转 + 等比缩放,法线直接用 3×3 部分)
        vec3 GrassObjectToWorld(vec3 p) { return (instanceObjectToWorld * vec4(p, 1.0)).xyz; }
        vec3 GrassObjectToWorldNormal(vec3 n) { return normalize(mat3(instanceObjectToWorld) * n); }

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

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

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

            // 3) 交互:每个交互体把附近的草沿水平方向推开,并往下压
            vec2 push = vec2(0.0);
            float press = 0.0;
            int count = int(_Ch20InteractorCount);
            for (int k = 0; k < count; k++)
            {
                vec4 it = _Ch20Interactors[k];
                vec2 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);
            vec2 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;
        }
        #endif
        ENDGLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }   // 自定义表面混合,没有 GBuffer Pass:Deferred 下也走前向
            GLSLPROGRAM
            #pragma multi_compile_fog
            #pragma multi_compile_instancing

            #include "Ch20/TerrainCommon.glsl"

            VARYING vec3 positionWS;
            VARYING vec3 normalWS;
            VARYING vec4 data;       // x = 高度 uv.y, y = 颜色随机值, z = 距离淡出, w = 雾

            #ifdef VERTEX
            void main()
            {
                SetupGrassInstance();
                float fade;
                vec3 posWS = GrassDeform(fade);
                positionWS = posWS;
                gl_Position = TransformWorldToHClip(posWS);
                normalWS = GrassObjectToWorldNormal(normalOS);
                // 每实例随机值 + 每片叶子的随机值,组合出颜色变化
                float variation = instanceVariation;
                data = vec4(texcoord0.y, fract(variation + color.r * 0.5), fade, ComputeFogFactor(gl_Position.z));
            }
            #endif

            #ifdef FRAGMENT
            // 4×4 Bayer 抖动阈值(0..1):按屏幕像素位置给出不同阈值,用 clip 做“半透明”
            float Bayer4x4(vec2 pixel)
            {
                const float m[16] = float[16](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);
                uvec2 p = uvec2(pixel) % 4u;
                return (m[p.y * 4u + p.x] + 0.5) / 16.0;
            }

            vec4 frag()
            {
                // 距离淡出的后半段:抖动裁剪,避免草在某个距离“啪”地整片消失
                if (data.z - Bayer4x4(gl_FragCoord.xy) < 0.0) discard;

                float h = data.x;
                vec3 albedo = mix(_RootColor.rgb, _TipColor.rgb, h);
                albedo = mix(albedo, _DryColor.rgb * mix(0.6, 1.0, h), smoothstep(1.0 - _DryAmount, 1.0, data.y));
                albedo *= mix(0.85, 1.15, fract(data.y * 7.31));

                // 双面:背面翻转法线;再往“上”方向拉,让整片草地的明暗更柔和统一
                vec3 n = normalize(normalWS) * (gl_FrontFacing ? 1.0 : -1.0);
                n = normalize(mix(n, vec3(0, 1, 0), _NormalUp));
                float occlusion = mix(0.35, 1.0, h);    // 根部互相遮挡,压暗
                vec4 color = Ch20ShadePBR(positionWS, n, gl_FragCoord, albedo, 0.0, 0.2, occlusion, data.w);

                // 逆光透射:视线与光线方向接近相反时,叶尖透出亮绿色
                Light mainLight = GetMainLight(TransformWorldToShadowCoord(positionWS));
                vec3 V = GetWorldSpaceNormalizeViewDir(positionWS);
                float back = pow(saturate(dot(V, -mainLight.direction)), 4.0);
                color.rgb += albedo * mainLight.color * (mainLight.shadowAttenuation * back * _Translucency * h);
                return color;
            }
            void main() { fragColor = frag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            GLSLPROGRAM
            #pragma multi_compile_instancing
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            // _LightDirection / _LightPosition 由引擎的 Lighting.glsl 声明

            #ifdef VERTEX
            // 阴影 Pass 必须做同样的顶点变形,否则影子不跟着草动
            void main()
            {
                SetupGrassInstance();
                float fade;
                vec3 posWS = GrassDeform(fade);
                vec3 normalWS = GrassObjectToWorldNormal(normalOS);
                vec3 lightDir = _CastingPunctualLightShadow > 0.5 ? normalize(_LightPosition - posWS) : _LightDirection;
                vec4 positionCS = TransformWorldToHClip(ApplyShadowBias(posWS, normalWS, lightDir));
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE * positionCS.w);   // UNITY_REVERSED_Z = 0
                gl_Position = positionCS;
            }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = vec4(0.0); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            GLSLPROGRAM
            #pragma multi_compile_instancing
            #ifdef VERTEX
            void main()
            {
                SetupGrassInstance();
                float fade;
                gl_Position = TransformWorldToHClip(GrassDeform(fade));
            }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = vec4(gl_FragCoord.z); }
            #endif
            ENDGLSL
        }
    }
}