I've been chasing GPU cycles since machines had 64K of RAM, and every few years the browser quietly swallows another thing that used to need a workstation. WebGPU was the one that made me want to find the ceiling. So I set myself a question: how much of a genuinely serious flood tool could I put inside a single browser tab, with no server behind it, and still have it feel like a tool rather than a toy?
The answer is Don Watch, a live flood-response digital twin of Sheffield's River Don.
Try it: https://donwatch.fortitude-omnis.group (best on a desktop browser with WebGPU)
It rebuilds the Don valley from open LIDAR elevation, drapes the real river over it, and replays the November 2019 Sheffield flood hour by hour from the Environment Agency's own gauge records. The water you watch rise is the level that was recorded at the time. Scrub the timeline and the flood tracks the readings. Switch to live mode and it runs against the EA's current feed instead.
Then the parts that were interesting to build.
The rendering runs on WebGPU
Terrain, water and post-processing all go through three.js on its WebGPURenderer, shaded in TSL (Three's node shading language) rather than hand-written GLSL strings.
The water isn't a flat plane with a normal map. It's a level-driven flood surface: the sim tracks the gauge height at the current instant, and a breadth-first fill spreads out from the real river vertices through submerged ground only, so isolated dips away from the river never fill. That height field is written to a texture the shader samples for both displacement and depth.
The fiddliest bug was z-fighting at the shoreline. The draped water and the terrain are two different resolutions of the same heightfield, so at the waterline they interpenetrate. A depth-buffer bias can't fix surfaces that actually cross, so the fix was geometric: lift the water a couple of metres so it always sits cleanly above the ground, which is sub-pixel at the camera distance and invisible.
The AI runs on your GPU too
The pitch a lot of "AI X" tools make is a thin front end sat in front of somebody else's cloud. I wanted the reverse. The model, an open model in the 1-to-2-billion range, loads straight into the browser through WebLLM and runs on the visitor's own GPU. No server, no API key, nothing leaving the machine. The inference backend sits behind an interface, so WebLLM is the default and there's a coherent scripted fallback for devices that can't run it.
There's a button that cuts the network, and everything keeps going, because the model is cached in OPFS and the flood data came down with the page.
Agents that show their working, over deterministic maths
Five small agents work the flood while it plays: an orchestrator hands out the job, two ingest agents read gauge and rainfall, a forecast agent projects ahead, a risk agent ranks the sites in the water's path, and a comms agent drafts a plain-language brief. Every step streams into a live reasoning feed and lights up a delegation graph.
The important design decision is what the model is not allowed to do. The hydrology, the projection and the risk thresholds are deterministic TypeScript. The model reads the results and writes them up. It doesn't produce the numbers. That's the honest place for a language model in a safety-adjacent tool, and it's why the whole thing is framed as a decision-support projection rather than a flood warning.
Offline by construction
It ships as a Next.js static export and installs as a progressive web app. A service worker caches the shell, the data and the model, so once it has run it runs with the network cut, on a locked-down laptop or in a field office with no signal.
The data is real and cited
Gauge and rainfall from the Environment Agency, the river, landmarks and place names from OpenStreetMap, terrain from open elevation data, and the official flood-warning areas and live sensors from the EA flood-monitoring API. Nothing is fabricated, and where a series is reconstructed it says so.
A few things I learned
- Real sensor networks are sparse. Central Sheffield has a handful of river-level gauges, not a dense grid. You show what's actually there and resist the urge to invent more.
- Serving a Next static export under a sub-path means every runtime fetch that basePath doesn't cover needs the prefix, and the service-worker scope moves with it.
- WebGPU plus a local LLM in one tab is a lot of GPU contention. Bracketing generation so the render loop can yield turned out to matter.
Have a poke around and let me know what you think: https://donwatch.fortitude-omnis.group
Happy to go deeper on the WebGPU or the on-device model side in the comments.
Top comments (0)