Skip to content

Column simulation

Source: src/gpu/pipelines/compute-step.ts

The simulation is a compute pass: one GPU thread per column, advancing the Column[] storage buffer. It’s the only pass that writes simulation state.

The compute shader runs with a workgroup size of 64; each invocation handles one column, guarded against overrun:

const columnCount = floor(resolution.x / cellSize);
if (gid.x >= columnCount) return;
const column = columns.$[gid.x];

Each step moves the head down by the column’s speed (in cells):

headY=headY+speed\text{headY}' = \text{headY} + \text{speed}

stepRate (Hz) controls how often this runs, decoupled from the render frame rate — the renderer interpolates between steps via stepProgress.

A column respawns when its head has fallen off the bottom and a per-step random roll passes:

const offscreen = nextHeadY * cellSize > resolution.y;
if (offscreen && randf.sample() > density) {
nextHeadY = 0;
nextSeed = randf.sample() * U32_MAX;
}

Two things worth noting:

  • The density semantic is inverted from intuition. density is the probability a column does not respawn this step. Higher density → respawn rarely → the screen empties out (sparser). At density = 1, randf.sample() > 1 is never true, so columns never respawn and the field drains. This matches the original 2D reference’s behavior.
  • Respawn rerolls the seed. A new seed means a new set of glyphs and a new brightness-jitter pattern for that column’s next descent (see Glyph rendering).

The RNG is seeded per thread, per frame from the column’s stored seed and the current time:

randf.seed2(vec2f(column.seed / U32_MAX, uniforms.$.time));

Seeding on time is deliberate — it’s why the respawn roll differs frame to frame. (It’s also why the paused settle loop advances time per iteration: a frozen time makes every roll identical and the field drains to black.) randf comes from @typegpu/noise; its functions are GPU-marked, so they compose into our 'use gpu' compute shader.

speed, depth, and tailLength are set once at column creation (on the CPU) and never change during simulation — only headY and seed mutate here. The per-column constants are what give each column its place in the parallax depth field.