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 { PoseEstimator, ROT_MODES, setRotMode, getRotMode } from './ar/pose.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 { ghostTransform } from './ghosts/behavior.js';
|
||||
import { ExhibitNet, installErrorReporter } from './net.js';
|
||||
@@ -26,7 +26,6 @@ const ENGINES = ['opencv', 'posit'];
|
||||
let engine = localStorage.getItem('nbx.engine') || 'opencv';
|
||||
if (!ENGINES.includes(engine)) engine = 'opencv';
|
||||
let cvState = 'idle'; // idle | loading | ready | failed
|
||||
let cvNote = ''; // download % / init stage while loading
|
||||
function setEngine(e) { engine = e; localStorage.setItem('nbx.engine', e); }
|
||||
|
||||
/* 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). */
|
||||
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)
|
||||
const _dbgEuler = new THREE.Euler();
|
||||
const _dbgV = new THREE.Vector3();
|
||||
@@ -81,7 +90,8 @@ let dbgPose = '';
|
||||
function dbgStatus() {
|
||||
const ws = netRef && netRef.ws ? ['conn','open','closing','closed'][netRef.ws.readyState] : 'none';
|
||||
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; }
|
||||
|
||||
@@ -128,7 +138,7 @@ function updateDbg(fusedQuat, markerCount, extra) {
|
||||
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 modeLine = engine === 'opencv'
|
||||
? `axis ${cvSolver ? cvSolver.getAxisMode() : 'std'} cv:${cvState}${cvNote ? ' ' + cvNote : ''}`
|
||||
? `axis ${cvSolver ? cvSolver.getAxisMode() : 'std'} cv:${cvState}`
|
||||
: `rot ${getRotMode()} cam ${camMode}`;
|
||||
dbgPose =
|
||||
`eng ${engine} ${freezeGhosts ? 'FROZEN' : 'moving'}\n` +
|
||||
@@ -184,18 +194,19 @@ async function applyLanding() {
|
||||
document.title = L.title || 'Newbury Nights';
|
||||
}
|
||||
|
||||
/* Lazy-load the OpenCV.js WASM (~13MB, cached hard by the server). On failure the
|
||||
* posit engine takes over automatically — visitors always get a working exhibit. */
|
||||
/* Boot the OpenCV worker. All 13MB of parse + WASM compile happens off the main
|
||||
* thread, so the render loop keeps running while it loads; the posit engine
|
||||
* covers tracking meanwhile and permanently if the worker fails. */
|
||||
function ensureOpenCV() {
|
||||
if (cvState === 'ready' || cvState === 'loading') return;
|
||||
// 'failed' falls through: tapping back to opencv retries the load
|
||||
cvState = 'loading';
|
||||
loadOpenCV('/vendor/opencv.js', (p) => { cvNote = typeof p === 'number' ? p + '%' : String(p); }).then(() => {
|
||||
cvState = 'ready'; cvNote = '';
|
||||
cvSolver.load().then(() => {
|
||||
cvState = 'ready';
|
||||
if (engine === 'opencv') toast('Tracking engine ready');
|
||||
}).catch((e) => {
|
||||
cvState = 'failed'; cvNote = '';
|
||||
console.warn('opencv.js unavailable, falling back to posit', e);
|
||||
cvState = 'failed';
|
||||
console.warn('opencv worker unavailable, falling back to posit', e);
|
||||
if (engine === 'opencv') { setEngine('posit'); toast('Using classic tracking'); }
|
||||
});
|
||||
}
|
||||
@@ -396,7 +407,13 @@ let frameCount = 0;
|
||||
|
||||
function loop(t) {
|
||||
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.
|
||||
const W = 960;
|
||||
const H = Math.round(W * video.videoHeight / video.videoWidth);
|
||||
@@ -416,7 +433,7 @@ function loop(t) {
|
||||
tracking.lastSeen = performance.now();
|
||||
|
||||
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);
|
||||
if (fused) {
|
||||
camera3.position.copy(fused.position);
|
||||
|
||||
Reference in New Issue
Block a user