# Sundial: paged (virtual) sun shadows for three.js WebGPURenderer, MIT, on npm

**URL:** <https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912>\
**Category:** Resources\
**Tags:** shadow-map, npm\
**Created:** [October 1, 2026, 7:57pm UTC](https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912 "2026-10-01T19:57:51Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![pOqpOq](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/poqpoq/32/72897_2.png) [@pOqpOq](https://discourse.threejs.org/u/pOqpOq)\
**Post date:** [October 1, 2026, 7:57pm UTC](https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912/1 "2026-10-01T19:57:51Z")

</div>

Hi all,

Sundial is a virtual (paged) shadow map for one directional light, and it now has a three.js adapter for `WebGPURenderer`:

- **Live demo:** [Sundial for three.js](https://increasinglyhuman.github.io/poqpoq-sundial/) (needs a WebGPU browser)

- **Minimal example:** [Sundial: minimal three.js example](https://increasinglyhuman.github.io/poqpoq-sundial/examples/three-basic/)

- **Repo:** [GitHub - increasinglyHuman/poqpoq-sundial: Paged (virtual) sun shadows for WebGPU — engine-agnostic core + Babylon.js and three.js adapters · GitHub](https://github.com/increasinglyHuman/poqpoq-sundial) (MIT)

- **npm:** `@poqpoq/sundial` (three r186; the adapter is `@poqpoq/sundial/three`)

 ![three-forum-forest](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/1/a/1a64f684806abb201409b264d0977be6f9a4bf6d.jpeg)  
_The forest road in the live demo: every dapple on the ground is a leaf card’s shadow._

**How it plugs in.** It installs as the sun’s `light.shadow.shadowNode`, the same hook `CSMShadowNode` uses. So every node material lit by the sun receives the shadow with no per-material changes, and your other lights keep three’s shadows as usual.

```javascript
import * as THREE from "three/webgpu";
import { SundialThree } from "@poqpoq/sundial/three";
​
await renderer.init();
if (SundialThree.isSupported(renderer)) {
  const sundial = new SundialThree(renderer, scene, camera, sun, {
    sceneMin: [-128, -10, -128], sceneMax: [128, 60, 128],
  });
  sundial.setCasters(SundialThree.castersIn(scene)); // every mesh with castShadow
  sundial.start();
}

```

**What it does.** The sun’s shadow is a clipmap of 128×128 pages in one depth pool. A compute pass reads the camera depth each frame and requests the page whose texels match each visible surface’s pixel footprint. A page is drawn once and then kept, so a static scene costs very little to keep shadowed, and near the camera the texels get small (7.8 mm at the finest level). Page marking, allocation, culling and drawing all run on the GPU.

Casters can be static meshes, `InstancedMesh`, alpha-tested foliage (read from `map` or `alphaMap` at `alphaTest`), moving meshes and `SkinnedMesh`. When something moves, only the moving caster is redrawn over each page’s cached static depth. Antialiasing, `reversedDepthBuffer` and post-processing all work. `blendShadowMap: true` also folds in three’s standard shadow map for the same light, for casters you’d rather keep there (animated characters, say).

 ![three-forum-village](https://canada1.discourse-cdn.com/flex035/uploads/threejs/original/3X/1/e/1ed6a156e0bc12046e2eaa4a1968c623223bd0f9.jpeg)

**Numbers.** These are from our lab scene (a 256 m sim: terrain, 1,400 alpha-tested trees and a small village, 1600×900 with MSAA), frame ms on an Intel Xe-LPG iGPU. The comparison is `CSMShadowNode` configured to match:

| | **village** | **overview** | **forest** |
| --- | --- | --- | --- |
| shadows off | 3.84 | 3.96 | 3.53 |
| CSMShadowNode 3×2048² | 7.08 | 6.76 | 6.62 |
| CSMShadowNode 4×4096² | 10.77 | 9.92 | 9.98 |
| Sundial | 6.43 | 6.69 | 5.89 |

On an RTX 5060 the pattern is similar (details in the README). The lab is in the repo (`three.html`) if you’d like to measure on your own hardware.

**Honest limits.** WebGPU only, and one directional light. The adapter relies on a few three.js internals (the README lists them), so it’s tested against r186 and the peer range is narrow; we’ll follow three’s releases. Morph targets aren’t applied to casters. And the cost is in receiving the shadow, so it grows with how many shadowed pixels overlap on screen: looking down onto a dense canopy of leaf cards (the demo’s Overview), Sundial runs a little slower than `CSMShadowNode` on an Intel iGPU. A cheaper receiver path for foliage is on our list.

It was built for poqpoq World ([https://poqpoq.com/](https://poqpoq.com/)), where it’s the default sun shadow on WebGPU. It began life as a Babylon.js project, and three.js support arrived in 0.3. Both adapters drive the same core, so the shadows are identical.

**Alpha testers wanted.** If you have a WebGPU browser, we’d love you to walk around poqpoq World and tell us how it runs on your GPU. Any engine background is welcome. Feedback here or on GitHub.

Thanks to the three.js team for the node system. `shadowNode` made this a clean fit.

Allen (p0qp0q)

---

<div class="post-metadata">

**Author:** ![Usnul](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/usnul/32/2355_2.png) [@Usnul](https://discourse.threejs.org/u/Usnul)\
**Post date:** [October 2, 2026, 8:45pm UTC](https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912/2 "2026-10-02T20:45:02Z")

</div>

Kudos to you, very impressive work.

I don’t think VSM is ready for prime time on WebGPU yet, due to lack of multi-view rendering, but your idea of splitting casters into static and dynamic set kind works.

Well done 🥳

---

<div class="post-metadata">

**Author:** ![pOqpOq](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/poqpoq/32/72897_2.png) [@pOqpOq](https://discourse.threejs.org/u/pOqpOq)\
**Post date:** [October 2, 2026, 10:54pm UTC](https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912/3 "2026-10-02T22:54:01Z")

</div>

Thanks — that means a lot. Big yes on multiview; without it every page is its own pass, and that’s exactly why the static/dynamic split exists. Static depth is cached per page, so a moving caster only redraws itself: the pages it crosses get their cached static depth copied back and just the dynamic casters drawn over it, capped at a per-frame page budget (64 by default). In the forest scene that’s ~0.05 ms of shadow raster for 40 movers versus ~0.37 ms re-rendering everything. When multiview lands in WebGPU I’ll revisit how pages get rendered, since batching pages into one pass is the obvious next win.

Thanks again,  
Allen

---

<div class="post-metadata">

**Author:** ![Usnul](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/usnul/32/2355_2.png) [@Usnul](https://discourse.threejs.org/u/Usnul)\
**Post date:** [October 2, 2026, 11:18pm UTC](https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912/4 "2026-10-02T23:18:38Z")

</div>

Another 2 things I thought about while working on this myself a while back, might be of interest to someone:

1. often you render nearby tiles, so you can actual enlarge the viewport to fit multiple tiles and mask out what you don’t need
2. RTX, pretty obvious one, if you’re tracing rays - you don’t need to worry about vertex transform and any other per-view limitations. But at that point you might as well just do RTX soft shadows for the main view maybe

---

<div class="post-metadata">

**Author:** ![jrlazz](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.threejs.org/jrlazz/32/64383_2.png) [@jrlazz](https://discourse.threejs.org/u/jrlazz)\
**Post date:** [October 3, 2026, 1:59am UTC](https://discourse.threejs.org/t/sundial-paged-virtual-sun-shadows-for-three-js-webgpurenderer-mit-on-npm/94912/5 "2026-10-03T01:59:15Z")

</div>

A brilliant job… beautiful. 😉  
PS: And runs very well in my Intel(R) Core™ i3-8130U CPU @ 2.20GHz (2.21 GHz).
