← Devlog

Devlog · September 24, 2026 · First shared on the Babylon.js forum

Sundial: Paged (Virtual) Sun Shadows for Babylon.js on WebGPU

A hillside in pOqpOq World on WebGPU, with tree trunks and a dappled canopy casting Sundial shadows
pOqpOq World on WebGPU: the tree trunks and the dappled canopy are Sundial shadows

We’ve been building a virtual shadow map for Babylon.js on WebGPU, and it’s public: Sundial. It’s MIT, it’s on npm, and it’s the default sun shadow for every WebGPU visitor to pOqpOq World.

Why it exists. Our worlds are forests and prim-heavy builds. On integrated GPUs, cascaded shadow maps were eating half the frame, and our watchdog kept switching shadows off for players on those machines. Sundial is built from the other direction.

What it is. One directional light, shadowed through a clipmap of 128×128 pages kept in a single depth pool. Each frame, a compute pass reads the camera’s depth and asks for the page whose texels match each visible surface’s pixel footprint, so resolution follows the viewer. A page is drawn once and then kept, so a static scene costs almost nothing to keep shadowed. Marking, paging, culling and rasterization all run on the GPU: three indirect draws a frame, whatever the size of the scene. The finest texels are 7.8 mm.

Moving things are cheap too. Each page also keeps a copy of its static-only depth. When a door, a vehicle or a character moves, the pages it crosses get that copy pasted back, and only the moving caster is redrawn on top, not every tree and building under it. With 40 movers walking through our test forest, shadow drawing went from 0.37 ms to 0.05 ms a frame on an RTX 5060. Skinned meshes cast too: the GPU skins the real mesh in the shadow pass.

Numbers (lab scene at 1600×900 with 4× MSAA, frame time in ms, medians of 5 interleaved rounds):

On a busy community sim in World, the frame rate went from 29 fps to 41 fps.

Using it:

import { SundialBabylon } from "@poqpoq/sundial/babylon";

if (SundialBabylon.isSupported(engine)) {          // keep your CSM for WebGL2
  const sundial = new SundialBabylon(scene, sun, { sceneMin: [-128, -10, -128], sceneMax: [128, 60, 128] });
  sundial.addCaster(terrain);
  sundial.addCaster(leaves);                       // alpha-tested casters just work
  sundial.addCaster(windmill, { dynamic: true });  // moves: redraws only itself
  sundial.addCaster(hero, { dynamic: true });      // has a skeleton: casts its skinned pose
  sundial.addReceivers(scene.materials);           // Standard and PBR, via a material plugin
  sundial.start();
}

Things we learned the hard way on an Intel iGPU, which may help anyone writing custom shadow or material code on WebGPU:

Limits. WebGPU only, and one directional light. For many characters, a small cascaded shadow alongside is still a fine choice: the receiver combines the two with min(), so nothing is darkened twice. That’s what pOqpOq World does for its avatars.

Along the way we sent two fixes upstream, and both shipped in Babylon 9.28.0: #18934 (the atmosphere’s PBR plugin now attaches only to materials in its own scene) and #18936 (StandardMaterial’s depth pre-pass honours every alpha source). Big thanks to the Babylon team for the careful reviews.

Since then: the core never needed Babylon (it talks only to a GPUDevice), and the three.js adapter the forum asked about shipped in 0.3 as @poqpoq/sundial/three, with the live demo above. The package is at 0.4 now.

Feedback and bug reports are very welcome, on GitHub or on the forum.

— Allen