|
|
@@ -10,6 +10,7 @@ import { Capsule } from './vendor/jsm/math/Capsule.js';
|
|
|
|
|
|
let renderer, scene, camera, raf, container, clock;
|
|
|
let octree, collider, startPoint, params;
|
|
|
+let startYaw = 0, startPitch = 0;
|
|
|
const velocity = new THREE.Vector3();
|
|
|
const dir = new THREE.Vector3();
|
|
|
let onFloor = false;
|
|
|
@@ -88,16 +89,16 @@ export async function startViewer(glbUrl) {
|
|
|
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);
|
|
|
+ // Spawn STANDING on the floor at the most OPEN spot, facing the scene. Blindly
|
|
|
+ // spawning at the horizontal centre buries the camera inside geometry for
|
|
|
+ // object-centric captures (a bonsai, a statue…) → a dark close-up. Sample the
|
|
|
+ // floor and pick the standing position with the most horizontal clearance.
|
|
|
+ const spawn = chooseSpawn(model, box, size, center, diag, params.eye);
|
|
|
+ startPoint = spawn.point;
|
|
|
+ startYaw = spawn.yaw;
|
|
|
+ startPitch = -0.12; // a gentle downward tilt to frame the scene
|
|
|
+ window.__params = Object.assign(
|
|
|
+ { diag, floorY: spawn.floorY, clearance: Math.round(spawn.clr * 1000) / 1000 }, params);
|
|
|
resetPlayer();
|
|
|
|
|
|
window.addEventListener('resize', onResize);
|
|
|
@@ -109,6 +110,46 @@ export async function startViewer(glbUrl) {
|
|
|
animate();
|
|
|
}
|
|
|
|
|
|
+// Find a good first-person spawn: sample floor points across the scene, keep the
|
|
|
+// one with the most open horizontal clearance (so the camera isn't wedged in a
|
|
|
+// wall), and face it toward the scene's centre of geometry. Falls back to the
|
|
|
+// scene centre if nothing samples cleanly.
|
|
|
+function chooseSpawn(model, box, size, center, diag, eye) {
|
|
|
+ const ray = new THREE.Raycaster();
|
|
|
+ const down = new THREE.Vector3(0, -1, 0);
|
|
|
+ const dirs = [];
|
|
|
+ for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2; dirs.push(new THREE.Vector3(Math.cos(a), 0, Math.sin(a))); }
|
|
|
+ const N = 4, inset = 0.18;
|
|
|
+ const cands = [];
|
|
|
+ const floorAt = (x, z) => {
|
|
|
+ ray.set(new THREE.Vector3(x, box.max.y + diag * 0.1, z), down);
|
|
|
+ const h = ray.intersectObject(model, true)[0];
|
|
|
+ return h ? h.point.y : null;
|
|
|
+ };
|
|
|
+ const sample = (x, z) => {
|
|
|
+ const fy = floorAt(x, z);
|
|
|
+ if (fy === null) return;
|
|
|
+ const p = new THREE.Vector3(x, fy + eye, z);
|
|
|
+ let clr = diag;
|
|
|
+ for (const d of dirs) { ray.set(p, d); const h = ray.intersectObject(model, true)[0]; if (h) clr = Math.min(clr, h.distance); }
|
|
|
+ cands.push({ point: p, floorY: fy, clr });
|
|
|
+ };
|
|
|
+ for (let ix = 0; ix <= N; ix++) for (let iz = 0; iz <= N; iz++) {
|
|
|
+ sample(box.min.x + size.x * (inset + (1 - 2 * inset) * ix / N),
|
|
|
+ box.min.z + size.z * (inset + (1 - 2 * inset) * iz / N));
|
|
|
+ }
|
|
|
+ sample(center.x, center.z);
|
|
|
+ if (!cands.length) {
|
|
|
+ const fy = floorAt(center.x, center.z);
|
|
|
+ return { point: new THREE.Vector3(center.x, (fy ?? box.min.y) + eye, center.z), yaw: 0, floorY: fy ?? box.min.y, clr: 0 };
|
|
|
+ }
|
|
|
+ cands.sort((a, b) => b.clr - a.clr);
|
|
|
+ const best = cands[0];
|
|
|
+ const dx = center.x - best.point.x, dz = center.z - best.point.z;
|
|
|
+ best.yaw = (Math.abs(dx) + Math.abs(dz) < diag * 1e-3) ? 0 : Math.atan2(-dx, -dz);
|
|
|
+ return best;
|
|
|
+}
|
|
|
+
|
|
|
function resetPlayer() {
|
|
|
collider = new Capsule(
|
|
|
startPoint.clone().add(new THREE.Vector3(0, params.radius, 0)),
|
|
|
@@ -117,6 +158,7 @@ function resetPlayer() {
|
|
|
);
|
|
|
velocity.set(0, 0, 0);
|
|
|
camera.position.copy(collider.end);
|
|
|
+ camera.rotation.set(startPitch, startYaw, 0); // face the scene, not a wall
|
|
|
}
|
|
|
export function recenter() { if (collider) resetPlayer(); }
|
|
|
|