feat(world): HDRI-Himmel „Nowhere Road" mit Umgebungslicht und scharfen Kaskaden-Schatten
- Himmel: 4k-HDRI von Poly Haven (CC0, tools/assets/hdri.mjs) statt Sky-Shader. Kuppel zeigt das Foto ab 12°, darunter weiches Band und Dunst in Nebelfarbe; Umgebungslicht und Spiegelungen per PMREM; Sonne aus dem Bild gekappt und als Licht übernommen (77°, Stärke 2,5) - Schatten: CSM mit vier Stufen (25/100/350/1000 m, je 2048²), Schutznetz für beleuchtete Materialien, Normal-Bias 1,5 Texel je Stufe gegen Streifen auf doppelseitigen Materialien (Figur, Autos) - Startfigur wirft Schatten, Palmen und Laternen empfangen Schatten, Wasser als ruhige See - Debug-Panel: Sonne, Himmelslicht, Himmelshelligkeit; Namensnennung um das HDRI ergänzt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
034b12537a
commit
f76e97b69d
15 files changed
+762
-41
No files matched your search
+24
-33
@@ -1,11 +1,12 @@
|
||||
import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, DirectionalLight, Fog, Mesh,
|
||||
import { ACESFilmicToneMapping, BoxGeometry, Fog, Mesh,
|
||||
PerspectiveCamera, PlaneGeometry, Scene, Timer, Vector3, WebGLRenderer } from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { Sky } from "three/examples/jsm/objects/Sky.js";
|
||||
import { playerController } from "three-player-controller";
|
||||
import { ChunkStreamer } from "./world/streaming.js";
|
||||
import { createMaterials } from "./world/materials.js";
|
||||
import { createSky } from "./world/sky.js";
|
||||
import { createSunShadows } from "./world/shadows.js";
|
||||
import { PropLayer } from "./world/props.js";
|
||||
import { chunkForWorld, threeToWorld } from "./world/coords.js";
|
||||
import { createCharacterModule } from "./characters/index.js";
|
||||
@@ -35,11 +36,21 @@ container.appendChild(renderer.domElement);
|
||||
const camera = new PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.5, 2500);
|
||||
// Pflichtoption für player.init: der Controller übernimmt die Kamera, braucht aber controls.domElement als Eingabeziel.
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
scene.add(new AmbientLight(0xffffff, 1.5));
|
||||
const sun = new DirectionalLight(0xffffff, 2);
|
||||
sun.position.set(60, 120, 40);
|
||||
sun.castShadow = true;
|
||||
scene.add(sun);
|
||||
|
||||
// Himmel, Umgebungslicht und Sonne aus dem HDRI "Nowhere Road" (src/world/sky.js) statt Sky-Shader,
|
||||
// AmbientLight und fest gesetzter Sonne. Das Foto hat harte Mittagssonne — mit seinen echten
|
||||
// Verhältnissen blieben Schatten fast schwarz. Himmelslicht und Kuppel laufen deshalb fünffach, die
|
||||
// Sonne mit ihrer gemessenen Stärke (2,5); nachjustierbar im Debug-Panel unter "Welt".
|
||||
const SUN_STRENGTH = 1;
|
||||
const SKY_LIGHT = 5;
|
||||
const SKY_BRIGHTNESS = 5;
|
||||
scene.fog = new Fog(0xffffff, 500, 2200); // Farbe setzt sky.setBrightness() passend zum Horizont
|
||||
const sky = await createSky({ renderer, scene });
|
||||
sky.setBrightness(SKY_BRIGHTNESS);
|
||||
scene.environmentIntensity = SKY_LIGHT;
|
||||
// Kaskaden-Schatten statt eines einzelnen DirectionalLight (src/world/shadows.js): Figur, Laternen
|
||||
// und Palmen scharf, Gebäude bis 1 km. sun.intensity steuert alle Stufenlichter (Regler "Sonne").
|
||||
const sun = createSunShadows({ scene, camera, sun: { ...sky.sun, intensity: sky.sun.intensity * SUN_STRENGTH } });
|
||||
|
||||
// Ein Material je Schicht aus buildChunkGeometry() (Task 7), jetzt mit echten Poly-Haven-
|
||||
// Texturen statt Flächenfarben (Task 9, src/world/materials.js). buildings trägt zusätzlich
|
||||
@@ -51,15 +62,6 @@ const materials = await createMaterials(renderer.capabilities.getMaxAnisotropy()
|
||||
// Kartenbild im Debug-Panel.
|
||||
const manifest = await fetch("./data/vice-a/manifest.json").then((r) => r.json());
|
||||
|
||||
// Himmel und Nebel über der Szene; sunPosition zeigt in dieselbe Richtung wie das Sonnenlicht.
|
||||
const sky = new Sky();
|
||||
sky.scale.setScalar(45000);
|
||||
scene.add(sky);
|
||||
sky.material.uniforms.turbidity.value = 8;
|
||||
sky.material.uniforms.rayleigh.value = 1.6;
|
||||
sky.material.uniforms.sunPosition.value.copy(sun.position.clone().normalize());
|
||||
scene.fog = new Fog(0xbcd7ea, 500, 2200);
|
||||
|
||||
// Meer rund um den Ausschnitt: eine einzige große Ebene unter/um die Chunks, keine je Chunk.
|
||||
const water = new Mesh(new PlaneGeometry(4000, 4000), materials.water);
|
||||
water.rotation.x = -Math.PI / 2;
|
||||
@@ -183,7 +185,7 @@ function requestTeleport(position) {
|
||||
player.reset(new Vector3(position.x, position.y, position.z));
|
||||
}
|
||||
|
||||
createPanel({ player, characterModule, scene, manifest, requestTeleport });
|
||||
createPanel({ player, characterModule, scene, sky, sun, manifest, requestTeleport });
|
||||
|
||||
// Minimap auf Taste M (docs/superpowers/specs/2026-09-13-minimap-design.md). Position über
|
||||
// playerPosition — im Fahrzeug liefert die Kapsel nur den lokalen Sitzplatz-Offset. Die Richtung
|
||||
@@ -224,17 +226,6 @@ player.addCollider({ motion: "static", shape: { kind: "mesh", source: seafloor }
|
||||
const props = new PropLayer();
|
||||
await props.load();
|
||||
|
||||
// Schattenkamera folgt dem Spieler, statt eine feste Box zu benutzen.
|
||||
function updateShadow(target) {
|
||||
sun.position.set(target.x + 120, 240, target.z + 80);
|
||||
sun.target.position.copy(target);
|
||||
sun.target.updateMatrixWorld();
|
||||
sun.shadow.camera.left = -150; sun.shadow.camera.right = 150;
|
||||
sun.shadow.camera.top = 150; sun.shadow.camera.bottom = -150;
|
||||
sun.shadow.camera.far = 600;
|
||||
sun.shadow.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
// Chunk-Collider treffen erst nach und nach ein (Fetch + asynchroner BVH-Bau, siehe
|
||||
// streaming.js). setSkipCapsuleCollision(true) schaltet nur das horizontale Herausdrücken
|
||||
// ab; applyGravity läuft im Controller unabhängig davon weiter (CharacterMovement.ts), die
|
||||
@@ -254,7 +245,7 @@ let vehiclesSpawned = false;
|
||||
|
||||
function tick(delta) {
|
||||
// Befund A (Fix-Runde 3): playerPosition() statt player.playerCapsule.position an jeder
|
||||
// der sechs Stellen, die "wo ist der Spieler" brauchen — sitzt er in einem Fahrzeug, hängt
|
||||
// der fünf Stellen, die "wo ist der Spieler" brauchen — sitzt er in einem Fahrzeug, hängt
|
||||
// die Kapsel als Kind an dessen vehicleGroup und ihre .position ist nur noch der lokale
|
||||
// Sitzplatz-Offset, nicht die Weltposition (Begründung in src/player.js). Ohne diese
|
||||
// Umstellung hört das Streaming beim Fahren auf nachzuladen.
|
||||
@@ -291,7 +282,7 @@ function tick(delta) {
|
||||
// requestTeleport() ruft player.reset() synchron auf, noch bevor dieser tick() läuft, und
|
||||
// reset() steigt bei aktivem Fahrzeug zuerst aus (controllerMode -> 0, s. PlayerController.
|
||||
// reset()) — hier kann also nie ein Fahrzeug aktiv sein. playerPosition() ändert an diesem
|
||||
// Aufruf inhaltlich nichts, macht die Stelle aber konsistent mit den übrigen fünf.
|
||||
// Aufruf inhaltlich nichts, macht die Stelle aber konsistent mit den übrigen vier.
|
||||
if (pendingTeleport) {
|
||||
if (streamer.isReadyAt(playerPosition(player))) {
|
||||
player.setSkipCapsuleCollision(false);
|
||||
@@ -331,10 +322,10 @@ renderer.setAnimationLoop((t) => {
|
||||
const delta = Math.min(timer.getDelta(), 1 / 40);
|
||||
player.update(delta);
|
||||
tick(delta);
|
||||
// Befund A: Schatten folgen der aktuellen Position (Fahrzeug beim Fahren, sonst Kapsel) —
|
||||
// sonst bliebe die Schattenkamera beim Fahren auf dem Einstiegspunkt stehen.
|
||||
updateShadow(playerPosition(player));
|
||||
materials.water.normalMap.offset.x += delta * 0.01;
|
||||
// Nach player.update/tick steht die Kamera für diesen Frame fest; die Schattenstufen folgen
|
||||
// ihrem Blick — beim Fahren wie zu Fuß, ohne eigene Positionsquelle.
|
||||
sun.update();
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
addEventListener("resize", () => {
|
||||
|
||||
Reference in new issue
Block a user