@displayxr/inline3d 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/README.md +15 -0
- package/index.d.ts +57 -0
- package/js/inline3d-player.js +2219 -0
- package/js/inline3d-splat-diag.js +15 -1
- package/js/inline3d-splat-layer-rig.js +343 -125
- package/js/inline3d-splat-live.js +87 -7
- package/js/inline3d-splat-playcanvas.js +128 -24
- package/js/inline3d-splat-rig-map.js +369 -0
- package/js/inline3d-splat.js +8 -1
- package/js/inline3d.js +208 -12
- package/package.json +8 -1
- package/player.d.ts +273 -0
- package/splat.d.ts +41 -9
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// inline3d-splat-rig-map.js — each photo through ITS OWN camera rig, whatever camera rig the
|
|
2
|
+
// runtime's views were located for. Internal to ./inline3d-splat-playcanvas.js (and
|
|
3
|
+
// ./inline3d-splat-live.js).
|
|
4
|
+
//
|
|
5
|
+
// THE BUG THIS FIXES. A live transition (crossfade, wavefront, the particle transitions) keeps the
|
|
6
|
+
// outgoing photo on screen while the incoming photo's rig is declared. The runtime's views are
|
|
7
|
+
// then the INCOMING rig's, and the outgoing photo was mapped back to its own framing by a node
|
|
8
|
+
// chain N = R_o·K_o·D_o·D_n⁻¹·K_n⁻¹, D = diag(c·t, c·t, c). That maps the incoming WINDOW onto
|
|
9
|
+
// the outgoing one exactly, but it scales the EYES with it: a camera rig's eyes do not scale with
|
|
10
|
+
// its window (they are the viewer's eyes, times metersToVirtual), so the outgoing photo was drawn
|
|
11
|
+
// from eyes scaled by (c_o·t_o)/(c_n·t_n) — its disparity and head parallax jumped by that factor
|
|
12
|
+
// the moment the rig switched. And a declaration reaches the views a frame or more later, so for
|
|
13
|
+
// those frames the chain mapped views that were still the OUTGOING rig's (and the incoming photo
|
|
14
|
+
// was drawn through them).
|
|
15
|
+
//
|
|
16
|
+
// NO KOOIMA HERE, and no frustum is built. The runtime's projection matrices are used verbatim
|
|
17
|
+
// (only their depth rows are rescaled, as clampProjectionDepth already does). What this does is
|
|
18
|
+
// exact algebra on the runtime's outputs and the descriptors we declared:
|
|
19
|
+
//
|
|
20
|
+
// 1. A camera rig is a PORTAL (displayxr-common's dxr_camera3d_compute_view): every view looks,
|
|
21
|
+
// from its own eye, through ONE window rectangle on the convergence plane — centre
|
|
22
|
+
// Wc = N0 + D·fwd, half-height t·D (t = tan(vfov/2)), half-width aspect·t·D — and its eye is
|
|
23
|
+
// N0 + R·m·(π·c̄ + ι·(q − c̄)) (q the viewer's eye relative to the nominal viewer, c̄ their
|
|
24
|
+
// centroid, π/ι the parallax/ipd factors, m metersToVirtual).
|
|
25
|
+
// 2. WHICH rig a view set was located for is read off the views: under the right descriptor
|
|
26
|
+
// every view's frustum crosses the window plane exactly in the window (`viewResidual`). The
|
|
27
|
+
// candidates are the descriptors declared recently (`RigTracker`); none within tolerance ⇒
|
|
28
|
+
// unknown ⇒ nothing is remapped (the caller keeps its old path).
|
|
29
|
+
// 3. From the located rig F to a target rig T, per view: the eye maps exactly (step 1 inverted
|
|
30
|
+
// and re-applied — the eye centroid and offsets come from the views themselves), and the
|
|
31
|
+
// affine map A_i that sends F's window onto T's (in-plane scale σ = t_T·D_T / t_F·D_F, the
|
|
32
|
+
// same for x and y: both windows have the canvas's aspect) and F's eye onto T's eye maps
|
|
33
|
+
// every ray from F's eye through a window point onto the ray from T's eye through the
|
|
34
|
+
// matching window point. So
|
|
35
|
+
//
|
|
36
|
+
// viewInv_T,i = A_i · viewInv_F,i proj_T,i = proj_F,i (depth rows rescaled)
|
|
37
|
+
//
|
|
38
|
+
// renders EXACTLY what T's own view i would (same pixel for every point: both frusta are
|
|
39
|
+
// the pyramid through the window, and A_i maps one pyramid onto the other with the window
|
|
40
|
+
// rectangle onto the window rectangle). Vertical FOV, convergence, metersToVirtual and the
|
|
41
|
+
// two factors may all differ. Only the depth mapping moves (A_i scales depth by a_z), which
|
|
42
|
+
// is why the near/far planes are divided by a_z: the target keeps the runtime's near/far.
|
|
43
|
+
//
|
|
44
|
+
// A display rig is NOT mapped: its eyes are absolute (the viewer's z includes the nominal
|
|
45
|
+
// distance, which the browser does not expose), so display ↔ camera would need that one scalar.
|
|
46
|
+
// Everything here is PLAIN ARITHMETIC on arrays, unit-testable without a GPU.
|
|
47
|
+
|
|
48
|
+
/** A view set matches a rig when every residual is below this, in window HALF-HEIGHTS (≈0.2 px at 720 px). */
|
|
49
|
+
export const RIG_MATCH_TOL = 5e-4;
|
|
50
|
+
|
|
51
|
+
/** How many recently declared rigs the tracker keeps as candidates. */
|
|
52
|
+
export const RIG_HISTORY = 8;
|
|
53
|
+
|
|
54
|
+
const FIELDS = ['convergenceDiopters', 'verticalFov', 'metersToVirtual', 'ipdFactor', 'parallaxFactor', 'virtualDisplayHeight', 'perspectiveFactor'];
|
|
55
|
+
|
|
56
|
+
/** Rotate v by unit quaternion q (xyzw). */
|
|
57
|
+
function rotate(q, v) {
|
|
58
|
+
const [x, y, z, w] = q;
|
|
59
|
+
const tx = 2 * (y * v[2] - z * v[1]);
|
|
60
|
+
const ty = 2 * (z * v[0] - x * v[2]);
|
|
61
|
+
const tz = 2 * (x * v[1] - y * v[0]);
|
|
62
|
+
return [v[0] + w * tx + (y * tz - z * ty), v[1] + w * ty + (z * tx - x * tz), v[2] + w * tz + (x * ty - y * tx)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
66
|
+
|
|
67
|
+
/** A value copy of a declared descriptor (the SDK rewrites its camera-rig object in place), or null. */
|
|
68
|
+
export function rigSnapshot(rig) {
|
|
69
|
+
if (!rig || typeof rig !== 'object') return null;
|
|
70
|
+
const p = rig.position || {};
|
|
71
|
+
const q = rig.orientation || {};
|
|
72
|
+
const o = {
|
|
73
|
+
type: rig.type,
|
|
74
|
+
position: { x: +p.x || 0, y: +p.y || 0, z: +p.z || 0 },
|
|
75
|
+
orientation: { x: +q.x || 0, y: +q.y || 0, z: +q.z || 0, w: q.w === undefined ? 1 : +q.w },
|
|
76
|
+
};
|
|
77
|
+
for (const k of FIELDS) if (rig[k] !== undefined) o[k] = rig[k];
|
|
78
|
+
return o;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Same declared values (snapshots)? */
|
|
82
|
+
export function sameRig(a, b) {
|
|
83
|
+
if (a === b) return true;
|
|
84
|
+
if (!a || !b || a.type !== b.type) return false;
|
|
85
|
+
for (const k of ['x', 'y', 'z']) if (a.position[k] !== b.position[k]) return false;
|
|
86
|
+
for (const k of ['x', 'y', 'z', 'w']) if (a.orientation[k] !== b.orientation[k]) return false;
|
|
87
|
+
for (const k of FIELDS) if (a[k] !== b[k]) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The portal of a CAMERA-rig descriptor, or null (a display rig, convergence at infinity, a
|
|
93
|
+
* factor of 0 — nothing to invert — or a non-finite field).
|
|
94
|
+
* @returns {{ N0: number[], q: number[], right: number[], up: number[], back: number[], Wc: number[], D: number, t: number, m: number, ipd: number, par: number }|null}
|
|
95
|
+
*/
|
|
96
|
+
export function cameraPortal(rig) {
|
|
97
|
+
if (!rig || rig.type !== 'camera') return null;
|
|
98
|
+
const inv = rig.convergenceDiopters;
|
|
99
|
+
const vfov = rig.verticalFov;
|
|
100
|
+
if (!(Number.isFinite(inv) && inv > 0) || !(Number.isFinite(vfov) && vfov > 0 && vfov < Math.PI)) return null;
|
|
101
|
+
const m = Number.isFinite(rig.metersToVirtual) && rig.metersToVirtual > 0 ? rig.metersToVirtual : 1;
|
|
102
|
+
const ipd = rig.ipdFactor ?? 1;
|
|
103
|
+
const par = rig.parallaxFactor ?? 1;
|
|
104
|
+
if (!(Number.isFinite(ipd) && ipd > 0) || !(Number.isFinite(par) && par > 0)) return null;
|
|
105
|
+
const p = rig.position || {};
|
|
106
|
+
const o = rig.orientation || {};
|
|
107
|
+
let q = [+o.x || 0, +o.y || 0, +o.z || 0, o.w === undefined ? 1 : +o.w];
|
|
108
|
+
const qn = Math.hypot(q[0], q[1], q[2], q[3]);
|
|
109
|
+
if (!(qn > 0)) return null;
|
|
110
|
+
q = q.map((c) => c / qn);
|
|
111
|
+
const N0 = [+p.x || 0, +p.y || 0, +p.z || 0];
|
|
112
|
+
const right = rotate(q, [1, 0, 0]);
|
|
113
|
+
const up = rotate(q, [0, 1, 0]);
|
|
114
|
+
const back = rotate(q, [0, 0, 1]);
|
|
115
|
+
const D = 1 / inv;
|
|
116
|
+
const Wc = [N0[0] - D * back[0], N0[1] - D * back[1], N0[2] - D * back[2]];
|
|
117
|
+
return { N0, q, right, up, back, Wc, D, t: Math.tan(vfov / 2), m, ipd, par };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Window-local coordinates (right, up, toward the viewer) of a point, about the window centre. */
|
|
121
|
+
function local(P, X) {
|
|
122
|
+
const d = [X[0] - P.Wc[0], X[1] - P.Wc[1], X[2] - P.Wc[2]];
|
|
123
|
+
return [dot(P.right, d), dot(P.up, d), dot(P.back, d)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* How far one runtime view is from being a view of portal P, in window half-heights: its
|
|
128
|
+
* rotation must be P's, and its frustum must cross P's window plane exactly in the window
|
|
129
|
+
* (centred on Wc, t·D tall). Reads the projection's off-axis tangents; builds nothing.
|
|
130
|
+
*/
|
|
131
|
+
export function viewResidual(entry, P) {
|
|
132
|
+
const m = entry.pose;
|
|
133
|
+
const pr = entry.proj;
|
|
134
|
+
// rotation: the pose's columns against the rig's axes (unit vectors; 1e-7 is float32 noise)
|
|
135
|
+
const rot = Math.max(
|
|
136
|
+
Math.abs(m[0] - P.right[0]), Math.abs(m[1] - P.right[1]), Math.abs(m[2] - P.right[2]),
|
|
137
|
+
Math.abs(m[4] - P.up[0]), Math.abs(m[5] - P.up[1]), Math.abs(m[6] - P.up[2]),
|
|
138
|
+
Math.abs(m[8] - P.back[0]), Math.abs(m[9] - P.back[1]), Math.abs(m[10] - P.back[2]),
|
|
139
|
+
);
|
|
140
|
+
if (!(pr[0] > 0) || !(pr[5] > 0) || Math.abs(pr[11] + 1) > 1e-6 || Math.abs(pr[4]) > 1e-6) return Infinity;
|
|
141
|
+
const u = local(P, [m[12], m[13], m[14]]);
|
|
142
|
+
if (!(u[2] > 0)) return Infinity;
|
|
143
|
+
const tl = (pr[8] - 1) / pr[0];
|
|
144
|
+
const tr = (pr[8] + 1) / pr[0];
|
|
145
|
+
const tb = (pr[9] - 1) / pr[5];
|
|
146
|
+
const tt = (pr[9] + 1) / pr[5];
|
|
147
|
+
const h = P.t * P.D;
|
|
148
|
+
const cx = u[0] + 0.5 * (tl + tr) * u[2];
|
|
149
|
+
const cy = u[1] + 0.5 * (tb + tt) * u[2];
|
|
150
|
+
const hy = 0.5 * (tt - tb) * u[2];
|
|
151
|
+
return Math.max(rot, Math.abs(cx) / h, Math.abs(cy) / h, Math.abs(hy - h) / h);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The worst view's residual for portal P (Infinity when P is null). */
|
|
155
|
+
export function viewsResidual(entries, P) {
|
|
156
|
+
if (!P || !entries?.length) return Infinity;
|
|
157
|
+
let r = 0;
|
|
158
|
+
for (const e of entries) r = Math.max(r, viewResidual(e, P));
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function mul4(a, b, out = new Float64Array(16)) {
|
|
163
|
+
const r = new Float64Array(16);
|
|
164
|
+
for (let c = 0; c < 4; c++) {
|
|
165
|
+
for (let row = 0; row < 4; row++) {
|
|
166
|
+
r[c * 4 + row] = a[row] * b[c * 4] + a[4 + row] * b[c * 4 + 1] + a[8 + row] * b[c * 4 + 2] + a[12 + row] * b[c * 4 + 3];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
out.set(r);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Inverse of an affine column-major 4×4 (last row 0 0 0 1). Null when singular. */
|
|
174
|
+
export function invertAffine(m, out = new Float64Array(16)) {
|
|
175
|
+
const a = m[0], b = m[4], c = m[8];
|
|
176
|
+
const d = m[1], e = m[5], f = m[9];
|
|
177
|
+
const g = m[2], h = m[6], i = m[10];
|
|
178
|
+
const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
|
|
179
|
+
const det = a * A + b * B + c * C;
|
|
180
|
+
if (!(Math.abs(det) > 1e-300)) return null;
|
|
181
|
+
const k = 1 / det;
|
|
182
|
+
const r00 = A * k, r01 = -(b * i - c * h) * k, r02 = (b * f - c * e) * k;
|
|
183
|
+
const r10 = B * k, r11 = (a * i - c * g) * k, r12 = -(a * f - c * d) * k;
|
|
184
|
+
const r20 = C * k, r21 = -(a * h - b * g) * k, r22 = (a * e - b * d) * k;
|
|
185
|
+
const tx = m[12], ty = m[13], tz = m[14];
|
|
186
|
+
out[0] = r00; out[1] = r10; out[2] = r20; out[3] = 0;
|
|
187
|
+
out[4] = r01; out[5] = r11; out[6] = r21; out[7] = 0;
|
|
188
|
+
out[8] = r02; out[9] = r12; out[10] = r22; out[11] = 0;
|
|
189
|
+
out[12] = -(r00 * tx + r01 * ty + r02 * tz);
|
|
190
|
+
out[13] = -(r10 * tx + r11 * ty + r12 * tz);
|
|
191
|
+
out[14] = -(r20 * tx + r21 * ty + r22 * tz);
|
|
192
|
+
out[15] = 1;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The eyes of target portal T for the same physical eyes that produced these views of portal F
|
|
198
|
+
* (dxr_display3d_apply_eye_factors_n inverted for F, re-applied for T). Rig space.
|
|
199
|
+
*/
|
|
200
|
+
export function mapEyes(entries, F, T) {
|
|
201
|
+
const n = entries.length;
|
|
202
|
+
const l = [];
|
|
203
|
+
const c = [0, 0, 0];
|
|
204
|
+
for (const e of entries) {
|
|
205
|
+
const d = [e.pose[12] - F.N0[0], e.pose[13] - F.N0[1], e.pose[14] - F.N0[2]];
|
|
206
|
+
const v = [dot(F.right, d) / F.m, dot(F.up, d) / F.m, dot(F.back, d) / F.m];
|
|
207
|
+
l.push(v);
|
|
208
|
+
c[0] += v[0] / n; c[1] += v[1] / n; c[2] += v[2] / n;
|
|
209
|
+
}
|
|
210
|
+
const kc = T.par / F.par;
|
|
211
|
+
const ko = T.ipd / F.ipd;
|
|
212
|
+
return l.map((v) => {
|
|
213
|
+
const w = [0, 1, 2].map((k) => T.m * (kc * c[k] + ko * (v[k] - c[k])));
|
|
214
|
+
return [0, 1, 2].map((k) => T.N0[k] + T.right[k] * w[0] + T.up[k] * w[1] + T.back[k] * w[2]);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The affine map A (column-major 4×4) that sends F's window onto T's window (in-plane scale σ)
|
|
220
|
+
* and eye Ef onto eye Et. Null when either eye is not in front of its window.
|
|
221
|
+
*/
|
|
222
|
+
export function portalMap(F, T, Ef, Et, sigma = (T.t * T.D) / (F.t * F.D)) {
|
|
223
|
+
const uf = local(F, Ef);
|
|
224
|
+
const ut = local(T, Et);
|
|
225
|
+
if (!(uf[2] > 0) || !(ut[2] > 0)) return null;
|
|
226
|
+
// L (window-local, F → T): [σ 0 ax; 0 σ ay; 0 0 az], with L·uf = ut.
|
|
227
|
+
const ax = (ut[0] - sigma * uf[0]) / uf[2];
|
|
228
|
+
const ay = (ut[1] - sigma * uf[1]) / uf[2];
|
|
229
|
+
const az = ut[2] / uf[2];
|
|
230
|
+
// A(X) = Wc_T + B_T · L · B_Fᵀ · (X − Wc_F), B = [right up back] (columns).
|
|
231
|
+
const BT = [T.right, T.up, T.back];
|
|
232
|
+
const BF = [F.right, F.up, F.back];
|
|
233
|
+
const Lc = [[sigma, 0, 0], [0, sigma, 0], [ax, ay, az]]; // columns of L
|
|
234
|
+
// M = B_T · L · B_Fᵀ: column j = Σ_k (B_T · L)_col_k · BF[k][j]
|
|
235
|
+
const TL = Lc.map((col) => [0, 1, 2].map((r) => BT[0][r] * col[0] + BT[1][r] * col[1] + BT[2][r] * col[2]));
|
|
236
|
+
const A = new Float64Array(16);
|
|
237
|
+
for (let j = 0; j < 3; j++) {
|
|
238
|
+
for (let r = 0; r < 3; r++) A[j * 4 + r] = TL[0][r] * BF[0][j] + TL[1][r] * BF[1][j] + TL[2][r] * BF[2][j];
|
|
239
|
+
}
|
|
240
|
+
for (let r = 0; r < 3; r++) A[12 + r] = T.Wc[r] - (A[r] * F.Wc[0] + A[4 + r] * F.Wc[1] + A[8 + r] * F.Wc[2]);
|
|
241
|
+
A[15] = 1;
|
|
242
|
+
return { A, az };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Rewrite a projection's depth rows for near/far divided by `k` (the map's depth scale). */
|
|
246
|
+
function scaleDepth(P, k, out) {
|
|
247
|
+
out.set(P);
|
|
248
|
+
if (!(k > 0) || k === 1) return out;
|
|
249
|
+
if (Math.abs(P[11] + 1) > 1e-6 || Math.abs(P[15]) > 1e-6) return out; // not a perspective: leave it
|
|
250
|
+
const n = P[14] / (P[10] - 1);
|
|
251
|
+
if (Math.abs(P[10] + 1) < 1e-9) {
|
|
252
|
+
out[14] = -2 * (n / k); // infinite far: only the near plane moves
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
const f = P[14] / (P[10] + 1);
|
|
256
|
+
const n2 = n / k;
|
|
257
|
+
const f2 = f / k;
|
|
258
|
+
out[10] = -(f2 + n2) / (f2 - n2);
|
|
259
|
+
out[14] = (-2 * f2 * n2) / (f2 - n2);
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* These views (located for portal F) as portal T's views. Per view: { proj, viewInv, view, eye }
|
|
265
|
+
* — what RenderView.setView takes, and the target eye (rig space) for the camera node. Null when
|
|
266
|
+
* there is nothing to do (F and T the same portal) or the map does not exist (an eye at or behind
|
|
267
|
+
* a window): the caller keeps the views as they are.
|
|
268
|
+
*/
|
|
269
|
+
export function remapViews(entries, F, T, into = []) {
|
|
270
|
+
if (!F || !T || !entries?.length) return null;
|
|
271
|
+
if (F === T) return null;
|
|
272
|
+
const Et = mapEyes(entries, F, T);
|
|
273
|
+
const sigma = (T.t * T.D) / (F.t * F.D);
|
|
274
|
+
into.length = entries.length;
|
|
275
|
+
for (let i = 0; i < entries.length; i++) {
|
|
276
|
+
const e = entries[i];
|
|
277
|
+
const Ef = [e.pose[12], e.pose[13], e.pose[14]];
|
|
278
|
+
const pm = portalMap(F, T, Ef, Et[i], sigma);
|
|
279
|
+
if (!pm) return null;
|
|
280
|
+
const o = (into[i] ||= { proj: new Float64Array(16), viewInv: new Float64Array(16), view: new Float64Array(16), eye: [0, 0, 0] });
|
|
281
|
+
mul4(pm.A, e.pose, o.viewInv);
|
|
282
|
+
if (!invertAffine(o.viewInv, o.view)) return null;
|
|
283
|
+
scaleDepth(e.proj, pm.az, o.proj);
|
|
284
|
+
o.eye[0] = Et[i][0];
|
|
285
|
+
o.eye[1] = Et[i][1];
|
|
286
|
+
o.eye[2] = Et[i][2];
|
|
287
|
+
o.x = e.x;
|
|
288
|
+
o.y = e.y;
|
|
289
|
+
o.width = e.width;
|
|
290
|
+
o.height = e.height;
|
|
291
|
+
}
|
|
292
|
+
return into;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Two portals with the same window (pose, convergence, vertical FOV) — only their eyes can differ. */
|
|
296
|
+
export function sameWindow(a, b) {
|
|
297
|
+
return a.D === b.D && a.t === b.t && a.N0.every((x, k) => x === b.N0[k]) && a.q.every((x, k) => x === b.q[k]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Rigid pose matrix (column-major) from a portal's orientation at `eye` — a camera node's pose. */
|
|
301
|
+
export function nodePose(T, eye, out = new Float64Array(16)) {
|
|
302
|
+
out[0] = T.right[0]; out[1] = T.right[1]; out[2] = T.right[2]; out[3] = 0;
|
|
303
|
+
out[4] = T.up[0]; out[5] = T.up[1]; out[6] = T.up[2]; out[7] = 0;
|
|
304
|
+
out[8] = T.back[0]; out[9] = T.back[1]; out[10] = T.back[2]; out[11] = 0;
|
|
305
|
+
out[12] = eye[0]; out[13] = eye[1]; out[14] = eye[2]; out[15] = 1;
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The rigs this tile declared, newest last, and which one a view set was located for.
|
|
311
|
+
* `note(rig)` on every declaration; `locate(entries)` per drawn frame.
|
|
312
|
+
*/
|
|
313
|
+
export class RigTracker {
|
|
314
|
+
constructor({ history = RIG_HISTORY, tol = RIG_MATCH_TOL } = {}) {
|
|
315
|
+
this.history = history;
|
|
316
|
+
this.tol = tol;
|
|
317
|
+
/** [{ id, rig (snapshot), portal (or null) }], oldest first. */
|
|
318
|
+
this.rigs = [];
|
|
319
|
+
this._seq = 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** A declaration (the value, not the object: the SDK rewrites its descriptor in place). */
|
|
323
|
+
note(rig) {
|
|
324
|
+
const snap = rigSnapshot(rig);
|
|
325
|
+
const last = this.rigs[this.rigs.length - 1];
|
|
326
|
+
if (last && sameRig(last.rig, snap)) return last;
|
|
327
|
+
const entry = { id: ++this._seq, rig: snap, portal: cameraPortal(snap) };
|
|
328
|
+
this.rigs.push(entry);
|
|
329
|
+
if (this.rigs.length > this.history) this.rigs.splice(0, this.rigs.length - this.history);
|
|
330
|
+
return entry;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The rig declared last (what the drawing photo wants now), or null. */
|
|
334
|
+
get latest() {
|
|
335
|
+
return this.rigs[this.rigs.length - 1] || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The declared camera rig these views were located for, or null when none matches within
|
|
340
|
+
* tolerance (a display rig, a runtime clamp, a rig older than the history).
|
|
341
|
+
*
|
|
342
|
+
* The views pin down the WINDOW (pose, convergence, vertical FOV) exactly, but not the eye
|
|
343
|
+
* factors: any eyes are consistent with a window (metersToVirtual, ipdFactor and
|
|
344
|
+
* parallaxFactor only move the eyes). When several candidates match — rigs that differ ONLY in
|
|
345
|
+
* those — `prefer` wins if it is among them (the rig declared when these views were pulled:
|
|
346
|
+
* Blink's one frame of latency), else the newest.
|
|
347
|
+
*/
|
|
348
|
+
locate(entries, prefer = null) {
|
|
349
|
+
let best = null;
|
|
350
|
+
let bestR = Infinity;
|
|
351
|
+
const matches = [];
|
|
352
|
+
for (let i = this.rigs.length - 1; i >= 0; i--) {
|
|
353
|
+
const c = this.rigs[i];
|
|
354
|
+
if (!c.portal) continue;
|
|
355
|
+
const r = viewsResidual(entries, c.portal);
|
|
356
|
+
if (r <= this.tol) matches.push(c); // newest first
|
|
357
|
+
if (r < bestR) {
|
|
358
|
+
bestR = r;
|
|
359
|
+
best = c;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
this.lastResidual = bestR;
|
|
363
|
+
if (!best || !(bestR <= this.tol)) return null;
|
|
364
|
+
// Among the rigs sharing the best match's window (they differ only in the eye factors):
|
|
365
|
+
// `prefer` if it is one of them, else the newest.
|
|
366
|
+
const group = matches.filter((c) => sameWindow(c.portal, best.portal));
|
|
367
|
+
return group.includes(prefer) ? prefer : group[0];
|
|
368
|
+
}
|
|
369
|
+
}
|
package/js/inline3d-splat.js
CHANGED
|
@@ -385,6 +385,9 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
385
385
|
setLayerRig() {
|
|
386
386
|
throw new Error("@displayxr/inline3d/splat: setLayerRig() is implemented on the PlayCanvas backend only (addSplat(…, { engine:'playcanvas' })).");
|
|
387
387
|
},
|
|
388
|
+
setLayerRigOptions() {
|
|
389
|
+
throw new Error("@displayxr/inline3d/splat: setLayerRigOptions() is implemented on the PlayCanvas backend only (addSplat(…, { engine:'playcanvas' })).");
|
|
390
|
+
},
|
|
388
391
|
/** Not on this backend: per-eye SBS materials are a PlayCanvas-backend feature. */
|
|
389
392
|
makeSbsMaterial() {
|
|
390
393
|
throw new Error("@displayxr/inline3d/splat: makeSbsMaterial() is implemented on the PlayCanvas backend only (addSplat(…, { engine:'playcanvas' })).");
|
|
@@ -773,7 +776,11 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
773
776
|
pending.push(['setLayerRig', args]);
|
|
774
777
|
return out;
|
|
775
778
|
},
|
|
776
|
-
|
|
779
|
+
setLayerRigOptions: (...args) => {
|
|
780
|
+
pending.push(['setLayerRigOptions', args]);
|
|
781
|
+
return out;
|
|
782
|
+
},
|
|
783
|
+
layerRigState: () => ({ display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'the PlayCanvas adapter has not loaded yet', viewerDistance: 0.6, gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null }),
|
|
777
784
|
makeSbsMaterial: () => {
|
|
778
785
|
throw new Error('@displayxr/inline3d/splat: makeSbsMaterial() needs the engine — call it after `await handle.ready`.');
|
|
779
786
|
},
|