@displayxr/inline3d 1.29.0 → 1.31.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 +187 -0
- package/README.md +9 -5
- package/call.d.ts +318 -238
- package/camera.d.ts +257 -0
- package/dist/call.js +17 -12
- package/index.d.ts +56 -0
- package/js/call/capture.js +4 -212
- package/js/call/disparity.js +4 -432
- package/js/call/element.js +14 -10
- package/js/call/options.js +132 -0
- package/js/call/signaling.js +66 -9
- package/js/call/ui.js +106 -9
- package/js/call/wire.js +8 -153
- package/js/camera/capture.js +220 -0
- package/js/camera/converge.js +82 -0
- package/js/camera/disparity.js +432 -0
- package/js/camera/geometry.js +80 -0
- package/js/camera/metadata.js +398 -0
- package/js/inline3d-call.js +435 -320
- package/js/inline3d-camera.js +678 -0
- package/js/inline3d-model-playcanvas.js +1 -0
- package/js/inline3d-model.js +1 -0
- package/js/inline3d-splat-deferred.js +153 -0
- package/js/inline3d-splat-pc-entry.js +45 -0
- package/js/inline3d-splat-playcanvas.js +14 -3
- package/js/inline3d-splat.js +3 -145
- package/js/inline3d-viewer-ease.js +360 -0
- package/js/inline3d-viewer.js +15 -4
- package/js/inline3d.js +13 -0
- package/model.d.ts +8 -1
- package/package.json +22 -2
- package/splat-playcanvas.d.ts +4 -0
- package/splat.d.ts +8 -1
- package/viewer.d.ts +9 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// inline3d-viewer-ease.js — ease the eye views across a tracking acquisition or loss.
|
|
2
|
+
//
|
|
3
|
+
// THE SNAP THIS REMOVES. While nobody is tracked the session's views come from the runtime's
|
|
4
|
+
// NOMINAL viewer (two eyes straight in front of the window at the nominal distance). The frame a
|
|
5
|
+
// viewer is acquired, the views jump to the tracked eyes — tens to hundreds of millimetres in one
|
|
6
|
+
// frame — and so does everything drawn from them: the per-eye cameras, head parallax, display-rig
|
|
7
|
+
// layers. The reverse happens on a loss wherever the vendor does not animate it. The browser
|
|
8
|
+
// passes the views through and the core SDK deliberately does too (inline3d.js `_frame`), so the
|
|
9
|
+
// ease lives in the SDK's RENDERERS (SceneViewer, the PlayCanvas splat and model viewers), on the
|
|
10
|
+
// matrices they already copy out of every view, and every page that renders through them gets it.
|
|
11
|
+
//
|
|
12
|
+
// WHAT IT DOES. On a trigger, the difference between what was DRAWN last frame and what the
|
|
13
|
+
// session reports now is frozen as an offset, and the offset decays to zero over `durationMs`
|
|
14
|
+
// (smoothstep by default) while the live views keep moving underneath — so the eyes leave from
|
|
15
|
+
// where they were and arrive on the tracked viewer without lagging behind it once the window ends.
|
|
16
|
+
//
|
|
17
|
+
// pose_out = pose_now + w · A_now · d_local d_local = A_then⁻¹ · (eye_drawn − eye_now)
|
|
18
|
+
// proj_out = proj_now + w · (proj_drawn − proj_now) w = 1 − ease(t / duration)
|
|
19
|
+
//
|
|
20
|
+
// The position offset is held in the VIEW's own frame (A = the pose's 3×3), so a rig the page
|
|
21
|
+
// moves during the ease (orbit, zoom) carries the offset with it. The projection is offset
|
|
22
|
+
// element-wise, and that is exact, not an approximation: every DisplayXR frustum (the display
|
|
23
|
+
// rig's window-relative Kooima and the camera rig's portal, displayxr-common dxr_view_math.c) has
|
|
24
|
+
// its off-axis terms and its focal terms AFFINE in the eye position, with fixed near/far — so the
|
|
25
|
+
// eased (pose, proj) pair is the view the runtime itself would report for the eased eye. A view's
|
|
26
|
+
// frustum still crosses its rig's window exactly, which is what ./inline3d-splat-rig-map.js
|
|
27
|
+
// reads the located rig off.
|
|
28
|
+
//
|
|
29
|
+
// TRIGGERS, in order of trust:
|
|
30
|
+
// 1. `trackingState` edge ('searching' <-> 'tracking', DisplayXR Browser patch 0195+). For
|
|
31
|
+
// `armMs` after it (the edge frame included) any frame whose eyes move more than `armedJump`
|
|
32
|
+
// (in eye separations) starts — or restarts — the ease from what was drawn, because the edge
|
|
33
|
+
// and the jump need not land on the same frame. An edge without a jump changes nothing.
|
|
34
|
+
// 2. Where the browser reports no state ('unknown'), a jump of more than `jump` eye separations
|
|
35
|
+
// in one frame. A seated viewer's head moves well under that per frame; a nominal ↔ tracked
|
|
36
|
+
// switch is a multiple of it.
|
|
37
|
+
// Eye positions for the jump test are read off the PROJECTIONS (focal and off-axis terms), in
|
|
38
|
+
// eye-separation units, so the test is blind to rig changes, scene scale and canvas size.
|
|
39
|
+
//
|
|
40
|
+
// WHAT IT LEAVES ALONE. A view-count change (a 2D <-> 3D rendering-mode switch: the eased mode
|
|
41
|
+
// switch, ./inline3d-mode-switch.js, owns that) and a buffer reallocation reset it. One view
|
|
42
|
+
// (mono) is passed through. Replayed (last-good) frames are not re-eased. The panel's own optical
|
|
43
|
+
// switch (lens/backlight state on acquisition) is hardware and is not touched by anything here.
|
|
44
|
+
//
|
|
45
|
+
// Pure arithmetic on Float32Array(16) pairs; no WebXR, no DOM. test/viewer-ease.test.mjs.
|
|
46
|
+
|
|
47
|
+
/** Default ease window (ms). Long enough to read as a glide, short enough not to feel laggy. */
|
|
48
|
+
export const VIEWER_EASE_DEFAULT_MS = 300;
|
|
49
|
+
/** Default curve: smoothstep (the mode switch's own default; zero velocity at both ends). */
|
|
50
|
+
export const VIEWER_EASE_DEFAULT_EASING = 'smoothstep';
|
|
51
|
+
/** Untracked-state jump detector: eye movement in one frame, in eye separations. */
|
|
52
|
+
export const VIEWER_EASE_JUMP = 0.5;
|
|
53
|
+
/** After a tracking edge: the (lower) per-frame movement that restarts the ease, in separations. */
|
|
54
|
+
export const VIEWER_EASE_ARMED_JUMP = 0.15;
|
|
55
|
+
/** How long after a tracking edge the lower threshold applies (ms). */
|
|
56
|
+
export const VIEWER_EASE_ARM_MS = 600;
|
|
57
|
+
|
|
58
|
+
const EASINGS = {
|
|
59
|
+
linear: (t) => t,
|
|
60
|
+
smoothstep: (t) => t * t * (3 - 2 * t),
|
|
61
|
+
easeoutcubic: (t) => 1 - (1 - t) ** 3,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const now = () => (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now());
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a page's `viewerEase` option. `false` / `{ enabled:false }` / `{ durationMs:0 }`
|
|
68
|
+
* disables; `true` / undefined is the default; an object overrides fields.
|
|
69
|
+
*
|
|
70
|
+
* @param {boolean|{enabled?:boolean,durationMs?:number,easing?:string}} [opt]
|
|
71
|
+
* @returns {{enabled:boolean,durationMs:number,easing:string}}
|
|
72
|
+
*/
|
|
73
|
+
export function resolveViewerEaseOption(opt) {
|
|
74
|
+
const out = { enabled: true, durationMs: VIEWER_EASE_DEFAULT_MS, easing: VIEWER_EASE_DEFAULT_EASING };
|
|
75
|
+
if (opt === false) out.enabled = false;
|
|
76
|
+
else if (opt && typeof opt === 'object') {
|
|
77
|
+
if (opt.enabled === false) out.enabled = false;
|
|
78
|
+
if (Number.isFinite(opt.durationMs) && opt.durationMs >= 0) out.durationMs = opt.durationMs;
|
|
79
|
+
if (typeof opt.easing === 'string') {
|
|
80
|
+
const k = opt.easing.toLowerCase().replace(/[-_\s]/g, '');
|
|
81
|
+
if (EASINGS[k]) out.easing = k;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!(out.durationMs > 0)) out.enabled = false;
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Eye position in window-width units, read off a projection's focal and off-axis terms. */
|
|
89
|
+
function eyeFromProj(P, out) {
|
|
90
|
+
const p0 = P[0];
|
|
91
|
+
const p5 = P[5];
|
|
92
|
+
const aspect = p5 !== 0 ? p0 / p5 : 1; // = H / W for a window-relative frustum
|
|
93
|
+
out[0] = -P[8] / 2;
|
|
94
|
+
out[1] = (-P[9] / 2) * aspect;
|
|
95
|
+
out[2] = p0 / 2;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
100
|
+
|
|
101
|
+
/** Inverse of the upper-left 3×3 of a column-major 4×4, into a 9-array (column-major). False if singular. */
|
|
102
|
+
function inv3(M, out) {
|
|
103
|
+
const a = M[0], b = M[4], c = M[8];
|
|
104
|
+
const d = M[1], e = M[5], f = M[9];
|
|
105
|
+
const g = M[2], h = M[6], i = M[10];
|
|
106
|
+
const A = e * i - f * h;
|
|
107
|
+
const B = -(d * i - f * g);
|
|
108
|
+
const C = d * h - e * g;
|
|
109
|
+
const det = a * A + b * B + c * C;
|
|
110
|
+
if (!(Math.abs(det) > 1e-12)) return false;
|
|
111
|
+
const k = 1 / det;
|
|
112
|
+
// row-major inverse entries
|
|
113
|
+
const r00 = A * k, r01 = -(b * i - c * h) * k, r02 = (b * f - c * e) * k;
|
|
114
|
+
const r10 = B * k, r11 = (a * i - c * g) * k, r12 = -(a * f - c * d) * k;
|
|
115
|
+
const r20 = C * k, r21 = -(a * h - b * g) * k, r22 = (a * e - b * d) * k;
|
|
116
|
+
out[0] = r00; out[3] = r01; out[6] = r02;
|
|
117
|
+
out[1] = r10; out[4] = r11; out[7] = r12;
|
|
118
|
+
out[2] = r20; out[5] = r21; out[8] = r22;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* One renderer's ease state. Feed it the matrices it is about to draw, once per LIVE frame,
|
|
124
|
+
* in place; it rewrites them while an ease is running and leaves them untouched otherwise.
|
|
125
|
+
*/
|
|
126
|
+
export class ViewerEase {
|
|
127
|
+
/**
|
|
128
|
+
* @param {object} [o] resolveViewerEaseOption() fields, plus test knobs (`clock`, `jump`,
|
|
129
|
+
* `armedJump`, `armMs`).
|
|
130
|
+
*/
|
|
131
|
+
constructor(o = {}) {
|
|
132
|
+
const r = resolveViewerEaseOption(o);
|
|
133
|
+
this.enabled = r.enabled;
|
|
134
|
+
this.durationMs = r.durationMs;
|
|
135
|
+
this.easing = r.easing;
|
|
136
|
+
this.jump = Number.isFinite(o.jump) ? o.jump : VIEWER_EASE_JUMP;
|
|
137
|
+
this.armedJump = Number.isFinite(o.armedJump) ? o.armedJump : VIEWER_EASE_ARMED_JUMP;
|
|
138
|
+
this.armMs = Number.isFinite(o.armMs) ? o.armMs : VIEWER_EASE_ARM_MS;
|
|
139
|
+
this.clock = typeof o.clock === 'function' ? o.clock : now;
|
|
140
|
+
this.reset();
|
|
141
|
+
/** What the last apply() did, for diagnostics: { weight, reason: null|'armed-jump'|'jump'|'easing', jump }. */
|
|
142
|
+
this.last = { weight: 0, reason: null, jump: 0 };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Change the tuning at run time (the same shapes the option takes). */
|
|
146
|
+
configure(opt) {
|
|
147
|
+
const r = resolveViewerEaseOption(opt);
|
|
148
|
+
this.enabled = r.enabled;
|
|
149
|
+
this.durationMs = r.durationMs;
|
|
150
|
+
this.easing = r.easing;
|
|
151
|
+
if (!this.enabled) this._active = false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Forget everything (a view-count change, a buffer reallocation, a mono stretch). */
|
|
155
|
+
reset() {
|
|
156
|
+
this._n = 0;
|
|
157
|
+
this._drawn = []; // per view: { proj, pose } as DRAWN last frame
|
|
158
|
+
this._seen = []; // per view: eye (window units) as REPORTED last frame
|
|
159
|
+
this._off = []; // per view: { d: [3] local, dp: Float64Array(16) }
|
|
160
|
+
this._active = false;
|
|
161
|
+
this._t0 = 0;
|
|
162
|
+
this._state = null; // last tracking state seen
|
|
163
|
+
this._armedUntil = -Infinity;
|
|
164
|
+
this._eye = [0, 0, 0];
|
|
165
|
+
this._inv = new Float64Array(9);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Is an ease running right now? */
|
|
169
|
+
get active() {
|
|
170
|
+
return this._active;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ease these entries in place. `entries[i].proj` / `.pose` are column-major Float32Array(16)
|
|
175
|
+
* (or any 16-array), already holding THIS frame's reported matrices.
|
|
176
|
+
*
|
|
177
|
+
* @param {Array<{proj:ArrayLike<number>,pose:ArrayLike<number>}>} entries
|
|
178
|
+
* @param {'tracking'|'searching'|'unknown'|string|null} [trackingState]
|
|
179
|
+
* @param {number} [t] ms; defaults to the clock.
|
|
180
|
+
* @returns {number} the weight applied this frame (0 = passed through, 1 = fully the old views).
|
|
181
|
+
*/
|
|
182
|
+
apply(entries, trackingState, t = this.clock()) {
|
|
183
|
+
const L = this.last;
|
|
184
|
+
L.weight = 0;
|
|
185
|
+
L.reason = null;
|
|
186
|
+
L.jump = 0;
|
|
187
|
+
const n = entries ? entries.length : 0;
|
|
188
|
+
if (!this.enabled || n < 2) {
|
|
189
|
+
if (n !== this._n) this.reset();
|
|
190
|
+
this._remember(entries, n);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
if (n !== this._n) {
|
|
194
|
+
// A view-count change is a rendering-mode switch: the mode switch's ramp owns it.
|
|
195
|
+
this.reset();
|
|
196
|
+
this._remember(entries, n);
|
|
197
|
+
this._state = trackingState || null;
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── triggers ──
|
|
202
|
+
const state = trackingState === 'tracking' || trackingState === 'searching' ? trackingState : null;
|
|
203
|
+
const edge = state !== null && this._state !== null && state !== this._state;
|
|
204
|
+
if (state !== null) this._state = state;
|
|
205
|
+
if (edge) this._armedUntil = t + this.armMs;
|
|
206
|
+
|
|
207
|
+
const jump = this._jump(entries, n);
|
|
208
|
+
L.jump = jump;
|
|
209
|
+
// An edge only ARMS: it is the jump that is eased, on whichever frame it lands (the state can
|
|
210
|
+
// lead the views by a frame). An edge with no jump — a vendor that already animates the eyes —
|
|
211
|
+
// changes nothing.
|
|
212
|
+
let trigger = null;
|
|
213
|
+
if (t <= this._armedUntil && jump > this.armedJump) trigger = 'armed-jump';
|
|
214
|
+
else if (state === null && jump > this.jump) trigger = 'jump';
|
|
215
|
+
|
|
216
|
+
if (trigger) {
|
|
217
|
+
this._start(entries, n, t);
|
|
218
|
+
L.reason = trigger;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── apply ──
|
|
222
|
+
let w = 0;
|
|
223
|
+
if (this._active) {
|
|
224
|
+
const u = this.durationMs > 0 ? (t - this._t0) / this.durationMs : 1;
|
|
225
|
+
if (u >= 1) this._active = false;
|
|
226
|
+
else {
|
|
227
|
+
w = 1 - EASINGS[this.easing](Math.max(0, u));
|
|
228
|
+
for (let i = 0; i < n; i++) {
|
|
229
|
+
const e = entries[i];
|
|
230
|
+
const o = this._off[i];
|
|
231
|
+
const P = e.pose;
|
|
232
|
+
const d = o.d;
|
|
233
|
+
// A_now · d_local, added to the live position.
|
|
234
|
+
P[12] += w * (P[0] * d[0] + P[4] * d[1] + P[8] * d[2]);
|
|
235
|
+
P[13] += w * (P[1] * d[0] + P[5] * d[1] + P[9] * d[2]);
|
|
236
|
+
P[14] += w * (P[2] * d[0] + P[6] * d[1] + P[10] * d[2]);
|
|
237
|
+
const Q = e.proj;
|
|
238
|
+
for (let k = 0; k < 16; k++) Q[k] += w * o.dp[k];
|
|
239
|
+
}
|
|
240
|
+
if (!L.reason) L.reason = 'easing';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
L.weight = w;
|
|
244
|
+
this._remember(entries, n);
|
|
245
|
+
return w;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Largest per-view eye move since the last frame, in eye separations (0 when unknown). */
|
|
249
|
+
_jump(entries, n) {
|
|
250
|
+
if (this._seen.length !== n) return 0;
|
|
251
|
+
const eye = this._eye;
|
|
252
|
+
let sep = 0;
|
|
253
|
+
const a = eyeFromProj(entries[0].proj, [0, 0, 0]);
|
|
254
|
+
const b = eyeFromProj(entries[n - 1].proj, [0, 0, 0]);
|
|
255
|
+
sep = dist3(a, b);
|
|
256
|
+
const prevSep = dist3(this._seen[0], this._seen[n - 1]);
|
|
257
|
+
// Floor: a typical IPD at this distance (~0.1 of the eye depth), so a vendor pair collapsed
|
|
258
|
+
// onto one point (separation ~0) does not turn every millimetre into a "jump".
|
|
259
|
+
const s = Math.max(sep, prevSep, 0.1 * Math.abs((a[2] + b[2]) / 2));
|
|
260
|
+
if (!(s > 1e-6)) return 0;
|
|
261
|
+
let m = 0;
|
|
262
|
+
for (let i = 0; i < n; i++) {
|
|
263
|
+
eyeFromProj(entries[i].proj, eye);
|
|
264
|
+
m = Math.max(m, dist3(eye, this._seen[i]));
|
|
265
|
+
}
|
|
266
|
+
return m / s;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Freeze the offset between what was drawn last frame and what is reported now. */
|
|
270
|
+
_start(entries, n, t) {
|
|
271
|
+
if (this._drawn.length !== n) return;
|
|
272
|
+
for (let i = 0; i < n; i++) {
|
|
273
|
+
const e = entries[i];
|
|
274
|
+
const D = this._drawn[i];
|
|
275
|
+
const o = (this._off[i] ||= { d: [0, 0, 0], dp: new Float64Array(16) });
|
|
276
|
+
const dx = D.pose[12] - e.pose[12];
|
|
277
|
+
const dy = D.pose[13] - e.pose[13];
|
|
278
|
+
const dz = D.pose[14] - e.pose[14];
|
|
279
|
+
if (inv3(e.pose, this._inv)) {
|
|
280
|
+
const I = this._inv;
|
|
281
|
+
o.d[0] = I[0] * dx + I[3] * dy + I[6] * dz;
|
|
282
|
+
o.d[1] = I[1] * dx + I[4] * dy + I[7] * dz;
|
|
283
|
+
o.d[2] = I[2] * dx + I[5] * dy + I[8] * dz;
|
|
284
|
+
} else {
|
|
285
|
+
o.d[0] = dx;
|
|
286
|
+
o.d[1] = dy;
|
|
287
|
+
o.d[2] = dz;
|
|
288
|
+
}
|
|
289
|
+
for (let k = 0; k < 16; k++) o.dp[k] = D.proj[k] - e.proj[k];
|
|
290
|
+
}
|
|
291
|
+
this._active = true;
|
|
292
|
+
this._t0 = t;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Keep this frame's drawn matrices and reported eyes for the next one. */
|
|
296
|
+
_remember(entries, n) {
|
|
297
|
+
this._n = n;
|
|
298
|
+
if (!this.enabled || n < 2) {
|
|
299
|
+
this._drawn.length = 0;
|
|
300
|
+
this._seen.length = 0;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
for (let i = 0; i < n; i++) {
|
|
304
|
+
const e = entries[i];
|
|
305
|
+
const D = (this._drawn[i] ||= { proj: new Float64Array(16), pose: new Float64Array(16) });
|
|
306
|
+
for (let k = 0; k < 16; k++) {
|
|
307
|
+
D.proj[k] = e.proj[k];
|
|
308
|
+
D.pose[k] = e.pose[k];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this._drawn.length = n;
|
|
312
|
+
this._seen.length = n;
|
|
313
|
+
// The REPORTED eyes: undo this frame's offset from the projection before keeping it.
|
|
314
|
+
const w = this.last.weight;
|
|
315
|
+
const raw = (this._raw ||= new Float64Array(16));
|
|
316
|
+
for (let i = 0; i < n; i++) {
|
|
317
|
+
const Q = entries[i].proj;
|
|
318
|
+
const o = this._off[i];
|
|
319
|
+
if (w > 0 && o) for (let k = 0; k < 16; k++) raw[k] = Q[k] - w * o.dp[k];
|
|
320
|
+
this._seen[i] = eyeFromProj(w > 0 && o ? raw : Q, this._seen[i] || [0, 0, 0]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The session's tracking state as the frame sees it, or null (no frame / old browser). */
|
|
326
|
+
export function frameTrackingState(frame) {
|
|
327
|
+
try {
|
|
328
|
+
const s = frame && frame.session ? frame.session.trackingState : undefined;
|
|
329
|
+
return s === 'tracking' || s === 'searching' || s === 'unknown' ? s : null;
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── session-wide default ──
|
|
336
|
+
// `createInline3D({ viewerEase })` sets the default for every renderer drawing through that
|
|
337
|
+
// session; a renderer's own `viewerEase` option overrides it. Keyed by the XRSession object, so
|
|
338
|
+
// renderers find it from the XRFrame they are handed and need no reference to the manager.
|
|
339
|
+
const SESSION_OPTS = new WeakMap();
|
|
340
|
+
|
|
341
|
+
/** Record a session's `viewerEase` option (createInline3D does this). */
|
|
342
|
+
export function setSessionViewerEase(session, opt) {
|
|
343
|
+
if (session && (typeof session === 'object' || typeof session === 'function')) SESSION_OPTS.set(session, opt);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The ViewerEase a renderer should use for frames of this session: its own option when it has
|
|
348
|
+
* one (anything but undefined), else the session's, else the default (on).
|
|
349
|
+
*/
|
|
350
|
+
export function viewerEaseFor(frame, windowOpt) {
|
|
351
|
+
let opt = windowOpt;
|
|
352
|
+
if (opt === undefined) {
|
|
353
|
+
try {
|
|
354
|
+
opt = frame && frame.session ? SESSION_OPTS.get(frame.session) : undefined;
|
|
355
|
+
} catch {
|
|
356
|
+
opt = undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return new ViewerEase(opt === true ? undefined : opt);
|
|
360
|
+
}
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
scaleViewport,
|
|
73
73
|
mismatchWarning,
|
|
74
74
|
} from './inline3d-buffer-limit.js';
|
|
75
|
+
import { viewerEaseFor, frameTrackingState } from './inline3d-viewer-ease.js';
|
|
75
76
|
|
|
76
77
|
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
|
77
78
|
// NaN/Infinity into a transform silently blanks the tile — three propagates it into the
|
|
@@ -281,9 +282,14 @@ export class SceneViewer {
|
|
|
281
282
|
renderScale = 1,
|
|
282
283
|
feather = 0,
|
|
283
284
|
pitchLimit = PITCH_LIMIT,
|
|
285
|
+
viewerEase,
|
|
284
286
|
} = opts;
|
|
285
287
|
|
|
286
288
|
this._THREE = THREE;
|
|
289
|
+
// The tracking-acquisition ease (./inline3d-viewer-ease.js): undefined = the session's
|
|
290
|
+
// createInline3D({ viewerEase }) default. Built on the first 3D frame, which knows the session.
|
|
291
|
+
this._viewerEaseOpt = viewerEase;
|
|
292
|
+
this.viewerEase = null;
|
|
287
293
|
this.canvas = canvas;
|
|
288
294
|
this.vH = virtualDisplayHeight;
|
|
289
295
|
this.fit = fit;
|
|
@@ -681,7 +687,7 @@ export class SceneViewer {
|
|
|
681
687
|
* stale sub-rect, which smears). A one-frame-stale eye pose is imperceptible; a smear and a
|
|
682
688
|
* black frame are not.
|
|
683
689
|
*/
|
|
684
|
-
onFrame(views, layer) {
|
|
690
|
+
onFrame(views, layer, frame) {
|
|
685
691
|
if (this._disposed) return;
|
|
686
692
|
// A lazily-activated tile can start weaving after the page already fell back to mono (or
|
|
687
693
|
// after a scroll-away/scroll-back). Take the buffer back to the SBS shape when that happens
|
|
@@ -725,7 +731,12 @@ export class SceneViewer {
|
|
|
725
731
|
vps.push(vp);
|
|
726
732
|
}
|
|
727
733
|
|
|
728
|
-
// Validated: this frame WILL draw over everything it clears.
|
|
734
|
+
// Validated: this frame WILL draw over everything it clears. Copy the matrices first (the
|
|
735
|
+
// last-good cache), ease a tracking acquisition/loss on the copies, and draw from them, so
|
|
736
|
+
// the live draw and a later replay are the same numbers.
|
|
737
|
+
this._cacheGood(views, vps, !eye);
|
|
738
|
+
const g = this._lastGood;
|
|
739
|
+
if (eye) (this.viewerEase ||= viewerEaseFor(frame, this._viewerEaseOpt)).apply(g.entries, frameTrackingState(frame));
|
|
729
740
|
const r = this.renderer;
|
|
730
741
|
// getViewport() splits canvas.width; a drawing buffer the browser clamped behind our back
|
|
731
742
|
// (bufferScale().mismatch) gets the same split mapped onto its real size, never canvas.width/2.
|
|
@@ -737,7 +748,7 @@ export class SceneViewer {
|
|
|
737
748
|
r.setViewport(vp.x, vp.y, vp.width, vp.height);
|
|
738
749
|
r.setScissor(vp.x, vp.y, vp.width, vp.height);
|
|
739
750
|
if (eye) {
|
|
740
|
-
eye.
|
|
751
|
+
eye.setFromMatrices(g.entries[i].proj, g.entries[i].pose);
|
|
741
752
|
r.render(this.scene, eye.camera);
|
|
742
753
|
} else {
|
|
743
754
|
r.render(this.scene, this.monoCamera);
|
|
@@ -745,7 +756,6 @@ export class SceneViewer {
|
|
|
745
756
|
if (this._feather) this._feather.render(r, vp);
|
|
746
757
|
}
|
|
747
758
|
r.setScissorTest(false);
|
|
748
|
-
this._cacheGood(views, vps, !eye);
|
|
749
759
|
}
|
|
750
760
|
|
|
751
761
|
/**
|
|
@@ -977,6 +987,7 @@ export class SceneViewer {
|
|
|
977
987
|
if (this._disposed) return;
|
|
978
988
|
const box = this.canvas.getBoundingClientRect();
|
|
979
989
|
if (box.width < 1 || box.height < 1) return;
|
|
990
|
+
this.viewerEase?.reset(); // a new window size moves every projection: not a viewer jump
|
|
980
991
|
const dpr = Math.min(window.devicePixelRatio || 1, 2) * this.renderScale;
|
|
981
992
|
// Clamp to the device's GL limits BEFORE sizing (./inline3d-buffer-limit.js): a store past
|
|
982
993
|
// MAX_TEXTURE_SIZE is silently clamped by the browser while getViewport() keeps splitting
|
package/js/inline3d.js
CHANGED
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
// three entry points are re-exported below so a page has a single import site.
|
|
32
32
|
import { undock, undockAvailable, undockUrl, tileScreenRect, setUndockLayerResolver } from './inline3d-undock.js';
|
|
33
33
|
export { undock, undockAvailable, undockUrl, tileScreenRect };
|
|
34
|
+
import { setSessionViewerEase } from './inline3d-viewer-ease.js';
|
|
35
|
+
// The tracking-acquisition ease, for pages that render their own addScene from the views.
|
|
36
|
+
export { ViewerEase, resolveViewerEaseOption, frameTrackingState, VIEWER_EASE_DEFAULT_MS } from './inline3d-viewer-ease.js';
|
|
34
37
|
|
|
35
38
|
// The eased 2D<->3D transition. A pure state machine (no DOM, no WebXR) ported from the native
|
|
36
39
|
// `dxr::ModeSwitch`, so the browser eases the disparity around a mode switch the same way — and in
|
|
@@ -558,6 +561,14 @@ function noteRigWinsOverHeight() {
|
|
|
558
561
|
* layer; no buffer reallocation) and back on 'tracking', over the mode switch's duration;
|
|
559
562
|
* 'unknown' leaves it where it is. Scene windows are never touched: the page owns those
|
|
560
563
|
* pixels — listen for `trackingstatechange` and do the same for a scene.
|
|
564
|
+
* @param {boolean|{enabled?:boolean,durationMs?:number,easing?:string}} [opts.viewerEase] The
|
|
565
|
+
* default, for every SDK renderer drawing through this session (`./viewer`'s SceneViewer,
|
|
566
|
+
* `./splat`, `./model`, both engines), of the TRACKING EASE: when a viewer is acquired
|
|
567
|
+
* (the views jump from the runtime's nominal viewer to the tracked eyes) or lost, the eye
|
|
568
|
+
* views glide from where they were drawn to the new ones over `durationMs` (300) on
|
|
569
|
+
* `easing` ('smoothstep' | 'linear' | 'easeOutCubic') instead of snapping in one frame.
|
|
570
|
+
* On by default; `false` restores the snap. A renderer's own `viewerEase` option
|
|
571
|
+
* overrides this. A page rendering its own addScene uses the exported `ViewerEase`.
|
|
561
572
|
* @returns {Promise<Inline3D | {supported:false, trackingState:'unknown', error?:Error}>} the
|
|
562
573
|
* manager, which also carries
|
|
563
574
|
* the display API (`getDisplayInfo` / `getRenderingModes` / `requestRenderingMode` /
|
|
@@ -572,6 +583,7 @@ export async function createInline3D(opts = {}) {
|
|
|
572
583
|
autoChrome = true,
|
|
573
584
|
modeSwitch = null,
|
|
574
585
|
untrackedFallback = 'none',
|
|
586
|
+
viewerEase,
|
|
575
587
|
} = opts;
|
|
576
588
|
// The unsupported shapes carry `trackingState` too, so a page can read it without branching on
|
|
577
589
|
// `supported` first. There is no on() here to pair with it: nothing would ever fire.
|
|
@@ -584,6 +596,7 @@ export async function createInline3D(opts = {}) {
|
|
|
584
596
|
} catch (e) {
|
|
585
597
|
return { supported: false, trackingState: 'unknown', error: e };
|
|
586
598
|
}
|
|
599
|
+
if (viewerEase !== undefined) setSessionViewerEase(session, viewerEase);
|
|
587
600
|
let refSpace = null;
|
|
588
601
|
try {
|
|
589
602
|
refSpace = await session.requestReferenceSpace(referenceSpace);
|
package/model.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
4
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
|
-
import type { FirstWovenResult } from './index.js';
|
|
5
|
+
import type { FirstWovenResult, ViewerEaseOption } from './index.js';
|
|
6
6
|
import type { PlayCanvasSplatViewer, SplatCameraPose, SplatFrameInfo } from './splat.js';
|
|
7
7
|
|
|
8
8
|
export interface ModelOptions {
|
|
@@ -41,6 +41,13 @@ export interface ModelOptions {
|
|
|
41
41
|
*/
|
|
42
42
|
renderScale?: number;
|
|
43
43
|
feather?: number;
|
|
44
|
+
/**
|
|
45
|
+
* The tracking ease (default on): when a viewer is acquired or lost and the views jump (the
|
|
46
|
+
* runtime's nominal viewer <-> the tracked eyes) the eye cameras glide from where they were
|
|
47
|
+
* drawn to the new views instead of snapping in one frame. Unset: the session's
|
|
48
|
+
* `createInline3D({ viewerEase })`, else on. `false` restores the snap.
|
|
49
|
+
*/
|
|
50
|
+
viewerEase?: ViewerEaseOption;
|
|
44
51
|
/**
|
|
45
52
|
* How the mesh is lit.
|
|
46
53
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Turn any HTML <canvas> into a
|
|
3
|
+
"version": "1.31.0",
|
|
4
|
+
"description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
7
7
|
"main": "./js/inline3d.js",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./splat.d.ts",
|
|
24
24
|
"import": "./js/inline3d-splat.js"
|
|
25
25
|
},
|
|
26
|
+
"./splat/playcanvas": {
|
|
27
|
+
"types": "./splat-playcanvas.d.ts",
|
|
28
|
+
"import": "./js/inline3d-splat-pc-entry.js"
|
|
29
|
+
},
|
|
26
30
|
"./model": {
|
|
27
31
|
"types": "./model.d.ts",
|
|
28
32
|
"import": "./js/inline3d-model-entry.js"
|
|
@@ -36,6 +40,10 @@
|
|
|
36
40
|
"types": "./player.d.ts",
|
|
37
41
|
"import": "./js/inline3d-player.js"
|
|
38
42
|
},
|
|
43
|
+
"./camera": {
|
|
44
|
+
"types": "./camera.d.ts",
|
|
45
|
+
"import": "./js/inline3d-camera.js"
|
|
46
|
+
},
|
|
39
47
|
"./call": {
|
|
40
48
|
"types": "./call.d.ts",
|
|
41
49
|
"import": "./js/inline3d-call.js"
|
|
@@ -59,9 +67,12 @@
|
|
|
59
67
|
"js/inline3d-undock.js",
|
|
60
68
|
"js/inline3d-mode-switch.js",
|
|
61
69
|
"js/inline3d-buffer-limit.js",
|
|
70
|
+
"js/inline3d-viewer-ease.js",
|
|
62
71
|
"js/inline3d-three.js",
|
|
63
72
|
"js/inline3d-viewer.js",
|
|
64
73
|
"js/inline3d-splat.js",
|
|
74
|
+
"js/inline3d-splat-pc-entry.js",
|
|
75
|
+
"js/inline3d-splat-deferred.js",
|
|
65
76
|
"js/inline3d-sog.js",
|
|
66
77
|
"js/inline3d-splat-perf.js",
|
|
67
78
|
"js/inline3d-splat-rig.js",
|
|
@@ -79,8 +90,10 @@
|
|
|
79
90
|
"js/inline3d-pc-look.js",
|
|
80
91
|
"js/inline3d-playcanvas-engine.js",
|
|
81
92
|
"js/inline3d-player.js",
|
|
93
|
+
"js/inline3d-camera.js",
|
|
82
94
|
"js/inline3d-call.js",
|
|
83
95
|
"js/inline3d-call-full.js",
|
|
96
|
+
"js/camera/",
|
|
84
97
|
"js/call/",
|
|
85
98
|
"js/lift/",
|
|
86
99
|
"dist/",
|
|
@@ -88,8 +101,10 @@
|
|
|
88
101
|
"three.d.ts",
|
|
89
102
|
"viewer.d.ts",
|
|
90
103
|
"splat.d.ts",
|
|
104
|
+
"splat-playcanvas.d.ts",
|
|
91
105
|
"model.d.ts",
|
|
92
106
|
"player.d.ts",
|
|
107
|
+
"camera.d.ts",
|
|
93
108
|
"call.d.ts",
|
|
94
109
|
"call-full.d.ts",
|
|
95
110
|
"CHANGELOG.md",
|
|
@@ -102,6 +117,7 @@
|
|
|
102
117
|
"webxr",
|
|
103
118
|
"inline-3d",
|
|
104
119
|
"glasses-free-3d",
|
|
120
|
+
"spatial-display",
|
|
105
121
|
"stereo",
|
|
106
122
|
"3d-display",
|
|
107
123
|
"lightfield",
|
|
@@ -149,6 +165,10 @@
|
|
|
149
165
|
"build:dist": "node tools/dist/build.mjs",
|
|
150
166
|
"prepack": "node tools/dist/build.mjs",
|
|
151
167
|
"test:e2e": "node test/e2e/call-element.e2e.mjs",
|
|
168
|
+
"test:e2e:camera": "node test/e2e/camera.e2e.mjs",
|
|
169
|
+
"api:check": "node tools/api-surface.mjs --check",
|
|
170
|
+
"api:update": "node tools/api-surface.mjs --update",
|
|
171
|
+
"check:snippets": "node tools/check-snippets.mjs",
|
|
152
172
|
"test:bundlers": "node test/bundlers/run.mjs"
|
|
153
173
|
},
|
|
154
174
|
"devDependencies": {
|
package/splat.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
4
|
import type { SceneViewer, SubjectBounds, OrbitPose } from './viewer.js';
|
|
5
|
-
import type { FirstWovenResult } from './index.js';
|
|
5
|
+
import type { FirstWovenResult, ViewerEaseOption } from './index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The knobs behind `SplatOptions.perf`. Every one is a Spark 2.1.0 setting except `alphaRadius`,
|
|
@@ -462,6 +462,13 @@ export interface SplatOptions {
|
|
|
462
462
|
sky?: boolean;
|
|
463
463
|
/** PlayCanvas: a CAP on the projection's far plane (only ever lowers it). Unset: untouched. */
|
|
464
464
|
farClip?: number;
|
|
465
|
+
/**
|
|
466
|
+
* The tracking ease (default on): when a viewer is acquired or lost and the views jump (the
|
|
467
|
+
* runtime's nominal viewer <-> the tracked eyes) the eye cameras glide from where they were
|
|
468
|
+
* drawn to the new views instead of snapping in one frame. Unset: the session's
|
|
469
|
+
* `createInline3D({ viewerEase })`, else on. `false` restores the snap.
|
|
470
|
+
*/
|
|
471
|
+
viewerEase?: ViewerEaseOption;
|
|
465
472
|
/** Camera rig only: the distance in world metres that sits ON the glass. */
|
|
466
473
|
convergence?: number;
|
|
467
474
|
/**
|
package/viewer.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Type definitions for @displayxr/inline3d/viewer.
|
|
2
2
|
// EXPERIMENTAL — not covered by the 1.x semver promise. See docs/sdk-stability.md.
|
|
3
3
|
|
|
4
|
+
import type { ViewerEaseOption } from './index.js';
|
|
5
|
+
|
|
4
6
|
/** Model-space bounds of the subject a viewer frames on. */
|
|
5
7
|
export interface SubjectBounds {
|
|
6
8
|
center: number[];
|
|
@@ -39,6 +41,13 @@ export interface SceneViewerOptions {
|
|
|
39
41
|
feather?: number;
|
|
40
42
|
/** Pitch clamp in degrees (default [-60, 60]). */
|
|
41
43
|
pitchLimit?: number[];
|
|
44
|
+
/**
|
|
45
|
+
* The tracking ease (default on): when a viewer is acquired or lost and the views jump (the
|
|
46
|
+
* runtime's nominal viewer <-> the tracked eyes) the eye cameras glide from where they were
|
|
47
|
+
* drawn to the new views instead of snapping in one frame. Unset: the session's
|
|
48
|
+
* `createInline3D({ viewerEase })`, else on. `false` restores the snap.
|
|
49
|
+
*/
|
|
50
|
+
viewerEase?: ViewerEaseOption;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export interface OrbitPose {
|