● S4C WebGPU wasm32

Quarry

One quarry face, 120 metres across and receding 180, generated from hashed noise and decimated into a hierarchy of clusters. The camera walks down it and back, for ever. Not a game — the geometry is the content, and what it is here to show is a mesh changing detail while you watch and never catching your eye doing it.

quarry — crcbl
Starting…
Click the canvas first. A canvas has to hold keyboard focus to receive keys, and browsers will not start audio until you have interacted with the page. Clicking away pauses the game and lets go of whatever key was held — no platform sends the release for a key that was down when focus left.
Touch reaches the engine. A contact arrives as an ordinary pointer, and the first one is also the gesture a browser waits for before it will start audio. Whether this demo can be played with one is in its controls below — not all of them can. Switching tabs pauses the game and lets go of whatever was held: no platform sends the release for an input that was down when focus left.

what you are looking at

A dolly, a cluster hierarchy, and the seam between them.

The face recedes, and that is the whole reason it is shaped like that. A flat wall sits at one distance from the camera, so every part of it wants the same level of detail and there is nothing for a per-cluster decision to decide. This one runs 180 metres away from you, so the near benches and the far wall of the same mesh sit at screen-space errors an order of magnitude apart.

The camera is the dolly, running on the simulation clock. It covers ninety metres in thirty seconds — three metres a second, a fifth of the speed the free camera flies at — and then turns round and walks back up the face rather than cutting to the start. That is deliberate: a jump back to the beginning would put ninety metres of translation into one frame, which is precisely the artefact this sample exists to say does not happen.

Detail arrives, and it arrives without popping. The hierarchy is a DAG of clusters, each a decimation of the ones below it, and a level is chosen by comparing a group's projected error against a pixel budget. As the camera closes, that error grows and finer geometry is substituted in. The switch has hysteresis on it, so something sitting on the boundary does not flip back and forth between two levels frame after frame — which is what popping is, and which is the thing this whole sample exists to say does not happen.

On this page the choice is made once for the whole face, not once per cluster: see below for why a browser gets that arm and what it costs. So the substitution you are watching for is the face as a whole getting finer as the dolly closes, rather than a patch of rock a few metres ahead changing on its own.

The LOD view is where you can look at the choice directly. ESC and the LOD VIEW row tints geometry by the level it was decimated to instead of shading it. Because the level here is chosen per instance, that is one colour across the whole face rather than the patchwork a machine with a mesh stage shows — which is the comparison rather than a shortfall, and the clearest single picture of what the path difference actually means.

F3 is the receipts. The panel names the geometry path, the binding model and the lighting path this frame was drawn through, where the camera is, the triangle count the cut is a reduction of, the pixel budget it was selected under, and how many instances survived culling — read back off the GPU a few frames behind, and labelled with the frame they came from rather than printed as if they were this one's. Against the cluster count it says no amplification stage on this path, in words, rather than printing the zero the clearing pass left: on this page there is no per-cluster stage to count, and a zero there would claim every cluster had been rejected in a frame that drew all of them.

controls

Nothing to play. There is a camera, and there is a tint.

The page opens already moving, on the dolly. The other two cameras — the fixed pose the golden frames are taken from, and one you fly yourself — are rows on the pause menu rather than keys, because all three share one lens: a free camera with a lens of its own would give you a frame nobody could hold against the reference.

why this one is worth publishing

Because a browser can only draw it one way, and that is the point.

The engine picks its geometry path from what the device offers, and it has three. On a machine with a mesh stage it selects a level of detail per cluster, so one mesh can be drawn from several levels at once across its own surface. WebGPU exposes no mesh shader and no GPU-side draw count, so this page takes the third arm — GeometryPath::IndirectPerBatch — with no branch anywhere for it to take instead. That means the level is chosen once per instance and the whole face is drawn at one level at a time.

That is the honest picture of what a browser visitor gets, and it is the reason the page is worth having: the detail still arrives as the camera closes, the switch is still hysteretic, and the LOD view still shows you which level you are looking at — one flat tint rather than a patchwork, which is the comparison rather than a shortfall. Open the console and the heartbeat names the arm it resolved to once a second, so you do not have to take that on trust.

what is actually running

The native sample, with three differences the browser forces.

Start-up is polled across several frames, because requestDevice is a promise and the main thread cannot block on it. The frame loop is driven by requestAnimationFrame out here rather than by a loop inside the engine. And the clock is the browser's, because wasm32-unknown-unknown has no Instant.

There is no fourth difference about saved data: this demo keeps nothing, because there is nothing to keep. It loads nothing either — the face is generated from a hash of each lattice coordinate at start-up rather than read off a disk, so it is the same rock on every machine, and the shaders and the font atlas are compiled into the module.

It will run without a GPU, and it will crawl. This is one of the site's heaviest frames — thirty-two thousand triangles before the cut even starts, and a cull pass over the hierarchy every frame. Driven under Chromium's bundled SwiftShader it boots, opens a device and draws the face correctly, at roughly half the simulation rate the same build reaches on a discrete GPU. If the dolly is moving in slow motion, that is what you are looking at.
Open the console for the engine's log. The wasm module imports nothing, not even console.log, so log lines are queued inside wasm and drained by the page each frame. crcbl.audio(), crcbl.saves() and crcbl.logLevel(4) are there too.