@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
|
@@ -30,6 +30,15 @@
|
|
|
30
30
|
// pick index. Tests "the page's picks are what blocks the main thread".
|
|
31
31
|
// nolayerrig — handle.setLayerRig is recorded but never applied: every layer stays on the eye
|
|
32
32
|
// camera and the photo's camera rig (the pre-1.23 path, byte for byte).
|
|
33
|
+
// oldrig — no rig tracking (./inline3d-splat-rig-map.js): the views are drawn as the runtime
|
|
34
|
+
// located them, and a live outgoing photo goes back on the pre-1.24 node chain (which
|
|
35
|
+
// scales its disparity by the two rigs' window ratio at the swap). A/B for "the
|
|
36
|
+
// outgoing photo jumps when the incoming rig is declared".
|
|
37
|
+
//
|
|
38
|
+
// Per frame, `rigAt` / `rigIn` / `rigOut` say which declared rig the views were located for
|
|
39
|
+
// (the tracker's id; null = none matched), which rig the current photo was drawn through
|
|
40
|
+
// (`+` = remapped to it) and how the live outgoing photo was drawn ('own' = its rig as located,
|
|
41
|
+
// 'remapped', 'eye', 'chain'; null = no live outgoing).
|
|
33
42
|
//
|
|
34
43
|
// WHAT RUNS ON THE MAIN THREAD, per transition phase (the fifth cause: a stall at the swap's END
|
|
35
44
|
// that is not the SDK's transition at all):
|
|
@@ -44,7 +53,7 @@
|
|
|
44
53
|
// after it (the page's continuation of `await setSource`).
|
|
45
54
|
|
|
46
55
|
/** The switches `diag` / `?dxrdiag` understand, besides the plain on values. */
|
|
47
|
-
export const DIAG_SWITCHES = Object.freeze(['norig', 'frozen', 'nowarm', 'cold', 'nooverlay', 'oldpick', 'nolayerrig']);
|
|
56
|
+
export const DIAG_SWITCHES = Object.freeze(['norig', 'frozen', 'nowarm', 'cold', 'nooverlay', 'oldpick', 'nolayerrig', 'oldrig']);
|
|
48
57
|
const ON_TOKENS = new Set(['1', 'on', 'true', 'yes']);
|
|
49
58
|
|
|
50
59
|
/**
|
|
@@ -191,6 +200,7 @@ export class DiagRecorder {
|
|
|
191
200
|
this._frameNo = 0;
|
|
192
201
|
this._open = null; // the transition being recorded
|
|
193
202
|
this.imageState = null; // () => { overlay: 'none' | 'frozen' | 'live', w }
|
|
203
|
+
this.rigState = null; // () => { at, in, out } (see the header)
|
|
194
204
|
this.longTasks = [];
|
|
195
205
|
this._lto = null;
|
|
196
206
|
this.rigLocked = null; // norig: the summary of the rig kept
|
|
@@ -373,6 +383,7 @@ export class DiagRecorder {
|
|
|
373
383
|
this._lastFrameAt = t;
|
|
374
384
|
this._frameNo++;
|
|
375
385
|
const img = this.imageState ? safe(this.imageState) : null;
|
|
386
|
+
const rs = this.rigState ? safe(this.rigState) : null;
|
|
376
387
|
const f = {
|
|
377
388
|
n: this._frameNo,
|
|
378
389
|
t: round1(t),
|
|
@@ -388,6 +399,9 @@ export class DiagRecorder {
|
|
|
388
399
|
afterRig: this._frameNo - this._rigFrame <= 3,
|
|
389
400
|
img: img ? img.overlay : 'none',
|
|
390
401
|
imgW: img ? round4(img.w) : 0,
|
|
402
|
+
rigAt: rs ? rs.at : null,
|
|
403
|
+
rigIn: rs ? rs.in : null,
|
|
404
|
+
rigOut: rs ? rs.out : null,
|
|
391
405
|
};
|
|
392
406
|
this.frames.push(f);
|
|
393
407
|
if (this.frames.length > FRAME_CAP) this.frames.splice(0, this.frames.length - FRAME_CAP);
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// nominal viewer distance in metres — the one quantity the browser does not expose (see the
|
|
20
20
|
// design note §5). Everything here is PLAIN ARITHMETIC on arrays, unit-testable without a GPU.
|
|
21
21
|
|
|
22
|
+
import { invertAffine } from './inline3d-splat-rig-map.js';
|
|
23
|
+
|
|
22
24
|
/** handle.setLayerRig's rigs. 'camera' is the default every layer starts on. */
|
|
23
25
|
export const LAYER_RIGS = Object.freeze(['display', 'camera']);
|
|
24
26
|
|
|
@@ -37,9 +39,41 @@ function rotate(q, v) {
|
|
|
37
39
|
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)];
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** The option keys setLayerRig / setLayerRigOptions take (the tile-wide rounding + plane). */
|
|
43
|
+
export const LAYER_RIG_OPTION_KEYS = Object.freeze(['viewerDistance', 'gain', 'planeOffset', 'planeDistance']);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* setLayerRig / setLayerRigOptions options, validated (throws at the call). Only the keys given
|
|
47
|
+
* are returned: they MERGE into the tile's options. `null` clears a key (back to its default).
|
|
48
|
+
* viewerDistance m, > 0 the nominal viewer distance n (default 0.6)
|
|
49
|
+
* gain > 0 an explicit rounding gain k instead of D/(m·n)
|
|
50
|
+
* planeOffset m, finite moves the stage TOWARD the viewer by this much on the panel (display
|
|
51
|
+
* space): the plane that lands on the glass moves back by offset·D/n
|
|
52
|
+
* planeDistance > 0 the plane that lands on the glass, as a distance from the photo's
|
|
53
|
+
* camera in world units (wins over planeOffset)
|
|
54
|
+
*/
|
|
55
|
+
export function validateLayerRigOptions(o = {}, who = 'setLayerRig') {
|
|
56
|
+
if (o === null || typeof o !== 'object') throw new TypeError(`@displayxr/inline3d/splat: ${who} options must be an object.`);
|
|
57
|
+
const unknown = Object.keys(o).filter((k) => !LAYER_RIG_OPTION_KEYS.includes(k));
|
|
58
|
+
if (unknown.length) throw new Error(`@displayxr/inline3d/splat: ${who} — unknown option(s) ${unknown.join(', ')}; expected ${LAYER_RIG_OPTION_KEYS.join(', ')}.`);
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const k of LAYER_RIG_OPTION_KEYS) {
|
|
61
|
+
if (o[k] === undefined) continue;
|
|
62
|
+
if (o[k] === null) {
|
|
63
|
+
out[k] = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const v = o[k];
|
|
67
|
+
const ok = k === 'planeOffset' ? Number.isFinite(v) : Number.isFinite(v) && v > 0;
|
|
68
|
+
if (!ok) throw new Error(`@displayxr/inline3d/splat: ${who} — bad ${k}: ${v}.`);
|
|
69
|
+
out[k] = v;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
/**
|
|
41
75
|
* setLayerRig's arguments, validated (throws at the call).
|
|
42
|
-
* @returns {{ rig: 'display'|'camera',
|
|
76
|
+
* @returns {{ rig: 'display'|'camera', opts: object }}
|
|
43
77
|
*/
|
|
44
78
|
export function validateLayerRig(layer, rig, o = {}) {
|
|
45
79
|
if (!(typeof layer === 'string' && layer.length) && !(Number.isInteger(layer) && layer >= 0) && !(layer && typeof layer === 'object' && Number.isInteger(layer.id))) {
|
|
@@ -48,13 +82,7 @@ export function validateLayerRig(layer, rig, o = {}) {
|
|
|
48
82
|
if (!LAYER_RIGS.includes(rig)) {
|
|
49
83
|
throw new Error(`@displayxr/inline3d/splat: setLayerRig — rig "${rig}", expected ${LAYER_RIGS.map((r) => `'${r}'`).join(' or ')}.`);
|
|
50
84
|
}
|
|
51
|
-
|
|
52
|
-
const num = (k) => {
|
|
53
|
-
if (o[k] === undefined) return null;
|
|
54
|
-
if (!Number.isFinite(o[k]) || !(o[k] > 0)) throw new Error(`@displayxr/inline3d/splat: setLayerRig — bad ${k}: ${o[k]}.`);
|
|
55
|
-
return o[k];
|
|
56
|
-
};
|
|
57
|
-
return { rig, viewerDistance: num('viewerDistance'), gain: num('gain') };
|
|
85
|
+
return { rig, opts: validateLayerRigOptions(o) };
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
/**
|
|
@@ -75,11 +103,24 @@ export function cameraRigFrame(rig) {
|
|
|
75
103
|
|
|
76
104
|
/** k = D / (m·n), or the caller's explicit gain. */
|
|
77
105
|
export function layerRigGain(frame, { viewerDistance = null, gain = null } = {}) {
|
|
78
|
-
if (gain !== null) return gain;
|
|
79
|
-
const n = viewerDistance !== null ? viewerDistance : DEFAULT_VIEWER_DISTANCE_M;
|
|
106
|
+
if (gain !== null && gain !== undefined) return gain;
|
|
107
|
+
const n = viewerDistance !== null && viewerDistance !== undefined ? viewerDistance : DEFAULT_VIEWER_DISTANCE_M;
|
|
80
108
|
return frame.D / (frame.m * n);
|
|
81
109
|
}
|
|
82
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The plane that lands on the GLASS, as a distance from the photo's camera (world units): the
|
|
113
|
+
* convergence distance D by default; `planeDistance` if given; else D·(1 + planeOffset/n) — a
|
|
114
|
+
* stage moved toward the viewer by `planeOffset` metres on the panel (near the plane the round rig
|
|
115
|
+
* maps world depth to panel depth at n/D). Always > 0.
|
|
116
|
+
*/
|
|
117
|
+
export function layerRigPlane(frame, { viewerDistance = null, planeOffset = null, planeDistance = null } = {}) {
|
|
118
|
+
if (planeDistance !== null && planeDistance !== undefined) return planeDistance;
|
|
119
|
+
if (planeOffset === null || planeOffset === undefined || planeOffset === 0) return frame.D;
|
|
120
|
+
const n = viewerDistance !== null && viewerDistance !== undefined ? viewerDistance : DEFAULT_VIEWER_DISTANCE_M;
|
|
121
|
+
return Math.max(frame.D * 1e-3, frame.D * (1 + planeOffset / n));
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
/**
|
|
84
125
|
* The window-fixing shear for one eye, as a column-major 4×4 (and its inverse), in the space the
|
|
85
126
|
* declared rig and the views share.
|
|
@@ -173,27 +214,47 @@ export function invertRigid(m, out = new Float64Array(16)) {
|
|
|
173
214
|
}
|
|
174
215
|
|
|
175
216
|
/**
|
|
176
|
-
* The round views for one frame: for each runtime view pose P_i (camera → rig space
|
|
177
|
-
* `viewInv = M_i⁻¹ · P_i` and `view = P_i⁻¹ · M_i` — what RenderView.setView takes.
|
|
178
|
-
*
|
|
179
|
-
*
|
|
217
|
+
* The round views for one frame: for each runtime view pose P_i (camera → rig space),
|
|
218
|
+
* `viewInv = S⁻¹ · M_i⁻¹ · P_i` and `view = P_i⁻¹ · M_i · S` — what RenderView.setView takes.
|
|
219
|
+
*
|
|
220
|
+
* S is the uniform scale by σ = D / plane about the photo camera N0: it sends the plane that
|
|
221
|
+
* should land on the glass (`plane`, a distance from N0) onto the photo's window plane, and every
|
|
222
|
+
* ray from N0 onto itself — so at the nominal viewpoint the picture is unchanged and only the
|
|
223
|
+
* depth that lands on the glass moves. Exact (a portal through the window at `plane`, seen from
|
|
224
|
+
* N0 + k·D/plane·(E − N0) … mapped by S onto the photo's own portal). σ = 1 without an offset.
|
|
225
|
+
*
|
|
226
|
+
* Null (the caller keeps the photo views) when there is nothing to do: no camera rig, fewer than
|
|
227
|
+
* two views, or a gain of exactly 1 with no plane offset.
|
|
180
228
|
*/
|
|
181
|
-
export function roundViews(entries, frame, k, into = []) {
|
|
182
|
-
|
|
229
|
+
export function roundViews(entries, frame, k, into = [], plane = frame ? frame.D : 1) {
|
|
230
|
+
const sigma = frame && plane > 0 ? frame.D / plane : 1;
|
|
231
|
+
if (!frame || !entries || entries.length < 2 || !(k > 0) || (k === 1 && sigma === 1)) return null;
|
|
183
232
|
into.length = entries.length;
|
|
233
|
+
const S = scaleAbout(frame.N0, sigma);
|
|
234
|
+
const Sinv = scaleAbout(frame.N0, 1 / sigma);
|
|
184
235
|
for (let i = 0; i < entries.length; i++) {
|
|
185
236
|
const P = entries[i].pose;
|
|
186
237
|
const o = (into[i] ||= { viewInv: new Float64Array(16), view: new Float64Array(16), M: new Float64Array(16), Minv: new Float64Array(16), eye: [0, 0, 0] });
|
|
187
238
|
const { Eround } = windowShear([P[12], P[13], P[14]], frame, k, o.M, o.Minv);
|
|
188
|
-
|
|
189
|
-
o.eye[
|
|
190
|
-
o.
|
|
191
|
-
mul4(o.
|
|
192
|
-
mul4(invertRigid(P), o.M, o.view);
|
|
239
|
+
// The virtual eye in rig space: S⁻¹(E′).
|
|
240
|
+
for (let a = 0; a < 3; a++) o.eye[a] = frame.N0[a] + (Eround[a] - frame.N0[a]) / sigma;
|
|
241
|
+
mul4(Sinv, mul4(o.Minv, P), o.viewInv);
|
|
242
|
+
mul4(mul4(invertAffine(P), o.M), S, o.view); // P is rigid, or affine once remapped to the declared rig (./inline3d-splat-rig-map.js)
|
|
193
243
|
}
|
|
194
244
|
return into;
|
|
195
245
|
}
|
|
196
246
|
|
|
247
|
+
/** X ↦ C + s·(X − C), column-major. */
|
|
248
|
+
function scaleAbout(C, s, out = new Float64Array(16)) {
|
|
249
|
+
out.fill(0);
|
|
250
|
+
out[0] = out[5] = out[10] = s;
|
|
251
|
+
out[12] = C[0] * (1 - s);
|
|
252
|
+
out[13] = C[1] * (1 - s);
|
|
253
|
+
out[14] = C[2] * (1 - s);
|
|
254
|
+
out[15] = 1;
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
197
258
|
/**
|
|
198
259
|
* Split a layer composition into contiguous RUNS for per-rig cameras. `entries` is the
|
|
199
260
|
* composition's sublayer list as layer ids, in draw order (opaque and transparent sublayers both
|
|
@@ -246,178 +307,335 @@ export function layerRuns(entries, display, eyeLayers) {
|
|
|
246
307
|
|
|
247
308
|
// ── the engine side ──────────────────────────────────────────────────────────────────────────
|
|
248
309
|
|
|
310
|
+
/** How many state lines the layer rig may WARN per tile (first 3D frame + every change). */
|
|
311
|
+
const MAX_STATE_WARNS = 12;
|
|
312
|
+
|
|
313
|
+
/** Name of a camera component's entity, for a reason string. */
|
|
314
|
+
const camName = (c) => c?.entity?.name || c?.name || '(unnamed camera)';
|
|
315
|
+
|
|
249
316
|
/**
|
|
250
|
-
* The per-rig cameras of one tile. Owned by the viewer; `sync` + `frame` are called by its draw
|
|
251
|
-
*
|
|
317
|
+
* The per-rig cameras of one tile. Owned by the viewer; `sync` + `frame` are called by its draw,
|
|
318
|
+
* on EVERY view path:
|
|
319
|
+
* renderviews — the eye camera draws N RenderViews; each run is ONE camera with N RenderViews.
|
|
320
|
+
* ncamera — the fallback: one eye camera per view (rect + projection override); each run is
|
|
321
|
+
* N cameras the same way, the display run's projections carrying the shear.
|
|
322
|
+
* The eye camera(s) keep their `layers` minus what moved; `restore()` puts them back.
|
|
252
323
|
*/
|
|
253
324
|
export class LayerRigCameras {
|
|
254
325
|
constructor(viewer) {
|
|
255
326
|
this.viewer = viewer;
|
|
256
|
-
/** What the page asked for: name or id →
|
|
327
|
+
/** What the page asked for: name or id → true. */
|
|
257
328
|
this.requests = new Map();
|
|
258
|
-
/** Those, resolved: layer id →
|
|
329
|
+
/** Those, resolved: layer id → the caller's key (the ones the eye camera draws right now). */
|
|
259
330
|
this.layers = new Map();
|
|
260
|
-
/** The
|
|
261
|
-
this.opts = {};
|
|
331
|
+
/** The tile's options: merged across setLayerRig / setLayerRigOptions calls. */
|
|
332
|
+
this.opts = { viewerDistance: null, gain: null, planeOffset: null, planeDistance: null };
|
|
262
333
|
this.disabled = false; // the kill switch (diag 'nolayerrig')
|
|
263
|
-
this.cams = { display:
|
|
334
|
+
this.cams = { display: [], post: [] };
|
|
264
335
|
this.moved = { display: [], post: [] };
|
|
265
336
|
this._key = '';
|
|
266
337
|
this._rvs = { display: [], post: [] };
|
|
267
338
|
this._round = [];
|
|
268
|
-
|
|
269
|
-
this.
|
|
339
|
+
this._syncReason = null;
|
|
340
|
+
this._warns = 0;
|
|
341
|
+
this._warnKey = '';
|
|
342
|
+
/** What the last drawn frame did (layerRigState reads it). */
|
|
343
|
+
this.last = { path: null, engaged: false, reason: 'no frame drawn yet', gain: null, planeM: null, views: 0, located: null, rounded: false };
|
|
270
344
|
}
|
|
271
345
|
|
|
272
346
|
get active() {
|
|
273
347
|
return !this.disabled && this.requests.size > 0;
|
|
274
348
|
}
|
|
275
349
|
|
|
276
|
-
/** Resolve a name / id / pc.Layer to
|
|
277
|
-
resolve(
|
|
350
|
+
/** Resolve a name / id / pc.Layer to a layer of this tile's composition (null if unknown). */
|
|
351
|
+
resolve(key) {
|
|
278
352
|
const comp = this.viewer.app?.scene?.layers;
|
|
279
353
|
if (!comp) return null;
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
const l = comp.getLayerByName?.(layer);
|
|
283
|
-
return l ? l.id : null;
|
|
354
|
+
if (Number.isInteger(key)) return comp.getLayerById?.(key) || { id: key };
|
|
355
|
+
return comp.getLayerByName?.(key) || null;
|
|
284
356
|
}
|
|
285
357
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
* sync, so a layer the page adds AFTER calling setLayerRig is picked up when it appears.
|
|
289
|
-
*/
|
|
290
|
-
set(key, rig, o) {
|
|
358
|
+
/** Record a request by the caller's key (name, id); resolved on every sync. */
|
|
359
|
+
set(key, rig, o = {}) {
|
|
291
360
|
const k = key && typeof key === 'object' ? key.id : key;
|
|
292
|
-
if (rig === 'display') this.requests.set(k,
|
|
361
|
+
if (rig === 'display') this.requests.set(k, true);
|
|
293
362
|
else this.requests.delete(k);
|
|
294
|
-
this.
|
|
363
|
+
this.setOptions(o);
|
|
295
364
|
this._key = ''; // re-split on the next draw
|
|
296
365
|
}
|
|
297
366
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
for (const [k,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
367
|
+
/** Merge options (validated by the caller): a key given replaces, `null` clears. */
|
|
368
|
+
setOptions(o = {}) {
|
|
369
|
+
for (const [k, v] of Object.entries(o)) this.opts[k] = v;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The eye camera entities of the current view path. */
|
|
373
|
+
_eyeCams() {
|
|
374
|
+
const v = this.viewer;
|
|
375
|
+
if (v._viewPath === 'cameras') return (v._views || []).filter((c) => c?.camera);
|
|
376
|
+
return v.eye?.camera ? [v.eye] : [];
|
|
304
377
|
}
|
|
305
378
|
|
|
306
379
|
_makeCam(name, priority) {
|
|
307
380
|
const v = this.viewer;
|
|
308
|
-
const cam = v._makeCamera(name, null);
|
|
381
|
+
const cam = v._makeCamera(name, v._viewPath === 'cameras' && v.pc?.Vec4 ? new v.pc.Vec4(0, 0, 1, 1) : null);
|
|
309
382
|
cam.camera.priority = priority;
|
|
310
383
|
cam.camera.clearColorBuffer = false;
|
|
311
384
|
cam.camera.clearDepthBuffer = false;
|
|
312
385
|
cam.camera.clearStencilBuffer = false;
|
|
313
386
|
cam.camera.frustumCulling = false;
|
|
314
387
|
cam.camera.layers = [];
|
|
388
|
+
cam._dxrLayerRig = true;
|
|
389
|
+
if (v._viewPath === 'cameras') {
|
|
390
|
+
cam._dxrProj = new Float64Array(16);
|
|
391
|
+
cam.camera.calculateProjection = (out) => out.set(cam._dxrProj);
|
|
392
|
+
}
|
|
315
393
|
return cam;
|
|
316
394
|
}
|
|
317
395
|
|
|
318
|
-
/**
|
|
396
|
+
/** Ensure `n` cameras for a run (1 on renderviews, one per view on ncamera). */
|
|
397
|
+
_runCams(run, n, priority) {
|
|
398
|
+
const list = this.cams[run];
|
|
399
|
+
while (list.length < n) list.push(this._makeCam(`inline3d-eye-${run}${list.length ? `-${list.length}` : ''}`, priority));
|
|
400
|
+
return list;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Re-split the composition when the display set, the eye's layers or the composition moved.
|
|
405
|
+
* Returns whether a display run exists; `_syncReason` says why not.
|
|
406
|
+
*/
|
|
319
407
|
sync() {
|
|
320
408
|
const v = this.viewer;
|
|
321
|
-
const
|
|
409
|
+
const eyes = this._eyeCams();
|
|
322
410
|
const comp = v.app?.scene?.layers;
|
|
323
|
-
if (!eyeCam || !comp) return false;
|
|
324
411
|
if (!this.active) {
|
|
325
412
|
if (this.moved.display.length || this.moved.post.length) this.restore();
|
|
413
|
+
this._syncReason = this.disabled ? 'kill switch (?dxrdiag=nolayerrig)' : 'no layer on the display rig';
|
|
326
414
|
return false;
|
|
327
415
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const key = `${[...this.layers.keys()].join(',')}|${entries.join(',')}|${(eyeCam.layers || []).join(',')}`;
|
|
332
|
-
if (key === this._key) return this.moved.display.length > 0;
|
|
333
|
-
const runs = layerRuns(entries, new Set(this.layers.keys()), eyeLayers);
|
|
334
|
-
if (runs.interleaved.length && !this._warnedInterleaved) {
|
|
335
|
-
this._warnedInterleaved = true;
|
|
336
|
-
console.warn(
|
|
337
|
-
`[inline3d/splat] setLayerRig: layer(s) ${runs.interleaved.join(', ')} sit BETWEEN display-rig ` +
|
|
338
|
-
'sublayers; they are drawn before them (with the eye camera). Move them before or after the display layers.',
|
|
339
|
-
);
|
|
416
|
+
if (!eyes.length || !comp) {
|
|
417
|
+
this._syncReason = 'engine not ready';
|
|
418
|
+
return false;
|
|
340
419
|
}
|
|
341
|
-
|
|
342
|
-
this.moved.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
420
|
+
const ref = eyes[0].camera;
|
|
421
|
+
const eyeLayers = [...(ref.layers || []), ...this.moved.display, ...this.moved.post];
|
|
422
|
+
const orphans = []; // display layers NO camera draws: ours to draw (nobody else can lose them)
|
|
423
|
+
const entries = (comp.layerList || []).map((l) => l.id);
|
|
424
|
+
// Resolve every request; a layer the eye camera does not draw cannot be moved (another camera
|
|
425
|
+
// — the page's own, a reflection pass — draws it, or none does): say so, never silently.
|
|
426
|
+
this.layers.clear();
|
|
427
|
+
const missing = [];
|
|
428
|
+
const foreign = [];
|
|
429
|
+
for (const key of this.requests.keys()) {
|
|
430
|
+
const layer = this.resolve(key);
|
|
431
|
+
if (!layer) {
|
|
432
|
+
missing.push(`"${key}" is not in the tile's layer composition`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (!eyeLayers.includes(layer.id)) {
|
|
436
|
+
const by = (layer.cameras || []).filter((c) => !c?.entity?._dxrLayerRig).map(camName);
|
|
437
|
+
if (by.length) {
|
|
438
|
+
// Another camera (the page's own, a reflection pass) draws it: taking it away could break
|
|
439
|
+
// that camera's output, so it is left alone — and said.
|
|
440
|
+
foreign.push(`"${key}" is drawn by ${by.join(', ')}, not by the tile's eye camera — draw it with handle.engine.camera (or pass it to setLayerRig without adding it to any camera)`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
orphans.push(layer.id);
|
|
444
|
+
}
|
|
445
|
+
this.layers.set(layer.id, key);
|
|
347
446
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
447
|
+
const problems = [...missing, ...foreign];
|
|
448
|
+
const key = `${eyes.length}|${[...this.layers.keys()].join(',')}|${entries.join(',')}|${(ref.layers || []).join(',')}|${problems.length}`;
|
|
449
|
+
if (key !== this._key) {
|
|
450
|
+
const runs = layerRuns(entries, new Set(this.layers.keys()), [...eyeLayers, ...orphans]);
|
|
451
|
+
if (runs.interleaved.length && !this._warnedInterleaved) {
|
|
452
|
+
this._warnedInterleaved = true;
|
|
453
|
+
console.warn(
|
|
454
|
+
`[inline3d/splat] setLayerRig: layer(s) ${runs.interleaved.join(', ')} sit BETWEEN display-rig ` +
|
|
455
|
+
'sublayers; they are drawn before them (with the eye camera). Move them before or after the display layers.',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
this.moved.display = runs.display;
|
|
459
|
+
this.moved.post = runs.post;
|
|
460
|
+
for (const e of eyes) e.camera.layers = runs.pre.slice();
|
|
461
|
+
this._runLayers = { display: runs.display, post: runs.post };
|
|
462
|
+
this._key = `${eyes.length}|${[...this.layers.keys()].join(',')}|${entries.join(',')}|${runs.pre.join(',')}|${problems.length}`;
|
|
351
463
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return runs.display.length > 0;
|
|
464
|
+
this._problems = problems;
|
|
465
|
+
this._syncReason = this.moved.display.length ? null : problems.join('; ') || 'no display layer resolved';
|
|
466
|
+
return this.moved.display.length > 0;
|
|
356
467
|
}
|
|
357
468
|
|
|
358
|
-
/** Every moved layer back on the eye camera, run cameras off (the kill switch / last layer off). */
|
|
469
|
+
/** Every moved layer back on the eye camera(s), run cameras off (the kill switch / last layer off). */
|
|
359
470
|
restore() {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
471
|
+
const back = [...this.moved.display, ...this.moved.post];
|
|
472
|
+
for (const e of this._eyeCams()) {
|
|
473
|
+
const cur = e.camera.layers || [];
|
|
474
|
+
e.camera.layers = [...cur, ...back.filter((id) => !cur.includes(id))];
|
|
364
475
|
}
|
|
365
476
|
this.moved = { display: [], post: [] };
|
|
366
|
-
|
|
367
|
-
if (this.cams.post) this.cams.post.enabled = false;
|
|
477
|
+
for (const list of Object.values(this.cams)) for (const c of list) c.enabled = false;
|
|
368
478
|
this._key = '';
|
|
369
479
|
}
|
|
370
480
|
|
|
371
481
|
/**
|
|
372
|
-
* Per drawn frame, after the eye camera
|
|
373
|
-
*
|
|
482
|
+
* Per drawn frame, after the eye camera(s) are set up. `rig` is the descriptor the runtime
|
|
483
|
+
* located these views with; `located` whether the views were VERIFIED against it (the rig map
|
|
484
|
+
* read it off the views) — false means it is the SDK's own record, unverified. Records what
|
|
485
|
+
* happened in `last` and WARNs one line on the first 3D frame and on every change.
|
|
374
486
|
*/
|
|
375
|
-
frame(entries, rect, f, rig) {
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
487
|
+
frame(entries, rect, f, rig, { located = null, residual = null } = {}) {
|
|
488
|
+
const v = this.viewer;
|
|
489
|
+
const ncam = v._viewPath === 'cameras';
|
|
490
|
+
const L = this.last;
|
|
491
|
+
L.views = entries.length;
|
|
492
|
+
L.path = entries.length < 2 ? 'mono' : ncam ? 'ncamera' : 'renderviews';
|
|
493
|
+
L.located = located;
|
|
494
|
+
L.residual = residual;
|
|
495
|
+
const hasRun = this.sync();
|
|
379
496
|
const frame = cameraRigFrame(rig);
|
|
380
|
-
const k = frame ? layerRigGain(frame, this.opts
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
497
|
+
const k = frame ? layerRigGain(frame, this.opts) : null;
|
|
498
|
+
const plane = frame ? layerRigPlane(frame, this.opts) : null;
|
|
499
|
+
L.gain = k;
|
|
500
|
+
L.planeM = plane;
|
|
501
|
+
L.photoConvergenceM = frame ? frame.D : null;
|
|
502
|
+
const round = hasRun && frame ? roundViews(entries, frame, k, this._round, plane) : null;
|
|
503
|
+
let reason = null;
|
|
504
|
+
if (!hasRun) reason = this._syncReason;
|
|
505
|
+
else if (entries.length < 2) reason = 'mono (one view): nothing to round — identical to the photo rig';
|
|
506
|
+
else if (!rig) reason = 'no declared view rig for these views';
|
|
507
|
+
else if (!frame) reason = rig.type === 'camera' ? 'camera rig with no finite convergence' : `the declared rig is a ${rig.type || 'non-camera'} rig (setRig('display') / setVideo): already the display rig`;
|
|
508
|
+
else if (!round) reason = 'gain 1 and no plane offset: identical to the photo rig';
|
|
509
|
+
L.engaged = !!round;
|
|
510
|
+
L.rounded = L.engaged;
|
|
511
|
+
L.reason = reason || (this._problems?.length ? `engaged; ${this._problems.join('; ')}` : null);
|
|
512
|
+
if (hasRun) {
|
|
513
|
+
const nViews = entries.length;
|
|
514
|
+
for (const run of ['display', 'post']) {
|
|
515
|
+
const layers = this._runLayers?.[run] || [];
|
|
516
|
+
const need = layers.length ? (ncam ? nViews : 1) : 0;
|
|
517
|
+
const list = need ? this._runCams(run, need, run === 'display' ? 1 : 2) : this.cams[run];
|
|
518
|
+
for (let c = 0; c < list.length; c++) {
|
|
519
|
+
list[c].enabled = c < need;
|
|
520
|
+
if (c < need) list[c].camera.layers = layers;
|
|
521
|
+
}
|
|
522
|
+
if (!need) continue;
|
|
523
|
+
const r = run === 'display' ? round : null;
|
|
524
|
+
if (ncam) this._frameNcam(list, entries, rect, r, run);
|
|
525
|
+
else this._frameRenderViews(list[0], entries, rect, f, r, run);
|
|
526
|
+
if (run === 'display') for (let c = 0; c < need; c++) list[c].camera.clearDepthBuffer = !!r; // two spaces never share a depth test
|
|
399
527
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
528
|
+
}
|
|
529
|
+
this._warnState();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
_frameRenderViews(cam, entries, rect, f, r, run) {
|
|
533
|
+
const pc = this.viewer.pc;
|
|
534
|
+
const rvs = this._rvs[run];
|
|
535
|
+
if (rvs.length !== entries.length) {
|
|
536
|
+
rvs.length = 0;
|
|
537
|
+
for (let i = 0; i < entries.length; i++) rvs.push(new pc.RenderView());
|
|
538
|
+
cam.camera.camera.xrViews = rvs.slice();
|
|
539
|
+
}
|
|
540
|
+
for (let i = 0; i < entries.length; i++) {
|
|
541
|
+
const e = entries[i];
|
|
542
|
+
if (r) rvs[i].setView(e.proj, r[i].viewInv, r[i].view);
|
|
543
|
+
else rvs[i].setView(e.proj, e.pose);
|
|
544
|
+
const [x, y, w, h] = rect(e);
|
|
545
|
+
rvs[i].setViewport(x, y, w, h);
|
|
546
|
+
}
|
|
547
|
+
if (cam._frustumKey !== this.viewer._frustumKey) {
|
|
548
|
+
cam._frustumKey = this.viewer._frustumKey;
|
|
549
|
+
cam.camera.camera.setXrProperties({ ...f, horizontalFov: false });
|
|
550
|
+
}
|
|
551
|
+
// The node drives transparent sorting (the views ignore it): on the (round) first eye.
|
|
552
|
+
const p = entries[0].node || entries[0].pose; // `node`: a rigid pose when the views are remapped
|
|
553
|
+
const q = quatOf(p);
|
|
554
|
+
cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
555
|
+
if (r) cam.setLocalPosition(r[0].eye[0], r[0].eye[1], r[0].eye[2]);
|
|
556
|
+
else cam.setLocalPosition(p[12], p[13], p[14]);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The N-camera path, mirroring the viewer's own fallback: camera i at view i's rect, its node on
|
|
561
|
+
* the view's rigid pose, and a projection override. The engine then draws with
|
|
562
|
+
* proj' · node⁻¹ (node under the rig node), so the display run's shear rides in the projection:
|
|
563
|
+
* proj' = proj · view_round · node ⇒ proj' · node⁻¹ = proj · view_round. Culling is off on these
|
|
564
|
+
* cameras (the sheared frustum is not the node's).
|
|
565
|
+
*/
|
|
566
|
+
_frameNcam(list, entries, rect, r, run) {
|
|
567
|
+
const v = this.viewer;
|
|
568
|
+
const pc = v.pc;
|
|
569
|
+
const el = v.canvas;
|
|
570
|
+
const W = el?.width || 1;
|
|
571
|
+
const H = el?.height || 1;
|
|
572
|
+
for (let i = 0; i < entries.length; i++) {
|
|
573
|
+
const e = entries[i];
|
|
574
|
+
const cam = list[i];
|
|
575
|
+
const node = e.node || e.pose;
|
|
576
|
+
if (r) mul4(e.proj, mul4(r[i].view, node), cam._dxrProj);
|
|
577
|
+
else if (e.node) mul4(e.proj, mul4(invertAffine(e.pose), node), cam._dxrProj);
|
|
578
|
+
else cam._dxrProj.set(e.proj);
|
|
579
|
+
const fr = v._frustumOf ? v._frustumOf(e.proj) : null;
|
|
580
|
+
if (fr) {
|
|
581
|
+
cam.camera.fov = fr.fov;
|
|
582
|
+
cam.camera.nearClip = fr.nearClip;
|
|
583
|
+
cam.camera.farClip = fr.farClip;
|
|
403
584
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const q = quatOf(
|
|
585
|
+
const [x, y, w, h] = rect(e);
|
|
586
|
+
cam.camera.rect = pc?.Vec4 ? new pc.Vec4(x / W, y / H, w / W, h / H) : [x / W, y / H, w / W, h / H];
|
|
587
|
+
const q = quatOf(node);
|
|
407
588
|
cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
408
|
-
|
|
409
|
-
else cam.setLocalPosition(p[12], p[13], p[14]);
|
|
410
|
-
if (run === 'display') {
|
|
411
|
-
// Two camera spaces never share a depth test; in mono (and at gain 1) they are one space.
|
|
412
|
-
cam.camera.clearDepthBuffer = !!r;
|
|
413
|
-
this.last.rounded = !!r;
|
|
414
|
-
}
|
|
589
|
+
cam.setLocalPosition(node[12], node[13], node[14]);
|
|
415
590
|
}
|
|
416
591
|
}
|
|
417
592
|
|
|
593
|
+
/** One WARN line on the first 3D frame and on every change of engaged / reason / path. */
|
|
594
|
+
_warnState() {
|
|
595
|
+
const L = this.last;
|
|
596
|
+
if (L.path === 'mono' && !this._warnedOnce3d) return; // wait for the first 3D frame
|
|
597
|
+
if (!this.requests.size) return; // nothing asked for: nothing to report
|
|
598
|
+
const key = `${L.path}|${L.engaged}|${L.reason}`;
|
|
599
|
+
if (key === this._warnKey || this._warns >= MAX_STATE_WARNS) return;
|
|
600
|
+
this._warnKey = key;
|
|
601
|
+
this._warns++;
|
|
602
|
+
if (L.path !== 'mono') this._warnedOnce3d = true;
|
|
603
|
+
console.warn(`[inline3d/splat] setLayerRig: ${layerRigLine(this.state())}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** The public state (handle.layerRigState). */
|
|
607
|
+
state() {
|
|
608
|
+
const L = this.last;
|
|
609
|
+
return {
|
|
610
|
+
display: [...this.requests.keys()],
|
|
611
|
+
disabled: this.disabled,
|
|
612
|
+
path: L.path,
|
|
613
|
+
engaged: !!L.engaged,
|
|
614
|
+
rounded: !!L.engaged,
|
|
615
|
+
reason: this.disabled ? 'kill switch (?dxrdiag=nolayerrig)' : L.reason,
|
|
616
|
+
viewerDistance: this.opts.viewerDistance ?? DEFAULT_VIEWER_DISTANCE_M,
|
|
617
|
+
gain: L.gain,
|
|
618
|
+
planeM: L.planeM,
|
|
619
|
+
photoConvergenceM: L.photoConvergenceM ?? null,
|
|
620
|
+
planeOffset: this.opts.planeOffset ?? 0,
|
|
621
|
+
located: L.located,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
418
625
|
destroy() {
|
|
419
626
|
this.restore();
|
|
420
|
-
for (const
|
|
421
|
-
this.cams = { display:
|
|
627
|
+
for (const list of Object.values(this.cams)) for (const c of list) c?.destroy?.();
|
|
628
|
+
this.cams = { display: [], post: [] };
|
|
422
629
|
}
|
|
423
630
|
}
|
|
631
|
+
|
|
632
|
+
/** layerRigState() as one console line (the WARN, and what a page's HUD can print). */
|
|
633
|
+
export function layerRigLine(st) {
|
|
634
|
+
const n = (x, d = 3) => (x === null || x === undefined ? '-' : Number(x).toFixed(d));
|
|
635
|
+
return (
|
|
636
|
+
`path=${st.path ?? '-'} engaged=${st.engaged} layers=[${st.display.join(', ')}] ` +
|
|
637
|
+
`viewerDistance=${n(st.viewerDistance, 2)}m gain=${n(st.gain)} planeM=${n(st.planeM)} ` +
|
|
638
|
+
`photoConvergenceM=${n(st.photoConvergenceM)} planeOffset=${n(st.planeOffset)}m ` +
|
|
639
|
+
`located=${st.located === null || st.located === undefined ? '-' : st.located} reason=${st.reason ?? '-'}`
|
|
640
|
+
);
|
|
641
|
+
}
|