@displayxr/inline3d 1.19.1 → 1.19.3
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 +51 -0
- package/README.md +3 -1
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/js/inline3d-model-playcanvas.js +2 -2
- package/js/inline3d-splat-effects.js +88 -5
- package/js/inline3d-splat-playcanvas.js +193 -51
- package/js/inline3d-splat-shared.js +32 -0
- package/js/inline3d-splat.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
|
|
|
5
5
|
`./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
|
|
6
6
|
which tier they touch, because that is what tells you whether an upgrade can move your pixels.
|
|
7
7
|
|
|
8
|
+
## 1.19.3 — 2026-09-24
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, `./model`). No API change and no pixel change on a core that
|
|
11
|
+
has view rigs.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A view rig the wall cannot take is no longer dropped silently** (#36).
|
|
16
|
+
- Every scene backend (`./splat` Spark and PlayCanvas, `./model` PlayCanvas) declared its rig
|
|
17
|
+
with `handle?.setViewRig?.(rig)`. On a handle from a core that predates view rigs, that call
|
|
18
|
+
does nothing.
|
|
19
|
+
- A page that vendors an older `inline3d.js` and hands that wall to `./splat` therefore had its
|
|
20
|
+
CAMERA rig thrown away. The wall wove the metric photo scene on the `addScene` display-rig
|
|
21
|
+
shorthand (`virtualDisplayHeight` 0.24): way off in convergence and scale in 3D, correct in 2D.
|
|
22
|
+
That is how the gallery's Spatial View wall regressed; the rig this SDK declared was the same
|
|
23
|
+
from 1.8.0 to 1.19.2.
|
|
24
|
+
- Now it logs one `console.warn` per page ("the view rig was dropped: your inline3d core
|
|
25
|
+
predates view rigs …") carrying the dropped rig.
|
|
26
|
+
- The Spark path used `handle?.setViewRig(…)` without the second `?.`, so it would have THROWN
|
|
27
|
+
there. It now takes the same warning path.
|
|
28
|
+
- Regression tests pin the declared rig for a camera-rig asset with `zoom {1, 2, relax}` and the
|
|
29
|
+
`assemble` reveal, at rest and after the reveal, on the fake engine.
|
|
30
|
+
|
|
31
|
+
## 1.19.2 — 2026-09-24
|
|
32
|
+
|
|
33
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). No API change and no pixel
|
|
34
|
+
change: every frame of a `crossfade` or `wavefront` is the one 1.19.1 draws, and the end is still a
|
|
35
|
+
plain `cut` (MAE 0.000).
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **The live `wavefront` costs about one photo's draw, not two** (#36).
|
|
40
|
+
- Its depth ridge was an entity-scope work-buffer modifier: a full rewrite of the new photo's
|
|
41
|
+
1.18M-gaussian work buffer and a CPU re-sort, every frame (0.9–1.0 of each per frame, measured).
|
|
42
|
+
It now runs at render time on the eye camera's gsplat manager, as the particle transitions do:
|
|
43
|
+
0 rewrites and 0 sorts in the window.
|
|
44
|
+
- Each photo is drawn only on its side of the front: the new one left of where its commit
|
|
45
|
+
starts, the old one right of where it ends, so the two overlap only in the band. A gaussian
|
|
46
|
+
whose on-screen footprint (bounded the way the engine sizes its quad) lies wholly on the far
|
|
47
|
+
side in every eye gets alpha 0, which the engine culls in the vertex stage. Frames with the
|
|
48
|
+
cull on and off are bit-identical (both eyes, colour and alpha, 5 points of the window).
|
|
49
|
+
- An engine whose managers are not reachable keeps the entity-scope ridge (`_transitionPath`
|
|
50
|
+
says which ran).
|
|
51
|
+
- **No shader links inside a `crossfade` or `wavefront` window.** The overlay's two quads compiled
|
|
52
|
+
on the window's first frames (30–180 ms on the M1). `prepareSource()` now warms them whatever
|
|
53
|
+
transition follows, `prepareSource(src, { transition: 'wavefront' })` also builds the render-time
|
|
54
|
+
ridge and cull, and an unprepared `setSource` warms them while its asset loads.
|
|
55
|
+
|
|
56
|
+
Measured pacing and GPU cost: [`docs/splat-effects.md` § Wavefront: one draw's worth](docs/splat-effects.md#wavefront-one-draws-worth).
|
|
57
|
+
The live `crossfade` still draws both photos in full: at mid-fade every pixel needs both images.
|
|
58
|
+
|
|
8
59
|
## 1.19.1 — 2026-09-24
|
|
9
60
|
|
|
10
61
|
Touches packaging only (`./splat` and `./model`, `engine: 'playcanvas'`). No pixels move: the
|
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ its own half, with no second canvas ([§setVideo](docs/playcanvas-adapter.md#set
|
|
|
55
55
|
optional peer `playcanvas` (`>=2.22.3 <3`) and reads `.sog`, `.ply` and a Streamed-SOG
|
|
56
56
|
`lod-meta.json`. **Spark stays the default**; a page that never passes `engine` never loads
|
|
57
57
|
`playcanvas`. What differs, and why: [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md).
|
|
58
|
+
The repo's splat sample (`samples/splat/`) renders with PlayCanvas and keeps `?engine=spark` as
|
|
59
|
+
its A/B switch.
|
|
58
60
|
Bundlers: the engine is reached through a named-import module (`js/inline3d-playcanvas-engine.js`,
|
|
59
61
|
so the unused ~40% of the engine tree-shakes away); its sort workers are Blob URLs (CSP
|
|
60
62
|
`worker-src blob:`), and esbuild needs `node:worker_threads` marked external.
|
|
@@ -160,7 +162,7 @@ samples/
|
|
|
160
162
|
convergence + comfort, the attach pattern, C to A/B a display rig
|
|
161
163
|
windows/ mixed 3D windows — still photos + a live video + a real-time three.js scene,
|
|
162
164
|
each woven with one SDK call, all on one session
|
|
163
|
-
splat/ a 3D Gaussian splat in a tile
|
|
165
|
+
splat/ a 3D Gaussian splat in a tile (PlayCanvas; ?engine=spark), with a 2D plate over it
|
|
164
166
|
model/ a glTF mesh, a mesh+splat scene, and a Draco-COMPRESSED glTF (PlayCanvas; ?engine=three)
|
|
165
167
|
composition/ the 14-case 2D/3D overlap matrix — demo AND standing hardware regression
|
|
166
168
|
surface; red cases ship red (see samples/README.md)
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -74,3 +74,15 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
74
74
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
75
75
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
76
76
|
THE SOFTWARE.
|
|
77
|
+
|
|
78
|
+
## Sample assets (repo samples only, not in the npm package)
|
|
79
|
+
|
|
80
|
+
The pages under `samples/` show these assets. None is in the published `@displayxr/inline3d`
|
|
81
|
+
package.
|
|
82
|
+
|
|
83
|
+
| Asset | Used by | Source | Credit and licence |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| Damaged Helmet (`DamagedHelmet.glb`) | `samples/model/` (tiles A and B), loaded at run time, not vendored | [KhronosGroup/glTF-Sample-Assets](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/c6a6bd13ab2b3c685c7903d03561b8a9392f38b8/Models/DamagedHelmet) at commit `c6a6bd13ab2b3c685c7903d03561b8a9392f38b8`, via jsDelivr, unmodified | © 2018 ctxwing (rebuild and conversion to glTF 2.0), [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/legalcode). Earlier version of the model ("Battle Damaged Sci-fi Helmet") © 2016 theblueturtle_, [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/legalcode). As stated in the asset's `README.md` and `LICENSE.md`. |
|
|
86
|
+
| Duck, Draco variant (`samples/model/assets/glTF-Draco/`) | `samples/model/` (tile C) | Khronos glTF-Sample-Assets | © Sony, [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). |
|
|
87
|
+
| Butterfly splat (`samples/splat/assets/butterfly.sog`) | `samples/splat/`, `samples/model/` (tile B) | [Spark](https://sparkjs.dev/) `assets/splats/butterfly.spz`, converted to SOG with PlayCanvas splat-transform (177,132 gaussians, 3 SH bands kept) | Butterfly splat © World Labs (from Spark, sparkjs.dev), used with permission; converted to SOG. |
|
|
88
|
+
| Draco decoder (`vendor/draco/`) | `samples/model/` | three.js `examples/jsm/libs/draco/` | © The Draco Authors, Apache-2.0. |
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// └── eye camera, N RenderViews, TONEMAP_NEUTRAL, no sky layer
|
|
39
39
|
|
|
40
40
|
import { PlayCanvasSplatViewer, pageRigTRS, pageViewAxis, pageViewRig } from './inline3d-splat-playcanvas.js';
|
|
41
|
-
import { resolveControls, normalizeCameraPose, now, MAX_DT_S } from './inline3d-splat-shared.js';
|
|
41
|
+
import { resolveControls, normalizeCameraPose, now, MAX_DT_S, declareViewRig } from './inline3d-splat-shared.js';
|
|
42
42
|
import {
|
|
43
43
|
NEUTRAL_STUDIO,
|
|
44
44
|
neutralStudioRadiance,
|
|
@@ -551,7 +551,7 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
551
551
|
if (dm > 0) d = dm / (ax.scale || 1);
|
|
552
552
|
}
|
|
553
553
|
out.viewRig = pageViewRig({ verticalFovDeg: viewer.page.fov, convergence: d ?? 2, comfortDepth: ctl.comfortDepth }, out.viewRig || {});
|
|
554
|
-
handle
|
|
554
|
+
declareViewRig(handle, out.viewRig);
|
|
555
555
|
};
|
|
556
556
|
if (typeof opts.onBeforeFrame === 'function') {
|
|
557
557
|
const cb = opts.onBeforeFrame;
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
// same splats; a decision keyed on a screen position would differ between the eyes and read as
|
|
36
36
|
// rivalry. So no effect here reads gl_FragCoord or a view matrix, and the custom hook exposes no
|
|
37
37
|
// screen-space input. (The one image-space piece, setSource's frame snapshot, lives in the
|
|
38
|
-
// adapter and samples each eye's own half at the zero-disparity plane.)
|
|
38
|
+
// adapter and samples each eye's own half at the zero-disparity plane.) The one exception,
|
|
39
|
+
// `wipecull`, is not an effect: it only skips gaussians no shown pixel can receive.
|
|
39
40
|
//
|
|
40
41
|
// ── Sort caveat ───────────────────────────────────────────────────────────────────────────────
|
|
41
42
|
//
|
|
@@ -46,7 +47,12 @@
|
|
|
46
47
|
import { coverageExponent, FADE_TRANSMITTANCE_FLOOR } from './inline3d-splat-shared.js';
|
|
47
48
|
|
|
48
49
|
/** The fixed composition order of a generated chunk. */
|
|
49
|
-
export const STAGE_ORDER = Object.freeze(['grade', 'clip', 'reveal', 'pulse', 'custom']);
|
|
50
|
+
export const STAGE_ORDER = Object.freeze(['grade', 'clip', 'reveal', 'pulse', 'custom', 'cull']);
|
|
51
|
+
|
|
52
|
+
/** The most eye views setSource's wavefront culls for (more: every gaussian is drawn). */
|
|
53
|
+
export const WIPE_CULL_MAX_VIEWS = 4;
|
|
54
|
+
const IDENTITY16 = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
55
|
+
const ZERO4 = new Float32Array(4);
|
|
50
56
|
|
|
51
57
|
/** Named easings (a function `(x) => y` on [0, 1] is accepted too). */
|
|
52
58
|
export const EASINGS = Object.freeze({
|
|
@@ -529,6 +535,74 @@ void ${P}color(vec3 c, inout vec4 col) {}
|
|
|
529
535
|
}),
|
|
530
536
|
},
|
|
531
537
|
|
|
538
|
+
// Internal: setSource's wavefront, both photos — draw each photo only on ITS side of the image
|
|
539
|
+
// front. NOT an effect: nothing it removes could have reached a pixel the overlay shows (the
|
|
540
|
+
// frame is bit-exact with it on or off), it only saves the fragments of the half nobody sees.
|
|
541
|
+
// Each gaussian whose on-screen footprint lies wholly on the far side of the front in EVERY eye
|
|
542
|
+
// gets alpha 0, which the engine's own alpha clip culls in the vertex stage. The footprint is
|
|
543
|
+
// bounded the way the engine sizes its quad (gsplatCorner): λ₁ ≤ ‖J‖²·s²·σ²max + 0.3 with
|
|
544
|
+
// ‖J‖² = (f/z)²·(1 + (x² + y²)/z²), l₁ = 2·√(2λ₁), and a corner offset ≤ l₁ + l₂ ≤ 2·l₁, plus
|
|
545
|
+
// 4 px. The front is the overlay's own (setSnapshotAlpha's wipe), in each eye's viewport-
|
|
546
|
+
// relative NDC. The eye views are the camera's (its parent's world transform · each view's
|
|
547
|
+
// pose), set by the adapter just before the engine renders — the live outgoing camera has its
|
|
548
|
+
// own. It is the LAST stage, so it sees the final centre and scale of every stage before it.
|
|
549
|
+
// Per mesh instance: `side` +1 keeps the right of `edge` (the outgoing photo), −1 the left
|
|
550
|
+
// (the incoming one); the material default is off.
|
|
551
|
+
wipecull: {
|
|
552
|
+
stage: 'cull',
|
|
553
|
+
kind: 'transition',
|
|
554
|
+
internal: true,
|
|
555
|
+
defaults: { durationMs: 0, easing: 'linear', holdMs: 0, direction: 'in' },
|
|
556
|
+
glsl: (P) => {
|
|
557
|
+
const views = [0, 1, 2, 3];
|
|
558
|
+
return `
|
|
559
|
+
uniform float ${P}on;
|
|
560
|
+
uniform float ${P}side;
|
|
561
|
+
uniform float ${P}edge;
|
|
562
|
+
uniform float ${P}n;
|
|
563
|
+
${views.map((i) => `uniform mat4 ${P}V${i};\nuniform vec4 ${P}X${i};\nuniform vec4 ${P}W${i};\nuniform vec4 ${P}K${i};`).join('\n')}
|
|
564
|
+
bool ${P}cut;
|
|
565
|
+
// true when some of the footprint may reach the kept side of the front in this view
|
|
566
|
+
bool ${P}reach(mat4 V, vec4 X, vec4 Wr, vec4 K, vec3 c, float s) {
|
|
567
|
+
vec4 p = vec4(c, 1.0);
|
|
568
|
+
vec4 v = V * p;
|
|
569
|
+
float z = -v.z;
|
|
570
|
+
float w = dot(Wr, p);
|
|
571
|
+
if (z <= 1e-6 || w <= 1e-6) return true; // at or behind the eye: the engine decides
|
|
572
|
+
float xn = dot(X, p) / w;
|
|
573
|
+
float jz = K.x / z;
|
|
574
|
+
float j2 = jz * jz * (1.0 + dot(v.xy, v.xy) / (z * z));
|
|
575
|
+
float l1 = 2.0 * sqrt(2.0 * (j2 * K.z * s * s + 0.3));
|
|
576
|
+
return ${P}side * (xn - ${P}edge) + (2.0 * l1 + 8.0) * K.y > 0.0;
|
|
577
|
+
}
|
|
578
|
+
void ${P}center(inout vec3 c) {}
|
|
579
|
+
void ${P}rs(vec3 oc, vec3 mc, inout vec4 r, inout vec3 sc) {
|
|
580
|
+
${P}cut = false;
|
|
581
|
+
if (${P}on < 0.5) return;
|
|
582
|
+
float s = max(sc.x, max(sc.y, sc.z));
|
|
583
|
+
${views.map((i) => ` if (${i > 0 ? `${P}n > ${i}.5 && ` : ''}${P}reach(${P}V${i}, ${P}X${i}, ${P}W${i}, ${P}K${i}, mc, s)) return;`).join('\n')}
|
|
584
|
+
${P}cut = true;
|
|
585
|
+
}
|
|
586
|
+
void ${P}color(vec3 c, inout vec4 col) { if (${P}cut) col.a = 0.0; }
|
|
587
|
+
`;
|
|
588
|
+
},
|
|
589
|
+
// opts.cull() → { side, edge, views: [{ V, X, W, K }] } (at most WIPE_CULL_MAX_VIEWS), or null
|
|
590
|
+
// (off: every gaussian drawn). No `cull` (the material default): off.
|
|
591
|
+
uniforms: (ctx, inst) => {
|
|
592
|
+
const c = typeof inst.opts.cull === 'function' ? inst.opts.cull() : null;
|
|
593
|
+
const on = !!(c && c.views.length && c.views.length <= WIPE_CULL_MAX_VIEWS);
|
|
594
|
+
const out = { on: on ? 1 : 0, side: on ? c.side : 1, edge: on ? c.edge : 0, n: on ? c.views.length : 0 };
|
|
595
|
+
for (let i = 0; i < WIPE_CULL_MAX_VIEWS; i++) {
|
|
596
|
+
const v = on ? c.views[i] : null;
|
|
597
|
+
out['V' + i] = v ? v.V : IDENTITY16;
|
|
598
|
+
out['X' + i] = v ? v.X : ZERO4;
|
|
599
|
+
out['W' + i] = v ? v.W : ZERO4;
|
|
600
|
+
out['K' + i] = v ? v.K : ZERO4;
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
|
|
532
606
|
// Internal: setSource's crossfade FALLBACK (no frame snapshot) — the coverage remap on one
|
|
533
607
|
// entity, driven by the adapter.
|
|
534
608
|
xfade: {
|
|
@@ -1528,9 +1602,18 @@ export class SplatEffects {
|
|
|
1528
1602
|
* setSource's shader pre-warm compiles ahead of the transition.
|
|
1529
1603
|
*/
|
|
1530
1604
|
sharedChunkCode(name, effect, opts) {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1605
|
+
return this.sharedChunkCodeFor([[name, effect, opts]]);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/** sharedChunkCode for several driveShared bodies at once: [[name, effect, opts], …]. */
|
|
1609
|
+
sharedChunkCodeFor(list) {
|
|
1610
|
+
const names = new Set(list.map((x) => x[0]));
|
|
1611
|
+
const insts = [...(this.scopes.get('tile')?.values() ?? [])].filter((i) => !names.has(i.name));
|
|
1612
|
+
for (const [name, effect, opts] of list) {
|
|
1613
|
+
const o = resolveEffectOptions(effect, { ...opts, scope: 'tile', direction: 'in', progress: 1 }, 'set', { internal: true });
|
|
1614
|
+
insts.push({ name, def: EFFECTS[effect], opts: o });
|
|
1615
|
+
}
|
|
1616
|
+
return composeModifier(insts).code;
|
|
1534
1617
|
}
|
|
1535
1618
|
|
|
1536
1619
|
_setupInstance(inst) {
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
resolveControls,
|
|
100
100
|
normalizeCameraPose,
|
|
101
101
|
coverageExponent,
|
|
102
|
+
declareViewRig,
|
|
102
103
|
} from './inline3d-splat-shared.js';
|
|
103
104
|
|
|
104
105
|
/**
|
|
@@ -693,6 +694,8 @@ export class PlayCanvasSplatViewer {
|
|
|
693
694
|
this._snap = null;
|
|
694
695
|
/** setSource's live outgoing asset (./inline3d-splat-live.js), made on first use. */
|
|
695
696
|
this._live = null;
|
|
697
|
+
/** (entries, rect) => void: once per drawn frame, after the views are set, before the engine renders. */
|
|
698
|
+
this.onBeforeRender = null;
|
|
696
699
|
this.vH = virtualDisplayHeight;
|
|
697
700
|
this.fit = fit;
|
|
698
701
|
this.margin = margin;
|
|
@@ -1447,6 +1450,34 @@ export class PlayCanvasSplatViewer {
|
|
|
1447
1450
|
return { rig: this.rigMatrix(), pose, c, t: Math.tan((this.mono.fov * DEG) / 2) };
|
|
1448
1451
|
}
|
|
1449
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* One camera's eye views as the engine composes them this frame (RenderView.updateTransforms:
|
|
1455
|
+
* the camera node's PARENT world transform · each view's pose), packed for the wavefront's cull
|
|
1456
|
+
* (EFFECTS.wipecull): V = the view matrix, X / W = rows 0 and 3 of proj · V, K = (the engine's
|
|
1457
|
+
* focal f = viewport width · proj[0], 1 / viewport width, the view's squared scale). `into` is
|
|
1458
|
+
* reused frame to frame. Null when a matrix is singular.
|
|
1459
|
+
*/
|
|
1460
|
+
cullViews(cam, entries, rect, into = []) {
|
|
1461
|
+
const parent = cam?.parent?.getWorldTransform?.()?.data ?? null;
|
|
1462
|
+
into.length = entries.length;
|
|
1463
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1464
|
+
const e = entries[i];
|
|
1465
|
+
const V = mat4Invert(parent ? mat4Mul(parent, e.pose) : e.pose);
|
|
1466
|
+
if (!V) return null;
|
|
1467
|
+
const C = mat4Mul(e.proj, V);
|
|
1468
|
+
const w = Math.max(1, rect(e)[2]);
|
|
1469
|
+
const o = (into[i] ||= { V: new Float32Array(16), X: new Float32Array(4), W: new Float32Array(4), K: new Float32Array(4) });
|
|
1470
|
+
o.V.set(V);
|
|
1471
|
+
o.X[0] = C[0]; o.X[1] = C[4]; o.X[2] = C[8]; o.X[3] = C[12];
|
|
1472
|
+
o.W[0] = C[3]; o.W[1] = C[7]; o.W[2] = C[11]; o.W[3] = C[15];
|
|
1473
|
+
o.K[0] = w * e.proj[0];
|
|
1474
|
+
o.K[1] = 1 / w;
|
|
1475
|
+
o.K[2] = V[0] * V[0] + V[1] * V[1] + V[2] * V[2];
|
|
1476
|
+
o.K[3] = 0;
|
|
1477
|
+
}
|
|
1478
|
+
return into;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1450
1481
|
/** Can this tile render a live outgoing asset (the single-camera RenderView path)? */
|
|
1451
1482
|
get canLiveOutgoing() {
|
|
1452
1483
|
return LiveOutgoing.supported(this);
|
|
@@ -1643,6 +1674,8 @@ export class PlayCanvasSplatViewer {
|
|
|
1643
1674
|
placeNode(this.eye, entries[0].pose);
|
|
1644
1675
|
// setSource's live outgoing: the same views on its own camera, into its own target.
|
|
1645
1676
|
if (this._live?.active) this._live.sync(entries, rect, f);
|
|
1677
|
+
// setSource's wavefront: this frame's eye views, as the engine is about to compose them.
|
|
1678
|
+
this.onBeforeRender?.(entries, rect);
|
|
1646
1679
|
} else {
|
|
1647
1680
|
// Fallback: one camera per view, `rect` + `calculateProjection`.
|
|
1648
1681
|
const cams = this._views;
|
|
@@ -2316,7 +2349,6 @@ const SET_RIG_ENV_TONE_MAPPING = Object.freeze({ neutral: 'neutral', room: 'none
|
|
|
2316
2349
|
export const SET_RIG_TYPES = Object.freeze(['display', 'camera', 'auto']);
|
|
2317
2350
|
const SET_RIG_FITS = ['contain', 'cover', 'height', 'none'];
|
|
2318
2351
|
let warnedSetRigKeys = false;
|
|
2319
|
-
|
|
2320
2352
|
const VIDEO_BUSY = (what) =>
|
|
2321
2353
|
`@displayxr/inline3d/splat: ${what}() while a video is on — call handle.setVideo(null) first ` +
|
|
2322
2354
|
'(the video holds the display rig and hides the splat until it exits).';
|
|
@@ -2800,7 +2832,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2800
2832
|
},
|
|
2801
2833
|
out.viewRig || {},
|
|
2802
2834
|
);
|
|
2803
|
-
handle
|
|
2835
|
+
declareViewRig(handle, out.viewRig);
|
|
2804
2836
|
if (!out.rig) return;
|
|
2805
2837
|
out.rig.convergence = d;
|
|
2806
2838
|
if (Math.abs(d - pageFocus.fired) > 1e-3 || source !== pageFocus.source) {
|
|
@@ -2873,7 +2905,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
2873
2905
|
out: camRigObj,
|
|
2874
2906
|
},
|
|
2875
2907
|
);
|
|
2876
|
-
handle
|
|
2908
|
+
declareViewRig(handle, out.viewRig);
|
|
2877
2909
|
}
|
|
2878
2910
|
viewer.onFocusChange = (f) => {
|
|
2879
2911
|
pushViewRig(false);
|
|
@@ -3163,7 +3195,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3163
3195
|
parallaxFactor: f.parallaxFactor ?? 1,
|
|
3164
3196
|
perspectiveFactor: f.perspectiveFactor ?? 1,
|
|
3165
3197
|
});
|
|
3166
|
-
handle
|
|
3198
|
+
declareViewRig(handle, out.viewRig);
|
|
3167
3199
|
}
|
|
3168
3200
|
|
|
3169
3201
|
/**
|
|
@@ -3569,7 +3601,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3569
3601
|
} else {
|
|
3570
3602
|
declaredDisplay = s.declaredDisplay;
|
|
3571
3603
|
out.viewRig = s.viewRig;
|
|
3572
|
-
handle
|
|
3604
|
+
declareViewRig(handle, out.viewRig);
|
|
3573
3605
|
}
|
|
3574
3606
|
} else out.viewRig = s.viewRig;
|
|
3575
3607
|
lastConvergence = s.lastConvergence;
|
|
@@ -3680,7 +3712,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3680
3712
|
const loaded = await loadOne(pcModule, app, src, { background: true });
|
|
3681
3713
|
perfSpan('prepareSource', t0);
|
|
3682
3714
|
// The transition the page declared: compile its shader now, in the dwell, not on its first frame.
|
|
3683
|
-
|
|
3715
|
+
await prewarmTransition(warm); // null (no transition declared): the overlay only
|
|
3684
3716
|
const entry = { loaded, state: 'ready', dispose: null };
|
|
3685
3717
|
const prepared = {
|
|
3686
3718
|
[PREPARED_TAG]: true,
|
|
@@ -3721,46 +3753,61 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3721
3753
|
* engine whose internals differ just compiles on the transition's first frame, as before.
|
|
3722
3754
|
*/
|
|
3723
3755
|
async function prewarmTransition(plan) {
|
|
3724
|
-
|
|
3725
|
-
|
|
3756
|
+
// The chunk variant the transition installs (particles, wavefront), and the overlay's two quads
|
|
3757
|
+
// (crossfade, wavefront and the particles all composite through them; created on the first
|
|
3758
|
+
// capture otherwise, and compiled on its draw: 30–180 ms on the first frames of the window).
|
|
3759
|
+
const chunkKey = plan?.particles
|
|
3760
|
+
? `${plan.particles.in.effect}|${plan.particles.in.opts.order ?? ''}`
|
|
3761
|
+
: plan?.transition === 'wavefront'
|
|
3762
|
+
? 'wavefront'
|
|
3763
|
+
: null;
|
|
3764
|
+
const wantChunk = !!(fx && chunkKey && !prewarmed.has(chunkKey));
|
|
3765
|
+
const wantOverlay = !prewarmed.has('overlay');
|
|
3766
|
+
if (!wantChunk && !wantOverlay) return;
|
|
3726
3767
|
await yieldIdle();
|
|
3727
|
-
if (removed
|
|
3768
|
+
if (removed) return;
|
|
3728
3769
|
try {
|
|
3729
3770
|
const pc = pcModule;
|
|
3730
|
-
const cams = [viewer.eye, viewer._live?.cam].filter(Boolean);
|
|
3731
|
-
const code = fx.sharedChunkCode('transition', plan.particles.in.effect, { order: plan.particles.in.opts.order });
|
|
3732
3771
|
const made = [];
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
const
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3772
|
+
if (wantChunk && fx) {
|
|
3773
|
+
const cams = [viewer.eye, viewer._live?.cam].filter(Boolean);
|
|
3774
|
+
const code = plan.particles
|
|
3775
|
+
? fx.sharedChunkCode('transition', plan.particles.in.effect, { order: plan.particles.in.opts.order })
|
|
3776
|
+
: fx.sharedChunkCodeFor([
|
|
3777
|
+
['transition', 'wavefront', {}],
|
|
3778
|
+
['transition-cull', 'wipecull', {}],
|
|
3779
|
+
]);
|
|
3780
|
+
let issued = 0;
|
|
3781
|
+
for (const cam of cams) {
|
|
3782
|
+
const mi = managerMi(cam, null);
|
|
3783
|
+
const src = mi?.material;
|
|
3784
|
+
const camera = cam.camera?.camera;
|
|
3785
|
+
if (!src?.shaderDesc || !camera?.shaderParams || typeof src.getShaderVariant !== 'function') continue;
|
|
3786
|
+
const m = new pc.ShaderMaterial(src.shaderDesc);
|
|
3787
|
+
src.defines.forEach((v, k) => m.setDefine(k, v));
|
|
3788
|
+
m.shaderChunks.copy(src.shaderChunks);
|
|
3789
|
+
m.getShaderChunks(pc.SHADERLANGUAGE_GLSL).set('gsplatModifyVS', code);
|
|
3790
|
+
m.blendState = src.blendState;
|
|
3791
|
+
made.push(m.getShaderVariant({
|
|
3792
|
+
device: viewer.app.graphicsDevice,
|
|
3793
|
+
scene: viewer.app.scene,
|
|
3794
|
+
objDefs: mi._shaderDefs,
|
|
3795
|
+
cameraShaderParams: camera.shaderParams,
|
|
3796
|
+
pass: 0, // SHADER_FORWARD
|
|
3797
|
+
sortedLights: [],
|
|
3798
|
+
viewUniformFormat: viewer.app.renderer?.viewUniformFormat ?? null,
|
|
3799
|
+
vertexFormat: mi.mesh?.vertexBuffer?.format,
|
|
3800
|
+
}));
|
|
3801
|
+
issued++;
|
|
3802
|
+
}
|
|
3803
|
+
if (issued) prewarmed.add(chunkKey);
|
|
3757
3804
|
}
|
|
3758
|
-
// the overlay's two quads (created on the first capture otherwise, and compiled on its draw)
|
|
3759
3805
|
const cam = viewer.eye?.camera?.camera;
|
|
3760
|
-
if (cam?.shaderParams) {
|
|
3806
|
+
if (wantOverlay && cam?.shaderParams) {
|
|
3761
3807
|
viewer._ensureSnapshotOverlay();
|
|
3808
|
+
let issued = 0;
|
|
3762
3809
|
for (const p of viewer._snap?.parts || []) {
|
|
3763
|
-
|
|
3810
|
+
const v = p.mat.getShaderVariant?.({
|
|
3764
3811
|
device: viewer.app.graphicsDevice,
|
|
3765
3812
|
scene: viewer.app.scene,
|
|
3766
3813
|
objDefs: p.mi._shaderDefs,
|
|
@@ -3769,16 +3816,18 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3769
3816
|
sortedLights: [],
|
|
3770
3817
|
viewUniformFormat: viewer.app.renderer?.viewUniformFormat ?? null,
|
|
3771
3818
|
vertexFormat: p.mi.mesh?.vertexBuffer?.format,
|
|
3772
|
-
})
|
|
3819
|
+
});
|
|
3820
|
+
if (v) (made.push(v), issued++);
|
|
3773
3821
|
}
|
|
3822
|
+
if (issued) prewarmed.add('overlay');
|
|
3774
3823
|
}
|
|
3775
|
-
if (issued) prewarmed.add(key);
|
|
3776
3824
|
await finalizeWhenLinked(made.filter(Boolean));
|
|
3777
3825
|
} catch (err) {
|
|
3778
3826
|
console.info('[inline3d/splat] transition shader pre-warm skipped', err);
|
|
3779
3827
|
}
|
|
3780
3828
|
}
|
|
3781
3829
|
|
|
3830
|
+
|
|
3782
3831
|
/**
|
|
3783
3832
|
* Creating a program only ISSUES its compile + link; the browser resolves the link when the
|
|
3784
3833
|
* program is first queried — the engine's first draw, blocking (the 35–50 ms GetProgramiv the
|
|
@@ -3845,6 +3894,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3845
3894
|
await first.catch(() => null); // a failed first asset may be replaced
|
|
3846
3895
|
if (!app || removed) return out;
|
|
3847
3896
|
const pc = pcModule;
|
|
3897
|
+
// Not prepared: compile what the transition draws with (its chunk, the overlay) while the asset
|
|
3898
|
+
// loads, rather than on the window's first frames. Best effort, not awaited.
|
|
3899
|
+
if (!prep && current && plan.transition !== 'cut') prewarmTransition(plan);
|
|
3848
3900
|
const loaded = prep ? prep.loaded : await loadOne(pc, app, next);
|
|
3849
3901
|
if (removed || gen !== sourceGen) {
|
|
3850
3902
|
app.assets.remove(loaded.asset);
|
|
@@ -3977,15 +4029,24 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3977
4029
|
});
|
|
3978
4030
|
} else release(prev); // the snapshot shows it from here on
|
|
3979
4031
|
const band = plan.band;
|
|
4032
|
+
finishers.push(() => {
|
|
4033
|
+
viewer._transitionState = null;
|
|
4034
|
+
viewer.setSnapshotAlpha(0);
|
|
4035
|
+
viewer.releaseSnapshot();
|
|
4036
|
+
});
|
|
4037
|
+
if (fx && managerMi(viewer.eye, null)) {
|
|
4038
|
+
playWavefrontRender({ plan, entity, live, finishers, finish, isFinished: () => finished, pumpLive });
|
|
4039
|
+
await done;
|
|
4040
|
+
return out;
|
|
4041
|
+
}
|
|
4042
|
+
// FALLBACK (an engine whose managers are not reachable): the ridge as an entity-scope
|
|
4043
|
+
// modifier — correct, but it rewrites the new photo's work buffer and re-sorts it every frame.
|
|
4044
|
+
viewer._transitionPath = 'entity';
|
|
3980
4045
|
const ridge = fx.play(
|
|
3981
4046
|
'wavefront',
|
|
3982
4047
|
{ durationMs: plan.durationMs, easing: plan.easing, band, ridge: plan.ridge, ridgeMaxDisparity: plan.ridgeMaxDisparity, scope: 'entity' },
|
|
3983
4048
|
{ entity, gate: afterTicks(2), internal: true },
|
|
3984
4049
|
);
|
|
3985
|
-
finishers.push(() => {
|
|
3986
|
-
viewer.setSnapshotAlpha(0);
|
|
3987
|
-
viewer.releaseSnapshot();
|
|
3988
|
-
});
|
|
3989
4050
|
viewer._hooks.push(() => {
|
|
3990
4051
|
if (finished || removed) return false;
|
|
3991
4052
|
if (live && !live.fits) {
|
|
@@ -4064,6 +4125,94 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4064
4125
|
return out;
|
|
4065
4126
|
}
|
|
4066
4127
|
|
|
4128
|
+
/** The mesh instance of the gsplat manager that `cam` draws `layer` with (any layer: null). */
|
|
4129
|
+
const managerMi = (cam, layer) => {
|
|
4130
|
+
const cd = viewer.app?.renderer?.gsplatDirector?.camerasMap?.get?.(cam?.camera?.camera);
|
|
4131
|
+
if (!cd?.layersMap) return null;
|
|
4132
|
+
if (layer) return cd.layersMap.get(layer)?.gsplatManager?.renderer?.meshInstance ?? null;
|
|
4133
|
+
for (const ld of cd.layersMap.values()) if (ld?.gsplatManager?.renderer?.meshInstance) return ld.gsplatManager.renderer.meshInstance;
|
|
4134
|
+
return null;
|
|
4135
|
+
};
|
|
4136
|
+
|
|
4137
|
+
/**
|
|
4138
|
+
* setSource's wavefront, RENDER-TIME (the default): the ridge is a tile-scope body whose values
|
|
4139
|
+
* sit on the eye camera's manager (the incoming photo), as the particle transitions do — no
|
|
4140
|
+
* work-buffer rewrite and no re-sort per frame. Each photo is drawn only on its side of the
|
|
4141
|
+
* front (EFFECTS.wipecull, the last stage): the incoming one left of where its commit starts,
|
|
4142
|
+
* the outgoing one (on the live camera) right of where it ends, so the two share only the
|
|
4143
|
+
* `band`. The overlay still wipes the same two live images on the same lt; the frame is the
|
|
4144
|
+
* one the full draws give, bit for bit. The clock starts on the second tick (the incoming
|
|
4145
|
+
* asset's first frame builds its work buffer), as the crossfade's does.
|
|
4146
|
+
*/
|
|
4147
|
+
function playWavefrontRender({ plan, entity, live, finishers, finish, isFinished, pumpLive }) {
|
|
4148
|
+
const band = plan.band;
|
|
4149
|
+
const ease = typeof plan.easing === 'function' ? plan.easing : EASINGS[plan.easing] || EASINGS.linear;
|
|
4150
|
+
const eyeMi = () => managerMi(viewer.eye, null);
|
|
4151
|
+
const liveMi = () => (live?.active ? managerMi(live.cam, live.layer) : null);
|
|
4152
|
+
const ridgeFx = fx.driveShared('transition', 'wavefront', {});
|
|
4153
|
+
const cullFx = fx.driveShared('transition-cull', 'wipecull', {});
|
|
4154
|
+
const ridgeIn = ridgeFx.side(eyeMi, { band, ridge: plan.ridge, ridgeMaxDisparity: plan.ridgeMaxDisparity });
|
|
4155
|
+
const cull = { in: null, out: null };
|
|
4156
|
+
const cullIn = cullFx.side(eyeMi, { cull: () => cull.in });
|
|
4157
|
+
const cullOut = live ? cullFx.side(liveMi, { cull: () => cull.out }) : null;
|
|
4158
|
+
const views = { in: [], out: [] };
|
|
4159
|
+
let amount = 0;
|
|
4160
|
+
ridgeIn.set(0);
|
|
4161
|
+
viewer._transitionPath = 'render';
|
|
4162
|
+
// Where each photo still shows, in each eye's viewport-relative NDC (setSnapshotAlpha's wipe:
|
|
4163
|
+
// column u has committed lt = clamp((t − u·(1 − band)) / band, 0, 1) to the new photo).
|
|
4164
|
+
const clampEdge = (x) => Math.min(3, Math.max(-3, x));
|
|
4165
|
+
viewer.onBeforeRender = (entries, rect) => {
|
|
4166
|
+
if (isFinished()) return;
|
|
4167
|
+
const span = 1 - band;
|
|
4168
|
+
// band 1: the whole picture is the band — draw both in full. `_wipeCull = false`: the
|
|
4169
|
+
// diagnostics switch the exactness gate compares against (docs §Gates).
|
|
4170
|
+
if (!(span > 1e-6) || viewer._wipeCull === false) {
|
|
4171
|
+
cull.in = cull.out = null;
|
|
4172
|
+
cullIn.set(amount);
|
|
4173
|
+
cullOut?.set(amount);
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
const vin = viewer.cullViews(viewer.eye, entries, rect, views.in);
|
|
4177
|
+
cull.in = vin ? { side: -1, edge: clampEdge((2 * amount) / span - 1), views: vin } : null;
|
|
4178
|
+
cullIn.set(amount);
|
|
4179
|
+
if (cullOut) {
|
|
4180
|
+
const vout = viewer.cullViews(live.cam, entries, rect, views.out);
|
|
4181
|
+
cull.out = vout ? { side: 1, edge: clampEdge((2 * (amount - band)) / span - 1), views: vout } : null;
|
|
4182
|
+
cullOut.set(amount);
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
finishers.push(() => {
|
|
4186
|
+
if (viewer.onBeforeRender) viewer.onBeforeRender = null;
|
|
4187
|
+
ridgeFx.remove();
|
|
4188
|
+
cullFx.remove();
|
|
4189
|
+
});
|
|
4190
|
+
let t0 = null;
|
|
4191
|
+
let ticks = 0;
|
|
4192
|
+
viewer._hooks.push((t) => {
|
|
4193
|
+
if (removed) return (finish(), false);
|
|
4194
|
+
if (isFinished()) return false;
|
|
4195
|
+
if (live && !live.fits) {
|
|
4196
|
+
finish(); // a resize or a 2D/3D switch mid-window: end now
|
|
4197
|
+
return false;
|
|
4198
|
+
}
|
|
4199
|
+
pumpLive();
|
|
4200
|
+
if (++ticks >= 2 && t0 === null) {
|
|
4201
|
+
t0 = t;
|
|
4202
|
+
ridgeIn.restart(); // the eyes' frame, now the new rig and first frames are in
|
|
4203
|
+
}
|
|
4204
|
+
const raw = t0 === null ? 0 : plan.durationMs > 0 ? Math.min(1, Math.max(0, (t - t0) / plan.durationMs)) : 1;
|
|
4205
|
+
amount = ease(raw);
|
|
4206
|
+
viewer._transitionState = { raw, live: !!live }; // diagnostics
|
|
4207
|
+
ridgeIn.set(amount);
|
|
4208
|
+
if (!viewer.setSnapshotAlpha(1, { t: amount, band, views: viewer.viewsAcross }) || raw >= 1) {
|
|
4209
|
+
finish();
|
|
4210
|
+
return false;
|
|
4211
|
+
}
|
|
4212
|
+
return true;
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4067
4216
|
/**
|
|
4068
4217
|
* setSource's particle transitions (swarm, burst, shimmer-cross, dust), once the frame snapshot
|
|
4069
4218
|
* is up. The OUTGOING asset plays its effect in reverse, on the live camera, keyed on its own
|
|
@@ -4083,13 +4232,6 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
4083
4232
|
// (outgoing). No work-buffer rewrite and no re-sort per frame. An engine that does not expose
|
|
4084
4233
|
// the managers' mesh instances gets the entity-scope modifiers instead (a full work-buffer
|
|
4085
4234
|
// rewrite + a re-sort per asset per frame: correct, but it hitches on a 1.18M photo).
|
|
4086
|
-
const managerMi = (cam, layer) => {
|
|
4087
|
-
const cd = viewer.app?.renderer?.gsplatDirector?.camerasMap?.get?.(cam?.camera?.camera);
|
|
4088
|
-
if (!cd?.layersMap) return null;
|
|
4089
|
-
if (layer) return cd.layersMap.get(layer)?.gsplatManager?.renderer?.meshInstance ?? null;
|
|
4090
|
-
for (const ld of cd.layersMap.values()) if (ld?.gsplatManager?.renderer?.meshInstance) return ld.gsplatManager.renderer.meshInstance;
|
|
4091
|
-
return null;
|
|
4092
|
-
};
|
|
4093
4235
|
const eyeMi = () => managerMi(viewer.eye, null);
|
|
4094
4236
|
const liveMi = () => (live?.active ? managerMi(live.cam, live.layer) : null);
|
|
4095
4237
|
const shared = particle.out.effect === particle.in.effect && eyeMi() ? fx.driveShared('transition', particle.in.effect, { order: particle.in.opts.order }) : null;
|
|
@@ -514,3 +514,35 @@ export function coverageExponent(c, L = FADE_OPTICAL_DEPTH) {
|
|
|
514
514
|
if (c >= 1) return 1;
|
|
515
515
|
return -Math.log(1 - c * (1 - Math.exp(-L))) / L;
|
|
516
516
|
}
|
|
517
|
+
|
|
518
|
+
// ── view-rig declaration (all three backends) ────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
let warnedNoViewRig = false;
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Declare a view rig on the scene handle. A handle without `setViewRig` is a core that predates
|
|
524
|
+
* view rigs (inline3d.js before the view-rig commit — e.g. a page that vendors an older core and
|
|
525
|
+
* hands its wall to this adapter): the rig cannot reach the browser, and the wall weaves on the
|
|
526
|
+
* addScene display-rig shorthand instead. For a CAMERA rig that is a metric photo scene on a
|
|
527
|
+
* 0.24 m display rig — convergence and scale way off while 2D looks right — so it is said out
|
|
528
|
+
* loud, once per page, rather than dropped the way `handle?.setViewRig?.()` used to drop it.
|
|
529
|
+
* Returns whether the handle took the rig.
|
|
530
|
+
*/
|
|
531
|
+
export function declareViewRig(handle, rig) {
|
|
532
|
+
if (!handle) return false; // no wall (mono): nothing to declare to
|
|
533
|
+
if (typeof handle.setViewRig === 'function') {
|
|
534
|
+
handle.setViewRig(rig);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (rig?.type === 'camera' && !warnedNoViewRig) {
|
|
538
|
+
warnedNoViewRig = true;
|
|
539
|
+
console.warn(
|
|
540
|
+
'[inline3d/splat] the view rig was dropped: your inline3d core predates view rigs (its ' +
|
|
541
|
+
'addScene handle has no setViewRig), so the wall will use the addScene display-rig ' +
|
|
542
|
+
'shorthand, not this camera rig — the scene will be mis-converged and mis-scaled in 3D. ' +
|
|
543
|
+
'Update the core (js/inline3d.js) that created the wall to the same SDK version as ./splat.',
|
|
544
|
+
rig,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
package/js/inline3d-splat.js
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
STREAMED_NEEDS_PLAYCANVAS,
|
|
38
38
|
resolveControls,
|
|
39
39
|
normalizeCameraPose,
|
|
40
|
+
declareViewRig,
|
|
40
41
|
} from './inline3d-splat-shared.js';
|
|
41
42
|
import {
|
|
42
43
|
resolveRig,
|
|
@@ -486,7 +487,7 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
486
487
|
parallaxFactor: out.rig.parallaxFactor,
|
|
487
488
|
out: out.viewRig || {},
|
|
488
489
|
});
|
|
489
|
-
handle
|
|
490
|
+
declareViewRig(handle, out.viewRig);
|
|
490
491
|
}
|
|
491
492
|
viewer.onFocusChange = () => {
|
|
492
493
|
pushViewRig(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.3",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|