Skip to content

Animating shaders with the frame clock & wind

Every mesh shader can read a global frame block the engine binds at set 0, binding 4 — time, delta, frameIndex, and a global wind vector — so a shader can animate: grass and tree sway, water waves, scrolling flow maps, pulsing colour. This is engine functionality (ADR 0072 / PLM-236) — it works for any app, no content pack required.

The clock is engine-driven: you do nothing to advance time; it is monotonic seconds since launch, refreshed once per frame. Only the wind is app-set, via Graphics.setWind.

Companion guide: Authoring a custom-material pack shader. A real animated shader (e.g. water) usually uses both blocks — it scrolls its own flow map by plume3d_time() and binds its own maps at set 5.

// in your app's shader (apps/<app>/shaders/src/<name>.slang)
#include "plume3d.slang"
// then read: plume3d_time(), plume3d_delta(), plume3d_frameIndex(),
// plume3d_windDir(), plume3d_windStrength()
// in main.wren — direction xyz (need not be normalized), then strength
Graphics.setWind(1.0, 0.0, 0.0, 1.0)

Every app shader is compiled with engine/shaders/include on the include path, so #include "plume3d.slang" resolves. The include declares the frame block once and exposes the accessors:

accessormeaning
float plume3d_time()seconds since launch (monotonic)
float plume3d_delta()last frame delta (seconds)
float plume3d_frameIndex()monotonic frame counter (deterministic for screenshots)
float3 plume3d_windDir()global wind direction (as set; not normalized)
float plume3d_windStrength()global wind strength

2. Use them in your vertex or fragment code

Section titled “2. Use them in your vertex or fragment code”

A minimal grass-sway vertex shader (from the wind-sway example):

[shader("vertex")]
VSOutput vertex(VSInput input) {
VSOutput o;
float3 p = input.Position;
float tip = saturate(p.y * 0.5 + 0.5); // sway more toward the top
float sway = sin(plume3d_time() * 2.0 + p.x * 3.0)
* plume3d_windStrength() * 0.25 * tip; // phase by x so blades differ
p.x += sway;
o.PositionCS = float4(p, 1.0); // NDC demo; use view/proj in 3D
return o;
}
Graphics.setWind(x, y, z, strength) // persists across frames until changed

Wind defaults to (0, 0, 0, 0) — set it once in init(), or vary it over time for gusts. See Graphics.setWind and the Global frame uniforms section on the Graphics page.

The engine compiles apps/*/shaders/src/*.slang during the CMake build. Run your app and the clock advances automatically.

Take two screenshots a fraction of a second apart and confirm they differ — the reference demo does exactly this:

Terminal window
plume3d apps/wind_sway_demo # self-screenshots shot_a / shot_b, then exits

The two frames differ (mean pixel diff ~17 in the reference). That difference is the frame clock reaching the GPU; if the frames are identical, time isn’t flowing (check the #include).

  • Read the Wren-scripting rules first if you touch any .wren — the recurring traps live in the engine’s docs/wren-scripting-gotchas.md.
  • The frame block is always declared in the pipeline layout, so a shader that ignores it is unaffected — you only pay for it when you read it.
  • time is host-owned; the script VM cannot fabricate a clock. Do not try to pass time in yourself.
  • Deferred (not in v1): per-view cameraPos / nearFar — they are per-view and land with the scene-depth capability (Block RFX A#2). Until then, derive camera-relative effects from world position, or wait for A#2.