Making of Memento Mori

A visual meditation on impermanence, built from particles that were never meant to arrive.

The Concept

Every particle in this piece is a life. It spawns dim, unaware, somewhere in the dark — then begins its chase toward a single point of light: the attractor. As it accelerates, it brightens. Trails intensify. Urgency builds. And then, right as it reaches the thing it was chasing, it fades to nothing.

A new particle spawns immediately to take its place.

The chase was never about arrival. Arrival is dissolution. The meaning lives in the pursuit — in the accelerating, brightening moment before the ember goes out. Their final ember kindles another.

Building It

Memento Mori was built on the WebGL foundation from an earlier piece, reusing shader helpers, noise functions, and post-processing (bloom, gaussian blur) rather than starting from zero. The particle lifecycle — spawn, accelerate, intensify, fade, respawn — runs on simple attractor physics, layered with subtle noise-based turbulence so the motion never feels too clean or mechanical.

A few of the harder problems along the way:

  • Getting the attractor’s pulse to stay perfectly circular regardless of aspect ratio — clip space isn’t square on non-square viewports, so without correction, the pulse stretched into an oval on wide screens.
  • Respawn logic — particles originally only reset once they reached the center; out-of-bounds particles could drift off-canvas and just… vanish, breaking the cycle. Added proper bounds checking so nothing escapes the loop.
  • Balancing noise and physics — enough turbulence to feel alive, not so much that the central pull of the attractor gets lost.

The Architecture

This isn’t a single sketch file — it’s a self-contained rendering engine, built modular from day one so nothing here has to be rebuilt from scratch for the next piece.

config/       — ConfigManager, schema-driven settings, GPU metrics, responsive handling
shaders/      — Bloom, chromatic aberration, film grain, gaussian blur, shared definitions
utils/        — noise functions, vector math
webgl/        — buffer utils, fullscreen quad, shader utils

At full scale, the piece runs 40,000 particles simultaneously, with performance handled adaptively rather than assumed:

  • Schema-driven configuration — every tunable parameter (there are close to 40 of them, spanning attractor physics, particle behavior, noise, and four separate post-FX passes) is defined once, centrally, with its range, step, category, and label. The Tweakpane GUI builds itself entirely from that schema — add a parameter, and a matching control appears with zero manual UI code.
  • GPU detection and performance scaling — the engine profiles the device it’s running on and adjusts accordingly, including a dedicated set of mobile/tablet defaults (particle count drops to 12,000, blur radius and bloom quality scale down) so the piece stays smooth rather than just hoping for the best on weaker hardware.
  • Live performance monitoring — FPS, per-pass frame timings, and GPU metrics are tracked in real time, which made it possible to actually see where the frame budget was going rather than guessing while tuning bloom quality or particle count.
  • Resolution-independent rendering — canvas resolution multiplier and several parameters (particle size, blur radius) explicitly scale with resolution, so the piece looks correct whether it’s rendering at 1x or 2x device pixel ratio.
  • A shared post-processing pipeline — bloom, chromatic aberration, film grain, and gaussian blur are standalone, composable shader modules, not baked into one giant fragment shader. Each is reusable independently in future pieces.
  • Reusable WebGL scaffolding — buffer/framebuffer utilities, fullscreen quad setup, and shader compilation helpers don’t know or care what they’re rendering; the particle system is just one more consumer of that scaffolding.

See the Pen Memento Mori by Tibix (@Tibixx) on CodePen.