Building a Production WebGPU Engine... for a psychotherapy practice?
Source: Dev.to
The Challenge: Where Noise Becomes Flow
When building the digital presence for Therapy Warsaw, we faced an unusual requirement. We didn’t want stock photos or static illustrations. We wanted something that felt alive—a generative texture that was always changing, but never demanding attention.
The visual metaphor was simple: complex patterns finding clarity. A field of noise, slowly organizing itself into coherent, flowing lines.
Technical Requirements
- Organic & Dense: ~10,000 interacting particles.
- Performance Critical: 60 FPS on mobile while users scroll.
- Resilient: Must work on 10‑year‑old laptops (WebGL2) and bleeding‑edge devices (WebGPU).
- Framework‑Free: No React, no Three.js. Just controlled, fluid logic.
Architecture: Dual‑Stack WebGPU + WebGL2 Engine
Off‑Main‑Thread Rendering
The first rule of heavy graphics on the web: Get off the main thread.
| Thread | Responsibility |
|---|---|
| Main Thread | DOM, accessibility, routing, UI state |
| Worker Thread | Physics, geometry generation, rendering via OffscreenCanvas |
Even if the physics simulation hiccups, page scrolling stays smooth. Communication happens via a dedicated messaging system that syncs visual “Presets” (colors, speed, turbulence) without blocking.
// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
const offscreen = canvas.transferControlToOffscreen();
// Hand ownership to the worker
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
WebGPU Implementation
We started with WebGPU because Compute Shaders are a natural fit for particle systems.
Compute Passes
| Pass | Purpose |
|---|---|
| Map Pass | Generates noise textures (Burn, Density, Void maps) |
| Flow Pass | Calculates the vector field |
| Life Pass | Updates particle ages and handles resets |
| Physics Pass | Moves particles based on flow vectors |
The key performance win: avoiding CPU‑GPU round trips. The entire simulation stays on the GPU.
WebGL2 Fallback Using Transform Feedback
WebGPU support is growing but not universal, so we needed a fallback that didn’t become a “dumb” fallback.
- Transform Feedback lets WebGL2 update particle positions in the vertex shader and write them back to a buffer, mimicking compute shaders.
- This approach preserves feature parity without over‑burdening the CPU.
Smooth Parameter Transitions: Critical Damping Spring System
When a user navigates between pages, the visualization morphs (colors shift, chaos changes, speed adjusts). Simple linear interpolation looked robotic, so we implemented a critical damping spring system:
function updateSpring(state, target, dt) {
const tension = 120;
const friction = 20;
const displacement = target - state.value;
const force = tension * displacement - friction * state.velocity;
state.velocity += force * dt;
state.value += state.velocity * dt;
}
Every frame we update ~20 spring‑driven parameters and upload them to a Uniform Buffer Object (UBO), producing transitions that feel physical rather than computed.
Efficient Trail Rendering
Rendering thick lines traditionally means generating two triangles (six vertices) per segment—expensive for long trails.
Our Approach
- Store only the head position of each line.
- Inside the vertex shader, run a loop (~60 iterations) to re‑trace the path backwards through the flow field, reconstructing the trail on the fly.
Pros: Massive bandwidth reduction (1 point per line, not thousands of vertices).
Cons: Higher ALU cost per vertex.
On modern GPUs, ALU is cheap; bandwidth is expensive. This trade‑off lets us render thousands of long, smooth trails on mobile devices.
Result
The final site, therapywarsaw.com, features a living background—a quiet texture that reflects the nature of the work while remaining performant across a wide range of devices.
Open Source
The engine is open source:
github.com/23x2/generative-flow-field
Feel free to explore the shader pipeline or the Transform Feedback implementation.