@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
|
@@ -25,14 +25,29 @@
|
|
|
25
25
|
//
|
|
26
26
|
// THE RIG. The incoming asset's rig (camera rig: its capture pose, lens and convergence; display
|
|
27
27
|
// rig: its fit and focus) is adopted at once, as in the frozen path, so the views the runtime
|
|
28
|
-
// hands us
|
|
29
|
-
//
|
|
28
|
+
// hands us — once the declaration has reached them, a frame or more later — are the INCOMING
|
|
29
|
+
// photo's. The outgoing photo must keep being drawn through ITS OWN rig for the whole window.
|
|
30
|
+
//
|
|
31
|
+
// Two camera rigs (a photo slideshow: every photo a SOG with a camera block): the views are
|
|
32
|
+
// remapped, per frame, from the rig they were LOCATED for to the outgoing photo's rig, by the
|
|
33
|
+
// exact portal map of ./inline3d-splat-rig-map.js — window onto window, eye onto eye, the
|
|
34
|
+
// runtime's projection verbatim — under the outgoing photo's rig node R_o. Before the
|
|
35
|
+
// declaration arrives the located rig IS the outgoing one and the views pass through untouched.
|
|
36
|
+
//
|
|
37
|
+
// Otherwise (a display rig on either side, views no declared rig explains, the kill switch
|
|
38
|
+
// `?dxrdiag=oldrig`): the pre-1.24 node chain, which maps the incoming views back to the outgoing
|
|
39
|
+
// photo's framing:
|
|
30
40
|
// N = R_o · K_o · D_o · D_n⁻¹ · K_n⁻¹
|
|
31
41
|
// with R the rig node (inverse pivot), K the mono/capture pose in rig space and
|
|
32
42
|
// D = diag(c·t, c·t, c) (c = convergence distance, t = tan(vfov/2)). The eye poses (head
|
|
33
43
|
// motion) pass through untouched; the outgoing photo keeps its own window and zero-disparity
|
|
34
44
|
// plane. Two display-rig assets cancel to N = R_o (the old fit and focus); in controls:'page' the
|
|
35
|
-
// page owns the camera and N is the rig node itself.
|
|
45
|
+
// page owns the camera and N is the rig node itself. For two CAMERA rigs this chain is not exact:
|
|
46
|
+
// it scales the eyes with the window, by (c_o·t_o)/(c_n·t_n) — the outgoing photo's disparity and
|
|
47
|
+
// head parallax jumped by that factor at the swap (the "camera change right before the
|
|
48
|
+
// transition" report) — and it assumed the views were already the incoming rig's.
|
|
49
|
+
|
|
50
|
+
import { remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
|
|
36
51
|
|
|
37
52
|
/** setSource's `outgoing` option. */
|
|
38
53
|
export const OUTGOING_MODES = Object.freeze(['live', 'frozen']);
|
|
@@ -151,6 +166,12 @@ export class LiveOutgoing {
|
|
|
151
166
|
/** PRE-SORT (warm): the current asset is ALSO on the live layer, before the swap. */
|
|
152
167
|
this.warming = false;
|
|
153
168
|
this._warmLayers = null;
|
|
169
|
+
/** The outgoing photo's own declared rig (a RigTracker entry), taken at start(). */
|
|
170
|
+
this.ownRig = null;
|
|
171
|
+
this._map = [];
|
|
172
|
+
this._node = new Float64Array(16);
|
|
173
|
+
/** What the last sync drew through: 'eye' (the eye's views), 'own' (the photo's rig, as located), 'remapped', 'chain'. */
|
|
174
|
+
this.path = null;
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
/** Engine support for this path: RenderViews on one camera, render targets, layers. */
|
|
@@ -299,6 +320,8 @@ export class LiveOutgoing {
|
|
|
299
320
|
entity.gsplat.layers = [this.layer.id];
|
|
300
321
|
this.entity = entity;
|
|
301
322
|
this.oldFrame = oldFrame;
|
|
323
|
+
// Read BEFORE the incoming asset's rig is declared: the last declared rig is the outgoing's.
|
|
324
|
+
this.ownRig = v.rigTrack?.latest ?? null;
|
|
302
325
|
if (!warmed) this.frames = 0;
|
|
303
326
|
this.warming = false;
|
|
304
327
|
this._warmLayers = null;
|
|
@@ -347,10 +370,11 @@ export class LiveOutgoing {
|
|
|
347
370
|
}
|
|
348
371
|
|
|
349
372
|
/**
|
|
350
|
-
* Per frame, before the engine tick: the
|
|
351
|
-
*
|
|
373
|
+
* Per frame, before the engine tick: the views on the outgoing camera — through the outgoing
|
|
374
|
+
* photo's own rig (see the header) — the viewports, and the node chain. `rf` is the viewer's rig
|
|
375
|
+
* mapping for this frame (viewer._mapViews), or undefined.
|
|
352
376
|
*/
|
|
353
|
-
sync(entries, rect, frustum) {
|
|
377
|
+
sync(entries, rect, frustum, rf) {
|
|
354
378
|
if (!this.active && !this.warming) return;
|
|
355
379
|
const v = this.viewer;
|
|
356
380
|
const pc = v.pc;
|
|
@@ -360,11 +384,50 @@ export class LiveOutgoing {
|
|
|
360
384
|
for (let i = 0; i < entries.length; i++) rvs.push(new pc.RenderView());
|
|
361
385
|
this.cam.camera.camera.xrViews = rvs.slice();
|
|
362
386
|
}
|
|
387
|
+
// Which views: the pre-sort (and controls:'page', where both photos are the page camera's)
|
|
388
|
+
// draws what the eye draws; an active window draws the outgoing photo's own rig.
|
|
389
|
+
let mapped = null; // remapped views, or null = the entries as they are
|
|
390
|
+
let exact = false; // the rig map applies: no node chain
|
|
391
|
+
let nodeRig = null; // the portal the remapped views belong to (for the camera node)
|
|
392
|
+
const own = this.ownRig;
|
|
393
|
+
if (rf && v.rigTrack && (!this.active || v.pageCamera)) {
|
|
394
|
+
exact = true;
|
|
395
|
+
mapped = rf.remapped ? rf.entries : null;
|
|
396
|
+
nodeRig = rf.eye?.portal ?? null;
|
|
397
|
+
this.path = 'eye';
|
|
398
|
+
} else if (rf?.located && own?.portal && this.active && !v.pageCamera) {
|
|
399
|
+
if (rf.located === own || sameRig(rf.located.rig, own.rig)) {
|
|
400
|
+
exact = true;
|
|
401
|
+
this.path = 'own';
|
|
402
|
+
} else {
|
|
403
|
+
mapped = remapViews(entries, rf.located.portal, own.portal, this._map);
|
|
404
|
+
exact = !!mapped;
|
|
405
|
+
nodeRig = own.portal;
|
|
406
|
+
if (mapped) this.path = 'remapped';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!exact) this.path = 'chain';
|
|
363
410
|
for (let i = 0; i < entries.length; i++) {
|
|
364
|
-
rvs[i].setView(
|
|
411
|
+
if (mapped) rvs[i].setView(mapped[i].proj, mapped[i].viewInv, mapped[i].view);
|
|
412
|
+
else rvs[i].setView(entries[i].proj, entries[i].pose);
|
|
365
413
|
const [x, y, w, h] = rect(entries[i]);
|
|
366
414
|
rvs[i].setViewport(x, y, w, h);
|
|
367
415
|
}
|
|
416
|
+
// The wavefront's cull reads the views this camera really draws (viewer.cullViews).
|
|
417
|
+
if (mapped) {
|
|
418
|
+
const cull = (this._cull ||= []);
|
|
419
|
+
cull.length = mapped.length;
|
|
420
|
+
for (let i = 0; i < mapped.length; i++) {
|
|
421
|
+
const c = (cull[i] ||= {});
|
|
422
|
+
c.proj = mapped[i].proj;
|
|
423
|
+
c.pose = mapped[i].viewInv;
|
|
424
|
+
c.x = mapped[i].x;
|
|
425
|
+
c.y = mapped[i].y;
|
|
426
|
+
c.width = mapped[i].width;
|
|
427
|
+
c.height = mapped[i].height;
|
|
428
|
+
}
|
|
429
|
+
this.cam._dxrViews = cull;
|
|
430
|
+
} else this.cam._dxrViews = null;
|
|
368
431
|
if (frustum) {
|
|
369
432
|
const key = `${frustum.fov.toFixed(4)}|${frustum.aspectRatio.toFixed(4)}|${frustum.nearClip}|${frustum.farClip}`;
|
|
370
433
|
if (key !== this._frustumKey) {
|
|
@@ -374,6 +437,20 @@ export class LiveOutgoing {
|
|
|
374
437
|
}
|
|
375
438
|
// The node chain (see the header). controls:'page': the page's camera, for both assets.
|
|
376
439
|
const { n1, n2, n3 } = this.nodes;
|
|
440
|
+
if (exact) {
|
|
441
|
+
// The views are in rig space already: only the photo's rig node above them — the one the
|
|
442
|
+
// outgoing photo had at the swap (the pre-sort and controls:'page': the current one).
|
|
443
|
+
setTRS(n1, similarityTRS(this.active && !v.pageCamera && this.oldFrame ? this.oldFrame.rig : v.rigMatrix()));
|
|
444
|
+
n2.setLocalScale(1, 1, 1);
|
|
445
|
+
setTRS(n3, { position: [0, 0, 0], rotation: [0, 0, 0, 1], scale: 1 });
|
|
446
|
+
// The camera NODE (sort direction, LOD distance) on the first eye this camera draws from.
|
|
447
|
+
const pose = mapped && nodeRig ? nodePose(nodeRig, mapped[0].eye, this._node) : entries[0].pose;
|
|
448
|
+
const q = similarityTRS(pose).rotation;
|
|
449
|
+
this.cam.setLocalPosition(pose[12], pose[13], pose[14]);
|
|
450
|
+
this.cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
451
|
+
this.frames++;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
377
454
|
if (v.pageCamera || !this.oldFrame) {
|
|
378
455
|
const r = similarityTRS(v.rigMatrix());
|
|
379
456
|
setTRS(n1, r);
|
|
@@ -401,6 +478,9 @@ export class LiveOutgoing {
|
|
|
401
478
|
if (this.warming) return this.cancelWarm();
|
|
402
479
|
this.entity = null;
|
|
403
480
|
this.oldFrame = null;
|
|
481
|
+
this.ownRig = null;
|
|
482
|
+
this.path = null;
|
|
483
|
+
if (this.cam) this.cam._dxrViews = null;
|
|
404
484
|
this.active = false;
|
|
405
485
|
this.warmed = false;
|
|
406
486
|
if (this.cam) this.cam.enabled = false;
|
|
@@ -68,7 +68,8 @@ import {
|
|
|
68
68
|
} from './inline3d-splat-effects.js';
|
|
69
69
|
import { LiveOutgoing, resolveOutgoingOption, defaultOutgoing, yieldIdle } from './inline3d-splat-live.js';
|
|
70
70
|
import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNIFORM, makeSbsMaterial } from './inline3d-splat-video.js';
|
|
71
|
-
import { LayerRigCameras, validateLayerRig } from './inline3d-splat-layer-rig.js';
|
|
71
|
+
import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
|
|
72
|
+
import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
|
|
72
73
|
import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, DIAG_SWITCHES } from './inline3d-splat-diag.js';
|
|
73
74
|
import {
|
|
74
75
|
clamp,
|
|
@@ -823,6 +824,16 @@ export class PlayCanvasSplatViewer {
|
|
|
823
824
|
this.layerRigSource = null;
|
|
824
825
|
/** That descriptor as it stood when THIS frame's views were pulled (snapshotted in onFrame). */
|
|
825
826
|
this._rigSnap = null;
|
|
827
|
+
/**
|
|
828
|
+
* The rigs this tile declared and which one each frame's views were located for
|
|
829
|
+
* (./inline3d-splat-rig-map.js). Null = the kill switch (diag 'oldrig'): views drawn as
|
|
830
|
+
* located, the live outgoing on its pre-1.24 node chain.
|
|
831
|
+
*/
|
|
832
|
+
this.rigTrack = null;
|
|
833
|
+
/** This frame's rig mapping: { located, eye, remapped, entries } (see _mapViews). */
|
|
834
|
+
this.rigFrame = null;
|
|
835
|
+
this._eyeMap = [];
|
|
836
|
+
this._rigAtPull = null;
|
|
826
837
|
this._eyeSplit = NaN;
|
|
827
838
|
this.boxAspect = 1;
|
|
828
839
|
this.featherPx = feather > 0 ? feather : 0;
|
|
@@ -1136,6 +1147,8 @@ export class PlayCanvasSplatViewer {
|
|
|
1136
1147
|
// The rig these views were located with: Blink chained the rig declared BEFORE this callback,
|
|
1137
1148
|
// and the tick below may declare a new one (a focus ease) for the NEXT locate.
|
|
1138
1149
|
if (this.layerRigs?.active) this._rigSnap = snapshotRig(this.layerRigSource?.(), this._rigSnap);
|
|
1150
|
+
// Same moment, for the rig map: the rig these views were most likely located for (a tie-break).
|
|
1151
|
+
this._rigAtPull = this.rigTrack ? this.rigTrack.latest : null;
|
|
1139
1152
|
// BEFORE the tick: a pose the page sets in here is the one this very frame renders.
|
|
1140
1153
|
this._beforeFrame?.(views || null);
|
|
1141
1154
|
this._tick();
|
|
@@ -1588,6 +1601,44 @@ export class PlayCanvasSplatViewer {
|
|
|
1588
1601
|
}
|
|
1589
1602
|
}
|
|
1590
1603
|
|
|
1604
|
+
/**
|
|
1605
|
+
* This frame's rig mapping (./inline3d-splat-rig-map.js): which declared rig the views were
|
|
1606
|
+
* located for (`located`), which the current photo wants (`eye`, the last declared), and — only
|
|
1607
|
+
* when those differ and both are camera rigs — the views remapped to `eye` (`entries`: proj,
|
|
1608
|
+
* viewInv, view per view; `cull`: the same as { proj, pose } entries; `node`: the first eye's
|
|
1609
|
+
* rigid pose). Otherwise `remapped` is false and the views are drawn exactly as located.
|
|
1610
|
+
*/
|
|
1611
|
+
_mapViews(entries, prefer = null) {
|
|
1612
|
+
const rt = this.rigTrack;
|
|
1613
|
+
const rf = (this.rigFrame ||= { located: null, eye: null, remapped: false, entries: null, cull: [], node: new Float64Array(16) });
|
|
1614
|
+
rf.remapped = false;
|
|
1615
|
+
rf.located = rf.eye = null;
|
|
1616
|
+
if (!rt || this._mode !== '3d' || entries.length < 2) return rf;
|
|
1617
|
+
rf.located = rt.locate(entries, prefer);
|
|
1618
|
+
rf.eye = rt.latest;
|
|
1619
|
+
const L = rf.located;
|
|
1620
|
+
const T = rf.eye;
|
|
1621
|
+
if (!L || !T?.portal || L === T || sameRig(L.rig, T.rig)) return rf;
|
|
1622
|
+
const r = remapViews(entries, L.portal, T.portal, this._eyeMap);
|
|
1623
|
+
if (!r) return rf;
|
|
1624
|
+
rf.remapped = true;
|
|
1625
|
+
rf.entries = r;
|
|
1626
|
+
rf.cull.length = r.length;
|
|
1627
|
+
for (let i = 0; i < r.length; i++) {
|
|
1628
|
+
const c = (rf.cull[i] ||= {});
|
|
1629
|
+
c.proj = r[i].proj;
|
|
1630
|
+
c.pose = r[i].viewInv;
|
|
1631
|
+
c.x = r[i].x;
|
|
1632
|
+
c.y = r[i].y;
|
|
1633
|
+
c.width = r[i].width;
|
|
1634
|
+
c.height = r[i].height;
|
|
1635
|
+
c.node = null;
|
|
1636
|
+
}
|
|
1637
|
+
nodePose(T.portal, r[0].eye, rf.node);
|
|
1638
|
+
rf.cull[0].node = rf.node; // a rigid pose for a camera NODE (the views are affine)
|
|
1639
|
+
return rf;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1591
1642
|
/**
|
|
1592
1643
|
* The mono/capture camera as a lens frame, for the live outgoing's rig chain
|
|
1593
1644
|
* (./inline3d-splat-live.js): the rig node's matrix, the camera pose in rig space, the
|
|
@@ -1612,6 +1663,8 @@ export class PlayCanvasSplatViewer {
|
|
|
1612
1663
|
*/
|
|
1613
1664
|
cullViews(cam, entries, rect, into = []) {
|
|
1614
1665
|
const parent = cam?.parent?.getWorldTransform?.()?.data ?? null;
|
|
1666
|
+
// A camera drawing remapped views (./inline3d-splat-rig-map.js) publishes them as _dxrViews.
|
|
1667
|
+
if (cam?._dxrViews?.length === entries.length) entries = cam._dxrViews;
|
|
1615
1668
|
into.length = entries.length;
|
|
1616
1669
|
for (let i = 0; i < entries.length; i++) {
|
|
1617
1670
|
const e = entries[i];
|
|
@@ -1625,7 +1678,8 @@ export class PlayCanvasSplatViewer {
|
|
|
1625
1678
|
o.W[0] = C[3]; o.W[1] = C[7]; o.W[2] = C[11]; o.W[3] = C[15];
|
|
1626
1679
|
o.K[0] = w * e.proj[0];
|
|
1627
1680
|
o.K[1] = 1 / w;
|
|
1628
|
-
|
|
1681
|
+
// The view's largest column scale² (1 for a rigid view; a remapped view is affine).
|
|
1682
|
+
o.K[2] = Math.max(V[0] * V[0] + V[1] * V[1] + V[2] * V[2], V[4] * V[4] + V[5] * V[5] + V[6] * V[6], V[8] * V[8] + V[9] * V[9] + V[10] * V[10]);
|
|
1629
1683
|
o.K[3] = 0;
|
|
1630
1684
|
}
|
|
1631
1685
|
return into;
|
|
@@ -1857,12 +1911,18 @@ export class PlayCanvasSplatViewer {
|
|
|
1857
1911
|
for (let i = 0; i < entries.length; i++) rvs.push(new pc.RenderView());
|
|
1858
1912
|
this.eye.camera.camera.xrViews = rvs.slice();
|
|
1859
1913
|
}
|
|
1914
|
+
// The current photo through ITS rig (the last declared): the views are remapped only while
|
|
1915
|
+
// they were located for another one (a declaration still in flight, ./inline3d-splat-rig-map.js).
|
|
1916
|
+
const rf = this._mapViews(entries, cache ? cache.rigAt : null);
|
|
1917
|
+
const eyeViews = rf.remapped ? rf.entries : null;
|
|
1860
1918
|
for (let i = 0; i < entries.length; i++) {
|
|
1861
1919
|
const e = entries[i];
|
|
1862
|
-
rvs[i].setView(
|
|
1920
|
+
if (eyeViews) rvs[i].setView(eyeViews[i].proj, eyeViews[i].viewInv, eyeViews[i].view);
|
|
1921
|
+
else rvs[i].setView(e.proj, e.pose);
|
|
1863
1922
|
const [x, y, w, h] = rect(e);
|
|
1864
1923
|
rvs[i].setViewport(x, y, w, h);
|
|
1865
1924
|
}
|
|
1925
|
+
this.eye._dxrViews = eyeViews ? rf.cull : null;
|
|
1866
1926
|
// LOD and FOV-compensation read camera.fov/near/far, which under xrViews come from the
|
|
1867
1927
|
// XR properties — the frustum the views actually have, as XrManager does it.
|
|
1868
1928
|
const f = frustumFromProjection(entries[0].proj);
|
|
@@ -1873,13 +1933,19 @@ export class PlayCanvasSplatViewer {
|
|
|
1873
1933
|
}
|
|
1874
1934
|
// The camera NODE drives the sort direction and LOD distance; the views ignore it (they
|
|
1875
1935
|
// compose the node's PARENT with their own pose). Park it on the first eye.
|
|
1876
|
-
placeNode(this.eye, entries[0].pose);
|
|
1877
|
-
// setSource's live outgoing: the same views on its own camera
|
|
1878
|
-
|
|
1936
|
+
placeNode(this.eye, eyeViews ? rf.node : entries[0].pose);
|
|
1937
|
+
// setSource's live outgoing: the same views on its own camera (through ITS photo's rig),
|
|
1938
|
+
// into its own target.
|
|
1939
|
+
if (this._live?.active || this._live?.warming) this._live.sync(entries, rect, f, rf);
|
|
1879
1940
|
// setSource's wavefront: this frame's eye views, as the engine is about to compose them.
|
|
1880
1941
|
this.onBeforeRender?.(entries, rect);
|
|
1881
1942
|
// handle.setLayerRig: the display / post run cameras, on the same views (display: rounded).
|
|
1882
|
-
if (this.layerRigs
|
|
1943
|
+
if (this.layerRigs) {
|
|
1944
|
+
this.layerRigs.frame(eyeViews ? rf.cull : entries, rect, f, eyeViews ? rf.eye.rig : rf.located ? rf.located.rig : cache ? cache.rig : null, {
|
|
1945
|
+
located: this.rigTrack && entries.length > 1 ? !!rf.located : null,
|
|
1946
|
+
residual: this.rigTrack?.lastResidual ?? null,
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1883
1949
|
} else {
|
|
1884
1950
|
// Fallback: one camera per view, `rect` + `calculateProjection`.
|
|
1885
1951
|
const cams = this._views;
|
|
@@ -1911,6 +1977,15 @@ export class PlayCanvasSplatViewer {
|
|
|
1911
1977
|
cam.camera.rect = new pc.Vec4(x / W, y / H, w / W, h / H);
|
|
1912
1978
|
placeNode(cam, e.pose);
|
|
1913
1979
|
}
|
|
1980
|
+
// handle.setLayerRig on the N-camera path: N display / post cameras mirroring these, the
|
|
1981
|
+
// views as located (this path draws the photo through them too — no remap here).
|
|
1982
|
+
if (this.layerRigs) {
|
|
1983
|
+
const rf = this._mapViews(entries, cache ? cache.rigAt : null);
|
|
1984
|
+
this.layerRigs.frame(entries, rect, null, rf.located ? rf.located.rig : cache ? cache.rig : null, {
|
|
1985
|
+
located: this.rigTrack && entries.length > 1 ? !!rf.located : null,
|
|
1986
|
+
residual: this.rigTrack?.lastResidual ?? null,
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1914
1989
|
}
|
|
1915
1990
|
this._updateFeather(entries[0].width * sx, entries[0].height * sy);
|
|
1916
1991
|
// handle.makeSbsMaterial: the eye split, scene-wide (a material only declares the uniform).
|
|
@@ -1938,6 +2013,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1938
2013
|
g.bufW = el.width || 0;
|
|
1939
2014
|
g.bufH = el.height || 0;
|
|
1940
2015
|
g.rig = this._rigSnap ? { ...this._rigSnap } : null; // a replay re-uses the rig of its views
|
|
2016
|
+
g.rigAt = this._rigAtPull ?? null;
|
|
1941
2017
|
for (let i = 0; i < views.length; i++) {
|
|
1942
2018
|
const e = g.entries[i];
|
|
1943
2019
|
const vp = vps[i];
|
|
@@ -2869,6 +2945,15 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2869
2945
|
let rigLockArmed = false;
|
|
2870
2946
|
if (diag) {
|
|
2871
2947
|
diag.imageState = () => viewer.diagImageState();
|
|
2948
|
+
diag.rigState = () => {
|
|
2949
|
+
const rf = viewer.rigFrame;
|
|
2950
|
+
if (!viewer.rigTrack || !rf) return null;
|
|
2951
|
+
return {
|
|
2952
|
+
at: rf.located ? rf.located.id : null,
|
|
2953
|
+
in: rf.eye ? `${rf.eye.id}${rf.remapped ? '+' : ''}` : null,
|
|
2954
|
+
out: viewer._live?.active ? viewer._live.path : null,
|
|
2955
|
+
};
|
|
2956
|
+
};
|
|
2872
2957
|
diag.observeLongTasks();
|
|
2873
2958
|
diag.observeLongFrames();
|
|
2874
2959
|
diag.log(`on — switches [${[...diag.switches].join(', ') || 'none'}]; dump: copy(__dxrDiag.dump())`);
|
|
@@ -2876,7 +2961,6 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2876
2961
|
let removed = false;
|
|
2877
2962
|
// handle.setLayerRig: the per-rig cameras and where they read the declared rig from.
|
|
2878
2963
|
// Kill switch `nolayerrig`: requests are recorded but every layer stays on the eye camera.
|
|
2879
|
-
let warnedLayerRigPath = false;
|
|
2880
2964
|
function layerRigs() {
|
|
2881
2965
|
if (!viewer.layerRigs) {
|
|
2882
2966
|
viewer.layerRigs = new LayerRigCameras(viewer);
|
|
@@ -2971,26 +3055,31 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2971
3055
|
* on the camera rig ('camera'). No second full pass: one extra camera over the named layers,
|
|
2972
3056
|
* into the same target, in composition order. docs/playcanvas-adapter.md §setLayerRig and
|
|
2973
3057
|
* docs/proposals/layer-display-rig.md (the exact mapping, and why it is not Kooima).
|
|
2974
|
-
* `viewerDistance`
|
|
3058
|
+
* Options (`viewerDistance`, `gain`, `planeOffset`, `planeDistance`) are tile-wide and MERGE
|
|
3059
|
+
* (a key given replaces; `null` clears) — see setLayerRigOptions.
|
|
3060
|
+
* Works on both view paths (RenderViews and the N-camera fallback); layerRigState() says
|
|
3061
|
+
* whether it engaged and, if not, why.
|
|
2975
3062
|
*/
|
|
2976
3063
|
setLayerRig(layer, rig, o = {}) {
|
|
2977
3064
|
const r = validateLayerRig(layer, rig, o);
|
|
2978
|
-
|
|
2979
|
-
warnedLayerRigPath = true;
|
|
2980
|
-
console.warn('[inline3d/splat] setLayerRig needs the engine RenderView path; on the N-camera fallback the layer stays on the photo rig.');
|
|
2981
|
-
}
|
|
2982
|
-
layerRigs().set(layer, r.rig, { viewerDistance: r.viewerDistance, gain: r.gain });
|
|
3065
|
+
layerRigs().set(layer, r.rig, r.opts);
|
|
2983
3066
|
return out;
|
|
2984
3067
|
},
|
|
2985
|
-
/**
|
|
3068
|
+
/** Change the layer rig's tile-wide options live (no layer change): merge, `null` clears. */
|
|
3069
|
+
setLayerRigOptions(o = {}) {
|
|
3070
|
+
layerRigs().setOptions(validateLayerRigOptions(o, 'setLayerRigOptions'));
|
|
3071
|
+
return out;
|
|
3072
|
+
},
|
|
3073
|
+
/**
|
|
3074
|
+
* What setLayerRig is doing: `path` ('renderviews' | 'ncamera' | 'mono') and `engaged` (the
|
|
3075
|
+
* display-rig views were applied on the last drawn frame), `reason` when not, and the numbers
|
|
3076
|
+
* (`viewerDistance`, `gain`, `planeM` — the plane that lands on the glass, as a distance from
|
|
3077
|
+
* the photo's camera — `photoConvergenceM`, `planeOffset`, `located`).
|
|
3078
|
+
*/
|
|
2986
3079
|
layerRigState() {
|
|
2987
3080
|
const lr = viewer.layerRigs;
|
|
2988
|
-
return
|
|
2989
|
-
|
|
2990
|
-
disabled: !!lr?.disabled,
|
|
2991
|
-
rounded: !!lr?.last.rounded,
|
|
2992
|
-
gain: lr?.last.gain ?? null,
|
|
2993
|
-
};
|
|
3081
|
+
if (lr) return lr.state();
|
|
3082
|
+
return { display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'no layer on the display rig', viewerDistance: DEFAULT_VIEWER_DISTANCE_M, gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null };
|
|
2994
3083
|
},
|
|
2995
3084
|
/**
|
|
2996
3085
|
* An unlit material showing the left half of `texture` to left-eye views and the right half to
|
|
@@ -3109,6 +3198,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3109
3198
|
// toward `target` at the viewer's FOCUS_EASE unless snapped.
|
|
3110
3199
|
const pageFocus = { d: Number.NaN, target: Number.NaN, fired: Number.NaN, source: null };
|
|
3111
3200
|
|
|
3201
|
+
let initialViewRig = null; // the rig addScene is created with (controls:'page'), else the shorthand
|
|
3112
3202
|
if (wall && wall.supported) {
|
|
3113
3203
|
const onFrame = diag
|
|
3114
3204
|
? (views, layer) => {
|
|
@@ -3121,7 +3211,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3121
3211
|
// controls:'page' starts on a camera rig (attach, provisional FOV/convergence until the page
|
|
3122
3212
|
// and the waterfall say otherwise); the display rig's height means nothing there.
|
|
3123
3213
|
...(pageMode
|
|
3124
|
-
? { viewRig: pageViewRig({ verticalFovDeg: viewer.page.fov, convergence: 2, comfortDepth: ctl.comfortDepth }) }
|
|
3214
|
+
? { viewRig: (initialViewRig = pageViewRig({ verticalFovDeg: viewer.page.fov, convergence: 2, comfortDepth: ctl.comfortDepth })) }
|
|
3125
3215
|
: { virtualDisplayHeight }),
|
|
3126
3216
|
onLayerLost: viewer.onLayerLost,
|
|
3127
3217
|
...(observe ? { observe } : {}),
|
|
@@ -3130,6 +3220,20 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3130
3220
|
} else {
|
|
3131
3221
|
viewer.startMono();
|
|
3132
3222
|
}
|
|
3223
|
+
// Every rig this tile declares, by VALUE (./inline3d-splat-rig-map.js): which one a frame's
|
|
3224
|
+
// views were located for is read off the views, so each photo can be drawn through its own rig
|
|
3225
|
+
// while a declaration is in flight or while a live outgoing photo is still on screen.
|
|
3226
|
+
// `?dxrdiag=oldrig` turns it off (views drawn as located; the live outgoing's node chain).
|
|
3227
|
+
if (handle && typeof handle.setViewRig === 'function' && !diag?.has('oldrig')) {
|
|
3228
|
+
const rt = (viewer.rigTrack = new RigTracker());
|
|
3229
|
+
rt.note(initialViewRig || { type: 'display', virtualDisplayHeight });
|
|
3230
|
+
const push = handle.setViewRig;
|
|
3231
|
+
handle.setViewRig = (rig) => {
|
|
3232
|
+
const r = push.call(handle, rig);
|
|
3233
|
+
rt.note(rig);
|
|
3234
|
+
return r;
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3133
3237
|
let diagLoop = null;
|
|
3134
3238
|
if (diag) {
|
|
3135
3239
|
// Every rig push, logged with its values; under `norig`, every push after the first
|
|
@@ -3160,8 +3264,8 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3160
3264
|
if (opts.displayRigLayers !== undefined) {
|
|
3161
3265
|
const d = opts.displayRigLayers;
|
|
3162
3266
|
const list = Array.isArray(d) ? d : d && Array.isArray(d.layers) ? d.layers : null;
|
|
3163
|
-
if (!list) throw new TypeError('@displayxr/inline3d/splat: displayRigLayers — expected an array of layers, or { layers, viewerDistance?, gain? }.');
|
|
3164
|
-
const o = Array.isArray(d) ? {} :
|
|
3267
|
+
if (!list) throw new TypeError('@displayxr/inline3d/splat: displayRigLayers — expected an array of layers, or { layers, viewerDistance?, gain?, planeOffset?, planeDistance? }.');
|
|
3268
|
+
const o = Array.isArray(d) ? {} : Object.fromEntries(Object.entries(d).filter(([k]) => k !== 'layers'));
|
|
3165
3269
|
for (const l of list) out.setLayerRig(l, 'display', o);
|
|
3166
3270
|
}
|
|
3167
3271
|
// Replay what the page did before this module arrived — exclude() above all, which a product
|