Signal Field
A weekend question that became a six-week piece: what does a decade of earthquake data look like if you stop plotting it and start sculpting it? Two million points rendered as a single displaced mesh you can fly through at 60fps.
What it moved.
2.1M
Points rendered
Single draw call
60fps
Desktop
Integrated graphics
1
Draw call
For the entire field
6
Weeks
Concept to publish
The problem.
The source dataset is 2.1 million records. Every conventional charting approach either aggregates the interesting detail away or collapses under the point count.
The story is in the relationship between depth, magnitude and time, which is three dimensions plus geography — more than a flat chart can hold honestly.
It had to run on a laptop in a browser tab, not a workstation, or nobody would ever look at it.
What I built.
Data is baked into a floating-point texture at build time. The vertex shader reads it and displaces a plane, so the GPU does all the work and the CPU does almost none.
Magnitude drives displacement, depth drives colour through a perceptually uniform ramp, and time drives a travelling wave you can scrub.
One draw call for the entire field. Camera movement is decoupled from data updates, so scrubbing a decade never drops a frame.
How it fits together.
A deliberately small piece of software. One scene, one mesh, one material, and a build step that turns CSV into a texture.
Build
Node script parsing the source CSV, normalising it and packing values into an RGBA float texture.
Render
Three.js scene with a custom ShaderMaterial. Displacement, colour and the time wave all live in GLSL.
Interaction
Damped orbit controls plus a scrub timeline. GPU picking resolves the point under the cursor.
What it does.
Two million points, one draw call
The whole dataset is a single mesh, displaced entirely on the GPU.
Scrubbable decade
Drag through ten years and watch the surface breathe as events accumulate.
GPU picking
Hover any peak for the exact record behind it, resolved by rendering ids.
Perceptual colour ramp
Depth uses a uniform ramp so equal colour steps mean equal depth steps — the data is not lied about.
Graceful fallback
No WebGL2 gets a static rendered still and the underlying data table, not a blank canvas.
Seen from the inside.
Drag or scroll horizontally — 3 frames
A look at the code.
Displacement vertex shader
Where it got hard.
Texture precision
What broke
Packing magnitude into eight bits produced visible banding — the surface looked terraced where it should have been smooth.
How it was fixed
Moved to a half-float texture and normalised per channel against the real data range. Banding gone, memory cost acceptable.
Mobile GPUs
What broke
The full mesh melted mid-range phones, dropping to about eight frames per second and heating the device.
How it was fixed
Detect the renderer and drop to a quarter-resolution mesh with a simplified fragment shader. Visually close, and comfortably 60fps.
Scrubbing recomputed everything
What broke
The first version rebuilt geometry on every timeline change, which made the scrubber unusable.
How it was fixed
Time became a uniform. Scrubbing now changes one float, and the shader does the rest.
What I owned.
Everything — data pipeline, shaders, interaction and the writing that accompanies it.
How it ran.
Data
Week 1
Sourcing, cleaning and working out what the dataset could honestly support.
Shader
Weeks 2–4
Displacement, colour ramp and the time wave.
Interaction
Week 5
Camera damping, GPU picking and the scrub timeline.
Polish
Week 6
Mobile tier, fallbacks and the written piece.
What I took from it.
Moving work to the GPU is often an architecture decision, not an optimisation. Time-as-uniform was the whole project.
Perceptual colour matters. The first ramp looked better and misrepresented the data, which makes it worse.
A fallback that shows the underlying table respects the reader more than a 'browser not supported' card.
Constraints made this finishable. One scene, one mesh, six weeks, done.
What I'd do next.
WebGPU compute pass for on-the-fly filtering by magnitude band.
A guided narrative mode that flies the camera through notable events.
Next case study
RespiraScan AI
AI-powered respiratory bacteria classification using Deep Learning.