138 lines
6.4 KiB
JavaScript
138 lines
6.4 KiB
JavaScript
import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, DirectionalLight, 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";
|
|
|
|
const container = document.querySelector("#container");
|
|
const scene = new Scene();
|
|
const renderer = new WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
renderer.toneMapping = ACESFilmicToneMapping;
|
|
renderer.shadowMap.enabled = true;
|
|
container.appendChild(renderer.domElement);
|
|
|
|
const camera = new PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 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);
|
|
|
|
// 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
|
|
// die dort erzeugten Vertexfarben (Pastellton pro Gebäude) über der Stuckputz-Textur.
|
|
const materials = await createMaterials();
|
|
|
|
// 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;
|
|
water.position.set(500, -0.6, -1000);
|
|
scene.add(water);
|
|
|
|
// Vice Beach; Task 8, Schritt 5. Auch initPos, damit der Streamer von Anfang an den richtigen
|
|
// Chunk anfragt statt erst nach dem späteren reset() umzuschwenken.
|
|
const SPAWN = { x: 1699, y: 3, z: -743 };
|
|
|
|
const gltf = await new GLTFLoader().loadAsync("./models/ual.glb");
|
|
const player = new playerController();
|
|
await player.init({
|
|
scene, camera, controls,
|
|
initPos: new Vector3(SPAWN.x, SPAWN.y, SPAWN.z),
|
|
playerModelConfig: {
|
|
model: gltf.scene, animations: gltf.animations, scale: 0.005,
|
|
idleAnim: "Idle_Loop", walkAnim: "Walk_Loop", runAnim: "Sprint_Loop",
|
|
jumpAnim: ["Jump_Start", "Jump_Loop", "Jump_Land"],
|
|
headBoneName: "Head", rotateY: -Math.PI / 2, speed: 100, runSpeed: 600,
|
|
},
|
|
});
|
|
|
|
// Meeresboden: man watet hinein, statt endlos zu fallen. Einmalig, nicht je Chunk.
|
|
const seafloor = new Mesh(new BoxGeometry(4000, 1, 4000), materials.seafloor);
|
|
seafloor.position.set(500, -3.0, -1000); // Oberkante bei y = -2,5
|
|
scene.add(seafloor);
|
|
seafloor.updateMatrixWorld(true);
|
|
player.addCollider({ motion: "static", shape: { kind: "mesh", source: seafloor } });
|
|
|
|
// 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
|
|
// Kapsel würde also binnen Sekunden frei fallen, wenn man sie nur "schweben" ließe. Solange
|
|
// der Chunk unter dem Spawn nicht bereit ist, hält tick() sie deshalb aktiv auf Spawnhöhe
|
|
// fest (Ruling 53) statt sie fallen und vom Auffangnetz auffangen zu lassen — eine feste
|
|
// Zeitschranke, die danach die Kollision ohne vorhandenen Boden einschaltet, würde in eine
|
|
// endlose Fall-Auffang-Schleife laufen, sobald der Chunk dauerhaft ausbleibt. Nach 15s gibt
|
|
// es nur noch eine einmalige Warnung, kein Verhaltenswechsel.
|
|
const streamer = new ChunkStreamer({ scene, player, materials });
|
|
player.setSkipCapsuleCollision(true);
|
|
let settled = false;
|
|
let warnedSlowSpawn = false;
|
|
const startedAt = performance.now();
|
|
let lastSafe = { ...SPAWN };
|
|
|
|
function tick(delta) {
|
|
streamer.update(player.playerCapsule.position);
|
|
if (!settled) {
|
|
if (streamer.isReadyAt(player.playerCapsule.position)) {
|
|
settled = true;
|
|
player.setSkipCapsuleCollision(false);
|
|
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
|
|
} else {
|
|
player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z));
|
|
if (!warnedSlowSpawn && performance.now() - startedAt > 15000) {
|
|
warnedSlowSpawn = true;
|
|
console.warn(`Chunk am Spawn (${SPAWN.x}, ${SPAWN.z}) ist nach 15s immer noch nicht bereit.`);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
const p = player.playerCapsule.position;
|
|
// Ruling 52: an ein echtes Bodensignal binden statt an die Höhe — sonst würde auch der
|
|
// Scheitelpunkt eines Sprungs, ein Dach oder ein Moment im freien Fall als "sicher" gelten.
|
|
if (p.y < -30) player.reset(new Vector3(lastSafe.x, lastSafe.y, lastSafe.z));
|
|
else if (player.getIsOnGround()) lastSafe = { x: p.x, y: p.y + 1, z: p.z };
|
|
}
|
|
|
|
const timer = new Timer();
|
|
timer.connect(document);
|
|
renderer.setAnimationLoop((t) => {
|
|
timer.update(t);
|
|
const delta = Math.min(timer.getDelta(), 1 / 40);
|
|
player.update(delta);
|
|
tick(delta);
|
|
updateShadow(player.playerCapsule.position);
|
|
materials.water.normalMap.offset.x += delta * 0.01;
|
|
renderer.render(scene, camera);
|
|
});
|
|
addEventListener("resize", () => {
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
camera.aspect = container.clientWidth / container.clientHeight;
|
|
camera.updateProjectionMatrix();
|
|
});
|