Overview
Open the page, load any GLB/GLTF mesh -; or use the built-in demo scene -; and fly around a world rendered entirely in two colors. It's one HTML file, no install, three dither algorithms with live controls. The point is partly aesthetic and partly a little experiment: putting Bayer, screen-space noise, and Atkinson side by side under the same camera motion makes the difference between them obvious in a way a still image never will.
It came out of having a pile of meshes exported from WorldLabs Marble and wanting to do something with them that wasn't just another smooth-shaded preview.
How It Works
The 3D scene renders to a small offscreen buffer (256-;640px wide). Those pixels get pulled back to the CPU, converted to luminance, and thresholded to 1-bit there, then upscaled nearest-neighbor for the chunky edges. The dithering is the interesting bit, and the three methods behave very differently under motion:
- Bayer (ordered 8×8) and IGN (interleaved gradient noise) threshold purely as a function of screen position. Deterministic per pixel and temporally solid -; the pattern is welded to the screen while the world slides through it.
- Atkinson re-runs full error diffusion from scratch every frame. That sequential dependency chain -; each pixel's error feeding its neighbors -; is exactly what makes it unstable under motion. The result is the classic temporal "boil."
// screen-space thresholds: welded to the screen, stable under motion bayer8[(y & 7) * 8 + (x & 7)] // Bayer (52.9829189 * frac(0.06711056*x + 0.00583715*y)) % 1 // IGN
It's built on Three.js r160, with OrbitControls and
GLTFLoader from the addons, all loaded from CDN. The full pipeline is:
3D scene → low-res offscreen WebGL target → CPU readback → luminance
→ 1-bit dither → nearest-neighbor upscale → 2D canvas. The CPU readback
every frame is the performance ceiling, which is why the resolution drops help when
Atkinson gets heavy on a big mesh.
Panel controls let you swap the algorithm, change the offscreen resolution, dial exposure and gamma into the right range before thresholding (which matters a lot when you only have two values), invert to paper mode, and set autorotate speed.
Current Status
Live and working -; there's a demo scene (a ring of columns around a central icosahedron head, a small Manhole/Myst nod, with an obelisk on the horizon for the dither to chew on at distance), a sample Marble mesh you can download and load, and a recorded GIF in the repo. Loads any GLB; splats aren't supported (mesh only).
- Three dither algorithms with live exposure/gamma/resolution controls.
- Auto-demo scene plus drag-and-drop GLB/GLTF loading.
- Known ceiling: per-frame CPU readback -; drop to 256px if Atkinson lags on a large mesh.