Vegetation: wind and leaf material
Complete shader, vertex-color wind mask, alpha cutout, bounds and troubleshooting.
This example adds wind and alpha cutout to an existing 3D model. It does not generate tree geometry. A production asset needs a proper trunk, branches, foliage, UVs, materials and levels of detail; a shader cannot repair a poor model or lighting.
Prepare the model
- Separate bark and foliage into surfaces with separate materials. Bark normally needs an opaque material; the leaf texture needs meaningful alpha.
- In the modeling tool, paint the red vertex-color channel: 0 at fixed points, smoothly increasing to 1 at movable tips. Black holds a vertex; white gives full amplitude. UVs and vertex colors are different data.
- Export geometry with UVs, normals and vertex colors. After importing, check actual motion: lost colors can make the whole surface sway equally. Temporarily use
wind_amplitude = 0.3for diagnosis, then restore a small value. - Assign the leaf material only to the leaf surface. MeshInstance3D.MaterialOverride overrides all surface materials, which can accidentally turn bark into leaves. Use SetSurfaceOverrideMaterial with the relevant surface index for a single surface.
Start with one leafy branch, a directional light and a neutral environment. Confirm geometry and texture look correct without wind. Model scale must match the project.
Complete leaf shader
Create a Shader with this code and a ShaderMaterial using it. Assign an RGBA texture to leaf_albedo. Wind displaces vertices in the world XZ plane, rather than along the tree's local axes.
shader_type spatial;
render_mode cull_disabled, world_vertex_coords;
uniform sampler2D leaf_albedo : source_color, hint_default_white, filter_linear_mipmap, repeat_disable;
uniform float wind_amplitude : hint_range(0.0, 1.0) = 0.06;
uniform vec2 wind_direction = vec2(1.0, 0.0);
uniform float wind_frequency = 0.8;
uniform float wind_speed = 1.2;
uniform float wind_phase = 0.0;
uniform float alpha_cutoff : hint_range(0.0, 1.0) = 0.5;
uniform float backlight_strength : hint_range(0.0, 1.0) = 0.1;
void vertex() {
vec2 direction = wind_direction / max(length(wind_direction), 0.0001);
float phase = dot(VERTEX.xz, vec2(0.8, 0.6)) * wind_frequency;
float weight = clamp(COLOR.r, 0.0, 1.0);
float offset = sin(phase + TIME * wind_speed + wind_phase) * wind_amplitude * weight;
VERTEX.xz += direction * offset;
}
void fragment() {
vec4 leaf = texture(leaf_albedo, UV);
ALBEDO = leaf.rgb;
ALPHA = leaf.a;
ALPHA_SCISSOR_THRESHOLD = alpha_cutoff;
ROUGHNESS = 1.0;
BACKLIGHT = leaf.rgb * backlight_strength;
}COLOR.ris only the mobility mask. It does not multiply ALBEDO or paint leaves red.wind_amplitudesets maximum displacement in world units at weight 1. The length of wind_direction does not amplify movement; a zero direction disables it.wind_frequencychanges spatial frequency. It multiplies coordinates in the sine phase; it is not a wavelength in meters.wind_speedchanges phase speed; wind_phase offsets it in radians. Freeze wind time with wind_speed = 0 for inspection. With wind_frequency = 0, phases +π/2 and −π/2 produce opposite extremes.ALPHA_SCISSOR_THRESHOLDcuts out transparent regions. This is a hard threshold, not glass-like translucency. Soft edges require a separate approach and validation of the chosen antialiasing.cull_disabledshows both sides. Current Forward+ already flips the back-face normal before the user fragment function; repeatingNORMAL = -NORMALbreaks lighting.BACKLIGHTadds a simple contribution from light behind the surface. This is an artistic control, not a physical leaf-thickness model. Start with a low value.
Large bends need normal correction and coordinated branch animation. This example does not recalculate normals from the deformed surface, so keep displacement small. Moving an entire crown identically does not replace separate trunk, branch and leaf motion levels.
Create the material from C#
This helper accepts an existing Shader and Texture2D. It checks the shader type and assigns the texture using the exact uniform name.
using System;
using Reckfell;
public static class FoliageMaterial
{
public static ShaderMaterial Create(Shader shader, Texture2D leafTexture)
{
ArgumentNullException.ThrowIfNull(shader);
ArgumentNullException.ThrowIfNull(leafTexture);
if (shader.GetMode() != Shader.Mode.Spatial)
throw new ArgumentException("A spatial leaf shader is required.", nameof(shader));
var material = new ShaderMaterial { Shader = shader };
material.SetShaderParameter("leaf_albedo", leafTexture);
return material;
}
}Assign the result of FoliageMaterial.Create(shader, texture) to the leaf surface material. MaterialOverride is suitable for a MeshInstance3D containing only foliage. Loading paths depend on your project: do not copy invented asset paths. Shader.GetMode checks the declared type, not successful GPU compilation.
Bounds, lighting and frame cost
GPU deformation does not change the source Mesh AABB or automatically update collision. Without bounds padding, foliage can disappear near the camera edge or during shadow rendering. Increase ExtraCullMargin or set a CustomAabb covering every position. Current implementation grows local bounds before transforming them: at unit scale, padding at least wind_amplitude covers this example's displacement; recalculate local padding for reduced or nonuniform scale. Do not inflate an entire forest's bounds without measuring culling cost.
Check the shadow on the ground while the branch moves. Use a dynamic light with shadows enabled for this demonstration; a static baked shadow does not follow wind. This test pass does not separately validate shadow or baking quality.
Start with a small plant count. Large overlapping leaf cards increase processed pixel work; two-sided rendering and shadows also cost time. Reduce empty card area, prepare LOD and measure GPU time on the target device. MultiMesh reduces overhead for many instances but does not eliminate the cost of the leaves, transparent areas or shadows themselves.
Diagnose the result
| Symptom | Check |
|---|---|
| White rectangles instead of leaves | RGBA texture assigned to leaf_albedo, meaningful alpha and correct UVs |
| Roots move, or the entire object sways equally | Red vertex channel and preserved export colors |
| Wind moves in an unexpected direction | wind_direction uses world XZ; object rotation does not rotate wind direction |
| Foliage disappears near screen edges | Bounds covering maximum deformation, scale and visibility settings |
| Back faces are unexpectedly darker | Duplicate NORMAL flip, light direction and BACKLIGHT; two-sided rendering alone does not add light |
| Distant silhouette flickers | Texture mipmaps/filtering, alpha threshold, LOD and antialiasing |
| Bark becomes transparent | Global MaterialOverride used instead of the leaf surface material |
Checked on current Windows Forward+/Vulkan: the complete shader and C# helper, alpha cutout, both sides, 0/0.5/1 masks, mask clamping, direction and phase, live TIME motion, gradient and world displacement under translation, rotation and nonuniform scale. Small test cards are not plant illustrations or graphics-quality demonstrations. Real-model import, editor UI, strongly bent normals, shadow quality, a forest with LOD, performance and other backends were not separately exercised.
Related tasks: spatial shaders, shader language, MultiMesh, GeometryInstance3D.