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 strengthGraphics.setWind(1.0, 0.0, 0.0, 1.0)1. Include the shared ABI in your shader
Section titled “1. Include the shared ABI in your shader”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:
| accessor | meaning |
|---|---|
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;}3. Set the wind from Wren (optional)
Section titled “3. Set the wind from Wren (optional)”Graphics.setWind(x, y, z, strength) // persists across frames until changedWind 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.
4. Build and run
Section titled “4. Build and run”The engine compiles apps/*/shaders/src/*.slang during the CMake build. Run your app and the clock
advances automatically.
Verify it works
Section titled “Verify it works”Take two screenshots a fraction of a second apart and confirm they differ — the reference demo does exactly this:
plume3d apps/wind_sway_demo # self-screenshots shot_a / shot_b, then exitsThe 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).
Gotchas
Section titled “Gotchas”- Read the Wren-scripting rules first if you touch any
.wren— the recurring traps live in the engine’sdocs/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.
timeis 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.
Related
Section titled “Related”- API:
Graphics.setWindand the Global frame uniforms section. - Example: Wind Sway (
apps/wind_sway_demo). - Companion guide: Custom-material pack shaders.