@displayxr/inline3d 1.24.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 +117 -0
- package/README.md +15 -0
- package/index.d.ts +57 -0
- package/js/inline3d-player.js +2219 -0
- package/js/inline3d-splat-layer-rig.js +341 -125
- package/js/inline3d-splat-playcanvas.js +35 -19
- package/js/inline3d-splat.js +8 -1
- package/js/inline3d.js +208 -12
- package/package.json +7 -1
- package/player.d.ts +273 -0
- package/splat.d.ts +38 -7
|
@@ -39,9 +39,41 @@ function rotate(q, v) {
|
|
|
39
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)];
|
|
40
40
|
}
|
|
41
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
|
+
|
|
42
74
|
/**
|
|
43
75
|
* setLayerRig's arguments, validated (throws at the call).
|
|
44
|
-
* @returns {{ rig: 'display'|'camera',
|
|
76
|
+
* @returns {{ rig: 'display'|'camera', opts: object }}
|
|
45
77
|
*/
|
|
46
78
|
export function validateLayerRig(layer, rig, o = {}) {
|
|
47
79
|
if (!(typeof layer === 'string' && layer.length) && !(Number.isInteger(layer) && layer >= 0) && !(layer && typeof layer === 'object' && Number.isInteger(layer.id))) {
|
|
@@ -50,13 +82,7 @@ export function validateLayerRig(layer, rig, o = {}) {
|
|
|
50
82
|
if (!LAYER_RIGS.includes(rig)) {
|
|
51
83
|
throw new Error(`@displayxr/inline3d/splat: setLayerRig — rig "${rig}", expected ${LAYER_RIGS.map((r) => `'${r}'`).join(' or ')}.`);
|
|
52
84
|
}
|
|
53
|
-
|
|
54
|
-
const num = (k) => {
|
|
55
|
-
if (o[k] === undefined) return null;
|
|
56
|
-
if (!Number.isFinite(o[k]) || !(o[k] > 0)) throw new Error(`@displayxr/inline3d/splat: setLayerRig — bad ${k}: ${o[k]}.`);
|
|
57
|
-
return o[k];
|
|
58
|
-
};
|
|
59
|
-
return { rig, viewerDistance: num('viewerDistance'), gain: num('gain') };
|
|
85
|
+
return { rig, opts: validateLayerRigOptions(o) };
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
/**
|
|
@@ -77,11 +103,24 @@ export function cameraRigFrame(rig) {
|
|
|
77
103
|
|
|
78
104
|
/** k = D / (m·n), or the caller's explicit gain. */
|
|
79
105
|
export function layerRigGain(frame, { viewerDistance = null, gain = null } = {}) {
|
|
80
|
-
if (gain !== null) return gain;
|
|
81
|
-
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;
|
|
82
108
|
return frame.D / (frame.m * n);
|
|
83
109
|
}
|
|
84
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
|
+
|
|
85
124
|
/**
|
|
86
125
|
* The window-fixing shear for one eye, as a column-major 4×4 (and its inverse), in the space the
|
|
87
126
|
* declared rig and the views share.
|
|
@@ -175,27 +214,47 @@ export function invertRigid(m, out = new Float64Array(16)) {
|
|
|
175
214
|
}
|
|
176
215
|
|
|
177
216
|
/**
|
|
178
|
-
* The round views for one frame: for each runtime view pose P_i (camera → rig space
|
|
179
|
-
* `viewInv = M_i⁻¹ · P_i` and `view = P_i⁻¹ · M_i` — what RenderView.setView takes.
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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.
|
|
182
228
|
*/
|
|
183
|
-
export function roundViews(entries, frame, k, into = []) {
|
|
184
|
-
|
|
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;
|
|
185
232
|
into.length = entries.length;
|
|
233
|
+
const S = scaleAbout(frame.N0, sigma);
|
|
234
|
+
const Sinv = scaleAbout(frame.N0, 1 / sigma);
|
|
186
235
|
for (let i = 0; i < entries.length; i++) {
|
|
187
236
|
const P = entries[i].pose;
|
|
188
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] });
|
|
189
238
|
const { Eround } = windowShear([P[12], P[13], P[14]], frame, k, o.M, o.Minv);
|
|
190
|
-
|
|
191
|
-
o.eye[
|
|
192
|
-
o.
|
|
193
|
-
mul4(o.
|
|
194
|
-
mul4(invertAffine(P), o.M, o.view); // P is rigid, or affine once remapped to the declared rig (./inline3d-splat-rig-map.js)
|
|
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)
|
|
195
243
|
}
|
|
196
244
|
return into;
|
|
197
245
|
}
|
|
198
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
|
+
|
|
199
258
|
/**
|
|
200
259
|
* Split a layer composition into contiguous RUNS for per-rig cameras. `entries` is the
|
|
201
260
|
* composition's sublayer list as layer ids, in draw order (opaque and transparent sublayers both
|
|
@@ -248,178 +307,335 @@ export function layerRuns(entries, display, eyeLayers) {
|
|
|
248
307
|
|
|
249
308
|
// ── the engine side ──────────────────────────────────────────────────────────────────────────
|
|
250
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
|
+
|
|
251
316
|
/**
|
|
252
|
-
* The per-rig cameras of one tile. Owned by the viewer; `sync` + `frame` are called by its draw
|
|
253
|
-
*
|
|
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.
|
|
254
323
|
*/
|
|
255
324
|
export class LayerRigCameras {
|
|
256
325
|
constructor(viewer) {
|
|
257
326
|
this.viewer = viewer;
|
|
258
|
-
/** What the page asked for: name or id →
|
|
327
|
+
/** What the page asked for: name or id → true. */
|
|
259
328
|
this.requests = new Map();
|
|
260
|
-
/** Those, resolved: layer id →
|
|
329
|
+
/** Those, resolved: layer id → the caller's key (the ones the eye camera draws right now). */
|
|
261
330
|
this.layers = new Map();
|
|
262
|
-
/** The
|
|
263
|
-
this.opts = {};
|
|
331
|
+
/** The tile's options: merged across setLayerRig / setLayerRigOptions calls. */
|
|
332
|
+
this.opts = { viewerDistance: null, gain: null, planeOffset: null, planeDistance: null };
|
|
264
333
|
this.disabled = false; // the kill switch (diag 'nolayerrig')
|
|
265
|
-
this.cams = { display:
|
|
334
|
+
this.cams = { display: [], post: [] };
|
|
266
335
|
this.moved = { display: [], post: [] };
|
|
267
336
|
this._key = '';
|
|
268
337
|
this._rvs = { display: [], post: [] };
|
|
269
338
|
this._round = [];
|
|
270
|
-
|
|
271
|
-
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 };
|
|
272
344
|
}
|
|
273
345
|
|
|
274
346
|
get active() {
|
|
275
347
|
return !this.disabled && this.requests.size > 0;
|
|
276
348
|
}
|
|
277
349
|
|
|
278
|
-
/** Resolve a name / id / pc.Layer to
|
|
279
|
-
resolve(
|
|
350
|
+
/** Resolve a name / id / pc.Layer to a layer of this tile's composition (null if unknown). */
|
|
351
|
+
resolve(key) {
|
|
280
352
|
const comp = this.viewer.app?.scene?.layers;
|
|
281
353
|
if (!comp) return null;
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
const l = comp.getLayerByName?.(layer);
|
|
285
|
-
return l ? l.id : null;
|
|
354
|
+
if (Number.isInteger(key)) return comp.getLayerById?.(key) || { id: key };
|
|
355
|
+
return comp.getLayerByName?.(key) || null;
|
|
286
356
|
}
|
|
287
357
|
|
|
288
|
-
/**
|
|
289
|
-
|
|
290
|
-
* sync, so a layer the page adds AFTER calling setLayerRig is picked up when it appears.
|
|
291
|
-
*/
|
|
292
|
-
set(key, rig, o) {
|
|
358
|
+
/** Record a request by the caller's key (name, id); resolved on every sync. */
|
|
359
|
+
set(key, rig, o = {}) {
|
|
293
360
|
const k = key && typeof key === 'object' ? key.id : key;
|
|
294
|
-
if (rig === 'display') this.requests.set(k,
|
|
361
|
+
if (rig === 'display') this.requests.set(k, true);
|
|
295
362
|
else this.requests.delete(k);
|
|
296
|
-
this.
|
|
363
|
+
this.setOptions(o);
|
|
297
364
|
this._key = ''; // re-split on the next draw
|
|
298
365
|
}
|
|
299
366
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
for (const [k,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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] : [];
|
|
306
377
|
}
|
|
307
378
|
|
|
308
379
|
_makeCam(name, priority) {
|
|
309
380
|
const v = this.viewer;
|
|
310
|
-
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);
|
|
311
382
|
cam.camera.priority = priority;
|
|
312
383
|
cam.camera.clearColorBuffer = false;
|
|
313
384
|
cam.camera.clearDepthBuffer = false;
|
|
314
385
|
cam.camera.clearStencilBuffer = false;
|
|
315
386
|
cam.camera.frustumCulling = false;
|
|
316
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
|
+
}
|
|
317
393
|
return cam;
|
|
318
394
|
}
|
|
319
395
|
|
|
320
|
-
/**
|
|
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
|
+
*/
|
|
321
407
|
sync() {
|
|
322
408
|
const v = this.viewer;
|
|
323
|
-
const
|
|
409
|
+
const eyes = this._eyeCams();
|
|
324
410
|
const comp = v.app?.scene?.layers;
|
|
325
|
-
if (!eyeCam || !comp) return false;
|
|
326
411
|
if (!this.active) {
|
|
327
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';
|
|
328
414
|
return false;
|
|
329
415
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const key = `${[...this.layers.keys()].join(',')}|${entries.join(',')}|${(eyeCam.layers || []).join(',')}`;
|
|
334
|
-
if (key === this._key) return this.moved.display.length > 0;
|
|
335
|
-
const runs = layerRuns(entries, new Set(this.layers.keys()), eyeLayers);
|
|
336
|
-
if (runs.interleaved.length && !this._warnedInterleaved) {
|
|
337
|
-
this._warnedInterleaved = true;
|
|
338
|
-
console.warn(
|
|
339
|
-
`[inline3d/splat] setLayerRig: layer(s) ${runs.interleaved.join(', ')} sit BETWEEN display-rig ` +
|
|
340
|
-
'sublayers; they are drawn before them (with the eye camera). Move them before or after the display layers.',
|
|
341
|
-
);
|
|
416
|
+
if (!eyes.length || !comp) {
|
|
417
|
+
this._syncReason = 'engine not ready';
|
|
418
|
+
return false;
|
|
342
419
|
}
|
|
343
|
-
|
|
344
|
-
this.moved.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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);
|
|
349
446
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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}`;
|
|
353
463
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
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;
|
|
358
467
|
}
|
|
359
468
|
|
|
360
|
-
/** 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). */
|
|
361
470
|
restore() {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
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))];
|
|
366
475
|
}
|
|
367
476
|
this.moved = { display: [], post: [] };
|
|
368
|
-
|
|
369
|
-
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;
|
|
370
478
|
this._key = '';
|
|
371
479
|
}
|
|
372
480
|
|
|
373
481
|
/**
|
|
374
|
-
* Per drawn frame, after the eye camera
|
|
375
|
-
*
|
|
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.
|
|
376
486
|
*/
|
|
377
|
-
frame(entries, rect, f, rig) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
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();
|
|
381
496
|
const frame = cameraRigFrame(rig);
|
|
382
|
-
const k = frame ? layerRigGain(frame, this.opts
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
401
527
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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;
|
|
405
584
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
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);
|
|
409
588
|
cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
410
|
-
|
|
411
|
-
else cam.setLocalPosition(p[12], p[13], p[14]);
|
|
412
|
-
if (run === 'display') {
|
|
413
|
-
// Two camera spaces never share a depth test; in mono (and at gain 1) they are one space.
|
|
414
|
-
cam.camera.clearDepthBuffer = !!r;
|
|
415
|
-
this.last.rounded = !!r;
|
|
416
|
-
}
|
|
589
|
+
cam.setLocalPosition(node[12], node[13], node[14]);
|
|
417
590
|
}
|
|
418
591
|
}
|
|
419
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
|
+
|
|
420
625
|
destroy() {
|
|
421
626
|
this.restore();
|
|
422
|
-
for (const
|
|
423
|
-
this.cams = { display:
|
|
627
|
+
for (const list of Object.values(this.cams)) for (const c of list) c?.destroy?.();
|
|
628
|
+
this.cams = { display: [], post: [] };
|
|
424
629
|
}
|
|
425
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
|
+
}
|