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.
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
- Save the file as WindFoliage.shader under Assets and create a material that uses Scripts For Unity > Wind Foliage.
- Assign a leaf or grass texture with transparency to Leaf Texture, and tune Alpha Cutoff.
- Use meshes whose UV v runs from 0 at the base to 1 at the tip, the usual layout for grass cards.
- 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)
// 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
- Stylized Water ShaderA URP stylized water shader for Unity 6: rolling vertex waves, a deep-to-horizon colour ramp, crest foam, a sun glint and optional shore foam from the depth texture.
- Gradient Skybox ShaderA URP stylized skybox shader for Unity 6: a three-colour gradient sky with a sun disc and glow that follow your main directional light. No textures needed.
- Toon Lit Shader with OutlineA URP toon shader for Unity 6: two-band cel shading that receives shadows, a hard specular highlight, a rim light and an inverted-hull outline, in one .shader file.
- Dissolve ShaderA URP dissolve shader for Unity 6 that burns a mesh away along procedural noise with a glowing HDR edge. No noise texture needed, and its shadow dissolves with it.