fieldplay3D

Vector field
A 3D take on anvaka/fieldplay. Particles are integrated in a WebGPU compute shader; trails are an HDR frame buffer that fades.

How it works

Every particle asks your field for its velocity, takes a step, and leaves a trail. The field is a WGSL function:

fn get_velocity(p: vec3f) -> vec3f {
  return vec3f(-p.z, 0.0, p.x);   // orbit the y axis
}

Optionally colour them yourself (choose Colour by → get_color()):

fn get_color(p: vec3f, v: vec3f) -> vec3f {
  return turbo(length(v) * 0.5);
}

Uniforms

u.timesimulation time (sum of time steps)
u.frameframe counter
u.cursorthe mouse, projected onto the plane through the orbit centre that faces the camera
u.cursorDown1.0 while a mouse button is held
u.boundsMin u.boundsMaxthe box particles live in

Helpers

noise(p)gradient noise, about −1…1
noised(p)vec4f: value and its gradient
fbm(p)four octaves of noise
curl(p)divergence-free noise — smoke and swirls
hash3(p) hash1(p)random per integer cell
rotateX(p, a) …Y …Zrotate a vector
turbo(t) viridis(t) hsv2rgb(c) palette(t, k)colour maps
PI TAUconstants

Pulse — sync to music

Under Pulse → Sync to: Aux Cord bot reads the beat map of whatever the bot is playing from its localhost endpoint (the bot must run on this machine; trim the latency with Sync offset). Shared audio analyses a tab or your screen's sound live — pick the Discord window and tick share audio. Microphone listens to the room. The knobs punch speed, glow and zoom on the beat, and your field sees the music too:

u.audiovec4f: bass, mid, high, level — each 0…1
u.beat1.0 on a beat, decaying to 0
u.phase0…1 from one beat to the next
u.bpmtempo
return v * (1.0 + 2.0 * u.beat) + normalize(p) * u.audio.x;

WGSL in one breath

let is immutable, var mutable. Types are inferred: let r = length(p);. Vectors are vec3f(x, y, z) or vec3f(0.0), swizzles work (p.xz). Write floats with a point (2.0) — a bare var a = 2; is an integer. Functions may be declared in any order. Ternaries are select(ifFalse, ifTrue, cond).

Saving your own

💾 saves the whole state — the field, every setting and the camera — into this browser under a name you give it, and it joins the top of the picker under Saved. Saving with the same name replaces it; 🗑 forgets the one you have selected. A saved field lives in this browser only, so send one elsewhere with the 🔗 link, which carries the same state in the address.

The cursor

Hold the left button over the flow and it is pulled toward the pointer; the middle button does the opposite. It is a bend rather than a shove — the force is proportional to the speed already there, so one strength reads the same in a gentle swirl and in a Lorenz attractor. Cursor → Left button switches it to push or swirl (and then the middle button pushes, or swirls the other way) or turns it off; Reach is a fraction of the box. The camera is on the right button and on shift, so orbiting never disturbs the flow.

Controls

hold leftpull the flow to the pointer
hold middlethe opposite — push it away
right-drag / one fingerorbit
shift-drag / ctrl-drag / two fingerspan
wheel / pinchzoom
spacepause
Rreset particles
Hhide the panel
Ffullscreen
ctrl+entercompile now

The link button copies a URL that holds the whole state — field, settings and camera.

WebGPU is not available

This page runs its particles in compute shaders, which need WebGPU. It works in Chrome and Edge 113+ on Windows, macOS and ChromeOS, Chrome 121+ on Android, and Safari 26 / Firefox 141+.

If you are on a supported browser, check that hardware acceleration is on (chrome://settings/system) and that the page is served over http://localhost or https.