Boom height, look offset and camera.up follow the subject's local gravity frame (eased ~1s, snap on retarget). Default world +y — behavior identical until a reorient zone supplies a frame.
- commit
- df23d15aa519493e4820462c8552d8214c6fa282
- parent
- b90bcc6f1334046203dffc455247c2dd2aecbc2f
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 30 | +++++++++++++++++++++++++++--- |
@@ -109,6 +109,11 @@ export interface CameraOptions {
109109 * marble → camera ray. Resolved per frame — pass a closure over the110110 * live scene. */111111 occluders?: () => readonly THREE.Object3D[];112+ /** Local up (gravity reorientation, M16): the follow cam's up vector,113+ * boom height and look offset follow the subject's LOCAL gravity114+ * frame — riding a wall the camera rolls with the world. Resolved per115+ * frame; smoothed internally. Default: world +y. */116+ up?: () => readonly [number, number, number];112117 }113118 114119 export function createCameras(@@ -202,6 +207,9 @@ export function createCameras(
202207 const bankC = new THREE.Vector3();203208 let bankNow = 0;204209 let lookInit = false;210+ /** Smoothed local-up (gravity reorientation) — follows opts.up. */211+ const upNow = new THREE.Vector3(0, 1, 0);212+ const upTarget = new THREE.Vector3(0, 1, 0);205213 206214 const api: Cameras = {207215 get mode() {@@ -273,21 +281,37 @@ export function createCameras(
273281 return;274282 }275283 if (!activeMarble) return;284+ // Local up: eased toward the supplier (a reorient twist rolls the285+ // shot over ~a second, never snaps) except on retarget. The boom286+ // height and look offset ride the same axis, so the standard287+ // framing survives walls and ceilings unchanged.288+ if (opts.up) {289+ const u = opts.up();290+ upTarget.set(u[0], u[1], u[2]);291+ if (!lookInit) upNow.copy(upTarget);292+ else {293+ upNow.lerp(294+ upTarget,295+ 1 - Math.exp(-2.5 * Math.max(dt, 1e-4)),296+ ).normalize();297+ }298+ }299+ camera.up.copy(upNow);276300 if (pathSampler) {277301 if (!pathSampler(activeMarble.s - trail, desired)) return;278- desired.y += height;302+ desired.addScaledVector(upNow, height);279303 } else if (activeCurve) {280304 const total = activeCurve.getLength();281305 if (total <= 0) return;282306 const behind = ((activeMarble.s - trail) % total + total) % total;283307 activeCurve.getPointAt(behind / total, desired);284- desired.y += height;308+ desired.addScaledVector(upNow, height);285309 } else {286310 return;287311 }288312 289313 lookTarget.copy(activeMarble.obj.position);290- lookTarget.y += LOOK_UP;314+ lookTarget.addScaledVector(upNow, LOOK_UP);291315 292316 // User adjust (zoom / orbit around the marble) with a debounced293317 // return: after a few quiet seconds the shot eases back to the