Particles

Ten thousand embers, one draw call: a particle system treats every ember as a single vertex given a life — position, velocity, age — rewritten in place every frame inside one persistent buffer. Nothing is allocated, nothing is freed; death is a respawn.

Core Idea

The fountain above is one object and one draw call, whether the slider says 500 or 10,000. That’s the entire reason particle systems exist: ten thousand meshes would drown the CPU in draw calls (CPU-bound, the classic way), but ten thousand vertices travel in a single buffer and the GPU draws them together.

The terminology is a ladder: a vertex is a coordinate; a point is that coordinate rendered as a small camera-facing sprite; a particle is a point given a life — movement, aging, fading. The behavior is the particle; the dot is just its costume.

A Particle Is a Row

Each particle is nothing but a row of numbers across a few parallel arrays:

Attribute Does
position where it is now
velocity where it’s going (vector addition, every frame)
age, lifetime when it will die
seed per-particle variety (size, tint, timing)

No faces, no edges — unlike a mesh, points have no connections between them, which is exactly right for smoke, sparks, rain, dust: volumetric stuff with no surface. Each point renders as a billboard — a tiny screen-aligned sprite that always faces the camera — textured through the material (a soft radial glow in the demo), and with additive blending overlapping embers sum toward white: glow for free.

The Loop: Integrate, Fade, Recycle

Every frame, for every particle: p += v·dt, v += g·dt, age += dt — and when age reaches lifetime, the particle dies by having its own cells rewritten with a fresh position and velocity at the emitter. Scrub to the death above and watch: nothing is allocated, nothing is freed. The arrays were built once, at maximum size; the garbage collector never hears about any of it, which is what keeps frame times flat. Even the hero’s count slider allocates nothing — it only moves the draw range over a buffer that already exists.

The GPU sees one upload per frame — the rewritten positions flagged with needsUpdate — the one legitimate exception to buffers’ upload-once rule, paid deliberately and kept small. Fade curves are plain interpolation: the demo’s quick-in/slow-out is two lerps on the particle’s age / lifetime.

An Emitter Is Just Code

There is no emitter object anywhere — “emitter” is the name for whatever logic writes a newborn’s cells: a fixed point for a fountain, a box for snow, a mesh surface for magic. Change the spawn rule and the same ten thousand rows become rain, a galaxy, or a swarm.

When the CPU Becomes the Ceiling

The loop above touches every particle in JavaScript — comfortable to ~10,000 at 60 fps. Past that, the update itself moves to the GPU: positions computed in a shader from a time uniform, or in compute shaders under WebGPU, where millions of particles cost the CPU nothing per frame. The data model doesn’t change — it’s the same rows, updated on the other side of the bus.

Connections

A particle system is the buffers article made kinetic — persistent typed arrays, rewritten in place, drawn as THREE.Points in Three.js. The physics is vector arithmetic at its plainest (velocity added to position, gravity added to velocity), the fades are interpolation on age, and the sprite’s transparency and glow are handled by the same fragment stage that runs the shading recipe on meshes.