unity-geometry-grass-shader icon indicating copy to clipboard operation
unity-geometry-grass-shader copied to clipboard

I converted the shader to support urp, i hope this helps

Open ZxDelt opened this issue 3 months ago • 2 comments

Shader "Custom/URP/GeometryGrassShader"
{
    Properties
    {
        _TranslucentGain("Translucent Gain", Range(0,1)) = 0.5

        _GroundTexture ("Ground Texture", 2D) = "white" {}

        _DisplacementTexture("Displacement Texture", 2D) = "grey" {}
        _DisplacementFactor("Displacement Factor", Float) = 2

        _GrassMask("Grass Mask", 2D) = "white" {}
        _GrassMaskThreshold("Mask Threshold", Range(0,1)) = 0.1

        _BendRotationRandom("Bend Rotation Random", Range(0, 1)) = 0.2

        _BladeWidth("Blade Width", Float) = 0.05
        _BladeWidthRandom("Blade Width Random", Float) = 0.02

        _BladeHeight("Blade Height", Float) = 0.5
        _BladeHeightRandom("Blade Height Random", Float) = 0.3

        _WindDistortionMap("Wind Distortion Map", 2D) = "white" {}
        _WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0)
        _WindStrength("Wind Strength", Float) = 1

        _BladeForward("Blade Forward Amount", Float) = 0.38
        _BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2

        _DisplacementLocation("Displacement Location", Vector) = (0,0,0,0)
        _DisplacementSize("Displacement Size", Float) = 1
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "Queue"          = "Geometry"
            "RenderType"     = "Opaque"
        }
        LOD 300
        Cull Off

        HLSLINCLUDE
        #pragma target 4.6
        #pragma require geometry

        // URP includes
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
        // Common.hlsl provides PI/HALF_PI/TWO_PI (fixes UNITY_PI issues on some versions)
        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"

        // Fallback defines if UNITY_* constants are missing in this URP version
        #ifndef UNITY_PI
            #define UNITY_PI PI
        #endif
        #ifndef UNITY_TWO_PI
            #define UNITY_TWO_PI TWO_PI
        #endif
        #ifndef UNITY_HALF_PI
            #define UNITY_HALF_PI HALF_PI
        #endif

        #define BLADE_SEGMENTS 3

        // SRP Batcher material CBUFFER
        CBUFFER_START(UnityPerMaterial)
            float _TranslucentGain;

            float _DisplacementFactor;
            float4 _DisplacementLocation;  // xyz used
            float _DisplacementSize;

            float _GrassMaskThreshold;

            float _BendRotationRandom;

            float _BladeWidth;
            float _BladeWidthRandom;
            float _BladeHeight;
            float _BladeHeightRandom;

            float2 _WindFrequency;
            float _WindStrength;

            float _BladeForward;
            float _BladeCurve;
        CBUFFER_END

        TEXTURE2D(_GroundTexture);       SAMPLER(sampler_GroundTexture);
        TEXTURE2D(_DisplacementTexture); SAMPLER(sampler_DisplacementTexture);
        TEXTURE2D(_GrassMask);           SAMPLER(sampler_GrassMask);
        TEXTURE2D(_WindDistortionMap);   SAMPLER(sampler_WindDistortionMap);

        float4 _WindDistortionMap_ST;

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

        struct Varyings
        {
            float4 positionCS : SV_POSITION;
            float3 normalWS   : TEXCOORD0;
            float4 uvPack     : TEXCOORD1; // x=bladeU, y=bladeV, z=baseUV.x, w=baseUV.y
            float3 positionWS : TEXCOORD2;
            float3 viewDirWS  : TEXCOORD3;
            float4 shadowCoord: TEXCOORD4;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

        // Simple random
        float rand(float3 co)
        {
            return frac(sin(dot(co, float3(12.9898, 78.233, 45.5432))) * 43758.5453);
        }

        float3x3 AngleAxis3x3(float angle, float3 axis)
        {
            float s, c;
            sincos(angle, s, c);
            float t = 1 - c;
            float x = axis.x, y = axis.y, z = axis.z;

            return float3x3(
                t*x*x + c,     t*x*y - s*z, t*x*z + s*y,
                t*x*y + s*z,   t*y*y + c,   t*y*z - s*x,
                t*x*z - s*y,   t*y*z + s*x, t*z*z + c
            );
        }

        Varyings Vert(Attributes v)
        {
            Varyings o;
            UNITY_SETUP_INSTANCE_ID(v);
            UNITY_TRANSFER_INSTANCE_ID(v, o);

            float3 positionWS = TransformObjectToWorld(v.positionOS);
            float3 normalWS   = TransformObjectToWorldNormal(v.normalOS);

            o.positionCS = TransformWorldToHClip(positionWS);
            o.positionWS = positionWS;
            o.normalWS   = normalWS;

            // pack base mesh UV in zw, blade UV will be set in GS
            o.uvPack = float4(0, 0, v.uv.xy);

            o.viewDirWS = GetWorldSpaceViewDir(positionWS);
            o.shadowCoord = TransformWorldToShadowCoord(positionWS);
            return o;
        }

        Varyings MakeVaryings(float3 posWS, float3 nrmWS, float2 bladeUV, float2 baseUV)
        {
            Varyings o;
            o.positionWS = posWS;
            o.normalWS   = nrmWS;
            o.positionCS = TransformWorldToHClip(posWS);
            o.uvPack     = float4(bladeUV, baseUV);
            o.viewDirWS  = GetWorldSpaceViewDir(posWS);
            o.shadowCoord = TransformWorldToShadowCoord(posWS);
            return o;
        }

        Varyings GenerateGrassVertexWS(
            float3 baseWS, float3x3 transformWS,
            float width, float height, float forward,
            float2 bladeUV, float2 baseUV,
            float3 tangentWS, float3 bitangentWS, float3 normalWS)
        {
            float3 tangentPoint   = float3(width, forward, height);
            float3 tangentNormal  = normalize(float3(0, -1, forward)); // matches original
            float3 localNormalWS  = mul(transformWS, tangentNormal);
            float3 localPosWS     = baseWS + mul(transformWS, tangentPoint);

            return MakeVaryings(localPosWS, localNormalWS, bladeUV, baseUV);
        }

        [maxvertexcount(BLADE_SEGMENTS * 2 + 1)]
        void Geo(triangle Varyings IN[3], inout TriangleStream<Varyings> triStream)
        {
            // Use first vertex as seed (same as original)
            float3 posWS = IN[0].positionWS;

            // Build an orthonormal frame from normal
            float3 nWS = normalize(IN[0].normalWS);
            float3 tWS = normalize(any(nWS.xyz != 0) ? cross(float3(0,1,0), nWS) : float3(1,0,0));
            float3 bWS = normalize(cross(nWS, tWS));

            float2 baseUV = IN[0].uvPack.zw;

            // Wind
            float2 windUV = posWS.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y;
            float2 windSample = (SAMPLE_TEXTURE2D_LOD(_WindDistortionMap, sampler_WindDistortionMap, windUV, 0).xy * 2.0h - 1.0h) * _WindStrength;
            float3 windAxis = normalize(float3(windSample.x, windSample.y, 0));
            float  windAngle = UNITY_PI * length(windSample);
            float3x3 windRotation = AngleAxis3x3(windAngle, windAxis);

            // Displacement influence
            float2 dispUVNorm = (posWS.xz - _DisplacementLocation.xz) / max(_DisplacementSize, 1e-4);
            float2 dispFetch = dispUVNorm;

            // clamp-mask trick from original
            float2 dispMaskUv = max(saturate(dispFetch.xy), saturate(1.0 - dispFetch.xy));
            float  dispMask   = floor(max(dispMaskUv.x, dispMaskUv.y));
            float2 dispSample = lerp((SAMPLE_TEXTURE2D_LOD(_DisplacementTexture, sampler_DisplacementTexture, dispFetch.xy, 0).xz - 0.5), float2(0.001, 0.001), dispMask);
            float3 dispAxis   = normalize(float3(dispSample.x, dispSample.y, 0));
            float  dispAngle  = -_DisplacementFactor * abs(dispSample.x + dispSample.y);
            float3x3 dispRotation = AngleAxis3x3(dispAngle, dispAxis);

            // Random facing/bend
            float3 seedA = float3(posWS.x, posWS.y, posWS.z);
            float3x3 facingRotation = AngleAxis3x3(rand(seedA) * UNITY_TWO_PI, float3(0,0,1));
            float3x3 bendRotation   = AngleAxis3x3(rand(seedA.zyx) * _BendRotationRandom * UNITY_HALF_PI, float3(-1,0,0));

            // Tangent-to-local in WS using orthonormal frame
            float3x3 TBN = float3x3(tWS, bWS, nWS);

            float3x3 baseFacing = mul(TBN, facingRotation);
            float3x3 fullXform  = mul(mul(mul(TBN, mul(windRotation, dispRotation)), facingRotation), bendRotation);

            // Mask sampling to decide blade presence
            float mask = SAMPLE_TEXTURE2D_LOD(_GrassMask, sampler_GrassMask, baseUV, 0).r;

            float height = ((rand(float3(posWS.z, posWS.y, posWS.x)) * 2 - 1) * _BladeHeightRandom + _BladeHeight) * mask;
            float width  = ((rand(float3(posWS.x, posWS.z, posWS.y)) * 2 - 1) * _BladeWidthRandom  + _BladeWidth)  * mask;
            float forward = rand(float3(posWS.y, posWS.y, posWS.z)) * _BladeForward;

            int segments = mask > _GrassMaskThreshold ? BLADE_SEGMENTS : 0;

            [loop]
            for (int i = 0; i < segments; i++)
            {
                float t = (float)i / (float)BLADE_SEGMENTS;
                float segH = height * t;
                float segW = width * (1 - t);
                float segF = pow(t, _BladeCurve) * forward;

                float3x3 useXform = (i == 0) ? baseFacing : fullXform;

                triStream.Append(GenerateGrassVertexWS(posWS, useXform, +segW, segH, segF, float2(0, t), baseUV, tWS, bWS, nWS));
                triStream.Append(GenerateGrassVertexWS(posWS, useXform, -segW, segH, segF, float2(1, t), baseUV, tWS, bWS, nWS));
            }

            if (segments > 0)
            {
                triStream.Append(GenerateGrassVertexWS(posWS, fullXform, 0, height, forward, float2(0.5, 1), baseUV, tWS, bWS, nWS));
            }
        }

        float3 EvaluateLighting(float3 nWS, float3 posWS, float4 shadowCoord)
        {
            // Main light
            Light mainLight = GetMainLight(shadowCoord);
            float NdotL = saturate(dot(nWS, mainLight.direction));
            NdotL = saturate(NdotL + _TranslucentGain);
            float shadow = mainLight.shadowAttenuation;

            float3 main = NdotL * shadow * mainLight.color;

            // Ambient/SH
            float3 sh = SampleSH(nWS);

            return main + sh;
        }
        ENDHLSL

        // Universal Forward (base)
        Pass
        {
            Name "ForwardBase"
            Tags { "LightMode"="UniversalForward" }
            Cull Off
            ZWrite On
            ZTest LEqual

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma geometry Geo
            #pragma fragment Frag

            // Lighting variants
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _ADDITIONAL_LIGHTS
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile_fragment _ _SHADOWS_SOFT
            #pragma multi_compile_fog

            float4 Frag(Varyings i) : SV_Target
            {
                float3 nWS = normalize(i.normalWS);
                float3 lighting = EvaluateLighting(nWS, i.positionWS, i.shadowCoord);
                float4 albedo = SAMPLE_TEXTURE2D(_GroundTexture, sampler_GroundTexture, i.uvPack.zw);
                float3 col = albedo.rgb * lighting;
                return float4(col, albedo.a);
            }
            ENDHLSL
        }

        // Forward Add (per-light additive)
        Pass
        {
            Name "ForwardAdd"
            Tags { "LightMode"="UniversalForwardAdd" }
            Blend One One
            Cull Off
            ZWrite Off
            ZTest LEqual

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma geometry Geo
            #pragma fragment FragAdd

            #pragma multi_compile _ _ADDITIONAL_LIGHTS
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile_fragment _ _SHADOWS_SOFT

            float4 FragAdd(Varyings i) : SV_Target
            {
                float3 nWS = normalize(i.normalWS);
                float3 posWS = i.positionWS;

                uint lightCount = GetAdditionalLightsCount();
                float3 add = 0;
                [loop]
                for (uint li = 0; li < lightCount; li++)
                {
                    Light l = GetAdditionalLight(li, posWS);
                    float ndl = saturate(dot(nWS, l.direction));
                    float3 c = ndl * l.color * l.distanceAttenuation * l.shadowAttenuation;
                    add += c;
                }

                float3 albedo = SAMPLE_TEXTURE2D(_GroundTexture, sampler_GroundTexture, i.uvPack.zw).rgb;
                return float4(add * albedo, 1);
            }
            ENDHLSL
        }

        // ShadowCaster (no external include; depth-only)
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            Cull Off
            ZWrite On
            ZTest LEqual
            ColorMask 0

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma geometry Geo
            #pragma fragment FragShadow

            float4 FragShadow(Varyings i) : SV_Target
            {
                // Depth-only; returning 0 writes depth via SV_Position
                return 0;
            }
            ENDHLSL
        }
    }

    FallBack Off
}

ZxDelt avatar Oct 13 '25 00:10 ZxDelt

That's sick! Thank you so much. Do you mind if I add this to the repository once I've got time? I'll add a credit link to your GH in the header.

Velorexe avatar Nov 04 '25 19:11 Velorexe

That's sick! Thank you so much. Do you mind if I add this to the repository once I've got time? I'll add a credit link to your GH in the header.

yes sure, that will help

ZxDelt avatar Nov 04 '25 20:11 ZxDelt