- Records are stepped groove amphitheaters (3 vinyl tiers + 4-tier spindle tower, stacked rotating cylinder colliders) - you climb a spinning staircase. Tonearm clearances raised to match. - Player physics: cylinder contacts resolve by minimum penetration (fixes a latent radial-slingshot bug), collider lips auto-step like voxel ledges, rim-standing radius grace. - Well seam ring raised (was a 7-deep inescapable trench, now a 1-step Technics seam). - Plywood ceiling with amber lamp panels + warm point lights; brighter warm lighting rig; colorful mixer knob caps; rim-crumble debris. - window.TURNCRAFT_CINE camera override for screenshots/trailers. Verified live: rim->plateau spiral climb at 33rpm, tower climb 83->87, seam escape, quest/win chain, 121 fps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
// 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);
|
|
}
|
|
},
|
|
};
|
|
}
|