HomeArticlesRendering & Graphics

GPGPU Particles: A Simulation Running Entirely on the GPU

Ping-pong float textures turn a fragment shader into a physics engine for hundreds of thousands of particles — no JavaScript loop in sight.

mysimulator teamUpdated June 2026≈ 8 min read▶ Open the simulation

The problem with a million particles in JavaScript

A CPU running JavaScript updates particles one at a time in a loop: read position, apply a force, integrate, write it back, move to the next particle. At 100,000 particles and 60 frames per second that is six million updates a second on a single thread — achievable for simple motion, but it collapses the moment the update involves neighbour lookups or per-particle branching. A GPU takes the opposite approach: instead of one fast core doing everything in sequence, it has thousands of small cores that each do the same tiny program on a different piece of data at the same time. If a particle update can be written as "the same short function applied independently to every particle," the GPU can run all of them essentially at once.

Particles as pixels: the ping-pong texture

The trick that made general-purpose GPU computing possible before compute shaders existed — and still the simplest cross-platform approach in WebGL — is to store particle state as pixels in a floating-point texture. A 512×512 texture holds 262,144 particles, one per texel, with the RGBA channels carrying position (x, y, z) and perhaps age or lifetime. Updating all particles becomes rendering one full-screen quad: the fragment shader runs once per texel, reads the previous position and velocity textures, applies the physics, and writes the new state to an output texture.

// simplified GLSL update fragment shader, one texel = one particle
vec4 pos = texture2D(texPosition, vUv);
vec4 vel = texture2D(texVelocity, vUv);

vec3 force = computeForce(pos.xyz);   // curl noise / gravity / boids rule
vel.xyz += force * dt;
pos.xyz += vel.xyz * dt;

gl_FragColor = pos;   // written into the OUTPUT texture, never the input

Because a GPU cannot safely read and write the same texture in one draw call — fragments are not guaranteed to execute in any particular order, and a read-after-write race would corrupt the simulation — the renderer keeps two copies of each state texture and alternates: this frame reads texture A and writes texture B, next frame reads B and writes A. That alternation is the "ping-pong" the technique is named after, and it is the single idea that everything else in a GPGPU simulation is built on top of.

live demo · a swarm of particles updated in parallel● LIVE

Four behaviours, one pipeline

Only the force function inside the update shader changes between behaviours; the ping-pong plumbing stays identical. Curl noise takes the curl (rotational component) of a 3D Perlin/simplex noise field, which is divergence-free by construction, so particles following it swirl like smoke without ever clumping or thinning out. N-body gravity sums the pull of every other particle (or of a handful of attractor points, which is far cheaper and visually similar) with a softened 1/(r²+ε) falloff to avoid singularities at close range. Boids evaluates the three classic steering rules — separation, alignment, cohesion — against particles found in a small neighbourhood, approximated on the GPU with a coarse spatial grid rather than a true neighbour search. The Lorenz attractor mode integrates the same three chaotic ODEs that drive the standalone Lorenz simulation, independently per particle from a different starting point, so the swarm traces the whole butterfly-shaped attractor at once instead of one single trajectory.

Reading the result back to draw it

The final position texture never needs to leave the GPU. A vertex shader assigned to a large point-sprite mesh looks up its own particle's position by sampling the same texture — using its vertex index to compute a UV coordinate — and places the point accordingly, entirely avoiding the extremely slow round trip of reading GPU memory back into JavaScript. This is why the technique scales so well: the CPU's job each frame is reduced to issuing two draw calls (update, then render), regardless of whether there are ten thousand particles or a million.

Frequently asked questions

What does GPGPU mean in a particle simulation?

General-Purpose computing on the GPU: using the graphics card's massively parallel shader cores for arithmetic rather than pixel colour. Here it means storing particle state in textures and updating every particle's physics in a fragment shader instead of a JavaScript loop.

Why ping-pong between two textures instead of writing in place?

A GPU cannot safely read and write the same texture within one draw call — there is no guaranteed order between fragments. Reading last frame's texture while writing to a separate output texture, then swapping which one is 'current' next frame, avoids that race entirely.

Why can this simulation run hundreds of thousands of particles at 60fps when a JavaScript loop could not?

A GPU fragment shader runs the same short program on thousands of pixels simultaneously across many shader cores, while JavaScript executes one particle at a time on a single CPU thread. Moving the per-particle update into a shader turns an O(n) serial loop into a parallel operation bound by texture bandwidth, not particle count.

Try it live

Everything above runs in your browser — open GPGPU Particle System and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.

▶ Open GPGPU Particle System simulation

What did you find?

Add reproduction steps (optional)