Vegetation and Wind Shader Development for Graphics
Trees and grass in games are not static meshes. They are thousands of instances, each of which must move convincingly in the wind, correctly receive lighting through thin leaves, not create z-fighting on semi-transparent faces, and not kill performance on mobile devices. Doing this correctly means solving several unrelated technical problems simultaneously.
Vertex Animation for Wind: Why Skeletal Animation Does Not Work
A tree with a thousand leaves cannot be animated through bones — that would be hundreds of skinned mesh renderer calls. The standard solution is vertex shader animation, where vertex movement is encoded in the shader itself and controlled by wind parameters through Material Property Block or Global Shader Property.
In ShaderGraph (URP/HDRP), the algorithm is built as follows: Transform Position node → World Position → add Sine-based offset along two axes (X and Z) with Time + Phase Offset. Phase Offset is critically important: it is encoded in vertex color or UV channel of the mesh (usually UV2) during asset preparation. Without phase offset, all tree branches move synchronously — this looks like mechanical swaying, not organic wind.
Hierarchical wind — the real differentiator of quality vegetation shader. Movement is divided into three levels:
- Trunk sway (swaying trunk) — low frequency, large amplitude, all tree geometry
- Branch flutter (branch movement) — medium frequency, smaller amplitude, encoded through R channel of Vertex Color
- Leaf shimmer (leaf tremor) — high frequency, minimal amplitude, G channel of Vertex Color
In the shader, each level is a separate Sine with different frequency and amplitude parameters. Weight for each level is taken from vertex color channel. This means the artist must bake vertex colors into the tree before export: at the trunk base R=0 (no branch flutter), at branch tips R=1.
Two-Sided Rendering and Alpha Clipping
Leaves are polygonal cards (billboard quads or meshcard strips) with alpha texture. Two types of problems:
Alpha Blending vs Alpha Clipping. Blending correctly sorts transparency, but requires correct depth sorting of all leaves, impossible without GPU-side sorting. In practice, for vegetation, Alpha Clipping (Alpha Test) is used: hard boundary by threshold, no blending. This gives artefacts at edges, but works correctly with depth buffer and instancing. Threshold (Cutoff) is usually 0.4–0.6 depending on texture.
Two-sided lighting. Lit shader by default lights only front face. For leaves, you need Two Sided material with Flip Normals on back face — otherwise the back of the leaf will be black under any lighting. In ShaderGraph, this is Two Sided checkbox in Graph Settings + Facing node to apply normal flip.
Subsurface scattering for leaves. Real leaves glow in sunlight. In URP for vegetation, Translucency approximation is used: take dot product between Light Direction and View Direction, add it to Albedo through Lerp with translucency color (warm green). This is ~4 nodes in ShaderGraph, adds +0.1ms to shader cost, but the difference in visual quality is obvious.
Instancing and GPU Instancing
Grass and trees require GPU Instancing — otherwise each bush is a separate draw call. In Unity, the correct approach for vegetation is:
-
Graphics.DrawMeshInstancedorGraphics.DrawMeshInstancedIndirectfor procedural vegetation - Unity Terrain Detail Mesh — built-in instancing for terrain detail
- SpeedTree integration (built into Unity, but requires SpeedTree license for editing)
The shader must support #pragma instancing_options and use UNITY_SETUP_INSTANCE_ID in vertex shader. In ShaderGraph this is automatic, but when writing custom HLSL through Custom Function node, you must explicitly add these pragma.
Material Property Block allows passing per-instance parameters (phase offset, wind strength multiplier) without creating a separate Material for each instance — critical for performance with hundreds of instances with different parameters.
Wind Setup Through Global Shader Properties
Wind in the scene is a global parameter. Correct architecture: WindController MonoBehaviour sets Shader.SetGlobalFloat("_WindStrength", strength) and Shader.SetGlobalVector("_WindDirection", dir) each frame (or on wind change event). All vegetation shaders read these global parameters through Global node in ShaderGraph (Custom Function: UNITY_ACCESS_INSTANCED_PROP for per-instance or just float from Global for shared).
This enables dynamic wind effects: intensification during storms, gust on nearby explosion, direction change throughout day-night cycle — all through one controller without changing materials.
| Task Type | Timeline |
|---|---|
| Grass shader (URP, mobile, with wind) | 2–4 days |
| Tree shader (URP, hierarchical wind, PBR leaves) | 4–7 days |
| Vegetation shader set (grass + shrubs + trees) | 1–2 weeks |
| HDRP with subsurface + translucency | 1–2 weeks |
Cost is calculated individually based on render pipeline, target platform, and graphics style discussion results.





