@displayxr/inline3d 1.27.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 +115 -0
- package/call.d.ts +374 -0
- package/index.d.ts +7 -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-playcanvas.js +88 -15
- 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 +14 -3
- 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;
|
|
@@ -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;
|
|
@@ -1320,6 +1335,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1320
1335
|
// The SDK's frame drives the engine: no second rAF. `tick` is the engine's own loop body.
|
|
1321
1336
|
app.requestAnimationFrame = () => {};
|
|
1322
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();
|
|
1323
1341
|
|
|
1324
1342
|
// Footprint fix (§ patchGsplatFootprint) and the perf quad-extent cap, both as chunk
|
|
1325
1343
|
// overrides. The gsplat chunks are registered by GSplatComponentSystem during init, so
|
|
@@ -1943,8 +1961,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1943
1961
|
const v = this._monoView();
|
|
1944
1962
|
e.proj = v.proj;
|
|
1945
1963
|
e.pose = v.pose;
|
|
1946
|
-
|
|
1947
|
-
e.
|
|
1964
|
+
const b = this._bufScale();
|
|
1965
|
+
e.width = b.w || c.width;
|
|
1966
|
+
e.height = b.h || c.height;
|
|
1948
1967
|
this._drawEntries(this._monoEntry, null);
|
|
1949
1968
|
}
|
|
1950
1969
|
|
|
@@ -1956,9 +1975,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1956
1975
|
const app = this.app;
|
|
1957
1976
|
if (!app || !this.pc) return false;
|
|
1958
1977
|
const pc = this.pc;
|
|
1959
|
-
const
|
|
1960
|
-
const sx = cache && cache.bufW > 0 && el.width ? el.width / cache.bufW : 1;
|
|
1961
|
-
const sy = cache && cache.bufH > 0 && el.height ? el.height / cache.bufH : 1;
|
|
1978
|
+
const { b, sx, sy } = this._entryScale(cache);
|
|
1962
1979
|
if (this.pageCamera) {
|
|
1963
1980
|
// The page's near/far join the caller's nearClip/farClip as a floor and a cap: only the
|
|
1964
1981
|
// depth mapping moves, the runtime's frustum (fov, skew, principal point) stays untouched.
|
|
@@ -2033,8 +2050,8 @@ export class PlayCanvasSplatViewer {
|
|
|
2033
2050
|
this._budgetViews = entries.length;
|
|
2034
2051
|
this._applyBudget();
|
|
2035
2052
|
}
|
|
2036
|
-
const W =
|
|
2037
|
-
const H =
|
|
2053
|
+
const W = b.w || 1;
|
|
2054
|
+
const H = b.h || 1;
|
|
2038
2055
|
for (let i = 0; i < entries.length; i++) {
|
|
2039
2056
|
const e = entries[i];
|
|
2040
2057
|
const cam = cams[i];
|
|
@@ -2120,18 +2137,68 @@ export class PlayCanvasSplatViewer {
|
|
|
2120
2137
|
const box = this.canvas.getBoundingClientRect();
|
|
2121
2138
|
if (box.width < 1 || box.height < 1) return;
|
|
2122
2139
|
const dpr = Math.min((typeof window !== 'undefined' && window.devicePixelRatio) || 1, 2) * this.renderScale;
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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;
|
|
2126
2152
|
this._updateMonoProjection();
|
|
2127
2153
|
const el = this.canvas;
|
|
2128
|
-
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
|
+
}
|
|
2129
2158
|
el.width = bufW;
|
|
2130
2159
|
el.height = h;
|
|
2160
|
+
this._bufScale(); // warns once if the browser clamped anyway
|
|
2131
2161
|
if (this._mode === 'mono') this._drawMono();
|
|
2132
2162
|
else this._replayLastGood();
|
|
2133
2163
|
}
|
|
2134
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
|
+
|
|
2135
2202
|
_tick() {
|
|
2136
2203
|
const t = now();
|
|
2137
2204
|
const dt = this._lastTick ? Math.min((t - this._lastTick) / 1000, MAX_DT_S) : 0;
|
|
@@ -3051,9 +3118,13 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3051
3118
|
let cancelPendingVideo = null;
|
|
3052
3119
|
let warnedAutoplayMuted = false;
|
|
3053
3120
|
|
|
3054
|
-
// The
|
|
3055
|
-
//
|
|
3056
|
-
|
|
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 });
|
|
3057
3128
|
// The current eye offset (after the unit-disc clamp), a fresh object per read.
|
|
3058
3129
|
Object.defineProperty(out, 'viewOffset', {
|
|
3059
3130
|
get: () => ({ x: viewer._viewOffset[0], y: viewer._viewOffset[1] }),
|
|
@@ -3147,7 +3218,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3147
3218
|
/**
|
|
3148
3219
|
* Change the per-eye buffer scale live (the `renderScale` option): e.g. full resolution on a
|
|
3149
3220
|
* screen that hides the splat and draws only stage quads, 0.6 elsewhere. The backing store is
|
|
3150
|
-
* 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.
|
|
3151
3224
|
*/
|
|
3152
3225
|
setRenderScale(s) {
|
|
3153
3226
|
if (typeof s !== 'number' || !Number.isFinite(s) || s <= 0 || s > 4) {
|
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();
|
package/js/inline3d.js
CHANGED
|
@@ -35,6 +35,10 @@ export { undock, undockAvailable, undockUrl, tileScreenRect };
|
|
|
35
35
|
// The eased 2D<->3D transition. A pure state machine (no DOM, no WebXR) ported from the native
|
|
36
36
|
// `dxr::ModeSwitch`, so the browser eases the disparity around a mode switch the same way — and in
|
|
37
37
|
// the same ORDER — as the native apps and the demos. See _requestRenderingModeEased.
|
|
38
|
+
// The device-limit clamp on a backing store (MAX_TEXTURE_SIZE & co.), shared with ./viewer,
|
|
39
|
+
// ./splat and ./model. Dependency-free.
|
|
40
|
+
import { probeBufferLimits, clampEyeBuffer, clampWarning, bufferScale, mismatchWarning } from './inline3d-buffer-limit.js';
|
|
41
|
+
|
|
38
42
|
import {
|
|
39
43
|
ModeSwitch,
|
|
40
44
|
MODE_SWITCH_DEFAULT_DURATION_MS,
|
|
@@ -2340,17 +2344,108 @@ class Inline3D {
|
|
|
2340
2344
|
win.autoExcluded.clear();
|
|
2341
2345
|
}
|
|
2342
2346
|
|
|
2343
|
-
/**
|
|
2344
|
-
|
|
2347
|
+
/**
|
|
2348
|
+
* The per-eye buffer size this window should have right now: explicit, or box × dpr — capped
|
|
2349
|
+
* at the SOURCE's own per-eye resolution for image and video windows.
|
|
2350
|
+
*
|
|
2351
|
+
* WHY THE CAP (measured, NP02J tablet, DisplayXR Browser 1.0.6): the player sample's 90vw
|
|
2352
|
+
* tile at dpr 2 asked for a 4608×1296 backing store for a 640×360-per-eye clip — 13× the
|
|
2353
|
+
* source's pixels, re-drawn every frame. The page fell to 25 fps and dropped 47 of 97 video
|
|
2354
|
+
* frames; the same clip in a 45vw tile (2304×648) ran at 57 fps with 0 dropped. Drawing a
|
|
2355
|
+
* source into a buffer larger than itself adds no detail: the compositor scales the layer to
|
|
2356
|
+
* the box either way, so upscaling here only spends GPU fill-rate. The cap keeps the BOX
|
|
2357
|
+
* aspect (the eyes stay correctly shaped) and only shrinks, never grows past box × dpr.
|
|
2358
|
+
* An explicit {width, height} is the page's call and is never capped by the source.
|
|
2359
|
+
*
|
|
2360
|
+
* THEN THE DEVICE LIMIT, on every window including an explicit size: the whole store (2 × eye
|
|
2361
|
+
* wide in SBS) must fit min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), or
|
|
2362
|
+
* the weave cannot take it as one texture. An Android 3D tablet (Adreno 740) reports 4096 where
|
|
2363
|
+
* desktops report 16384. Both axes shrink by one factor (./inline3d-buffer-limit.js), warned
|
|
2364
|
+
* once per window. `win.bufScale` records the total shrink so buffer-px decoration
|
|
2365
|
+
* (cornerRadius, feather) keeps its on-screen size.
|
|
2366
|
+
*/
|
|
2367
|
+
_eyeSize(win, sbs = true) {
|
|
2345
2368
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2369
|
+
let w = win.reqW || Math.round((win.canvas.clientWidth || 256) * dpr);
|
|
2370
|
+
let h = win.reqH || Math.round((win.canvas.clientHeight || 256) * dpr);
|
|
2371
|
+
let scale = 1;
|
|
2372
|
+
const eye = !win.reqW && !win.reqH && this._sourceEyeSize(win);
|
|
2373
|
+
if (eye) {
|
|
2374
|
+
// The smallest buffer that still holds every source pixel along the axis that fills the box.
|
|
2375
|
+
const s = Math.max(eye.w / w, eye.h / h);
|
|
2376
|
+
if (s < 1) {
|
|
2377
|
+
scale = s;
|
|
2378
|
+
w = Math.max(1, Math.round(w * s));
|
|
2379
|
+
h = Math.max(1, Math.round(h * s));
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
const c = clampEyeBuffer(w, h, probeBufferLimits(), { cols: sbs ? 2 : 1 });
|
|
2383
|
+
if (c.clamped) {
|
|
2384
|
+
scale *= c.scale;
|
|
2385
|
+
w = c.eyeW;
|
|
2386
|
+
h = c.eyeH;
|
|
2387
|
+
if (!win.warnedBufClamp) {
|
|
2388
|
+
win.warnedBufClamp = true;
|
|
2389
|
+
console.warn(clampWarning('[inline3d]', c));
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
win.bufScale = scale;
|
|
2393
|
+
return { w, h };
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
/** One eye of an image/video window's source (an SBS pair: left eye = left half), or null. */
|
|
2397
|
+
_sourceEyeSize(win) {
|
|
2398
|
+
if (win.kind !== 'video' && win.kind !== 'image') return null;
|
|
2399
|
+
const src = win.kind === 'video' ? win.video : win.img;
|
|
2400
|
+
if (!src) return null;
|
|
2401
|
+
const sw = src.videoWidth || src.naturalWidth || src.width;
|
|
2402
|
+
const sh = src.videoHeight || src.naturalHeight || src.height;
|
|
2403
|
+
if (!(sw > 1) || !(sh > 0)) return null;
|
|
2404
|
+
return { w: sw / 2, h: sh };
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* A scene canvas is the app's to size, so the core cannot clamp it — but it can SEE the browser
|
|
2409
|
+
* clamp it. Where the store exceeds the device's MAX_TEXTURE_SIZE (4096 on an Adreno 740 tablet)
|
|
2410
|
+
* the drawing buffer silently comes out smaller than canvas.width, while getViewport() keeps
|
|
2411
|
+
* splitting canvas.width: the eye boundary lands off-centre and the panel shows a double image.
|
|
2412
|
+
* Say so once. ./viewer, ./splat and ./model clamp before sizing and never trip this.
|
|
2413
|
+
*
|
|
2414
|
+
* Only called after the app has drawn a stereo frame, so the canvas already HAS its context:
|
|
2415
|
+
* getContext() of the same type returns it, of another type returns null — it never creates one.
|
|
2416
|
+
*/
|
|
2417
|
+
_checkSceneBuffer(win) {
|
|
2418
|
+
const c = win.canvas;
|
|
2419
|
+
if (!c || typeof c.getContext !== 'function') return;
|
|
2420
|
+
if (win.sceneGl === undefined) {
|
|
2421
|
+
let gl = null;
|
|
2422
|
+
try {
|
|
2423
|
+
gl = c.getContext('webgl2') || c.getContext('webgl');
|
|
2424
|
+
} catch {
|
|
2425
|
+
gl = null;
|
|
2426
|
+
}
|
|
2427
|
+
win.sceneGl = gl && typeof gl.drawingBufferWidth === 'number' ? gl : null;
|
|
2428
|
+
}
|
|
2429
|
+
if (!win.sceneGl) return;
|
|
2430
|
+
const b = bufferScale(c, win.sceneGl);
|
|
2431
|
+
if (b.mismatch) {
|
|
2432
|
+
win.warnedBufMismatch = true;
|
|
2433
|
+
console.warn(
|
|
2434
|
+
mismatchWarning(
|
|
2435
|
+
'[inline3d]',
|
|
2436
|
+
c,
|
|
2437
|
+
b,
|
|
2438
|
+
'getViewport() splits canvas.width, so the eyes are off-centre in the woven buffer. This ' +
|
|
2439
|
+
'scene canvas is sized by the page: keep 2 × eye width and the height within ' +
|
|
2440
|
+
'MAX_TEXTURE_SIZE / MAX_RENDERBUFFER_SIZE / MAX_VIEWPORT_DIMS, or map getViewport() onto ' +
|
|
2441
|
+
'gl.drawingBufferWidth/Height.',
|
|
2442
|
+
),
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2350
2445
|
}
|
|
2351
2446
|
|
|
2352
2447
|
_sizeBuffer(win, sbs) {
|
|
2353
|
-
const { w: boxW, h: boxH } = this._eyeSize(win);
|
|
2448
|
+
const { w: boxW, h: boxH } = this._eyeSize(win, sbs);
|
|
2354
2449
|
win.eyeW = boxW;
|
|
2355
2450
|
win.eyeH = boxH;
|
|
2356
2451
|
win.canvas.width = sbs ? boxW * 2 : boxW; // SBS = two eye tiles wide
|
|
@@ -2473,10 +2568,25 @@ class Inline3D {
|
|
|
2473
2568
|
const srcW = src.videoWidth || src.naturalWidth || src.width;
|
|
2474
2569
|
const srcH = src.videoHeight || src.naturalHeight || src.height;
|
|
2475
2570
|
if (!srcW || !srcH) return;
|
|
2571
|
+
// The buffer is capped at the source's per-eye resolution (_eyeSize), and a source's size is
|
|
2572
|
+
// only known once it has decoded — or changes on a new title. Re-derive on a change; the
|
|
2573
|
+
// resize clears the buffer, and the paint right below refills it.
|
|
2574
|
+
const srcKey = srcW + 'x' + srcH;
|
|
2575
|
+
if (win.srcKey !== srcKey) {
|
|
2576
|
+
win.srcKey = srcKey;
|
|
2577
|
+
if (win.ownsBuffer && !(win.reqW && win.reqH)) {
|
|
2578
|
+
const { w, h } = this._eyeSize(win, win.sbs);
|
|
2579
|
+
if (w !== win.eyeW || h !== win.eyeH) this._sizeBuffer(win, win.sbs);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
// cornerRadius / feather are buffer px at the UNCAPPED size; keep their on-screen size.
|
|
2583
|
+
const k = win.bufScale || 1;
|
|
2584
|
+
const radius = win.cornerRadius * k;
|
|
2585
|
+
const feather = win.feather * k;
|
|
2476
2586
|
ctx.clearRect(0, 0, c.width, c.height);
|
|
2477
2587
|
if (!win.sbs) {
|
|
2478
2588
|
// Flat fallback: left eye only, stretched to the square buffer.
|
|
2479
|
-
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, c.width, c.height,
|
|
2589
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, c.width, c.height, radius, feather);
|
|
2480
2590
|
return;
|
|
2481
2591
|
}
|
|
2482
2592
|
const halfDst = c.width / 2;
|
|
@@ -2487,16 +2597,16 @@ class Inline3D {
|
|
|
2487
2597
|
// as a squeezed pair). See _trackBakedStereo.
|
|
2488
2598
|
const stereo = Math.min(this._stereoFactor, this._bakedStereo);
|
|
2489
2599
|
if (stereo < 1) {
|
|
2490
|
-
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height,
|
|
2491
|
-
drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo,
|
|
2600
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, radius, feather); // L
|
|
2601
|
+
drawEyeBlend(ctx, src, srcW / 2, srcH, halfDst, c.height, stereo, radius, feather); // L+R
|
|
2492
2602
|
return;
|
|
2493
2603
|
}
|
|
2494
2604
|
// A single stretched draw maps SBS source → SBS buffer (left→left, right→right); the
|
|
2495
2605
|
// per-eye path is only needed to bake decoration (rounded corners / edge feather), which
|
|
2496
2606
|
// MUST be applied to each eye separately — see drawEye/featherEye.
|
|
2497
|
-
if (
|
|
2498
|
-
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height,
|
|
2499
|
-
drawEye(ctx, src, srcW / 2, 0, srcW / 2, srcH, halfDst, 0, halfDst, c.height,
|
|
2607
|
+
if (radius > 0 || feather > 0) {
|
|
2608
|
+
drawEye(ctx, src, 0, 0, srcW / 2, srcH, 0, 0, halfDst, c.height, radius, feather); // L
|
|
2609
|
+
drawEye(ctx, src, srcW / 2, 0, srcW / 2, srcH, halfDst, 0, halfDst, c.height, radius, feather); // R
|
|
2500
2610
|
} else {
|
|
2501
2611
|
ctx.drawImage(src, 0, 0, srcW, srcH, 0, 0, c.width, c.height);
|
|
2502
2612
|
}
|
|
@@ -2656,7 +2766,10 @@ class Inline3D {
|
|
|
2656
2766
|
win.onFrame(views, win.layer, f);
|
|
2657
2767
|
// A stereo frame the page drew without throwing. A short view list is the load
|
|
2658
2768
|
// fallback (a mono frame), which is not what a poster is waiting for.
|
|
2659
|
-
if (views.length >= 2)
|
|
2769
|
+
if (views.length >= 2) {
|
|
2770
|
+
win.fwStereo = true;
|
|
2771
|
+
if (!win.warnedBufMismatch) this._checkSceneBuffer(win);
|
|
2772
|
+
}
|
|
2660
2773
|
} catch (err) {
|
|
2661
2774
|
if (!win.frameThrewWarned) {
|
|
2662
2775
|
win.frameThrewWarned = true;
|
package/model.d.ts
CHANGED
|
@@ -34,7 +34,11 @@ export interface ModelOptions {
|
|
|
34
34
|
* so a long subject still fits once the turntable turns it (default true).
|
|
35
35
|
*/
|
|
36
36
|
fitSweep?: boolean;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Per-eye buffer scale; 0.5–0.7 is usually free (default 1). Clamped to the device's GL limits: the whole store (2 × eye wide in 3D) is kept within
|
|
39
|
+
* min(MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS), both axes shrunk by one factor,
|
|
40
|
+
* with a one-time console warning. The handle's `renderScale` reports the value in force.
|
|
41
|
+
*/
|
|
38
42
|
renderScale?: number;
|
|
39
43
|
feather?: number;
|
|
40
44
|
/**
|
|
@@ -176,6 +180,11 @@ export interface ModelHandle {
|
|
|
176
180
|
/** The last pose the page set (a copy), or null. */
|
|
177
181
|
getCameraPose(): SplatCameraPose | null;
|
|
178
182
|
|
|
183
|
+
/** The per-eye buffer scale in force: the `renderScale` option times the device-limit clamp. */
|
|
184
|
+
readonly renderScale: number;
|
|
185
|
+
/** The `renderScale` option as given. */
|
|
186
|
+
readonly renderScaleRequested: number;
|
|
187
|
+
|
|
179
188
|
remove(): void;
|
|
180
189
|
exclude(el: Element): void;
|
|
181
190
|
unexclude(el: Element): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
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",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./player.d.ts",
|
|
37
37
|
"import": "./js/inline3d-player.js"
|
|
38
38
|
},
|
|
39
|
+
"./call": {
|
|
40
|
+
"types": "./call.d.ts",
|
|
41
|
+
"import": "./js/inline3d-call.js"
|
|
42
|
+
},
|
|
39
43
|
"./undock": {
|
|
40
44
|
"types": "./index.d.ts",
|
|
41
45
|
"import": "./js/inline3d-undock.js"
|
|
@@ -46,6 +50,7 @@
|
|
|
46
50
|
"js/inline3d.js",
|
|
47
51
|
"js/inline3d-undock.js",
|
|
48
52
|
"js/inline3d-mode-switch.js",
|
|
53
|
+
"js/inline3d-buffer-limit.js",
|
|
49
54
|
"js/inline3d-three.js",
|
|
50
55
|
"js/inline3d-viewer.js",
|
|
51
56
|
"js/inline3d-splat.js",
|
|
@@ -66,12 +71,15 @@
|
|
|
66
71
|
"js/inline3d-pc-look.js",
|
|
67
72
|
"js/inline3d-playcanvas-engine.js",
|
|
68
73
|
"js/inline3d-player.js",
|
|
74
|
+
"js/inline3d-call.js",
|
|
75
|
+
"js/call/",
|
|
69
76
|
"index.d.ts",
|
|
70
77
|
"three.d.ts",
|
|
71
78
|
"viewer.d.ts",
|
|
72
79
|
"splat.d.ts",
|
|
73
80
|
"model.d.ts",
|
|
74
81
|
"player.d.ts",
|
|
82
|
+
"call.d.ts",
|
|
75
83
|
"CHANGELOG.md",
|
|
76
84
|
"README.md",
|
|
77
85
|
"LICENSE",
|