6.7 KiB
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 SkinnedMeshes 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.jsliefert die Richtung zur Sonne). Farbe und Stärke kommen vonsky.sunauf alle vier Stufenlichter. lightMargin400 m: Auch 250-m-Türme außerhalb des Kamerakegels werfen noch in die Stufe.lightFar3000 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
updateFrustumsneu 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 = BackSidewirkt 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.
- 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 (
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,MeshPhongMaterialundMeshToonMaterialbekommencsm.setupMaterialundneedsUpdate.- 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.fadeeingeschaltet (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.jscascadeBreaks: Meter werden zu Anteilen, Kappung bei kürzeremfar.prepareLitMaterialsmit echtemCSM: beleuchtete Materialien bekommenUSE_CSM(auch in Material-Arrays), unbeleuchtete nicht, kein zweites Vorbereiten.createSunShadows: Stufenlichter zeigen nachupdate()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.