Skip to content

Commit ede3795

Browse files
DennisSmolekclaude
andcommitted
feat: shadowmap-vsm + shadowmap-pointlight — wave 8 complete (65 examples)
Pair 4 closes wave 8: second consecutive zero-review-fix wave. VSM via shadows="variance" (verified against fiber variant table) with live blur knobs; cube shadow maps from orbiting striped shells (intensity/distance parity verified against original at review). Doc clarification both agents converged on: dash-path props apply any shadow.* scalar at runtime. AGENTS v0.16, HANDOFF wave-8 table. 65 examples, 65/65 smoke + contact sheet green on Metal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 51e7b0f commit ede3795

5 files changed

Lines changed: 414 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,13 @@ override lands with an UPSTREAM.md entry in the same commit.** Highlights:
346346

347347
## Changelog
348348

349+
- 2026-07-27 — v0.16 amendments from wave-8 pair 4 (shadowmap-vsm +
350+
shadowmap-pointlight, both zero-review-fix — wave 8 closes at 65 examples):
351+
clarification both shadow agents converged on independently — fiber dash-path
352+
props apply ANY `shadow.*` scalar at runtime (`shadow-radius`,
353+
`shadow-blurSamples`, `shadow-bias` all work live); the lights-spotlight
354+
imperative-sync precedent is about missing TYPED props, not capability.
355+
`shadows="variance"` → VSMShadowMap confirmed against the fiber variant table.
349356
- 2026-07-27 — v0.15 amendments from wave-8 pair 3 (volume-cloud + volume-fire,
350357
both zero-review-fix; fire is the biggest port yet — full GPU fluid sim): TSL
351358
stack rule (helpers with internal toVar/assign need Fn() — runtime-only

docs/HANDOFF.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Session Handoff — 2026-07-27 (overnight, continued: repo live + M2 waves 1–2)
22

3+
## Wave 8 (same night)
4+
5+
8 ports, 4 pairs + the B17 audit interlude (below). **65 examples total, 65/65
6+
smoke + contact sheet green on Metal.** Zero review fixes across all 8 ports —
7+
second consecutive zero-fix wave.
8+
9+
| Example | Notes | Cost |
10+
|---|---|---|
11+
| materials-matcap | EXR/JPG matcap swap, both decode paths | 107k |
12+
| materials-toon | 6³ toon lattice + ToonOutlinePassNode; pattern-(c) wrinkle (constructor takes Nodes where factory types don't) | 106k |
13+
| lines-fat | Line2NodeMaterial vs native strip + PiP inset; **found WebGPU setViewport/setScissor TOP-origin rule** | 118k |
14+
| lensflares | LensflareMesh field; occlusion test verified live; sRGB/linear setHSL parity flagged | 109k |
15+
| volume-cloud | Data3DTexture raymarch; **found TSL stack rule** (helpers with internal toVar/assign need Fn) | 97k |
16+
| volume-fire | biggest port ever (258k): 8-kernel GPU fluid sim + volumetric shadows + draggable emitter; legitimate ciSkip #5; B19 filed | 258k |
17+
| shadowmap-vsm | shadows="variance" verified; VSM blur knobs live via dash-paths | 100k |
18+
| shadowmap-pointlight | cube shadow maps, striped shells; intensity/distance parity checked at review | 100k |
19+
20+
Cumulative: **63 agent ports across 8 waves + 2 gate ports, zero manifest
21+
clobbers across 32+ concurrent pair-registrations.** AGENTS.md v0.12→v0.16.
22+
323
## Wave 8 interlude: corpus-wide B17 audit (same night)
424

525
Flames' B17 find (wave 7) triggered a full-corpus animation audit: two-frame

src/examples.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,5 +443,18 @@
443443
"original": "https://threejs.org/examples/#webgpu_volume_fire",
444444
"credits": "Volumetric fire simulation by the three.js authors; TeapotGeometry and the curl-noise TSL addon from the three.js examples",
445445
"ciSkip": "2M-voxel GPU fluid sim + raymarched volume at 120 Hz substeps cannot reach readiness on SwiftShader software raster"
446+
},
447+
{
448+
"slug": "shadowmap-pointlight",
449+
"title": "Shadow Map / Point Light",
450+
"tags": ["shadows", "pointlight", "cube-shadow-map", "canvas-texture", "node-material", "leva"],
451+
"original": "https://threejs.org/examples/#webgpu_shadowmap_pointlight"
452+
},
453+
{
454+
"slug": "shadowmap-vsm",
455+
"title": "Shadow Map / VSM",
456+
"tags": ["shadows", "vsm", "spotlight", "directional-light", "fog", "leva"],
457+
"original": "https://threejs.org/examples/#webgpu_shadowmap_vsm",
458+
"credits": "VSM Shadows example by Paul Brunt"
446459
}
447460
]
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* shadowmap-pointlight
3+
* R3F port of three.js `webgpu_shadowmap_pointlight`, running on WebGPU.
4+
* Original: https://threejs.org/examples/#webgpu_shadowmap_pointlight (~150 lines of JS)
5+
*
6+
* DEMONSTRATES
7+
* - Omnidirectional (cube) shadow maps from two orbiting `PointLight`s inside a
8+
* `BackSide` box room — every wall receives shadows from every direction at once,
9+
* configured entirely via fiber dash-path props (`shadow-bias`, `shadow-radius`,
10+
* `shadow-mapSize-*`) on `<pointLight castShadow>`
11+
* - A shadow-casting shell as a real scene-graph CHILD of its own light
12+
* (`<pointLight><mesh castShadow>`): a perforated sphere (2x2 `CanvasTexture` stripe
13+
* `alphaMap` + `alphaTest`) wrapped around the emitter, so the light rays out through
14+
* the cutouts and the rotating shell sweeps striped shadows across the room — the
15+
* same alpha-test path cuts the SHADOW silhouette too, since the shadow pass shares
16+
* the material's alpha test
17+
* - `shadows` (boolean) on `<Canvas>` = PCF shadow mapping, the same default the
18+
* original's WebGPURenderer uses — which is what keeps `shadow.radius` (a PCF blur
19+
* knob) live and worth exposing as a control
20+
* - Imperative per-frame light choreography in `useFrame` (Lissajous orbit + shell
21+
* rotation), each light on its own accumulated clock so a speed control scales both
22+
* without time jumps
23+
*
24+
* DIVERGENCE from original
25+
* - The original has no GUI; leva panel added (light colors, intensity, orbit speed,
26+
* shadow bias/radius) — same rationale as this corpus's other zero-GUI ports
27+
* (sky, shadowmap). Bias/radius apply live: three's WebGPU ShadowNode reads them
28+
* as reference nodes, no rebuild needed
29+
* - Both lights share ONE stripe `CanvasTexture` and one set of sphere geometries;
30+
* the original builds identical copies per light inside `createLight()`
31+
* - Speed control accumulates `delta * speed` per light instead of the original's
32+
* absolute `performance.now()` clock (light 2 keeps its +10000s phase offset)
33+
* - The light-marker sphere's over-driven color (`color * intensity`, the original's
34+
* hard-coded `multiplyScalar(200)`) now tracks the leva intensity, so dimming a
35+
* light dims its visible bulb too
36+
* - `renderer={{ toneMapping: NoToneMapping }}` — deliberate: the original renders
37+
* with the WebGPURenderer default (NoToneMapping); fiber's ACESFilmic default would
38+
* visibly mute the over-driven bulb markers and the walls' lit hot-spots
39+
* - OrbitControls -> this repo's CameraControls (via DemoHelpers); grid disabled
40+
* (`grid={false}`) — the room's own floor (y = -5) is a shadow receiver and an
41+
* infinite grid would float 5 units above it, mid-room
42+
*/
43+
import { useMemo, useRef } from 'react'
44+
import { Canvas, useFrame } from '@react-three/fiber/webgpu'
45+
import { folder, useControls } from 'leva'
46+
import {
47+
BackSide,
48+
CanvasTexture,
49+
Color,
50+
DoubleSide,
51+
NearestFilter,
52+
NoToneMapping,
53+
RepeatWrapping,
54+
SphereGeometry,
55+
} from 'three/webgpu'
56+
import type { PointLight } from 'three/webgpu'
57+
import { DemoHelpers } from '../utils/DemoHelpers'
58+
59+
// Constant shared assets (not mutable state — same module-scope rationale as
60+
// lights-pointlights' markerGeometry). The original rebuilds all three per light.
61+
const bulbGeometry = new SphereGeometry(0.3, 12, 6)
62+
const shellGeometry = new SphereGeometry(2, 32, 8)
63+
64+
// Ported from the original's `generateTexture()`: a 2x2 canvas, bottom row white,
65+
// top row transparent — tiled 4.5x vertically it becomes the shell's stripe cutouts.
66+
function createStripeTexture() {
67+
const canvas = document.createElement('canvas')
68+
canvas.width = 2
69+
canvas.height = 2
70+
const context = canvas.getContext('2d')!
71+
context.fillStyle = 'white'
72+
context.fillRect(0, 1, 2, 1)
73+
74+
const texture = new CanvasTexture(canvas)
75+
texture.magFilter = NearestFilter
76+
texture.wrapS = RepeatWrapping
77+
texture.wrapT = RepeatWrapping
78+
texture.repeat.set(1, 4.5)
79+
return texture
80+
}
81+
const stripeTexture = createStripeTexture()
82+
83+
interface ShadowLightProps {
84+
color: string
85+
intensity: number
86+
speed: number
87+
bias: number
88+
radius: number
89+
/** Phase offset in seconds — the original runs light 2 at `time + 10000`. */
90+
offset?: number
91+
}
92+
93+
// One orbiting point light: cube-shadow caster + over-driven bulb marker + the
94+
// perforated stripe shell that carves the raying shadows (see header DEMONSTRATES).
95+
function ShadowLight({ color, intensity, speed, bias, radius, offset = 0 }: ShadowLightProps) {
96+
const lightRef = useRef<PointLight>(null)
97+
const clockRef = useRef(offset)
98+
99+
// Original: `material.color.multiplyScalar(intensity)` — an unlit sphere driven far
100+
// past 1.0 so it reads as the glowing bulb under NoToneMapping.
101+
const bulbColor = useMemo(() => new Color(color).multiplyScalar(intensity), [color, intensity])
102+
103+
useFrame((_, delta) => {
104+
const light = lightRef.current
105+
if (!light) return
106+
clockRef.current += delta * speed
107+
const t = clockRef.current
108+
109+
// Lissajous orbit + shell spin, ported verbatim from the original's animate().
110+
light.position.set(Math.sin(t * 0.6) * 9, Math.sin(t * 0.7) * 9 + 6, Math.sin(t * 0.8) * 9)
111+
light.rotation.x = t
112+
light.rotation.z = t
113+
})
114+
115+
return (
116+
<pointLight
117+
ref={lightRef}
118+
color={color}
119+
intensity={intensity}
120+
distance={20}
121+
castShadow
122+
// Original comment: negative bias reduces self-shadowing on double-sided objects.
123+
shadow-bias={bias}
124+
shadow-radius={radius}
125+
shadow-mapSize-width={128}
126+
shadow-mapSize-height={128}
127+
>
128+
<mesh geometry={bulbGeometry}>
129+
<meshBasicMaterial color={bulbColor} />
130+
</mesh>
131+
<mesh geometry={shellGeometry} castShadow receiveShadow>
132+
<meshPhongNodeMaterial side={DoubleSide} alphaMap={stripeTexture} alphaTest={0.5} />
133+
</mesh>
134+
</pointLight>
135+
)
136+
}
137+
138+
// The 30x30x30 BackSide box everything happens inside — its inner faces are the
139+
// shadow receivers this example is about.
140+
function Room() {
141+
return (
142+
<mesh position={[0, 10, 0]} receiveShadow>
143+
<boxGeometry args={[30, 30, 30]} />
144+
<meshPhongNodeMaterial color="#a0adaf" shininess={10} specular="#111111" side={BackSide} />
145+
</mesh>
146+
)
147+
}
148+
149+
export default function ShadowmapPointlight() {
150+
const { speed, intensity, light1Color, light2Color, bias, radius } = useControls('shadowmap-pointlight', {
151+
speed: { value: 1, min: 0, max: 3, step: 0.05 },
152+
intensity: { value: 200, min: 0, max: 600, step: 10 },
153+
light1Color: { value: '#0088ff', label: 'light 1' },
154+
light2Color: { value: '#ff8888', label: 'light 2' },
155+
shadow: folder({
156+
bias: { value: -0.005, min: -0.02, max: 0.02, step: 0.0005 },
157+
radius: { value: 10, min: 0, max: 25, step: 0.5 },
158+
}),
159+
})
160+
161+
return (
162+
<Canvas
163+
// Deliberate NoToneMapping — see header DIVERGENCE.
164+
renderer={{ toneMapping: NoToneMapping }}
165+
shadows
166+
background="#000000"
167+
camera={{ position: [0, 10, 40], fov: 45, near: 1, far: 1000 }}
168+
>
169+
<ambientLight color="#111122" intensity={3} />
170+
<ShadowLight color={light1Color} intensity={intensity} speed={speed} bias={bias} radius={radius} />
171+
<ShadowLight color={light2Color} intensity={intensity} speed={speed} bias={bias} radius={radius} offset={10000} />
172+
<Room />
173+
<DemoHelpers grid={false} target={[0, 10, 0]} minDistance={5} maxDistance={120} />
174+
</Canvas>
175+
)
176+
}

0 commit comments

Comments
 (0)