Switchable camera-frame correction + tap-to-cycle rot/cam modes on debug overlay

This commit is contained in:
2026-08-04 09:57:55 +10:00
parent 3a961d64fd
commit f4d51b6def
+37 -14
View File
@@ -5,7 +5,7 @@
*/
import * as THREE from 'three';
import { createDetector, detectMarkers, quadArea } from './ar/detect.js';
import { PoseEstimator } from './ar/pose.js';
import { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.js';
import { WorldFuser } from './ar/fuse.js';
import { buildGhost } from './ghosts/loader.js';
import { ghostTransform } from './ghosts/behavior.js';
@@ -14,15 +14,19 @@ import { renderMarkdown } from './md.js';
const $ = (s) => document.querySelector(s);
/* The fused pose is a camera-in-world transform in OpenCV convention
* (+X right, +Y DOWN, +Z FORWARD along the view axis). A Three.js camera uses
* OpenGL convention (+X right, +Y UP, looks down LOCAL -Z). The correct change of
* basis between them is a 180° rotation about the camera's local Y axis: it flips
* the forward axis (+Z_cv view -> three.js -Z) AND the up axis (+Y_cv down ->
* three.js +Y up) together, leaving roll correct. (A 180° about X fixes the view
* axis too but leaves roll inverted — ghosts render upside-down when the phone is
* upright.) Applied after copying the fused quaternion each frame. */
const CV_TO_GL = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
/* Camera-frame correction applied to the fused quaternion (see pose.js for the
* rotation-frame conjugation that happens upstream). Switchable for on-device A/B:
* once pose.js conjugates the rotation correctly, the right camera correction here
* may be identity, Y180, X180, or Z180 depending on the net convention. The debug
* overlay cycles CAM_MODES. */
const CAM_CORR = {
none: new THREE.Quaternion(),
y180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI),
x180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI),
z180: new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI),
};
const CAM_MODES = ['none', 'y180', 'x180', 'z180'];
let camMode = 'none'; // default: none — rely on pose.js conjugation alone
let info = { mode: 'dev' };
let scene3, camera3, renderer, video, canvas2d, ctx2d;
@@ -50,6 +54,7 @@ function updateDbg(fusedQuat, markerCount) {
const up = _dbgV.set(0, 1, 0).applyQuaternion(camera3.quaternion).clone();
const v3 = v => `(${v.x.toFixed(2)},${v.y.toFixed(2)},${v.z.toFixed(2)})`;
dbgEl.textContent =
`rot ${getRotMode()} cam ${camMode} (tap to cycle)\n` +
`markers ${markerCount}\n${rawLine}\nview ${v3(view)}\nup ${v3(up)}\n` +
`pos ${v3(camera3.position)}`;
}
@@ -131,8 +136,25 @@ async function start() {
detector = createDetector();
fuser = new WorldFuser();
// dev-mode pose readout
if (info.mode === 'dev') { dbgEl = $('#dbg'); if (dbgEl) dbgEl.style.display = 'block'; }
// dev-mode pose readout + tap-to-cycle correction modes
if (info.mode === 'dev') {
dbgEl = $('#dbg');
if (dbgEl) {
dbgEl.style.display = 'block';
dbgEl.style.pointerEvents = 'auto';
// tap top ~60% cycles rotation mode; tap bottom cycles camera mode
dbgEl.addEventListener('click', (ev) => {
const r = dbgEl.getBoundingClientRect();
if ((ev.clientY - r.top) < r.height * 0.5) {
const i = ROT_MODES.indexOf(getRotMode());
setRotMode(ROT_MODES[(i + 1) % ROT_MODES.length]);
} else {
const i = CAM_MODES.indexOf(camMode);
camMode = CAM_MODES[(i + 1) % CAM_MODES.length];
}
});
}
}
// network
const net = new ExhibitNet();
@@ -241,8 +263,9 @@ function loop(t) {
const fused = fuser.fuse(estimates);
if (fused) {
camera3.position.copy(fused.position);
// convert OpenCV camera frame -> Three.js (OpenGL) camera frame
camera3.quaternion.copy(fused.quaternion).multiply(CV_TO_GL);
// pose.js already conjugated rotation into the three.js frame; apply any
// residual camera-frame correction (switchable via overlay)
camera3.quaternion.copy(fused.quaternion).multiply(CAM_CORR[camMode]);
updateDbg(fused.quaternion, fused.markerCount);
}
}