VOLERYField Studios

Field guide · build notes

How the murmuration flies

Everything flying on the Volery site is computed live: 46,656 starlings steered and drawn by your graphics card sixty times a second. No footage, no photographs, no recorded motion. These are the build notes.

The brief

A fictional nature cinematography studio whose brand is emergence itself. The hero had to be a murmuration of at least 40,000 birds over a dusk estuary, framed like a documentary crane shot: the camera authored, never orbit-controlled. A predator event every twenty seconds or so, so the flock never loops. Scroll steers the flock and the light through the studio's world, from golden hour to first stars.

Why 46,656 is the hard part

Classic boids ask every bird about every other bird. At 46,656 agents that is roughly 2.2 billion pair checks per frame, against a budget of sixteen milliseconds. No loop survives that, so the neighbourhood has to become cheap: each bird should read the crowd, not individuals. Conveniently, that is also what a real starling does: it tracks its nearest neighbours and the density and motion of the mass around it.

The engine keeps every position and velocity in RGBA32F textures (216 by 216 texels, one bird per texel) and runs the whole model in fragment shaders. The interesting part is how the birds find their neighbours without ever searching.

Binning by rasterisation

The estuary volume is a 48 by 24 by 48 grid, stored as a 384 by 144 texture atlas of z-slices. Every frame, each bird is drawn as a single GL point whose vertex shader computes which grid cell it occupies and emits exactly that texel. Additive blending does the counting: after one draw call the atlas holds the summed velocity and the population of every cell. The rasteriser is the binning data structure; no atomics, no sort, one pass over the flock.

ivec2 tc = ivec2(gl_VertexID % uSimW, gl_VertexID / uSimW);
vec3 p  = texelFetch(uPosT, tc, 0).xyz;
vVel    = texelFetch(uVelT, tc, 0).xyz;
vec3 c  = clamp(floor((p - uGridMin) / uCell), vec3(0.0), uCells - 1.0);
vec2 tile  = vec2(mod(c.z, uTiles.x), floor(c.z / uTiles.x));
vec2 texel = tile * uCells.xy + c.xy + 0.5;
gl_Position  = vec4(texel / uAtlas * 2.0 - 1.0, 0.0, 1.0);
gl_PointSize = 1.0;  // one bird lands in exactly one grid texel

The splat vertex shader, verbatim from js/flock.js. Blend mode ONE, ONE: the frame buffer accumulates (sum velocity, count).

A second pass runs a 27-tap tent blur over the atlas, turning raw bins into a smooth field of local mean velocity and density. Birds then sample that field with manual trilinear filtering, two bilinear reads mixed across slices.

The steering kernel

Each bird reads the field at its own position, plus six offsets for a density gradient, and steers with a handful of forces. Alignment pulls it toward the local mean velocity, which is how the falcon's panic crosses six hundred metres of birds in seconds. Density regulation pushes it up the gradient when the neighbourhood is sparse and down when it is crowded, which is where the sheets and waves come from.

vec4 f = field(p);                // blurred (sum velocity, count)
float rho = f.w;
vec3 meanV = f.xyz / max(f.w, 0.75);
acc += (meanV - v) * uKA;         // alignment: waves propagate here

float e = uCell;                  // central-difference density gradient
float gx = field(p + vec3(e,0,0)).w - field(p - vec3(e,0,0)).w;
float gy = field(p + vec3(0,e,0)).w - field(p - vec3(0,e,0)).w;
float gz = field(p + vec3(0,0,e)).w - field(p - vec3(0,0,e)).w;
vec3 g = vec3(gx, gy * 1.5, gz) / (2.0 * e);

// per-bird density preference: ragged edges, internal clumping,
// never a smooth ball. The single most important line for the look.
float myRho = uRhoT * (0.65 + 0.7 * fract(seed * 11.31));
float drive = clamp(myRho - rho, -9.0, 5.0);
acc += g * (uKD * drive);

The core of the velocity shader, verbatim. The remaining forces: an anisotropic anchor spring, a slow orbital swirl, the falcon's pressure field, per-bird wander, and speed regulation that never lets a starling hover.

Speed is clamped to a cruising band around 13 units per second. Starlings cannot stop, and that single constraint does more for the silhouette than any force: clumps stretch into ribbons because everyone is always moving.

The frame, end to end

Around 187,000 vertices and five small compute-style passes per frame. The birds are single triangles oriented along their view-space velocity, sized in world units, darkened and hazed by depth so density reads as tone, the way a real roost photograph does.

The authored camera

Five camera and behaviour keyframes, one per section: the wide hero, the manifesto's tight sphere stage right, the reel's distant band, the field notes' low pass over the water, and the credits' roost funnel. Scroll position eases between adjacent keyframes and the whole state lowpasses toward its target, so the move always feels like a crane, never a scrubber. A slow sinusoidal drift keeps the frame alive when you stop. Dusk itself is the scroll: the sun sinks, the amber dies, the stars arrive at the roost. There are no orbit controls; touch does not orbit.

The falcon

Every 15 to 26 seconds an unseen falcon crosses the volume on a randomised bezier: a moving pressure field with a gaussian falloff. Birds inside it flee and their cruise speed briefly rises; alignment then carries the shock outward as a wave, the flock compresses, splits, and reforms. Each pass is logged to the viewfinder's field log with the dusk timecode. Because the schedule, the path, and the recovery are stochastic, the murmuration never repeats.

Performance, measured

ConfigurationAgentsResult
Apple M4 Max, Chromium, ANGLE Metal, 1440 by 900 46,656 60 fps, 16.7 ms, 0 console errors
Same machine, SwiftShader software GL (headless default) 46,656 roughly 14 fps, stable, 0 errors
Mobile profile (small or coarse-pointer viewports) 16,384 128 by 128 state textures, wider fov, larger sprites

Measured 7 July 2026 with tools/qa/volery/measure.mjs sampling the engine's own frame counter, six one-second samples per run.

Degradation is designed, not accidental: the timestep clamps so slow machines get a slower flock rather than an unstable one, and if the smoothed frame time stays over 20 ms the canvas steps its pixel ratio down before it ever touches the agent count. The desktop floor stays above ten thousand birds.

Provenance and access

Every pixel is procedural: the sky gradient, sun path, clouds, stars, treeline, water streaks, grain, and all 46,656 birds. The only external requests are two Google Fonts, Sora and Karla. The social preview image is a frame of our own render. If you ask for reduced motion, the simulation warms 240 steps and holds one composed still; nothing on the page animates. If WebGL2 or float render targets are missing, a canvas-2D plate of the same distribution takes over, labelled honestly in the viewfinder. The canvas carries a full text description for screen readers.

What each pass caught