import { ACESFilmicToneMapping, AmbientLight, DirectionalLight, MeshStandardMaterial, PerspectiveCamera, 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 { playerController } from "three-player-controller"; import { ChunkStreamer } from "./world/streaming.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). buildings trägt die dort erzeugten // Vertexfarben (Pastellton pro Gebäude) und bleibt deshalb weiß grundiert; die übrigen Schichten // sind reine Auflagen/Landmassen ohne Vertexfarben und bekommen je einen flachen Grundton. const materials = { ground: new MeshStandardMaterial({ color: 0x6d7a52, roughness: 1 }), roads: new MeshStandardMaterial({ color: 0x3a3a3d, roughness: 0.9 }), bridges: new MeshStandardMaterial({ color: 0x8a8a8a, roughness: 0.8 }), buildings: new MeshStandardMaterial({ vertexColors: true, roughness: 0.85 }), sand: new MeshStandardMaterial({ color: 0xd9c48c, roughness: 1 }), park: new MeshStandardMaterial({ color: 0x4f7a3d, roughness: 1 }), urban: new MeshStandardMaterial({ color: 0xa8a8a0, roughness: 0.9 }), }; // 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, }, }); // Chunk-Collider treffen erst nach und nach ein (Fetch + asynchroner BVH-Bau, siehe // streaming.js). Bis der Chunk unter dem Spawn bereit ist — oder spätestens nach 15s — // bleibt die Kapselkollision aus, damit die Figur nicht an halb geladenen Gebäuden hängen // bleibt; das Auffangnetz unten fängt jeden tieferen Durchfall ab (auch während dieser Zeit). const streamer = new ChunkStreamer({ scene, player, materials }); player.setSkipCapsuleCollision(true); let settled = false; const startedAt = performance.now(); let lastSafe = { ...SPAWN }; function tick(delta) { streamer.update(player.playerCapsule.position); if (!settled && (streamer.isReadyAt(player.playerCapsule.position) || performance.now() - startedAt > 15000)) { settled = true; player.setSkipCapsuleCollision(false); player.reset(new Vector3(SPAWN.x, SPAWN.y, SPAWN.z)); } const p = player.playerCapsule.position; if (p.y < -30) player.reset(new Vector3(lastSafe.x, lastSafe.y, lastSafe.z)); else if (p.y > -1) 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); renderer.render(scene, camera); }); addEventListener("resize", () => { renderer.setSize(container.clientWidth, container.clientHeight); camera.aspect = container.clientWidth / container.clientHeight; camera.updateProjectionMatrix(); });