perf: throttle detection to 20Hz (render stays full rate), use the worker-backed solver, add fps to the HUD
This commit is contained in:
+29
-12
@@ -13,7 +13,7 @@ import * as THREE from 'three';
|
|||||||
import { createDetector, detectMarkers, quadArea } from './ar/detect.js';
|
import { createDetector, detectMarkers, quadArea } from './ar/detect.js';
|
||||||
import { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.js';
|
import { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.js';
|
||||||
import { WorldFuser } from './ar/fuse.js';
|
import { WorldFuser } from './ar/fuse.js';
|
||||||
import { CvBoardSolver, CV_AXIS_MODES, loadOpenCV, isCvReady } from './ar/solve-cv.js';
|
import { CvBoardSolver, CV_AXIS_MODES } from './ar/solve-cv.js';
|
||||||
import { buildGhost } from './ghosts/loader.js';
|
import { buildGhost } from './ghosts/loader.js';
|
||||||
import { ghostTransform } from './ghosts/behavior.js';
|
import { ghostTransform } from './ghosts/behavior.js';
|
||||||
import { ExhibitNet, installErrorReporter } from './net.js';
|
import { ExhibitNet, installErrorReporter } from './net.js';
|
||||||
@@ -26,7 +26,6 @@ const ENGINES = ['opencv', 'posit'];
|
|||||||
let engine = localStorage.getItem('nbx.engine') || 'opencv';
|
let engine = localStorage.getItem('nbx.engine') || 'opencv';
|
||||||
if (!ENGINES.includes(engine)) engine = 'opencv';
|
if (!ENGINES.includes(engine)) engine = 'opencv';
|
||||||
let cvState = 'idle'; // idle | loading | ready | failed
|
let cvState = 'idle'; // idle | loading | ready | failed
|
||||||
let cvNote = ''; // download % / init stage while loading
|
|
||||||
function setEngine(e) { engine = e; localStorage.setItem('nbx.engine', e); }
|
function setEngine(e) { engine = e; localStorage.setItem('nbx.engine', e); }
|
||||||
|
|
||||||
/* Camera-frame correction for the POSIT path (unchanged from v2; the opencv path
|
/* Camera-frame correction for the POSIT path (unchanged from v2; the opencv path
|
||||||
@@ -73,6 +72,16 @@ let clockOffset = 0; // serverNow - clientNow
|
|||||||
* via localStorage 'nbx.hfovDeg' (shared by both engines). */
|
* via localStorage 'nbx.hfovDeg' (shared by both engines). */
|
||||||
const HFOV_DEG = parseFloat(localStorage.getItem('nbx.hfovDeg')) || 60;
|
const HFOV_DEG = parseFloat(localStorage.getItem('nbx.hfovDeg')) || 60;
|
||||||
|
|
||||||
|
/* Detection cadence. Marker detection (getImageData + ArUco decode over a 960px
|
||||||
|
* frame) is by far the most expensive thing per frame, and running it on every
|
||||||
|
* rAF tick pins the CPU for no benefit — the crests aren't moving, the phone is.
|
||||||
|
* ~20Hz tracks hand movement fine and the 1e filter smooths between updates,
|
||||||
|
* while rendering stays at full frame rate. Override: localStorage 'nbx.detectHz'. */
|
||||||
|
const DETECT_HZ = parseFloat(localStorage.getItem('nbx.detectHz')) || 20;
|
||||||
|
const DETECT_INTERVAL_MS = 1000 / DETECT_HZ;
|
||||||
|
let lastDetectAt = 0;
|
||||||
|
let fps = 0, fpsFrames = 0, fpsAt = 0;
|
||||||
|
|
||||||
// Debug pose readout (dev mode)
|
// Debug pose readout (dev mode)
|
||||||
const _dbgEuler = new THREE.Euler();
|
const _dbgEuler = new THREE.Euler();
|
||||||
const _dbgV = new THREE.Vector3();
|
const _dbgV = new THREE.Vector3();
|
||||||
@@ -81,7 +90,8 @@ let dbgPose = '';
|
|||||||
function dbgStatus() {
|
function dbgStatus() {
|
||||||
const ws = netRef && netRef.ws ? ['conn','open','closing','closed'][netRef.ws.readyState] : 'none';
|
const ws = netRef && netRef.ws ? ['conn','open','closing','closed'][netRef.ws.readyState] : 'none';
|
||||||
return `ws ${ws} rx ${rxLog.join(',') || '-'}\n` +
|
return `ws ${ws} rx ${rxLog.join(',') || '-'}\n` +
|
||||||
`scene ${sceneLoaded ? anchorCount + ' anchors' : 'NOT LOADED'} ghost recs ${activeGhosts.size}\n`;
|
`scene ${sceneLoaded ? anchorCount + ' anchors' : 'NOT LOADED'} ghost recs ${activeGhosts.size}\n` +
|
||||||
|
`${fps} fps detect ${DETECT_HZ}Hz\n`;
|
||||||
}
|
}
|
||||||
function paintDbg() { if (dbgEl) dbgEl.textContent = dbgStatus() + dbgPose + dbgAim; }
|
function paintDbg() { if (dbgEl) dbgEl.textContent = dbgStatus() + dbgPose + dbgAim; }
|
||||||
|
|
||||||
@@ -128,7 +138,7 @@ function updateDbg(fusedQuat, markerCount, extra) {
|
|||||||
const up = _dbgV.set(0, 1, 0).applyQuaternion(camera3.quaternion).clone();
|
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)})`;
|
const v3 = v => `(${v.x.toFixed(2)},${v.y.toFixed(2)},${v.z.toFixed(2)})`;
|
||||||
const modeLine = engine === 'opencv'
|
const modeLine = engine === 'opencv'
|
||||||
? `axis ${cvSolver ? cvSolver.getAxisMode() : 'std'} cv:${cvState}${cvNote ? ' ' + cvNote : ''}`
|
? `axis ${cvSolver ? cvSolver.getAxisMode() : 'std'} cv:${cvState}`
|
||||||
: `rot ${getRotMode()} cam ${camMode}`;
|
: `rot ${getRotMode()} cam ${camMode}`;
|
||||||
dbgPose =
|
dbgPose =
|
||||||
`eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` +
|
`eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` +
|
||||||
@@ -184,18 +194,19 @@ async function applyLanding() {
|
|||||||
document.title = L.title || 'Newbury Nights';
|
document.title = L.title || 'Newbury Nights';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lazy-load the OpenCV.js WASM (~13MB, cached hard by the server). On failure the
|
/* Boot the OpenCV worker. All 13MB of parse + WASM compile happens off the main
|
||||||
* posit engine takes over automatically — visitors always get a working exhibit. */
|
* thread, so the render loop keeps running while it loads; the posit engine
|
||||||
|
* covers tracking meanwhile and permanently if the worker fails. */
|
||||||
function ensureOpenCV() {
|
function ensureOpenCV() {
|
||||||
if (cvState === 'ready' || cvState === 'loading') return;
|
if (cvState === 'ready' || cvState === 'loading') return;
|
||||||
// 'failed' falls through: tapping back to opencv retries the load
|
// 'failed' falls through: tapping back to opencv retries the load
|
||||||
cvState = 'loading';
|
cvState = 'loading';
|
||||||
loadOpenCV('/vendor/opencv.js', (p) => { cvNote = typeof p === 'number' ? p + '%' : String(p); }).then(() => {
|
cvSolver.load().then(() => {
|
||||||
cvState = 'ready'; cvNote = '';
|
cvState = 'ready';
|
||||||
if (engine === 'opencv') toast('Tracking engine ready');
|
if (engine === 'opencv') toast('Tracking engine ready');
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
cvState = 'failed'; cvNote = '';
|
cvState = 'failed';
|
||||||
console.warn('opencv.js unavailable, falling back to posit', e);
|
console.warn('opencv worker unavailable, falling back to posit', e);
|
||||||
if (engine === 'opencv') { setEngine('posit'); toast('Using classic tracking'); }
|
if (engine === 'opencv') { setEngine('posit'); toast('Using classic tracking'); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -396,7 +407,13 @@ let frameCount = 0;
|
|||||||
|
|
||||||
function loop(t) {
|
function loop(t) {
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
if (video && video.readyState >= 2) {
|
|
||||||
|
// rolling fps so the HUD shows what the render loop is actually managing
|
||||||
|
fpsFrames++;
|
||||||
|
if (t - fpsAt >= 1000) { fps = Math.round(fpsFrames * 1000 / (t - fpsAt)); fpsFrames = 0; fpsAt = t; }
|
||||||
|
|
||||||
|
if (video && video.readyState >= 2 && (t - lastDetectAt) >= DETECT_INTERVAL_MS) {
|
||||||
|
lastDetectAt = t;
|
||||||
// downscale for detection speed; corner precision scales with resolution.
|
// downscale for detection speed; corner precision scales with resolution.
|
||||||
const W = 960;
|
const W = 960;
|
||||||
const H = Math.round(W * video.videoHeight / video.videoWidth);
|
const H = Math.round(W * video.videoHeight / video.videoWidth);
|
||||||
@@ -416,7 +433,7 @@ function loop(t) {
|
|||||||
tracking.lastSeen = performance.now();
|
tracking.lastSeen = performance.now();
|
||||||
|
|
||||||
if (engine === 'opencv' && cvState === 'ready') {
|
if (engine === 'opencv' && cvState === 'ready') {
|
||||||
// v3 path: one joint solve over every visible crest
|
// v3 path: one joint solve over every visible crest (runs in the worker)
|
||||||
const fused = cvSolver.solve(markers, W, H, focal);
|
const fused = cvSolver.solve(markers, W, H, focal);
|
||||||
if (fused) {
|
if (fused) {
|
||||||
camera3.position.copy(fused.position);
|
camera3.position.copy(fused.position);
|
||||||
|
|||||||
Reference in New Issue
Block a user