Meridian15 Lab · Engine Teardown

420,000 particles, zero overdraw: a software rasterizer on the GPU

Stack enough particles and they smear into fog. This one holds 420,000 sharp, individually-resolvable specks at 60fps, over voids that are actually black. A WebGPU compute software-rasterizer for the Meridian15 hero. The architecture and the decisions, not the kernel.

420,000 particles 60fps zero overdraw true-black voids WebGPU compute TSL → WGSL
TL;DR

One GPU compute thread per particle packs a 16-bit depth and an rgb565 color into one 32-bit word and atomicMaxes it into a plain storage buffer used as a framebuffer. Depth sits in the high 16 bits, stored inverted (1 - ndc.z), so a single atomic does the depth test and the composite at once: nearest particle wins each pixel, no blending, no draw order. A fullscreen pass reads the buffer back by integer pixel index and resolves it.

Fig. 1: the pipeline, one atomic write per particle, one buffer read per pixel

420k particles one thread each COMPUTE KERNEL project → pixel pack 16-bit depth | rgb565 atomicMax → framebuffer depth inverted: nearest wins storage buffer = framebuffer (W×H) one 32-bit word / pixel RESOLVE fullscreen quad screenCoord → index unpack → pixel screen the 32-bit word depth : 16 rgb565 : 16
Sprites / GL points. Overdraw → fuzz, fireflies, gray "black".
Compute raster. One particle per pixel → crisp, true black.

The problem: density fights clarity

Every particle system built on sprites or GL points hits the same wall. To get density you stack thousands of additive billboards, and the moment they overlap they blend. Overdraw turns the image into fuzz, bright edges bloom into fireflies, and "empty" space is never actually black because dozens of near-transparent quads are still accumulating there. You can tune it for hours and barely move the ceiling, because the ceiling is the rendering model, not the parameters.

I wanted photographic density: hundreds of thousands of points, each a sharp, individually-resolvable speck, with true black where there is nothing. On WebGL that is not a tuning problem. It is impossible by API.

The idea: stop blending, start compositing

Instead of drawing particles as geometry and letting the blender sort it out, treat the frame as a buffer in memory and composite into it by hand. One GPU compute thread per particle:

  1. Project the particle to a pixel.
  2. Pack it into one 32-bit word: a 16-bit depth in the high half, an rgb565 color in the low 16.
  3. atomicMax that word into a storage-buffer framebuffer at the pixel's flat index.

Depth is stored inverted (1 - ndc.z), so the nearest particle produces the largest word. That means a single atomicMax performs the depth test and the composite in one operation: nearest-wins per pixel, deterministically, with no blending and no draw-order dependence. Fireflies are eliminated structurally, exactly one particle writes each pixel, so there is nothing to stack. A second atomic buffer counts hits per pixel, which becomes a controlled glow term in the resolve instead of uncontrolled additive bloom.

Then a fullscreen pass reads the framebuffer back. One quad, whose fragment takes the integer screenCoordinate, turns it into the same flat index, reads the packed word straight out of the storage buffer, and unpacks depth and color. Zero hardware overdraw. Exactly one particle decides each pixel. Crisp specks, true black voids, roughly 10x more particles than the sprite approach can hold before clarity collapses.

This is software rasterization, the technique that predates hardware rasterization, moved onto the GPU because compute shaders finally allow the scattered atomic writes to memory that the fixed-function pipeline will not give you.

Why WebGL cannot do this

A hard API boundary, not a performance gap. WebGL2 has no compute stage, no writable storage buffers, and no atomics. There is no way to express "every particle scatter-writes into a shared framebuffer with a depth-aware atomic." WebGPU added all three. This isn't WebGL tuned harder, it's a different class of renderer that cannot exist without compute and storage atomics. The whole thing is TSL (Three.js Shading Language, r184) compiling to WGSL on three/webgpu.

The parts that actually hurt

The headline is simple. The reasons it took real time are the WebGPU-specific traps:

Where it stands

It ships at 420,000 particles at a steady 60fps at the device's real framebuffer resolution, and it is the hero of fifteenthmeridian.com. That is the conservative setting for broad-device performance; the engine scales past a million particles at 60fps on a strong GPU. Honestly it is the small end of what I have been building, there is a lot more that isn't public yet.

To be straight about lineage: this is atomic-min/max framebuffer compositing, the compute point-cloud rasterizer family. The idea is not a secret, it has prior art and other studios have shipped their own. The work here is the execution: the inverted-depth packed-word composite, the int-atomic velocity grid, the binding-layout and buffer-budget engineering, and holding a clean 60fps through all of it. Once it runs, the shader is recoverable by anyone who wants to read it. That is fine. The interesting part was never the bytes, it was building the thing.

Built in-house at Meridian15

Engineered in-house, hand-written from the compute kernel up.

Real-time graphics built in-house at Meridian15. The rasterizer, the engine, and every shader behind it are hand-written. If you are building at the WebGPU frontier and want a hand, or just want to compare notes, the door is open.