import * as THREE from 'three'; import { getMaxPixelRatio, getQualityTier } from './quality.js'; let scene, camera, renderer; const _worldObjects = []; /** * @param {HTMLCanvasElement|null} existingCanvas — pass the saved canvas on * re-init so Three.js reuses the same DOM element instead of creating a new one */ export function initWorld(existingCanvas) { scene = new THREE.Scene(); scene.background = new THREE.Color(0x000000); scene.fog = new THREE.FogExp2(0x000000, 0.035); camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 500); camera.position.set(0, 12, 28); camera.lookAt(0, 0, 0); const tier = getQualityTier(); renderer = new THREE.WebGLRenderer({ antialias: tier !== 'low', canvas: existingCanvas || undefined, }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, getMaxPixelRatio())); renderer.outputColorSpace = THREE.SRGBColorSpace; if (!existingCanvas) { document.body.prepend(renderer.domElement); } addLights(scene); addGrid(scene, tier); return { scene, camera, renderer }; } function addLights(scene) { const ambient = new THREE.AmbientLight(0x001a00, 0.6); scene.add(ambient); const point = new THREE.PointLight(0x00ff41, 2, 80); point.position.set(0, 20, 0); scene.add(point); const fill = new THREE.DirectionalLight(0x003300, 0.4); fill.position.set(-10, 10, 10); scene.add(fill); } function addGrid(scene, tier) { const gridDivisions = tier === 'low' ? 20 : 40; const grid = new THREE.GridHelper(100, gridDivisions, 0x003300, 0x001a00); grid.position.y = -0.01; scene.add(grid); _worldObjects.push(grid); const planeGeo = new THREE.PlaneGeometry(100, 100); const planeMat = new THREE.MeshBasicMaterial({ color: 0x000a00, transparent: true, opacity: 0.5, }); const plane = new THREE.Mesh(planeGeo, planeMat); plane.rotation.x = -Math.PI / 2; plane.position.y = -0.02; scene.add(plane); _worldObjects.push(plane); } /** * Dispose only world-owned geometries, materials, and the renderer. * Agent and effect objects are disposed by their own modules before this runs. */ export function disposeWorld(disposeRenderer, _scene) { for (const obj of _worldObjects) { if (obj.geometry) obj.geometry.dispose(); if (obj.material) { const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; mats.forEach(m => { if (m.map) m.map.dispose(); m.dispose(); }); } } _worldObjects.length = 0; disposeRenderer.dispose(); } export function onWindowResize(camera, renderer) { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }