reckfellENGINE

Vegetation: wind and leaf material

Complete shader, vertex-color wind mask, alpha cutout, bounds and troubleshooting.

On this page

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

  1. Separate bark and foliage into surfaces with separate materials. Bark normally needs an opaque material; the leaf texture needs meaningful alpha.
  2. 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.
  3. 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.3 for diagnosis, then restore a small value.
  4. 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
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.r is only the mobility mask. It does not multiply ALBEDO or paint leaves red.
  • wind_amplitude sets maximum displacement in world units at weight 1. The length of wind_direction does not amplify movement; a zero direction disables it.
  • wind_frequency changes spatial frequency. It multiplies coordinates in the sine phase; it is not a wavelength in meters.
  • wind_speed changes 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_THRESHOLD cuts 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_disabled shows both sides. Current Forward+ already flips the back-face normal before the user fragment function; repeating NORMAL = -NORMAL breaks lighting.
  • BACKLIGHT adds 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.

C#
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

SymptomCheck
White rectangles instead of leavesRGBA texture assigned to leaf_albedo, meaningful alpha and correct UVs
Roots move, or the entire object sways equallyRed vertex channel and preserved export colors
Wind moves in an unexpected directionwind_direction uses world XZ; object rotation does not rotate wind direction
Foliage disappears near screen edgesBounds covering maximum deformation, scale and visibility settings
Back faces are unexpectedly darkerDuplicate NORMAL flip, light direction and BACKLIGHT; two-sided rendering alone does not add light
Distant silhouette flickersTexture mipmaps/filtering, alpha threshold, LOD and antialiasing
Bark becomes transparentGlobal 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.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.