clone

an endless marble-race generator in the browser; the url is the seed, so every track can be shared

main
marbles / src / client / scene.ts
scene.ts 2.9K
1import * as THREE from "three";23export interface SceneCtx {4 renderer: THREE.WebGLRenderer;5 scene: THREE.Scene;6 camera: THREE.PerspectiveCamera;7 sun: THREE.DirectionalLight;8 hemi: THREE.HemisphereLight;9 /** Playback control; zero-dt frames let orbit controls keep responding. */10 paused?: boolean;11 /** Per-frame hooks, called with dt (seconds) and elapsed time. */12 onFrame: Set<(dt: number, t: number) => void>;13}1415export function createScene(container: HTMLElement): SceneCtx {16 const renderer = new THREE.WebGLRenderer({ antialias: true });17 // Glass marbles (transmission) sample a scene texture — half-res is18 // visually free and halves the extra pass (r172+).19 renderer.transmissionResolutionScale = 0.5;20 renderer.setPixelRatio(Math.min(devicePixelRatio, 2));21 renderer.outputColorSpace = THREE.SRGBColorSpace;22 renderer.toneMapping = THREE.NeutralToneMapping;23 renderer.shadowMap.enabled = true;24 renderer.shadowMap.type = THREE.PCFSoftShadowMap;25 container.appendChild(renderer.domElement);2627 const scene = new THREE.Scene();28 scene.background = new THREE.Color(0x87b7e0);29 scene.fog = new THREE.Fog(0x87b7e0, 40, 140);3031 const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 500);32 camera.position.set(12, 10, 12);3334 const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x8a6f5a, 1.1);35 scene.add(hemi);3637 const sun = new THREE.DirectionalLight(0xfff4e0, 2.2);38 sun.position.set(18, 30, 12);39 sun.castShadow = true;40 sun.shadow.mapSize.set(2048, 2048);41 sun.shadow.bias = -0.0004;42 sun.shadow.normalBias = 0.02;43 scene.add(sun);44 scene.add(sun.target);4546 const onFrame = new Set<(dt: number, t: number) => void>();4748 function resize() {49 const w = container.clientWidth || innerWidth;50 const h = container.clientHeight || innerHeight;51 renderer.setSize(w, h);52 camera.aspect = w / h;53 camera.updateProjectionMatrix();54 }55 addEventListener("resize", resize);56 resize();5758 const ctx: SceneCtx = {59 renderer,60 scene,61 camera,62 sun,63 hemi,64 onFrame,65 paused: false,66 };67 const clock = new THREE.Clock();68 renderer.setAnimationLoop(() => {69 const dt = Math.min(clock.getDelta(), 0.1);70 const t = clock.elapsedTime;71 for (const fn of onFrame) fn(ctx.paused ? 0 : dt, t);72 renderer.render(scene, camera);73 });7475 return ctx;76}7778/** Fit the sun's shadow frustum tightly around a world-space bounding box. */79export function fitShadowsTo(80 sun: THREE.DirectionalLight,81 box: THREE.Box3,82): void {83 const center = box.getCenter(new THREE.Vector3());84 const size = box.getSize(new THREE.Vector3());85 const radius = Math.max(size.x, size.z) * 0.75 + 4;86 sun.target.position.copy(center);87 sun.position.copy(center).add(88 new THREE.Vector3(0.6, 1.2, 0.4).multiplyScalar(radius * 1.6),89 );90 const cam = sun.shadow.camera;91 cam.left = -radius;92 cam.right = radius;93 cam.top = radius;94 cam.bottom = -radius;95 cam.near = 0.5;96 cam.far = radius * 5;97 cam.updateProjectionMatrix();98}