Anchor combining
This commit is contained in:
+81
-7
@@ -182,6 +182,70 @@ function alignWorld(m2c, anchor) {
|
|||||||
lastWorldToCamera = chosen.clone();
|
lastWorldToCamera = chosen.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Multi-marker fusion --------------------------------------------------
|
||||||
|
// When several crests are visible at once, each yields its own world->camera
|
||||||
|
// estimate. Picking a single "winner" makes the world snap as detection flickers
|
||||||
|
// between them. Because every crest's world position is known and the rig is
|
||||||
|
// rigid, we instead FUSE all visible estimates into one consensus pose:
|
||||||
|
// * for each marker, resolve its two POS-IT solutions to the one consistent
|
||||||
|
// with last frame (defeats the planar flip), producing one world->camera;
|
||||||
|
// * weight each by confidence (bigger, lower-error, non-coasting = more trust);
|
||||||
|
// * average translations linearly and rotations via quaternion slerp.
|
||||||
|
// Result: smoother and more accurate than any single marker, with no snap.
|
||||||
|
const _p = new THREE.Vector3(), _q = new THREE.Quaternion(), _s = new THREE.Vector3();
|
||||||
|
|
||||||
|
function resolveMarkerWorldToCamera(m2c, anchor) {
|
||||||
|
// returns the single world->camera for this marker, flip-resolved vs last frame
|
||||||
|
const aToW = anchorToWorldMatrix(anchor);
|
||||||
|
const inv = new THREE.Matrix4().copy(aToW).invert();
|
||||||
|
const candBest = worldToCameraFor(m2c.best.M, inv);
|
||||||
|
if (!m2c.alt) return candBest;
|
||||||
|
const candAlt = worldToCameraFor(m2c.alt.M, inv);
|
||||||
|
if (!lastWorldToCamera) {
|
||||||
|
return (m2c.alt.err < m2c.best.err) ? candBest : candBest; // first lock: best
|
||||||
|
}
|
||||||
|
return matDist(candAlt, lastWorldToCamera) < matDist(candBest, lastWorldToCamera)
|
||||||
|
? candAlt : candBest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuseWorld(observations) {
|
||||||
|
// observations: [{ m2c, anchor, weight }]
|
||||||
|
if (observations.length === 0) return false;
|
||||||
|
|
||||||
|
// Resolve each to a single world->camera, decompose to pos+quat.
|
||||||
|
const parts = [];
|
||||||
|
for (const o of observations) {
|
||||||
|
const wc = resolveMarkerWorldToCamera(o.m2c, o.anchor);
|
||||||
|
const p = new THREE.Vector3(), q = new THREE.Quaternion(), s = new THREE.Vector3();
|
||||||
|
wc.decompose(p, q, s);
|
||||||
|
parts.push({ p, q, w: o.weight });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weighted translation average.
|
||||||
|
const totalW = parts.reduce((a, b) => a + b.w, 0) || 1;
|
||||||
|
const pos = new THREE.Vector3();
|
||||||
|
for (const pt of parts) pos.addScaledVector(pt.p, pt.w / totalW);
|
||||||
|
|
||||||
|
// Weighted rotation average via incremental slerp. Keep all quats on the same
|
||||||
|
// hemisphere as the first (q and -q are the same rotation; slerp needs care).
|
||||||
|
const q0 = parts[0].q.clone();
|
||||||
|
let quat = q0.clone();
|
||||||
|
let accum = parts[0].w;
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
let qi = parts[i].q.clone();
|
||||||
|
if (q0.dot(qi) < 0) qi.set(-qi.x, -qi.y, -qi.z, -qi.w); // hemisphere align
|
||||||
|
accum += parts[i].w;
|
||||||
|
quat.slerp(qi, parts[i].w / accum); // incremental weighted mean
|
||||||
|
quat.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fused = new THREE.Matrix4().compose(pos, quat, new THREE.Vector3(1, 1, 1));
|
||||||
|
world.matrix.copy(fused);
|
||||||
|
world.matrixWorldNeedsUpdate = true;
|
||||||
|
lastWorldToCamera = fused.clone();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Build occluders + anchor viz once scene arrives ----------------------
|
// ---- Build occluders + anchor viz once scene arrives ----------------------
|
||||||
function buildStaticWorld() {
|
function buildStaticWorld() {
|
||||||
// occluders: invisible depth-only boxes so ghosts hide behind buildings
|
// occluders: invisible depth-only boxes so ghosts hide behind buildings
|
||||||
@@ -355,21 +419,31 @@ function loop(now) {
|
|||||||
try { raw = detector.detect(img, { width: grab.width, height: grab.height }); } catch (_) { raw = []; }
|
try { raw = detector.detect(img, { width: grab.width, height: grab.height }); } catch (_) { raw = []; }
|
||||||
const markers = tracker.update(raw, now);
|
const markers = tracker.update(raw, now);
|
||||||
|
|
||||||
// 2) pick the best KNOWN marker (one whose id is an anchor) to align the world.
|
// 2) Fuse ALL visible KNOWN markers into one consensus world pose.
|
||||||
// Prefer the nearest, non-coasting, recognised marker.
|
// Each contributes an estimate weighted by confidence (area, low pose
|
||||||
let best = null;
|
// error, not coasting). Fusing removes the snap you get when a single
|
||||||
|
// "winner" marker flickers between two visible crests, and is more
|
||||||
|
// accurate because the rig geometry is known and rigid.
|
||||||
|
const observations = [];
|
||||||
|
let anyLive = false;
|
||||||
for (const m of markers) {
|
for (const m of markers) {
|
||||||
if (!anchorById.has(m.id)) continue;
|
if (!anchorById.has(m.id)) continue;
|
||||||
const m2c = markerToCameraMatrix(m);
|
const m2c = markerToCameraMatrix(m);
|
||||||
if (!m2c) continue;
|
if (!m2c) continue;
|
||||||
if (!best || (!m.coasting && m2c.dist < best.dist)) best = { m, m2c, dist: m2c.dist, coasting: m.coasting };
|
// confidence: lower pose error + live (not coasting) + nearer = more trust
|
||||||
|
const errW = 1 / (1 + (m2c.best.err ?? 0)); // lower error -> higher weight
|
||||||
|
const coastW = m.coasting ? 0.35 : 1; // trust live reads more
|
||||||
|
const nearW = 1 / (1 + m2c.dist / 1000); // nearer markers slightly favoured
|
||||||
|
const weight = errW * coastW * nearW;
|
||||||
|
observations.push({ m2c, anchor: anchorById.get(m.id), weight });
|
||||||
|
if (!m.coasting) anyLive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (best) {
|
if (observations.length > 0) {
|
||||||
alignWorld(best.m2c, anchorById.get(best.m.id));
|
fuseWorld(observations);
|
||||||
aligned = true; lastAlign = now;
|
aligned = true; lastAlign = now;
|
||||||
world.visible = true;
|
world.visible = true;
|
||||||
setLock(best.coasting ? 'holding' : 'locked');
|
setLock(anyLive ? 'locked' : 'holding');
|
||||||
} else if (aligned && now - lastAlign < REALIGN_GRACE_MS) {
|
} else if (aligned && now - lastAlign < REALIGN_GRACE_MS) {
|
||||||
// keep showing world briefly using last alignment
|
// keep showing world briefly using last alignment
|
||||||
setLock('holding');
|
setLock('holding');
|
||||||
|
|||||||
Reference in New Issue
Block a user