@displayxr/inline3d 1.26.0 → 1.28.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 +134 -0
- package/call.d.ts +374 -0
- package/index.d.ts +17 -1
- package/js/call/capture.js +167 -0
- package/js/call/lift.js +314 -0
- package/js/call/qr.js +283 -0
- package/js/call/sdp.js +86 -0
- package/js/call/signaling.js +398 -0
- package/js/call/transport.js +589 -0
- package/js/call/ui.js +146 -0
- package/js/call/wire.js +428 -0
- package/js/inline3d-buffer-limit.js +198 -0
- package/js/inline3d-call.js +1695 -0
- package/js/inline3d-model-playcanvas.js +6 -0
- package/js/inline3d-model.js +6 -0
- package/js/inline3d-player.js +3 -2
- package/js/inline3d-splat-layer-rig.js +79 -7
- package/js/inline3d-splat-playcanvas.js +94 -17
- package/js/inline3d-splat.js +5 -0
- package/js/inline3d-viewer.js +86 -10
- package/js/inline3d.js +127 -14
- package/model.d.ts +10 -1
- package/package.json +9 -1
- package/player.d.ts +3 -2
- package/splat.d.ts +26 -4
- package/viewer.d.ts +12 -1
|
@@ -455,6 +455,7 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
455
455
|
orbit,
|
|
456
456
|
idleSpin,
|
|
457
457
|
renderScale,
|
|
458
|
+
logTag: '[inline3d/model]',
|
|
458
459
|
flipY: false, // glTF is already Y-up
|
|
459
460
|
orbitMaxDeg: opts.orbitMaxDeg,
|
|
460
461
|
orbitEase: opts.orbitEase,
|
|
@@ -512,6 +513,11 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
512
513
|
unexclude: (el) => handle?.unexclude(el),
|
|
513
514
|
});
|
|
514
515
|
|
|
516
|
+
// The per-eye buffer scale in force (the request times the device-limit clamp,
|
|
517
|
+
// ./inline3d-buffer-limit.js) and the request as given. Accessors, so they stay live.
|
|
518
|
+
Object.defineProperty(out, 'renderScale', { get: () => viewer.effectiveRenderScale, enumerable: true, configurable: true });
|
|
519
|
+
Object.defineProperty(out, 'renderScaleRequested', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
520
|
+
|
|
515
521
|
if (wall && wall.supported) {
|
|
516
522
|
handle = wall.addScene(canvas, viewer.onFrame, {
|
|
517
523
|
...(pageMode
|
package/js/inline3d-model.js
CHANGED
|
@@ -379,6 +379,7 @@ export function addModel(wall, canvas, src, opts = {}) {
|
|
|
379
379
|
idleSpin,
|
|
380
380
|
renderScale,
|
|
381
381
|
feather,
|
|
382
|
+
logTag: '[inline3d/model]',
|
|
382
383
|
}).useEyeCamera(EyeCamera, EdgeFeather);
|
|
383
384
|
|
|
384
385
|
if (envMap) viewer.scene.environment = envMap;
|
|
@@ -404,6 +405,11 @@ export function addModel(wall, canvas, src, opts = {}) {
|
|
|
404
405
|
unexclude: (el) => handle?.unexclude(el),
|
|
405
406
|
};
|
|
406
407
|
|
|
408
|
+
// The per-eye buffer scale in force (the request times the device-limit clamp,
|
|
409
|
+
// ./inline3d-buffer-limit.js) and the request as given. Accessors, so they stay live.
|
|
410
|
+
Object.defineProperty(out, 'renderScale', { get: () => viewer.effectiveRenderScale, enumerable: true, configurable: true });
|
|
411
|
+
Object.defineProperty(out, 'renderScaleRequested', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
412
|
+
|
|
407
413
|
// Window first, content when it lands — same reasoning as ./splat: a grid should not appear
|
|
408
414
|
// one tile at a time in download order.
|
|
409
415
|
let handle = null;
|
package/js/inline3d-player.js
CHANGED
|
@@ -346,7 +346,8 @@ function drawFittedEye(ctx, src, srcW, srcH, layout, eye, dx, dy, dw, dh, fit, b
|
|
|
346
346
|
* Pick the first source this browser can play, from candidates listed BEST FIRST. Each candidate
|
|
347
347
|
* is a URL string, or `{ src, type }` where `type` is a full `canPlayType` string — e.g.
|
|
348
348
|
* `'video/webm; codecs="vp9, opus"'`. Codec-less types answer 'maybe' for anything in the
|
|
349
|
-
* container, which is why the full string matters:
|
|
349
|
+
* container, which is why the full string matters: only DisplayXR Browser 1.0.6+ plays H.264 /
|
|
350
|
+
* AAC (through the OS decoders, and not on a machine without hardware H.264 decode), and
|
|
350
351
|
* `'video/mp4'` alone still says 'maybe'. A 'probably' wins over an earlier 'maybe'. A candidate
|
|
351
352
|
* without a type is taken as-is if nothing typed was 'probably'. Warns and returns the first when
|
|
352
353
|
* nothing is playable, so the `error` event (and the poster) still happens the usual way.
|
|
@@ -382,7 +383,7 @@ export function pickSource(candidates, canPlayType) {
|
|
|
382
383
|
if (maybe) return maybe.src;
|
|
383
384
|
console.warn(
|
|
384
385
|
'[inline3d/player] none of the candidate sources is playable here — trying the first. The ' +
|
|
385
|
-
'
|
|
386
|
+
'VP9/AV1 + Opus in WebM plays everywhere; H.264/AAC MP4 needs DisplayXR Browser 1.0.6+.',
|
|
386
387
|
list.map((c) => c.type || '(untyped)')
|
|
387
388
|
);
|
|
388
389
|
return list[0].src;
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
// view_round_i = view_photo_i · M_i, proj_round_i = proj_photo_i
|
|
17
17
|
//
|
|
18
18
|
// k = D / (m·n): D the declared convergence (world units), m the declared metersToVirtual, n the
|
|
19
|
-
// nominal viewer distance in metres
|
|
20
|
-
//
|
|
19
|
+
// nominal viewer distance in metres. n comes from the page's `viewerDistance` if it passed one,
|
|
20
|
+
// else from the browser (XRDisplayInfo.nominalViewerPosition.z, DisplayXR Browser 0221+), else
|
|
21
|
+
// 0.6 m (design note §5, (c)1). Everything here is PLAIN ARITHMETIC on arrays, unit-testable
|
|
22
|
+
// without a GPU.
|
|
21
23
|
|
|
22
24
|
import { invertAffine } from './inline3d-splat-rig-map.js';
|
|
23
25
|
|
|
@@ -27,6 +29,32 @@ export const LAYER_RIGS = Object.freeze(['display', 'camera']);
|
|
|
27
29
|
/** Nominal viewer distance (m) when neither the page nor the platform says: the browser's own. */
|
|
28
30
|
export const DEFAULT_VIEWER_DISTANCE_M = 0.6;
|
|
29
31
|
|
|
32
|
+
/** How many times a tile asks the layer for its display info before giving up (rejections only). */
|
|
33
|
+
const MAX_DISPLAY_INFO_ASKS = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The runtime's nominal viewer DISTANCE n (m) from an XRDisplayInfo, or null when the browser does
|
|
37
|
+
* not report one. It is the position's **z** — the distance out of the glass along the display
|
|
38
|
+
* normal — not its length: the runtime's camera rig reads only `nominal_viewer->z`
|
|
39
|
+
* (displayxr-common dxr_view_math.c, `eye_local.z = m2v·(eye.z − nominal_z)`), and z is what
|
|
40
|
+
* D/n undoes. A browser without the field, a null info, or a z that is not finite and > 0 → null.
|
|
41
|
+
*/
|
|
42
|
+
export function nominalViewerDistanceFromInfo(info) {
|
|
43
|
+
const z = info && info.nominalViewerPosition ? info.nominalViewerPosition.z : undefined;
|
|
44
|
+
return typeof z === 'number' && Number.isFinite(z) && z > 0 ? z : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The n the layer rig uses, and where it came from: the page's `viewerDistance` wins ('page'),
|
|
49
|
+
* then the browser's nominal viewer ('browser'), then DEFAULT_VIEWER_DISTANCE_M ('default').
|
|
50
|
+
* @returns {{ value: number, source: 'page'|'browser'|'default' }}
|
|
51
|
+
*/
|
|
52
|
+
export function resolveViewerDistance(pageViewerDistance, browserViewerDistance) {
|
|
53
|
+
if (pageViewerDistance !== null && pageViewerDistance !== undefined) return { value: pageViewerDistance, source: 'page' };
|
|
54
|
+
if (browserViewerDistance !== null && browserViewerDistance !== undefined) return { value: browserViewerDistance, source: 'browser' };
|
|
55
|
+
return { value: DEFAULT_VIEWER_DISTANCE_M, source: 'default' };
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
/** A round eye may not reach the window plane: h(E') is floored at this fraction of D. */
|
|
31
59
|
const MIN_EYE_HEIGHT_FRAC = 1e-3;
|
|
32
60
|
|
|
@@ -45,7 +73,8 @@ export const LAYER_RIG_OPTION_KEYS = Object.freeze(['viewerDistance', 'gain', 'p
|
|
|
45
73
|
/**
|
|
46
74
|
* setLayerRig / setLayerRigOptions options, validated (throws at the call). Only the keys given
|
|
47
75
|
* 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
|
|
76
|
+
* viewerDistance m, > 0 the nominal viewer distance n (default: the browser's
|
|
77
|
+
* XRDisplayInfo.nominalViewerPosition.z, else 0.6)
|
|
49
78
|
* gain > 0 an explicit rounding gain k instead of D/(m·n)
|
|
50
79
|
* planeOffset m, finite moves the stage TOWARD the viewer by this much on the panel (display
|
|
51
80
|
* space): the plane that lands on the glass moves back by offset·D/n
|
|
@@ -330,6 +359,11 @@ export class LayerRigCameras {
|
|
|
330
359
|
this.layers = new Map();
|
|
331
360
|
/** The tile's options: merged across setLayerRig / setLayerRigOptions calls. */
|
|
332
361
|
this.opts = { viewerDistance: null, gain: null, planeOffset: null, planeDistance: null };
|
|
362
|
+
/** n as the browser reports it (XRDisplayInfo.nominalViewerPosition.z), once known; else null. */
|
|
363
|
+
this.browserViewerDistance = null;
|
|
364
|
+
this._infoAsks = 0;
|
|
365
|
+
this._infoPending = false;
|
|
366
|
+
this._infoSettled = false;
|
|
333
367
|
this.disabled = false; // the kill switch (diag 'nolayerrig')
|
|
334
368
|
this.cams = { display: [], post: [] };
|
|
335
369
|
this.moved = { display: [], post: [] };
|
|
@@ -369,6 +403,41 @@ export class LayerRigCameras {
|
|
|
369
403
|
for (const [k, v] of Object.entries(o)) this.opts[k] = v;
|
|
370
404
|
}
|
|
371
405
|
|
|
406
|
+
/** The n this tile uses right now, and its source ('page' | 'browser' | 'default'). */
|
|
407
|
+
viewerDistance() {
|
|
408
|
+
return resolveViewerDistance(this.opts.viewerDistance, this.browserViewerDistance);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Called with the frame's XRDisplayLayer: ask it ONCE for the display info and keep the
|
|
413
|
+
* browser's nominal viewer distance. Asynchronous — frames drawn before it resolves use the
|
|
414
|
+
* page's value or 0.6 m, and the state's `viewerDistanceSource` says which. A browser without
|
|
415
|
+
* getDisplayInfo (or without the field) leaves the default in place; a REJECTED ask (e.g. no live
|
|
416
|
+
* layer yet) is retried on later frames, up to MAX_DISPLAY_INFO_ASKS times.
|
|
417
|
+
*/
|
|
418
|
+
noteLayer(layer) {
|
|
419
|
+
if (this._infoSettled || this._infoPending || this._infoAsks >= MAX_DISPLAY_INFO_ASKS) return;
|
|
420
|
+
if (!layer || typeof layer.getDisplayInfo !== 'function') return;
|
|
421
|
+
this._infoAsks++;
|
|
422
|
+
this._infoPending = true;
|
|
423
|
+
let p;
|
|
424
|
+
try {
|
|
425
|
+
p = Promise.resolve(layer.getDisplayInfo());
|
|
426
|
+
} catch (e) {
|
|
427
|
+
p = Promise.reject(e);
|
|
428
|
+
}
|
|
429
|
+
return p.then(
|
|
430
|
+
(info) => {
|
|
431
|
+
this._infoPending = false;
|
|
432
|
+
this._infoSettled = true;
|
|
433
|
+
this.browserViewerDistance = nominalViewerDistanceFromInfo(info);
|
|
434
|
+
},
|
|
435
|
+
() => {
|
|
436
|
+
this._infoPending = false;
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
372
441
|
/** The eye camera entities of the current view path. */
|
|
373
442
|
_eyeCams() {
|
|
374
443
|
const v = this.viewer;
|
|
@@ -494,8 +563,9 @@ export class LayerRigCameras {
|
|
|
494
563
|
L.residual = residual;
|
|
495
564
|
const hasRun = this.sync();
|
|
496
565
|
const frame = cameraRigFrame(rig);
|
|
497
|
-
const
|
|
498
|
-
const
|
|
566
|
+
const opts = { ...this.opts, viewerDistance: this.viewerDistance().value };
|
|
567
|
+
const k = frame ? layerRigGain(frame, opts) : null;
|
|
568
|
+
const plane = frame ? layerRigPlane(frame, opts) : null;
|
|
499
569
|
L.gain = k;
|
|
500
570
|
L.planeM = plane;
|
|
501
571
|
L.photoConvergenceM = frame ? frame.D : null;
|
|
@@ -606,6 +676,7 @@ export class LayerRigCameras {
|
|
|
606
676
|
/** The public state (handle.layerRigState). */
|
|
607
677
|
state() {
|
|
608
678
|
const L = this.last;
|
|
679
|
+
const n = this.viewerDistance();
|
|
609
680
|
return {
|
|
610
681
|
display: [...this.requests.keys()],
|
|
611
682
|
disabled: this.disabled,
|
|
@@ -613,7 +684,8 @@ export class LayerRigCameras {
|
|
|
613
684
|
engaged: !!L.engaged,
|
|
614
685
|
rounded: !!L.engaged,
|
|
615
686
|
reason: this.disabled ? 'kill switch (?dxrdiag=nolayerrig)' : L.reason,
|
|
616
|
-
viewerDistance:
|
|
687
|
+
viewerDistance: n.value,
|
|
688
|
+
viewerDistanceSource: n.source,
|
|
617
689
|
gain: L.gain,
|
|
618
690
|
planeM: L.planeM,
|
|
619
691
|
photoConvergenceM: L.photoConvergenceM ?? null,
|
|
@@ -634,7 +706,7 @@ export function layerRigLine(st) {
|
|
|
634
706
|
const n = (x, d = 3) => (x === null || x === undefined ? '-' : Number(x).toFixed(d));
|
|
635
707
|
return (
|
|
636
708
|
`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)} ` +
|
|
709
|
+
`viewerDistance=${n(st.viewerDistance, 2)}m(${st.viewerDistanceSource ?? '-'}) gain=${n(st.gain)} planeM=${n(st.planeM)} ` +
|
|
638
710
|
`photoConvergenceM=${n(st.photoConvergenceM)} planeOffset=${n(st.planeOffset)}m ` +
|
|
639
711
|
`located=${st.located === null || st.located === undefined ? '-' : st.located} reason=${st.reason ?? '-'}`
|
|
640
712
|
);
|
|
@@ -71,6 +71,14 @@ import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNI
|
|
|
71
71
|
import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
|
|
72
72
|
import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
|
|
73
73
|
import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, DIAG_SWITCHES } from './inline3d-splat-diag.js';
|
|
74
|
+
import {
|
|
75
|
+
glBufferLimits,
|
|
76
|
+
probeBufferLimits,
|
|
77
|
+
clampEyeBuffer,
|
|
78
|
+
clampWarning,
|
|
79
|
+
bufferScale,
|
|
80
|
+
mismatchWarning,
|
|
81
|
+
} from './inline3d-buffer-limit.js';
|
|
74
82
|
import {
|
|
75
83
|
clamp,
|
|
76
84
|
finite,
|
|
@@ -890,6 +898,13 @@ export class PlayCanvasSplatViewer {
|
|
|
890
898
|
this.depthLimit = depthLimit;
|
|
891
899
|
this.fitSweep = fitSweep;
|
|
892
900
|
this.renderScale = renderScale;
|
|
901
|
+
// The device-limit clamp on the backing store (./inline3d-buffer-limit.js), as SceneViewer:
|
|
902
|
+
// the factor the last _resize applied on top of renderScale; warned once per viewer.
|
|
903
|
+
this._bufClamp = 1;
|
|
904
|
+
this._warnedBufClamp = false;
|
|
905
|
+
this._warnedBufMismatch = false;
|
|
906
|
+
/** Prefix for this viewer's warnings; ./model sets its own. */
|
|
907
|
+
this.logTag = opts.logTag || '[inline3d/splat]';
|
|
893
908
|
this.pitchLimit = pitchLimit;
|
|
894
909
|
this.idleSpin = this.pageCamera ? 0 : idleSpin;
|
|
895
910
|
this.flipY = opts.flipY !== false;
|
|
@@ -1184,7 +1199,11 @@ export class PlayCanvasSplatViewer {
|
|
|
1184
1199
|
if (this._mode !== '3d') this.stopMono();
|
|
1185
1200
|
// The rig these views were located with: Blink chained the rig declared BEFORE this callback,
|
|
1186
1201
|
// and the tick below may declare a new one (a focus ease) for the NEXT locate.
|
|
1187
|
-
if (this.layerRigs?.active)
|
|
1202
|
+
if (this.layerRigs?.active) {
|
|
1203
|
+
this._rigSnap = snapshotRig(this.layerRigSource?.(), this._rigSnap);
|
|
1204
|
+
// n for the layer rig: the browser's nominal viewer, asked once (async; no-op after).
|
|
1205
|
+
this.layerRigs.noteLayer(layer);
|
|
1206
|
+
}
|
|
1188
1207
|
// Same moment, for the rig map: the rig these views were most likely located for (a tie-break).
|
|
1189
1208
|
this._rigAtPull = this.rigTrack ? this.rigTrack.latest : null;
|
|
1190
1209
|
// BEFORE the tick: a pose the page sets in here is the one this very frame renders.
|
|
@@ -1316,6 +1335,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1316
1335
|
// The SDK's frame drives the engine: no second rAF. `tick` is the engine's own loop body.
|
|
1317
1336
|
app.requestAnimationFrame = () => {};
|
|
1318
1337
|
this.app = app;
|
|
1338
|
+
// The store may have been sized before the engine existed (from the probed limits, or none):
|
|
1339
|
+
// re-check it against THIS context's. A no-op when nothing moves.
|
|
1340
|
+
this._scheduleResize();
|
|
1319
1341
|
|
|
1320
1342
|
// Footprint fix (§ patchGsplatFootprint) and the perf quad-extent cap, both as chunk
|
|
1321
1343
|
// overrides. The gsplat chunks are registered by GSplatComponentSystem during init, so
|
|
@@ -1939,8 +1961,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1939
1961
|
const v = this._monoView();
|
|
1940
1962
|
e.proj = v.proj;
|
|
1941
1963
|
e.pose = v.pose;
|
|
1942
|
-
|
|
1943
|
-
e.
|
|
1964
|
+
const b = this._bufScale();
|
|
1965
|
+
e.width = b.w || c.width;
|
|
1966
|
+
e.height = b.h || c.height;
|
|
1944
1967
|
this._drawEntries(this._monoEntry, null);
|
|
1945
1968
|
}
|
|
1946
1969
|
|
|
@@ -1952,9 +1975,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1952
1975
|
const app = this.app;
|
|
1953
1976
|
if (!app || !this.pc) return false;
|
|
1954
1977
|
const pc = this.pc;
|
|
1955
|
-
const
|
|
1956
|
-
const sx = cache && cache.bufW > 0 && el.width ? el.width / cache.bufW : 1;
|
|
1957
|
-
const sy = cache && cache.bufH > 0 && el.height ? el.height / cache.bufH : 1;
|
|
1978
|
+
const { b, sx, sy } = this._entryScale(cache);
|
|
1958
1979
|
if (this.pageCamera) {
|
|
1959
1980
|
// The page's near/far join the caller's nearClip/farClip as a floor and a cap: only the
|
|
1960
1981
|
// depth mapping moves, the runtime's frustum (fov, skew, principal point) stays untouched.
|
|
@@ -2029,8 +2050,8 @@ export class PlayCanvasSplatViewer {
|
|
|
2029
2050
|
this._budgetViews = entries.length;
|
|
2030
2051
|
this._applyBudget();
|
|
2031
2052
|
}
|
|
2032
|
-
const W =
|
|
2033
|
-
const H =
|
|
2053
|
+
const W = b.w || 1;
|
|
2054
|
+
const H = b.h || 1;
|
|
2034
2055
|
for (let i = 0; i < entries.length; i++) {
|
|
2035
2056
|
const e = entries[i];
|
|
2036
2057
|
const cam = cams[i];
|
|
@@ -2116,18 +2137,68 @@ export class PlayCanvasSplatViewer {
|
|
|
2116
2137
|
const box = this.canvas.getBoundingClientRect();
|
|
2117
2138
|
if (box.width < 1 || box.height < 1) return;
|
|
2118
2139
|
const dpr = Math.min((typeof window !== 'undefined' && window.devicePixelRatio) || 1, 2) * this.renderScale;
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2140
|
+
// Clamp to the device's GL limits BEFORE sizing (./inline3d-buffer-limit.js). Measured on an
|
|
2141
|
+
// Adreno 740 tablet: MAX_TEXTURE_SIZE 4096, a 5120-wide request silently became a 4096-wide
|
|
2142
|
+
// drawing buffer, getViewport() still split 5120, and the eye boundary sat at 62.5%.
|
|
2143
|
+
const c = clampEyeBuffer(
|
|
2144
|
+
Math.max(1, Math.round(box.width * dpr)),
|
|
2145
|
+
Math.max(1, Math.round(box.height * dpr)),
|
|
2146
|
+
glBufferLimits(this.app?.graphicsDevice?.gl) || probeBufferLimits(),
|
|
2147
|
+
{ cols: this._mode === 'mono' ? 1 : 2 },
|
|
2148
|
+
);
|
|
2149
|
+
this._noteClamp(c);
|
|
2150
|
+
const bufW = c.bufW;
|
|
2151
|
+
const h = c.bufH;
|
|
2122
2152
|
this._updateMonoProjection();
|
|
2123
2153
|
const el = this.canvas;
|
|
2124
|
-
if (el.width === bufW && el.height === h)
|
|
2154
|
+
if (el.width === bufW && el.height === h) {
|
|
2155
|
+
this._bufScale(); // unchanged; still name a browser-side clamp (once)
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2125
2158
|
el.width = bufW;
|
|
2126
2159
|
el.height = h;
|
|
2160
|
+
this._bufScale(); // warns once if the browser clamped anyway
|
|
2127
2161
|
if (this._mode === 'mono') this._drawMono();
|
|
2128
2162
|
else this._replayLastGood();
|
|
2129
2163
|
}
|
|
2130
2164
|
|
|
2165
|
+
/** The renderScale in force: the request times the device-limit clamp (SceneViewer's twin). */
|
|
2166
|
+
get effectiveRenderScale() {
|
|
2167
|
+
return this.renderScale * this._bufClamp;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
_noteClamp(c) {
|
|
2171
|
+
this._bufClamp = c.scale;
|
|
2172
|
+
if (c.clamped && !this._warnedBufClamp) {
|
|
2173
|
+
this._warnedBufClamp = true;
|
|
2174
|
+
console.warn(clampWarning(this.logTag, c, this.renderScale));
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* How a frame's entries map into the buffer. Entries are in canvas-attribute px (getViewport()
|
|
2180
|
+
* splits canvas.width), cached with that size; they are drawn into the REAL drawing buffer.
|
|
2181
|
+
* Identity unless a resize happened since the cache or the browser clamped the buffer behind
|
|
2182
|
+
* the SDK (the device-limit clamp in _resize normally prevents that).
|
|
2183
|
+
*/
|
|
2184
|
+
_entryScale(cache) {
|
|
2185
|
+
const b = this._bufScale();
|
|
2186
|
+
const sx = cache && cache.bufW > 0 && b.w ? b.w / cache.bufW : 1;
|
|
2187
|
+
const sy = cache && cache.bufH > 0 && b.h ? b.h / cache.bufH : 1;
|
|
2188
|
+
return { b, sx, sy };
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/** The real drawing buffer vs canvas.width/height; warns once per viewer on a mismatch. */
|
|
2192
|
+
_bufScale() {
|
|
2193
|
+
const el = this.canvas;
|
|
2194
|
+
const b = bufferScale(el, this.app?.graphicsDevice?.gl || null);
|
|
2195
|
+
if (b.mismatch && !this._warnedBufMismatch) {
|
|
2196
|
+
this._warnedBufMismatch = true;
|
|
2197
|
+
console.warn(mismatchWarning(this.logTag, el, b));
|
|
2198
|
+
}
|
|
2199
|
+
return b;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2131
2202
|
_tick() {
|
|
2132
2203
|
const t = now();
|
|
2133
2204
|
const dt = this._lastTick ? Math.min((t - this._lastTick) / 1000, MAX_DT_S) : 0;
|
|
@@ -3047,9 +3118,13 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3047
3118
|
let cancelPendingVideo = null;
|
|
3048
3119
|
let warnedAutoplayMuted = false;
|
|
3049
3120
|
|
|
3050
|
-
// The
|
|
3051
|
-
//
|
|
3052
|
-
|
|
3121
|
+
// The per-eye buffer scale IN FORCE: the request (the `renderScale` option, or the last
|
|
3122
|
+
// setRenderScale) times the device-limit clamp — e.g. 0.8 for a request of 1 on a device whose
|
|
3123
|
+
// MAX_TEXTURE_SIZE cannot hold the full SBS store (./inline3d-buffer-limit.js).
|
|
3124
|
+
// `renderScaleRequested` is the request as given. Real accessors: Object.assign below would
|
|
3125
|
+
// copy a getter's value once.
|
|
3126
|
+
Object.defineProperty(out, 'renderScale', { get: () => viewer.effectiveRenderScale, enumerable: true, configurable: true });
|
|
3127
|
+
Object.defineProperty(out, 'renderScaleRequested', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
3053
3128
|
// The current eye offset (after the unit-disc clamp), a fresh object per read.
|
|
3054
3129
|
Object.defineProperty(out, 'viewOffset', {
|
|
3055
3130
|
get: () => ({ x: viewer._viewOffset[0], y: viewer._viewOffset[1] }),
|
|
@@ -3143,7 +3218,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3143
3218
|
/**
|
|
3144
3219
|
* Change the per-eye buffer scale live (the `renderScale` option): e.g. full resolution on a
|
|
3145
3220
|
* screen that hides the splat and draws only stage quads, 0.6 elsewhere. The backing store is
|
|
3146
|
-
* resized on the next animation frame (never mid-frame).
|
|
3221
|
+
* resized on the next animation frame (never mid-frame). The request is kept as given
|
|
3222
|
+
* (`renderScaleRequested`); the store is still clamped to the device's GL limits, and
|
|
3223
|
+
* `renderScale` reports what is in force. Returns the handle.
|
|
3147
3224
|
*/
|
|
3148
3225
|
setRenderScale(s) {
|
|
3149
3226
|
if (typeof s !== 'number' || !Number.isFinite(s) || s <= 0 || s > 4) {
|
|
@@ -3186,7 +3263,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3186
3263
|
layerRigState() {
|
|
3187
3264
|
const lr = viewer.layerRigs;
|
|
3188
3265
|
if (lr) return lr.state();
|
|
3189
|
-
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 };
|
|
3266
|
+
return { display: [], disabled: false, path: null, engaged: false, rounded: false, reason: 'no layer on the display rig', viewerDistance: DEFAULT_VIEWER_DISTANCE_M, viewerDistanceSource: 'default', gain: null, planeM: null, photoConvergenceM: null, planeOffset: 0, located: null };
|
|
3190
3267
|
},
|
|
3191
3268
|
/**
|
|
3192
3269
|
* An unlit material showing the left half of `texture` to left-eye views and the right half to
|
package/js/inline3d-splat.js
CHANGED
|
@@ -245,6 +245,7 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
245
245
|
idleSpin,
|
|
246
246
|
renderScale,
|
|
247
247
|
feather,
|
|
248
|
+
logTag: '[inline3d/splat]',
|
|
248
249
|
}).useEyeCamera(EyeCamera, EdgeFeather);
|
|
249
250
|
|
|
250
251
|
// Spark renders through the ordinary three.js pipeline, so splats and meshes co-exist and
|
|
@@ -411,6 +412,10 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
411
412
|
},
|
|
412
413
|
effects: () => [],
|
|
413
414
|
};
|
|
415
|
+
// The per-eye buffer scale in force (the request times the device-limit clamp,
|
|
416
|
+
// ./inline3d-buffer-limit.js) and the request as given. Accessors, so they stay live.
|
|
417
|
+
Object.defineProperty(out, 'renderScale', { get: () => viewer.effectiveRenderScale, enumerable: true, configurable: true });
|
|
418
|
+
Object.defineProperty(out, 'renderScaleRequested', { get: () => viewer.renderScale, enumerable: true, configurable: true });
|
|
414
419
|
|
|
415
420
|
// `src` may be a URL or the bytes themselves.
|
|
416
421
|
//
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -64,6 +64,14 @@ import {
|
|
|
64
64
|
MONO_NEAR,
|
|
65
65
|
MONO_FAR,
|
|
66
66
|
} from './inline3d-splat-shared.js';
|
|
67
|
+
import {
|
|
68
|
+
glBufferLimits,
|
|
69
|
+
clampEyeBuffer,
|
|
70
|
+
clampWarning,
|
|
71
|
+
bufferScale,
|
|
72
|
+
scaleViewport,
|
|
73
|
+
mismatchWarning,
|
|
74
|
+
} from './inline3d-buffer-limit.js';
|
|
67
75
|
|
|
68
76
|
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
|
69
77
|
// NaN/Infinity into a transform silently blanks the tile — three propagates it into the
|
|
@@ -283,6 +291,14 @@ export class SceneViewer {
|
|
|
283
291
|
this.depthLimit = depthLimit;
|
|
284
292
|
this.fitSweep = fitSweep;
|
|
285
293
|
this.renderScale = renderScale;
|
|
294
|
+
// The device-limit clamp on the backing store (./inline3d-buffer-limit.js): the factor _resize
|
|
295
|
+
// last had to apply on top of renderScale, 1 when the request fit. effectiveRenderScale reports
|
|
296
|
+
// the product. Warned once per viewer (one viewer per handle).
|
|
297
|
+
this._bufClamp = 1;
|
|
298
|
+
this._warnedBufClamp = false;
|
|
299
|
+
this._warnedBufMismatch = false;
|
|
300
|
+
/** Prefix for this viewer's warnings; ./splat and ./model set their own. */
|
|
301
|
+
this.logTag = opts.logTag || '[inline3d/viewer]';
|
|
286
302
|
this.pitchLimit = pitchLimit;
|
|
287
303
|
this.idleSpin = idleSpin;
|
|
288
304
|
|
|
@@ -711,10 +727,13 @@ export class SceneViewer {
|
|
|
711
727
|
|
|
712
728
|
// Validated: this frame WILL draw over everything it clears.
|
|
713
729
|
const r = this.renderer;
|
|
730
|
+
// getViewport() splits canvas.width; a drawing buffer the browser clamped behind our back
|
|
731
|
+
// (bufferScale().mismatch) gets the same split mapped onto its real size, never canvas.width/2.
|
|
732
|
+
const b = this._bufScale();
|
|
714
733
|
r.clear();
|
|
715
734
|
r.setScissorTest(true);
|
|
716
735
|
for (let i = 0; i < views.length; i++) {
|
|
717
|
-
const vp = vps[i];
|
|
736
|
+
const vp = scaleViewport(vps[i], b.sx, b.sy);
|
|
718
737
|
r.setViewport(vp.x, vp.y, vp.width, vp.height);
|
|
719
738
|
r.setScissor(vp.x, vp.y, vp.width, vp.height);
|
|
720
739
|
if (eye) {
|
|
@@ -753,7 +772,8 @@ export class SceneViewer {
|
|
|
753
772
|
this._tick();
|
|
754
773
|
const r = this.renderer;
|
|
755
774
|
r.clear();
|
|
756
|
-
|
|
775
|
+
const b = this._bufScale();
|
|
776
|
+
r.setViewport(0, 0, b.w, b.h);
|
|
757
777
|
r.render(this.scene, this.monoCamera);
|
|
758
778
|
};
|
|
759
779
|
this._monoRaf = requestAnimationFrame(loop);
|
|
@@ -849,9 +869,11 @@ export class SceneViewer {
|
|
|
849
869
|
const r = this.renderer;
|
|
850
870
|
const eye = g.mono ? null : this._ensureEye();
|
|
851
871
|
const el = this.renderer.domElement || this.canvas;
|
|
852
|
-
// A resize between the cache and the replay changes the buffer, not the split.
|
|
853
|
-
|
|
854
|
-
const
|
|
872
|
+
// A resize between the cache and the replay changes the buffer, not the split. The cache is in
|
|
873
|
+
// canvas-attribute px (getViewport's space); the target is the REAL drawing buffer.
|
|
874
|
+
const b = this._bufScale();
|
|
875
|
+
const sx = g.bufW > 0 && b.w ? b.w / g.bufW : 1;
|
|
876
|
+
const sy = g.bufH > 0 && b.h ? b.h / g.bufH : 1;
|
|
855
877
|
const scaled = sx !== 1 || sy !== 1;
|
|
856
878
|
r.clear();
|
|
857
879
|
r.setScissorTest(true);
|
|
@@ -903,7 +925,8 @@ export class SceneViewer {
|
|
|
903
925
|
if (this._mode === 'mono') {
|
|
904
926
|
const r = this.renderer;
|
|
905
927
|
r.clear();
|
|
906
|
-
|
|
928
|
+
const b = this._bufScale();
|
|
929
|
+
r.setViewport(0, 0, b.w, b.h);
|
|
907
930
|
r.render(this.scene, this.monoCamera);
|
|
908
931
|
return;
|
|
909
932
|
}
|
|
@@ -955,18 +978,71 @@ export class SceneViewer {
|
|
|
955
978
|
const box = this.canvas.getBoundingClientRect();
|
|
956
979
|
if (box.width < 1 || box.height < 1) return;
|
|
957
980
|
const dpr = Math.min(window.devicePixelRatio || 1, 2) * this.renderScale;
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
981
|
+
// Clamp to the device's GL limits BEFORE sizing (./inline3d-buffer-limit.js): a store past
|
|
982
|
+
// MAX_TEXTURE_SIZE is silently clamped by the browser while getViewport() keeps splitting
|
|
983
|
+
// canvas.width, which puts the eye boundary off-centre in the woven buffer.
|
|
984
|
+
const c = clampEyeBuffer(
|
|
985
|
+
Math.max(1, Math.round(box.width * dpr)),
|
|
986
|
+
Math.max(1, Math.round(box.height * dpr)),
|
|
987
|
+
glBufferLimits(this._gl()),
|
|
988
|
+
{ cols: this._mode === 'mono' ? 1 : 2 },
|
|
989
|
+
);
|
|
990
|
+
this._noteClamp(c);
|
|
991
|
+
const bufW = c.bufW;
|
|
992
|
+
const h = c.bufH;
|
|
961
993
|
// Cheap and always correct to refresh, whether or not the backing store moves.
|
|
962
994
|
this.monoCamera.aspect = box.width / box.height;
|
|
963
995
|
this.monoCamera.updateProjectionMatrix();
|
|
964
996
|
const el = this.renderer.domElement || this.canvas;
|
|
965
|
-
if (el.width === bufW && el.height === h)
|
|
997
|
+
if (el.width === bufW && el.height === h) {
|
|
998
|
+
this._bufScale(); // observer fired, geometry didn't move; still name a browser-side clamp
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
966
1001
|
this.renderer.setSize(bufW, h, false);
|
|
1002
|
+
this._bufScale(); // warns once if the browser clamped anyway
|
|
967
1003
|
this._repaintAfterResize();
|
|
968
1004
|
}
|
|
969
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* The renderScale actually in force: the request (`renderScale`) times the device-limit clamp
|
|
1008
|
+
* the last resize applied. Equal to renderScale wherever the store fits.
|
|
1009
|
+
*/
|
|
1010
|
+
get effectiveRenderScale() {
|
|
1011
|
+
return this.renderScale * this._bufClamp;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
_gl() {
|
|
1015
|
+
try {
|
|
1016
|
+
return this.renderer?.getContext?.() || null;
|
|
1017
|
+
} catch {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** Record a resize's clamp; warn once per viewer the first time the device limit bites. */
|
|
1023
|
+
_noteClamp(c) {
|
|
1024
|
+
this._bufClamp = c.scale;
|
|
1025
|
+
if (c.clamped && !this._warnedBufClamp) {
|
|
1026
|
+
this._warnedBufClamp = true;
|
|
1027
|
+
console.warn(clampWarning(this.logTag, c, this.renderScale));
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* The drawing buffer's real size and its ratio to canvas.width/height (the space getViewport()
|
|
1033
|
+
* reports in). Identity unless the browser clamped a store the limit query did not predict;
|
|
1034
|
+
* warns once per viewer when it did.
|
|
1035
|
+
*/
|
|
1036
|
+
_bufScale() {
|
|
1037
|
+
const el = this.renderer.domElement || this.canvas;
|
|
1038
|
+
const b = bufferScale(el, this._gl());
|
|
1039
|
+
if (b.mismatch && !this._warnedBufMismatch) {
|
|
1040
|
+
this._warnedBufMismatch = true;
|
|
1041
|
+
console.warn(mismatchWarning(this.logTag, el, b));
|
|
1042
|
+
}
|
|
1043
|
+
return b;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
970
1046
|
/** Damping + idle turntable. Called once per rendered frame, 3D or mono. */
|
|
971
1047
|
_tick() {
|
|
972
1048
|
const t = now();
|