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

Hovering over the flow pulls it toward the pointer, by default. It is a bend rather than a shove — the push is proportional to the speed already there, so one strength reads the same in a gentle swirl and in a Lorenz attractor. Cursor → Hovering switches it to push or swirl, or turns it off; Reach is a fraction of the box. Dragging belongs to the camera, so the flow is left alone while you orbit.

Controls

dragorbit
right-drag / shift-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.