TURNCRAFT/src/engine/renderer.ts
jing 49cab1f69b Concept-art pass: terraced records, spindle towers, warm lit room
- 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>
2026-07-13 21:48:00 +10:00

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);
}
},
};
}