Sundial
One open plaza and a sun on a scripted clock: a colonnade whose shadow crosses a cascade boundary, a plinth resting on the ground so its contact is checkable, and three counters hanging at graded heights so a penumbra that widens with distance is a thing you can look at. Flat and untextured on purpose — texture detail is exactly what hides a shadow artefact. Not a game: the shadow is the content.
the shadows, from the page
Every knob the sample binds to a key, as something a finger can reach.
Natively these are keys and rows on the pause panel. A seam that can only be
nudged by , and . and a sun that can only be stopped
by P are controls a phone cannot reach at all. The filter and the
seam and the two bias counts below drive the same
r_shadow_* console variable the key writes — one cell, three
writers, no copy of the state on this page. The sun's two are the fixture's
own clock rather than a setting, so they are taken up on its next fixed
step; the label is the request either way, and everything else here shows
what the engine holds after it wrote. The shadow atlas and the
cascade tint are a third kind again: they are the engine's own debug view,
one cell shared with every other sample and with the
debug_view console command — so putting one of the two up takes
the other down.
- Which filter the shadow runs
- The comparison seam
- …
- …
- …
- The sun's clock
- …
- Show the shadow atlas instead of the picture
- Tint the picture by the cascade its shadow came from
- Every knob and the clock back to where a run opens
what you are looking at
Four things, and each one is a shadow claim you can check.
The open pavement at a grazing sun. A large flat plane lit from a few degrees above the horizon is where shadow acne shows first, and the sweep puts the sun there at both ends of its arc. What you should see is an unbroken lit surface — a stipple or a moiré across it is the depth bias being too small for the angle.
The plinth's contact with the ground. It rests on the pavement, so the shadow has to start exactly where the plinth does. A gap between the two is peter-panning: the same bias that cures acne, pushed far enough to detach a shadow from the thing casting it. The two artefacts pull in opposite directions, which is why they are looked at together.
The colonnade crossing a cascade boundary. Its shadow runs from near the camera to far away, over the split between two cascades. A step in the shadow's softness or its offset where it crosses is a seam between the two maps; edges that shimmer as the sun moves are cascades that are not snapped to whole texels, which is the claim a still frame cannot check at all.
The three counters at graded heights. Each hangs a different distance above the pavement, so its penumbra should be visibly wider the further it is from what it lands on. That is the whole point of contact hardening, and cycling the filter to the box or disc rungs is what shows the difference: those two are the same width everywhere.
controls
Nothing to play. There is a camera, and there are the knobs above.
The page opens on the fixed pose the golden frames are taken from, with the sun a tenth of the way into its sweep. Swapping to the free camera is a row on the pause menu rather than a key, because the two cameras share one lens — a free camera with a lens of its own would give you a frame nobody could hold against the reference.
-
Swap between the fixed poses and the free camera
ESC, then ENTER on
CAMERA - Fly the free camera W A S D, Space and Shift for up and down, the arrows to look
- Cycle the filter F
- The seam: raise it, then walk it X, then , and .
- Walk the constant bias, and the normal offset [ ], then ; and '
- Walk either count out to the far end of its range 9 0 and 7 8 — the same two counts, eight texels to the press instead of half of one, which is how far out the plinth loses its shadow altogether
- Show the shadow atlas, or the cascade the frame sampled T, C
- Stop the sun, scrub it, and everything back P, - =, R
- Pause and resume Esc
- Fullscreen F11
- Debug overlay F3
- Debug console `
- Pause, fullscreen and the debug overlay keys — unless a control for them is listed above
- Debug console, and its on-screen keyboard the CONSOLE button, once you have touched the canvas
why this one is worth publishing
Because a shadow reviewed from stills is a shadow whose artefacts ship.
The engine picks its lighting path from what the device offers. WebGPU
exposes no ray query, so this page draws the plaza through
LightingPath::Rasterised by construction — which is the honest
answer for a fixture whose ray-traced shadow rung is not built yet, and it
leaves the cascaded shadow map carrying the whole picture. That is the tier
being compared here: three filters, either side of one seam, in one frame,
under a sun that moves.
Press F3 for the receipts. The panel names which geometry path, which binding model and which lighting path this frame was drawn through, which effects survived the four layers that resolve them, which filter each side of the seam is running, and what the atlas render and the scene draw cost — so a picture you think looks wrong comes with the arm that drew it.
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.
The sun's clock is none of those. It counts fixed steps and reads no wall clock at all, which is what makes tick k the same sun in every process and on every machine — so a page running at a tenth of the speed draws exactly the frames a desktop does, just fewer of them per second.
There is no fourth difference about saved data: this demo keeps nothing,
because there is no score to keep. It loads nothing either — the plaza's
geometry is built from literals in apps/sundial rather than
read off a disk, and the shaders and the font atlas are compiled into the
module.
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.