Files
gta6/docs/superpowers/specs/2026-09-13-schatten-design.md
T

88 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.