// TURNCRAFT — Lane A. The one place Three.js is configured: renderer, scene, // camera, lighting rig, fog. Also builds the block atlas/materials so callers // have a single entry point (createRenderer(...).atlas is handed to ChunkManager). // // Look (DESIGN §7): warm key light + cool hemi/ambient fill, near-black warm // background, fog tinted warm plywood-brown so booth walls fade like a horizon. // No OrbitControls here — demos add their own camera controls. import * as THREE from 'three'; import { buildAtlas, type Atlas } from './atlas'; export interface Renderer { renderer: THREE.WebGLRenderer; scene: THREE.Scene; camera: THREE.PerspectiveCamera; atlas: Atlas; /** Scale emissive (LED) glow; Lane E pulses this to the beat. Default 1. */ setEmissiveBoost(v: number): void; render(): void; resize(width?: number, height?: number): void; dispose(): void; } export function createRenderer(container: HTMLElement): Renderer { const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.05; container.appendChild(renderer.domElement); const scene = new THREE.Scene(); const bg = new THREE.Color(0x181009); // warm dusk haze (concept-art pass) scene.background = bg; scene.fog = new THREE.Fog(0x241a12, 80, 380); // warm plywood-brown room haze const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000); camera.position.set(0, 4, 8); // Warm key light (parallel; direction = position, target at origin). const key = new THREE.DirectionalLight(0xfff1dd, 2.6); key.position.set(70, 130, 45); scene.add(key); // Warm-cream sky / warm ground hemispheric fill — a lamp-lit room, not a void. const hemi = new THREE.HemisphereLight(0xd8c2a4, 0x4a3a28, 1.0); scene.add(hemi); // Small ambient lift so deep AO corners never go pure black. scene.add(new THREE.AmbientLight(0x2a241e, 0.35)); const atlas = buildAtlas(); const setEmissiveBoost = (v: number) => atlas.setEmissiveBoost(v); const resize = (width?: number, height?: number) => { // `||` (not `??`): clientWidth is 0 — not nullish — for an unlaid-out // container, so fall back to the window size in that case too. const w = width || container.clientWidth || window.innerWidth; const h = height || container.clientHeight || window.innerHeight; if (w === 0 || h === 0) return; renderer.setSize(w, h, true); camera.aspect = w / h; camera.updateProjectionMatrix(); }; resize(); // Self-contained resize handling so callers don't have to wire it up. const ro = new ResizeObserver(() => resize()); ro.observe(container); return { renderer, scene, camera, atlas, setEmissiveBoost, render: () => renderer.render(scene, camera), resize, dispose() { ro.disconnect(); atlas.dispose(); renderer.dispose(); if (renderer.domElement.parentElement === container) { container.removeChild(renderer.domElement); } }, }; }