Wind Foliage Shader for Unity URP

A URP grass and leaves shader for Unity 6: alpha-cut foliage that sways in gusts of wind, with a root-to-tip colour gradient. Shadows and depth sway with it.

Also called: grass shader, wind shader, vegetation shader, leaves shader.

Environment URP, Unity 6000.0+ intermediate MIT licensed

Use it for: Grass cards, bushes and tree leaves in a stylized scene, where a little movement makes the world feel alive.

Inspector Try the properties
Shader Scripts For Unity/Wind Foliage
None (Texture)
X Y Z W
WindFoliage Drag to rotate

The preview is a WebGL copy of the shader's maths with one fixed light, so it shows no shadows, Bloom or textures.

What it does

  • Gusting wind bend, strongest at the tips and zero at the roots
  • Wind direction, strength, speed and gust size on the material
  • Root and tip colours multiplied with an alpha-cut leaf texture
  • Double-sided, with back faces lit from their own side
  • Shadow caster and depth passes sway and cut out to match

How it works

Each vertex is moved along the wind direction by a mix of two sine waves of its world position and time, so neighbouring blades move together like a gust passing through. The movement is weighted by the square of the UV v coordinate, which is 0 at the root and 1 at the tip, so roots stay planted.

Setup

  1. Save the file as WindFoliage.shader under Assets and create a material that uses Scripts For Unity > Wind Foliage.
  2. Assign a leaf or grass texture with transparency to Leaf Texture, and tune Alpha Cutoff.
  3. Use meshes whose UV v runs from 0 at the base to 1 at the tip, the usual layout for grass cards.
  4. Set Wind Direction (x and z are used) and adjust Strength and Speed together.

Common mistakes

  • The whole mesh slides instead of bending: the mesh's UVs do not run from 0 at the root to 1 at the tip. Check the UV layout, or swap in vertex colour as the weight.
  • Grass looks too dark from behind: raise the scene's ambient light, or lower Shadow Strength.

Questions

Does it work with Unity's terrain grass?
It is written for grass and leaf meshes you place yourself, for example as GameObjects. Terrain detail grass is drawn with Unity's own terrain shaders.
Why does the whole mesh slide instead of bending?
The wind is weighted by the UV v coordinate, 0 at the root and 1 at the tip. Check that your grass mesh is laid out that way.
Does it support GPU instancing?
It is compatible with the SRP Batcher, which batches many renderers using the same shader. It does not include a GPU-instancing variant.

Source

WindFoliage.shader · 284 lines · ShaderLab and HLSL for the Universal Render Pipeline 17 (Unity 6)

WindFoliage.shader
cpp
// Wind Foliage: alpha-cut grass and leaves that sway in the wind, with a
// root-to-tip colour gradient and soft two-band lighting. Shadows sway too.
// UV v must run from 0 at the root to 1 at the tip. URP 17 (Unity 6).
// Scripts For Unity, MIT licensed.
Shader "Scripts For Unity/Wind Foliage"
{
    Properties
    {
        [MainTexture] _BaseMap ("Leaf Texture (alpha cut)", 2D) = "white" {}
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
        _RootColor ("Root Color", Color) = (0.12, 0.33, 0.16, 1)
        _TipColor ("Tip Color", Color) = (0.62, 0.86, 0.36, 1)
        _ShadowStrength ("Shadow Strength", Range(0, 1)) = 0.45
        _WindDirection ("Wind Direction", Vector) = (1, 0, 0.3, 0)
        _WindStrength ("Wind Strength", Range(0, 1)) = 0.25
        _WindSpeed ("Wind Speed", Range(0, 6)) = 1.8
        _WindFrequency ("Gust Size", Range(0.05, 4)) = 0.7
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "TransparentCutout"
            "Queue" = "AlphaTest"
            "RenderPipeline" = "UniversalPipeline"
        }

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

        CBUFFER_START(UnityPerMaterial)
            float4 _BaseMap_ST;
            float _Cutoff;
            half4 _RootColor;
            half4 _TipColor;
            float _ShadowStrength;
            float4 _WindDirection;
            float _WindStrength;
            float _WindSpeed;
            float _WindFrequency;
        CBUFFER_END

        TEXTURE2D(_BaseMap);
        SAMPLER(sampler_BaseMap);

        // Bends the vertex downwind; tips (uv.y = 1) move most, roots not at all
        float3 SfuApplyWind(float3 positionWS, float tipWeight)
        {
            float2 direction = normalize(_WindDirection.xz + 0.0001);
            float phase = dot(positionWS.xz, direction) * _WindFrequency - _Time.y * _WindSpeed;
            float gust = sin(phase) * 0.7 + sin(phase * 2.3 + 1.7) * 0.3;
            float bend = (gust * 0.5 + 0.5) * _WindStrength * tipWeight * tipWeight;
            positionWS.xz += direction * bend;
            positionWS.y -= bend * 0.35;
            return positionWS;
        }
        ENDHLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
            Cull Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #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_fog
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

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

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

            float4 SfuShadowCoord(float3 positionWS)
            {
            #if defined(_MAIN_LIGHT_SHADOWS_SCREEN)
                return ComputeScreenPos(TransformWorldToHClip(positionWS));
            #else
                return TransformWorldToShadowCoord(positionWS);
            #endif
            }

            Varyings vert(Attributes input)
            {
                Varyings output;
                float3 positionWS = SfuApplyWind(TransformObjectToWorld(input.positionOS.xyz), saturate(input.uv.y));
                output.positionWS = positionWS;
                output.positionCS = TransformWorldToHClip(positionWS);
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                output.height = saturate(input.uv.y);
                output.fogFactor = ComputeFogFactor(output.positionCS.z);
                return output;
            }

            half4 frag(Varyings input, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
            {
                half4 texel = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
                clip(texel.a - _Cutoff);

                float3 normalWS = normalize(input.normalWS) * IS_FRONT_VFACE(face, 1.0, -1.0);
                float3 albedo = lerp(_RootColor.rgb, _TipColor.rgb, input.height) * texel.rgb;

                // Soft two-band light; thin leaves also let light through from behind
                Light mainLight = GetMainLight(SfuShadowCoord(input.positionWS));
                float NdotL = abs(dot(normalWS, mainLight.direction));
                float lit = smoothstep(0.0, 0.25, NdotL) * mainLight.shadowAttenuation;
                float3 color = albedo * lerp(1.0 - _ShadowStrength, 1.0, lit) * mainLight.color;
                color += albedo * SampleSH(normalWS) * 0.3;

                return half4(MixFog(color, input.fogFactor), 1.0);
            }
            ENDHLSL
        }

        // Shadows sway and cut out with the leaves
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }
            ZWrite On
            ZTest LEqual
            ColorMask 0
            Cull Off

            HLSLPROGRAM
            #pragma vertex ShadowVert
            #pragma fragment ShadowFrag
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

            float3 _LightDirection;
            float3 _LightPosition;

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

            struct ShadowVaryings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            ShadowVaryings ShadowVert(ShadowAttributes input)
            {
                ShadowVaryings output;
                float3 positionWS = SfuApplyWind(TransformObjectToWorld(input.positionOS.xyz), saturate(input.uv.y));
                float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
            #if _CASTING_PUNCTUAL_LIGHT_SHADOW
                float3 lightDirectionWS = normalize(_LightPosition - positionWS);
            #else
                float3 lightDirectionWS = _LightDirection;
            #endif
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
            #if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #else
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #endif
                output.positionCS = positionCS;
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                return output;
            }

            half4 ShadowFrag(ShadowVaryings input) : SV_Target
            {
                clip(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a - _Cutoff);
                return 0;
            }
            ENDHLSL
        }

        // Depth with the same sway and cutout
        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode" = "DepthOnly" }
            ZWrite On
            ColorMask R
            Cull Off

            HLSLPROGRAM
            #pragma vertex DepthVert
            #pragma fragment DepthFrag

            struct DepthAttributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct DepthVaryings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            DepthVaryings DepthVert(DepthAttributes input)
            {
                DepthVaryings output;
                float3 positionWS = SfuApplyWind(TransformObjectToWorld(input.positionOS.xyz), saturate(input.uv.y));
                output.positionCS = TransformWorldToHClip(positionWS);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                return output;
            }

            half DepthFrag(DepthVaryings input) : SV_Target
            {
                clip(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a - _Cutoff);
                return input.positionCS.z;
            }
            ENDHLSL
        }

        // Normals with the same sway and cutout, for SSAO
        Pass
        {
            Name "DepthNormals"
            Tags { "LightMode" = "DepthNormals" }
            ZWrite On
            Cull Off

            HLSLPROGRAM
            #pragma vertex NormalsVert
            #pragma fragment NormalsFrag

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

            struct NormalsVaryings
            {
                float4 positionCS : SV_POSITION;
                float3 normalWS : TEXCOORD0;
                float2 uv : TEXCOORD1;
            };

            NormalsVaryings NormalsVert(NormalsAttributes input)
            {
                NormalsVaryings output;
                float3 positionWS = SfuApplyWind(TransformObjectToWorld(input.positionOS.xyz), saturate(input.uv.y));
                output.positionCS = TransformWorldToHClip(positionWS);
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                return output;
            }

            half4 NormalsFrag(NormalsVaryings input, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target
            {
                clip(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a - _Cutoff);
                float3 normalWS = normalize(input.normalWS) * IS_FRONT_VFACE(face, 1.0, -1.0);
                return half4(NormalizeNormalPerPixel(normalWS), 0.0);
            }
            ENDHLSL
        }
    }

    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}

More shaders