WebGPU City
A billion triangles of BIM buildings at 60 FPS in Safari on a laptop, with two draw calls and no browser flag.
- Built
- Stack
- TypeScript, WebGPU, WGSL, Python, ifcopenshell, Vite
How it works
A reply to a claim going around: that GPU-driven rendering in the browser is waiting on WebGPU's multi-draw extension, which today needs a Chrome flag. It is not. 224 IFC buildings from the TUM GNI BIM dataset, tiled six times over into 1.48 million instances and 1.004 billion triangles, render at 60 FPS in Safari on an M2 Pro with 14 ms of GPU time and none on the CPU. Every mesh is cut into 128-triangle clusters. A compute pass culls instances, picks a level of detail per instance and appends that level's clusters to a candidate list; a second pass tests each cluster against the frustum, a contribution threshold and a Hi-Z depth pyramid, then compacts the survivors into one index buffer. One drawIndexedIndirect draws the frame, and a second late pass draws what the previous frame's depth hid. The reference demo needs multiDrawIndexedIndirect behind a flag on an RTX 40-series to reach 29 FPS on the same scene.
Rebuilt from scratch after seeing Christopher Diggins' one-million-object WebGPU demo and its case for multi-draw. None of that code is used here.
Notes
- No playable build here: the scene file is 1.15 GB, which is a 1 GB download per visitor, so the Lab gets the flythrough video and the numbers and the viewer runs locally
- Two-level culling on the GPU: an instance pass does frustum, contribution and LOD selection, and only the chosen LOD's clusters go on to the per-cluster pass, so 14 million clusters cost 1.1 million tests
- Two-pass Hi-Z occlusion: clusters visible last frame draw first, a max-depth mip pyramid is built from that, and the rest are tested against it and drawn in a second indirect call, so the result is exact for the current frame with no popping
- The index buffer is rebuilt every frame by one workgroup per visible cluster; index values encode the cluster slot in the high bits and the vertex in the low byte, so the post-transform vertex cache still works and a pure vertex-pulling draw is 2.5x slower
- Per-mesh LOD chain, 25% and 6% quadric-simplified plus a twelve-triangle box, generated offline and picked on the GPU from projected size; the far city draws 14 million triangles rather than the billion it contains
- The reference path is in the same viewer for comparison: one drawIndexedIndirect per instance, which is what multi-draw exists to batch, and on this machine it is 1.48 million draw calls, 345 ms of CPU and 3 FPS
- IFC to clusters offline with ifcopenshell: geometry deduplicated per representation, Morton-sorted triangles chunked into clusters of at most 256 unique vertices, one binary scene file the viewer maps straight into GPU buffers
- Frame time tracks instances in view, not scene size: six times the city costs 2.4 times the GPU time; the missing piece for a flat cost is one merged proxy per building tile for the far field