docs: Entwurf und Umsetzungsplan für scharfe Kaskaden-Schatten

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-13 15:38:18 +02:00
1 parent c79b4f347b
commit 034b12537a
2 files changed
+493

No files matched your search

@@ -0,0 +1,87 @@
# Scharfe Schatten — Entwurf
**Datum:** 2026-09-13
**Zusammenhang:** Folgt auf den HDRI-Himmel (`src/world/sky.js`). Der Nutzer committet den Himmel erst, wenn Gebäude, alle Objekte und die Spielfigur scharfe, gut sichtbare Schatten werfen.
**Baut auf:** `src/world/sky.js` (Sonnenrichtung, -farbe und -stärke aus dem HDRI), `src/main.js` (bisher ein `DirectionalLight` mit `updateShadow()`).
## Ziel
Scharfe Schatten von Gebäuden, Palmen, Laternen, Autos und der Spielfigur, nah am Spieler gestochen scharf und bis in Sichtweite vorhanden.
## Befund (zur Laufzeit geprüft)
| Ursache | Beleg |
|---|---|
| Die Startfigur wirft keinen Schatten | Die `SkinnedMesh`es `Mannequin_1/_2` haben `castShadow = false`. `enableModelShadows` läuft nur in `applyCharacter`, also erst beim Figurenwechsel. |
| Die Schattenkarte ist zu grob | 512² Texel auf ±150 m ergeben 0,59 m pro Texel, dazu PCF und Bias 0. Laternen (~0,15 m), Palmenstämme und die Figur (~0,4 m) sind schmaler als ein Texel und werden zum Klecks. Mit 4096² ist der Laternenschatten scharf umrissen (zur Laufzeit getestet). |
| Gebäude außerhalb der Box bleiben ohne Schatten | Vom Spawn aus liegen die nächsten Gebäudewände 190–410 m entfernt, die Box reicht nur ±150 m um die Figur. |
| Kurze Schatten | Die Sonne des HDRI steht bei 77° (Figur ~0,4 m, 20-m-Gebäude ~4,6 m Schattenlänge). **Bleibt so**, siehe Entscheidungen. |
## Entscheidungen des Nutzers
| Frage | Entscheidung |
|---|---|
| Reichweite | Kaskaden-Schatten bis Sichtweite (CSM aus three.js), keine einzelne Karte um den Spieler. |
| Sonnenhöhe | Sonne des HDRI behalten (77°). Licht, Schatten und Spiegelungen kommen von der sichtbaren Sonne. |
## Aufbau
| Einheit | Aufgabe |
|---|---|
| `src/world/shadows.js` | Neu. `createSunShadows({ scene, camera, sun })` baut `CSM` aus `three/examples/jsm/csm/CSM.js` und liefert `{ csm, intensity (get/set), update() }`. Reine Helfer: `cascadeBreaks(splits, far)` und `prepareLitMaterials(root, csm)`. |
| `src/main.js` | Ersetzt `DirectionalLight` und `updateShadow()` durch `createSunShadows`. Die Schleife ruft `sunShadows.update()` vor `renderer.render`. Ans Panel geht `sun: sunShadows` (gleiche `intensity`-Schnittstelle, `panel.js` bleibt unverändert). |
| `src/characters/index.js` | `createCharacterModule` schaltet die Schatten auch fürs Startmodell ein. |
| `src/world/props.js` | Palmen und Laternen bekommen zusätzlich `receiveShadow`. |
### Stufen
Vier Stufen mit je 2048², eigene Grenzen über `mode: "custom"`. Die eingebaute Aufteilung `practical` (λ fest 0,5) legte die erste Grenze bei ~127 m.
| Stufe | Bis | Breite der Schattenbox (55° Sichtfeld, 16:9) | pro Texel |
|---|---|---|---|
| 1 | 25 m | ~53 m | ~2,6 cm |
| 2 | 100 m | ~212 m | ~10 cm |
| 3 | 350 m | ~743 m | ~36 cm |
| 4 | 1000 m | ~2124 m | ~1 m |
`maxFar` = 1000 m. Dahinter wirkt das Sonnenlicht ohne Schatten, der Nebel (500–2200 m) blendet den Übergang aus. `cascadeBreaks` rechnet die Meter in Anteile von `far = min(camera.far, maxFar)` um und kappt sie auf 1.
### Licht
- Richtung: `lightDirection = −sun.direction` (CSM erwartet die Laufrichtung des Lichts, `sky.js` liefert die Richtung zur Sonne). Farbe und Stärke kommen von `sky.sun` auf alle vier Stufenlichter.
- `lightMargin` 400 m: Auch 250-m-Türme außerhalb des Kamerakegels werfen noch in die Stufe. `lightFar` 3000 m deckt Rand plus Tiefe der fernsten Stufe (~1400 m) ab.
- Texel-Einrasten bringt `CSM.update()` bereits mit, das verhindert Flimmern beim Laufen.
- **Normal-Bias 1,5 Texel je Stufe** (bei 16:9 etwa 4 cm / 16 cm / 54 cm / 1,56 m), nach jedem `updateFrustums` neu berechnet. Nachtrag nach Nutzer-Screenshot mit Streifen auf Figur und Auto:
- *Ursache:* Figur, Autos (228 von 236 Meshes je SUV), Palmen und Laternen haben doppelseitige Materialien. Für die schreibt three.js die Vorderseiten in die Schattenkarte (`WebGLShadowMap`: `DoubleSide -> DoubleSide`, einseitige Materialien schreiben ihre Rückseiten). Mit dem CSM-Bias von 1e-6 verschatten sie sich selbst.
- *Gemessen (Hochpass-Energie im Bildausschnitt):* Motorhaube −49 %, Rücken der Figur −40 %. Der Schatten bleibt an Füßen und Reifen.
- *Verworfen:* `shadowSide = BackSide` wirkt gegen die Streifen genauso, lässt aber die einzelnen Blattflächen der Palmenkronen fast ohne Schatten (Schattenfläche 31 % -> 3 %).
- Wände und Boden (einseitig) waren nie betroffen. Der Tiefen-Bias bleibt beim CSM-Standard.
### Schutznetz für Materialien
CSM ersetzt `lights_fragment_begin` global. Nur Materialien mit `USE_CSM` bekommen je Pixel genau ein Stufenlicht, jedes andere beleuchtete Material summiert alle vier und wäre viermal zu hell. Deshalb bereitet `update()` vor jedem Frame alle noch nicht vorbereiteten beleuchteten Materialien vor:
- `MeshStandardMaterial` (inkl. Physical), `MeshLambertMaterial`, `MeshPhongMaterial` und `MeshToonMaterial` bekommen `csm.setupMaterial` und `needsUpdate`.
- Unbeleuchtete Materialien (Himmelskuppel-`ShaderMaterial`, `MeshBasicMaterial`, Linien) bleiben unberührt.
- Bereits vorbereitete Materialien (`csm.shaders.has`) werden übersprungen.
So sind nachgeladene Chunks, Autos, Figurenwechsel und Frisuren abgedeckt, ohne dass diese Stellen CSM kennen müssen. Kein Material im Projekt oder im Controller nutzt `onBeforeCompile`, das `setupMaterial` überschreibt (geprüft).
### Kamera
`update()` vergleicht die Projektionsmatrix der Kamera mit der vom letzten Frame. Ändern sich Seitenverhältnis, Sichtfeld, near oder far, folgt `csm.updateFrustums()`. Einen eigenen Resize-Haken braucht es nicht.
## Fehlerfälle und Risiken
- **Vergessenes Material:** Das Schutznetz fängt es ab, bevor es zum ersten Mal gezeichnet wird.
- **Übergänge zwischen den Stufen:** Werden Kanten zwischen den Stufen sichtbar, wird `csm.fade` eingeschaltet (kostet Shader-Zeit, weitet die Boxen).
- **Kosten:** Bis zu vier Schatten-Durchgänge pro Frame statt einem, rund 64 MB GPU-Speicher. Die Framezeit vorher/nachher wird gemessen.
## Tests
- `tests/shadows.test.js`
- `cascadeBreaks`: Meter werden zu Anteilen, Kappung bei kürzerem `far`.
- `prepareLitMaterials` mit echtem `CSM`: beleuchtete Materialien bekommen `USE_CSM` (auch in Material-Arrays), unbeleuchtete nicht, kein zweites Vorbereiten.
- `createSunShadows`: Stufenlichter zeigen nach `update()` in Sonnenrichtung. Die Stärke gilt für alle vier. Nachträglich hinzugefügte Meshes werden vorbereitet. Die Stufen folgen einer Änderung des Seitenverhältnisses.
- `tests/characters.test.js`: Die Startfigur wirft und empfängt Schatten.
- **Browser:** Nahaufnahmen von der Schattenseite (Figur, Laterne, Palme, Auto), Gebäude auf ~20 m und ~300 m, Übergänge zwischen den Stufen, keine Akne. Keine zu hellen Objekte, auch nach Figurenwechsel. Konsole sauber. Framezeit.