Files
gods-eye-view/server/vcam/phone/orientation.js
T
Claude 56586a234b feat(vcam): use a phone on the network as a virtual camera
SCENES > VCAM pairs a phone through a QR code and turns it into a
handheld camera, after Unreal's Live Link VCAM and Blender's VirtuCamera:

- Android Chrome tracks 6DoF through WebXR immersive-ar; iPhone Safari,
  which has no WebXR AR, tracks rotation through DeviceOrientation, and
  thumb sticks dolly, truck, crane and pan.
- A motion scale (1:1 walk to 1:10000 orbit), smoothing, height and
  horizon locks, hold/clutch and recenter, none of which make the camera
  jump. Focal length and scale-aware depth of field with tap-to-focus.
- The phone shows the app's render as a monitor (newest-frame JPEG pull).
- REC records a take as a version-7 track shot in the selected scene.
- The rig claims the camera through the navigation seam (Cockpit refuses
  it) and holds on any other navigation, scene run or globe input.

Pairing never exposes the app: a loopback-only session request starts a
separate HTTPS listener (VCAM_PORT, default 4174) that serves only the
static phone page and a token-paired relay, and stops after the last
session. Its certificate comes from a local CA built with node:crypto, which
an iPhone can trust once; without it the phone falls back from WebSocket to
HTTPS requests, which iOS allows past a certificate warning.

scripts/qa-vcam.mjs drives it end to end: the real phone page over the
HTTPS fallback, a scripted 6DoF phone, a recorded and replayed take, depth
of field and camera handoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015H1iLusTzWpVKitGic2AW8
2026-10-19 04:03:02 +00:00

46 lines
1.5 KiB
JavaScript

/**
* Device orientation to the virtual camera's Y-up pose frame (+x right,
* +y up, -z forward), shared by the phone page and its tests.
*/
export const quat = {
multiply(a, b) {
const [ax, ay, az, aw] = a;
const [bx, by, bz, bw] = b;
return [
aw * bx + ax * bw + ay * bz - az * by,
aw * by - ax * bz + ay * bw + az * bx,
aw * bz + ax * by - ay * bx + az * bw,
aw * bw - ax * bx - ay * by - az * bz,
];
},
axisAngle(axis, angle) {
const s = Math.sin(angle / 2);
return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(angle / 2)];
},
};
const DEG = Math.PI / 180;
// Device orientation is device→earth (x east, y north, z up). Rotating -90°
// about x maps that earth frame to the Y-up frame (north becomes -z).
const EARTH_TO_Y_UP = quat.axisAngle([1, 0, 0], -Math.PI / 2);
/** W3C DeviceOrientation (Z-X'-Y'') to a Y-up quaternion for the current screen orientation. */
export function orientationQuaternion(alpha, beta, gamma, screenAngle) {
const x = (beta || 0) * DEG;
const y = (gamma || 0) * DEG;
const z = (alpha || 0) * DEG;
const cX = Math.cos(x / 2);
const cY = Math.cos(y / 2);
const cZ = Math.cos(z / 2);
const sX = Math.sin(x / 2);
const sY = Math.sin(y / 2);
const sZ = Math.sin(z / 2);
const device = [
sX * cY * cZ - cX * sY * sZ,
cX * sY * cZ + sX * cY * sZ,
cX * cY * sZ + sX * sY * cZ,
cX * cY * cZ - sX * sY * sZ,
];
const screen = quat.axisAngle([0, 0, 1], -(screenAngle || 0) * DEG);
return quat.multiply(EARTH_TO_Y_UP, quat.multiply(device, screen));
}