vidar.games

Wind field simulation

2026-11-08 · Discuss on X

Sine wind sways every blade in step. A wind field remembers where air was pushed: a blast spreads out, a rolling rock leaves a wake, a vortex swirls and then fades.

The problem

Most grass wind is one sine wave plus noise. It looks fine until something happens in the scene. An explosion, a character running through, a spell: the grass ignores it, or each effect gets its own one-off bend code that knows nothing about the others.

How it works

The source (a Unity project) keeps a small 3D grid of wind velocity around the camera, 32 x 16 x 32 cells of 1 m, and updates it every frame:

  1. Shift: when the camera moves a whole cell, the grid's contents slide so the wind stays put in the world.
  2. Diffuse: each cell moves a little toward the average of its 6 neighbours (factor 0.00335, 3 passes).
  3. Motors: effects add velocity. Omni pushes outward, directional pushes one way, vortex spins around an axis, moving pushes along its path.
  4. Advect: the velocity carries itself along, forward and then backward.
  5. Grass samples the grid at its root and bends: the bend angle grows with wind strength, up to 72°.

There is no damping term. Wind fades by spreading thin, and values below the fixed-point step (1/4096) are cut to zero.

What I hit

The source writes advection with compute shaders and atomic adds: each cell scatters into the cells it moves to. WebGL2 has no compute shaders and no atomics, so every pass is a fragment shader rendering into a float texture, ping-ponged between two. I rewrote both advection passes as gathers: each cell reads the neighbours within 2 cells that could have sent it velocity. That matches the scatter as long as nothing moves more than 2 cells in one step, and at these settings nothing gets close.

My build

three.js 0.170, WebGL2. The 3D grid is a 256 x 64 float texture holding the 32 slices as tiles. Seven passes per step at 60 steps per second. 110,000 instanced blades read the field in the vertex shader with the source's bend formula.

Both halves of the video see the same three events. In my run, with no motors left, the average wind speed fell from 0.33 to 0.05 within 7 s and to zero within 17 s.

Trade-offs

When to use it

Open scenes with lots of grass, cloth or particles where gameplay events should visibly disturb the air. For a static backdrop, a sine wave is enough.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Technique source: GodOfWarWindSimulation by SaberZG, https://github.com/SaberZG/GodOfWarWindSimulation, MIT license, commit f34e6f7f75d01d56200f127519045ce5979d9c81. Ported to three.js and GLSL from its compute shaders and C# manager: the grid shift, the 6-neighbour diffusion, the omni / directional / vortex / moving wind motors, the forward and reverse advection, the fixed-point storage and channel merge clamp, and the grass bend function from its WindSystem shader include. The grid size, diffusion and advection factors and the motor force clamp come from the source. The advection passes were rewritten from scatter (atomic adds) to gather because WebGL2 has no compute shaders. No files were copied unchanged.

Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, npm package version 0.170.0. Used for rendering, float render targets and instancing. It is not the source of the wind technique.

My own code: the global sine wind baseline, the event script (shockwave, rolling rock, vortex), the procedural noise in place of the source's noise texture, the grass blade mesh and its static lean, the markers, the debug arrow view, the split screen, labels and video. No external models or assets. No IA model.