docs: Entwurf und Umsetzungsplan für scharfe Kaskaden-Schatten
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
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.
|
||||
Reference in new issue
Block a user