Sundial: paged (virtual) sun shadows for Babylon.js on WebGPU, MIT, on npm

Hi all,

We’ve been building a virtual shadow map for Babylon.js on WebGPU, and it’s now public:

poqpoq World on WebGPU: the tree trunks and the dappled canopy on the hillside are Sundial shadows.

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

Moving things are cheap too (new in 0.2). 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 (up to 8 influences, 256 bones).

It started from a practical problem. Our worlds are forests and prim-heavy builds, and on integrated GPUs CSM was eating half the frame, so our watchdog kept switching shadows off for iGPU players.

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

Intel Xe-LPG RTX 5060
shadows off 3.8–4.8 0.6–0.9
CSM, 3×2048² 8.8–10.5 2.9–3.4
Sundial 6.1–7.3 1.5–2.4

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

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();
}

The core has no Babylon imports (it only needs a GPUDevice), so a three.js adapter is possible. If that would interest you, please say so; it helps us decide what to build next.

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

  1. A storage write in a material shader, even one that never runs, turns off early-Z for the whole draw. Receivers have to be strictly read-only.

  2. StandardMaterial’s MATERIAL_ALPHATEST discards at the end of the shader, so guard any costly per-fragment work on the alpha test yourself.

  3. A dynamically indexed local array in a hot fragment path can spill on Intel (it cost us ~45 ms).

  4. iGPU A/B needs warm-ups and “flush” pages between runs, or you’ll measure clock-state transients.

Limits (0.2): WebGPU only; one directional light. Skinned casters don’t apply morph targets, and a big rig costs more than it should (every piece of the mesh is culled against the whole mesh’s box), so for many characters a small CSM 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. The adapter uses a few private Babylon internals, so the peer range is pinned to Babylon 9.

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 @Evgeni_Popov and @Deltakosh for the careful reviews, and to @RaananW for merging.

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

Allen (p0qp0q)

5 Likes

A quick update: Sundial 0.4 is on npm, and it now has a three.js adapter alongside the Babylon one.

Nothing changes for Babylon users. @poqpoq/sundial/babylon is the same adapter, and it never loads any three.js code. Under the hood, both adapters drive the same core and the same receiver shader, so improvements to the core reach both engines at once.

In the first post I mentioned that the core has no Babylon imports, so a three.js adapter was possible. It now exists.

Alpha testers wanted. Sundial is the default sun shadow for every WebGPU visitor to poqpoq World (poqpoq World - AI-Enhanced Open Metaverse | BlackBox Creative Suite). If you have a WebGPU browser and a few minutes, we’d love you to wander around and tell us what you see: hitches, shadow glitches, or just how it runs on your GPU. Any engine background is welcome. Replies here or GitHub issues are both perfect.