| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- // First-person walkthrough of a reconstructed scene.glb.
- // Octree + capsule collision (so you don't fall through walls/floor),
- // desktop pointer-lock + WASD, mobile joystick + drag-look.
- // All movement params derive from the scene's bounding box, so it feels
- // the same regardless of the (arbitrary, up-to-scale) COLMAP units.
- import * as THREE from 'three';
- import { GLTFLoader } from './vendor/jsm/loaders/GLTFLoader.js';
- import { Octree } from './vendor/jsm/math/Octree.js';
- import { Capsule } from './vendor/jsm/math/Capsule.js';
- let renderer, scene, camera, raf, container, clock;
- let octree, collider, startPoint, params;
- const velocity = new THREE.Vector3();
- const dir = new THREE.Vector3();
- let onFloor = false;
- const keys = {};
- const joy = { x: 0, y: 0 }; // -1..1 strafe / forward from the mobile stick
- let looking = null; // active drag-look pointer id
- const STEPS = 5;
- function disposeScene(obj) {
- obj.traverse((o) => {
- if (o.geometry) o.geometry.dispose();
- if (o.material) (Array.isArray(o.material) ? o.material : [o.material]).forEach((m) => m.dispose());
- });
- }
- export function stopViewer() {
- if (raf) cancelAnimationFrame(raf);
- raf = null;
- window.removeEventListener('resize', onResize);
- document.removeEventListener('keydown', onKey);
- document.removeEventListener('keyup', onKey);
- if (renderer) {
- renderer.domElement.remove();
- renderer.dispose();
- renderer = null;
- }
- if (scene) { disposeScene(scene); scene = null; }
- }
- export async function startViewer(glbUrl) {
- stopViewer();
- container = document.getElementById('viewer-canvas');
- clock = new THREE.Clock();
- scene = new THREE.Scene();
- scene.background = new THREE.Color(0x10141b);
- camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.01, 5000);
- camera.rotation.order = 'YXZ';
- scene.add(new THREE.HemisphereLight(0xffffff, 0x444455, 1.1));
- scene.add(new THREE.AmbientLight(0xffffff, 0.35));
- const sun = new THREE.DirectionalLight(0xffffff, 1.4);
- sun.position.set(1, 2, 1);
- scene.add(sun);
- renderer = new THREE.WebGLRenderer({ antialias: true });
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- renderer.setSize(container.clientWidth, container.clientHeight);
- renderer.outputColorSpace = THREE.SRGBColorSpace;
- container.appendChild(renderer.domElement);
- const gltf = await new GLTFLoader().loadAsync(glbUrl);
- const model = gltf.scene;
- scene.add(model);
- // --- derive scale-aware params from the bounding box ---
- const box = new THREE.Box3().setFromObject(model);
- const size = box.getSize(new THREE.Vector3());
- const center = box.getCenter(new THREE.Vector3());
- const diag = size.length() || 1;
- // Scaled to the scene diagonal so the "time to cross the room" feel is the
- // same at any (arbitrary, up-to-scale) reconstruction scale.
- params = {
- radius: diag * 0.01,
- eye: diag * 0.03,
- accelGround: diag * 0.5, // terminal walk speed ~= accelGround/4
- accelAir: diag * 0.16,
- gravity: diag * 0.6,
- jump: diag * 0.32,
- fallY: box.min.y - diag * 0.6,
- };
- camera.far = diag * 20;
- camera.updateProjectionMatrix();
- octree = new Octree();
- octree.fromGraphNode(model);
- // Spawn STANDING on the floor: raycast straight down through the scene centre.
- // (Dropping from the ceiling tunnels through thin reconstructed floors.)
- const ray = new THREE.Raycaster(
- new THREE.Vector3(center.x, box.max.y + diag * 0.1, center.z),
- new THREE.Vector3(0, -1, 0),
- );
- const hit = ray.intersectObject(model, true)[0];
- const floorY = hit ? hit.point.y : box.min.y;
- startPoint = new THREE.Vector3(center.x, floorY + params.eye, center.z);
- window.__params = Object.assign({ diag, floorY }, params);
- resetPlayer();
- window.addEventListener('resize', onResize);
- document.addEventListener('keydown', onKey);
- document.addEventListener('keyup', onKey);
- setupPointer();
- setupTouch();
- animate();
- }
- function resetPlayer() {
- collider = new Capsule(
- startPoint.clone().add(new THREE.Vector3(0, params.radius, 0)),
- startPoint.clone().add(new THREE.Vector3(0, params.eye, 0)),
- params.radius,
- );
- velocity.set(0, 0, 0);
- camera.position.copy(collider.end);
- }
- export function recenter() { if (collider) resetPlayer(); }
- function onResize() {
- if (!renderer) return;
- camera.aspect = container.clientWidth / container.clientHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(container.clientWidth, container.clientHeight);
- }
- function onKey(e) { keys[e.code] = e.type === 'keydown'; }
- // --- look: pointer lock (desktop) + drag (touch/mouse) ---
- function setupPointer() {
- const el = renderer.domElement;
- el.addEventListener('click', () => { if (!isTouch()) el.requestPointerLock(); });
- document.addEventListener('mousemove', (e) => {
- if (document.pointerLockElement === el) applyLook(e.movementX, e.movementY);
- });
- }
- function setupTouch() {
- const el = renderer.domElement;
- let last = null;
- el.addEventListener('touchstart', (e) => {
- const t = e.changedTouches[0];
- if (t.clientX < window.innerWidth * 0.4) return; // left zone = joystick
- looking = t.identifier; last = { x: t.clientX, y: t.clientY };
- }, { passive: true });
- el.addEventListener('touchmove', (e) => {
- for (const t of e.changedTouches) {
- if (t.identifier === looking && last) {
- applyLook(t.clientX - last.x, t.clientY - last.y);
- last = { x: t.clientX, y: t.clientY };
- }
- }
- }, { passive: true });
- el.addEventListener('touchend', (e) => {
- for (const t of e.changedTouches) if (t.identifier === looking) { looking = null; last = null; }
- }, { passive: true });
- }
- function applyLook(dx, dy) {
- camera.rotation.y -= dx / 500;
- camera.rotation.x -= dy / 500;
- camera.rotation.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, camera.rotation.x));
- }
- function isTouch() { return matchMedia('(pointer: coarse)').matches; }
- // --- physics (adapted from three.js games_fps example) ---
- function collide() {
- const r = octree.capsuleIntersect(collider);
- onFloor = false;
- if (r) {
- onFloor = r.normal.y > 0;
- collider.translate(r.normal.multiplyScalar(r.depth));
- }
- }
- function forward() {
- camera.getWorldDirection(dir); dir.y = 0; dir.normalize(); return dir;
- }
- function side() {
- camera.getWorldDirection(dir); dir.y = 0; dir.normalize(); dir.cross(camera.up); return dir;
- }
- function step(dt) {
- let damp = Math.exp(-4 * dt) - 1;
- if (!onFloor) { velocity.y -= params.gravity * dt; damp *= 0.1; }
- velocity.addScaledVector(velocity, damp);
- const sd = dt * (onFloor ? params.accelGround : params.accelAir);
- let f = 0, s = 0;
- if (keys['KeyW'] || keys['ArrowUp']) f += 1;
- if (keys['KeyS'] || keys['ArrowDown']) f -= 1;
- if (keys['KeyD'] || keys['ArrowRight']) s += 1;
- if (keys['KeyA'] || keys['ArrowLeft']) s -= 1;
- f += joy.y; s += joy.x;
- if (f) velocity.add(forward().multiplyScalar(sd * f));
- if (s) velocity.add(side().multiplyScalar(sd * s));
- if (onFloor && keys['Space']) velocity.y = params.jump;
- collider.translate(velocity.clone().multiplyScalar(dt));
- collide();
- camera.position.copy(collider.end);
- if (camera.position.y < params.fallY) resetPlayer();
- }
- function animate() {
- raf = requestAnimationFrame(animate);
- const dt = Math.min(0.05, clock.getDelta()) / STEPS;
- for (let i = 0; i < STEPS; i++) step(dt);
- renderer.render(scene, camera);
- window.__genreconTris = renderer.info.render.triangles; // test/debug readiness signal
- window.__genreconCam = [camera.position.x, camera.position.y, camera.position.z];
- window.__dbg = { onFloor, vel: [velocity.x, velocity.y, velocity.z], keys: Object.keys(keys).filter((k) => keys[k]) };
- window.__genreconReady = true;
- }
- // mobile joystick hook (called from app.js)
- export function setJoystick(x, y) { joy.x = x; joy.y = y; }
|