@displayxr/inline3d 1.21.1 → 1.23.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 +37 -0
- package/js/inline3d-splat-diag.js +242 -4
- package/js/inline3d-splat-layer-rig.js +423 -0
- package/js/inline3d-splat-playcanvas.js +317 -4
- package/js/inline3d-splat-video.js +97 -0
- package/js/inline3d-splat.js +18 -0
- package/package.json +2 -1
- package/splat.d.ts +70 -1
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// inline3d-splat-layer-rig.js — handle.setLayerRig(): draw chosen PlayCanvas layers through the
|
|
2
|
+
// DISPLAY rig while the splat (and the runtime's declared rig) stay on the photo's CAMERA rig.
|
|
3
|
+
// Internal to ./inline3d-splat-playcanvas.js. Design: docs/proposals/layer-display-rig.md.
|
|
4
|
+
//
|
|
5
|
+
// NO KOOIMA HERE. The runtime's projection matrices are used verbatim; nothing below builds a
|
|
6
|
+
// frustum, a tangent or an off-axis matrix. What it does instead is an exact identity:
|
|
7
|
+
//
|
|
8
|
+
// A camera rig is a portal: every view looks through ONE window rectangle on the convergence
|
|
9
|
+
// plane (centre Wc = N0 + D·fwd), from its own eye E_i. The round (display-rig) views look
|
|
10
|
+
// through the SAME window from E'_i = N0 + k·(E_i − N0) — the runtime's ipd/parallax factors
|
|
11
|
+
// are linear about the nominal viewpoint, and the photo's camera rig is the display rig with
|
|
12
|
+
// both factors at n/D (displayxr-common's rig conversion). Two portals through one window
|
|
13
|
+
// differ only by the eye, and the affine shear M_i that fixes the window plane pointwise and
|
|
14
|
+
// sends E'_i to E_i turns one picture into the other:
|
|
15
|
+
//
|
|
16
|
+
// view_round_i = view_photo_i · M_i, proj_round_i = proj_photo_i
|
|
17
|
+
//
|
|
18
|
+
// k = D / (m·n): D the declared convergence (world units), m the declared metersToVirtual, n the
|
|
19
|
+
// nominal viewer distance in metres — the one quantity the browser does not expose (see the
|
|
20
|
+
// design note §5). Everything here is PLAIN ARITHMETIC on arrays, unit-testable without a GPU.
|
|
21
|
+
|
|
22
|
+
/** handle.setLayerRig's rigs. 'camera' is the default every layer starts on. */
|
|
23
|
+
export const LAYER_RIGS = Object.freeze(['display', 'camera']);
|
|
24
|
+
|
|
25
|
+
/** Nominal viewer distance (m) when neither the page nor the platform says: the browser's own. */
|
|
26
|
+
export const DEFAULT_VIEWER_DISTANCE_M = 0.6;
|
|
27
|
+
|
|
28
|
+
/** A round eye may not reach the window plane: h(E') is floored at this fraction of D. */
|
|
29
|
+
const MIN_EYE_HEIGHT_FRAC = 1e-3;
|
|
30
|
+
|
|
31
|
+
/** Rotate v by unit quaternion q (xyzw). */
|
|
32
|
+
function rotate(q, v) {
|
|
33
|
+
const [x, y, z, w] = q;
|
|
34
|
+
const tx = 2 * (y * v[2] - z * v[1]);
|
|
35
|
+
const ty = 2 * (z * v[0] - x * v[2]);
|
|
36
|
+
const tz = 2 * (x * v[1] - y * v[0]);
|
|
37
|
+
return [v[0] + w * tx + (y * tz - z * ty), v[1] + w * ty + (z * tx - x * tz), v[2] + w * tz + (x * ty - y * tx)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* setLayerRig's arguments, validated (throws at the call).
|
|
42
|
+
* @returns {{ rig: 'display'|'camera', viewerDistance: number|null, gain: number|null }}
|
|
43
|
+
*/
|
|
44
|
+
export function validateLayerRig(layer, rig, o = {}) {
|
|
45
|
+
if (!(typeof layer === 'string' && layer.length) && !(Number.isInteger(layer) && layer >= 0) && !(layer && typeof layer === 'object' && Number.isInteger(layer.id))) {
|
|
46
|
+
throw new TypeError('@displayxr/inline3d/splat: setLayerRig(layer, …) — expected a layer name, a layer id, or a pc.Layer.');
|
|
47
|
+
}
|
|
48
|
+
if (!LAYER_RIGS.includes(rig)) {
|
|
49
|
+
throw new Error(`@displayxr/inline3d/splat: setLayerRig — rig "${rig}", expected ${LAYER_RIGS.map((r) => `'${r}'`).join(' or ')}.`);
|
|
50
|
+
}
|
|
51
|
+
if (o === null || typeof o !== 'object') throw new TypeError('@displayxr/inline3d/splat: setLayerRig options must be an object.');
|
|
52
|
+
const num = (k) => {
|
|
53
|
+
if (o[k] === undefined) return null;
|
|
54
|
+
if (!Number.isFinite(o[k]) || !(o[k] > 0)) throw new Error(`@displayxr/inline3d/splat: setLayerRig — bad ${k}: ${o[k]}.`);
|
|
55
|
+
return o[k];
|
|
56
|
+
};
|
|
57
|
+
return { rig, viewerDistance: num('viewerDistance'), gain: num('gain') };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The frame of a declared CAMERA-rig descriptor (XRViewRigInit shape), or null for anything else
|
|
62
|
+
* (a display rig, no rig, a convergence at infinity — no finite window to hold fixed).
|
|
63
|
+
* @returns {{ N0: number[], fwd: number[], D: number, m: number }|null}
|
|
64
|
+
*/
|
|
65
|
+
export function cameraRigFrame(rig) {
|
|
66
|
+
if (!rig || rig.type !== 'camera') return null;
|
|
67
|
+
const inv = rig.convergenceDiopters;
|
|
68
|
+
if (!Number.isFinite(inv) || !(inv > 0)) return null;
|
|
69
|
+
const p = rig.position || { x: 0, y: 0, z: 0 };
|
|
70
|
+
const o = rig.orientation || { x: 0, y: 0, z: 0, w: 1 };
|
|
71
|
+
const m = Number.isFinite(rig.metersToVirtual) && rig.metersToVirtual > 0 ? rig.metersToVirtual : 1;
|
|
72
|
+
const fwd = rotate([o.x || 0, o.y || 0, o.z || 0, o.w === undefined ? 1 : o.w], [0, 0, -1]);
|
|
73
|
+
return { N0: [p.x || 0, p.y || 0, p.z || 0], fwd, D: 1 / inv, m };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** k = D / (m·n), or the caller's explicit gain. */
|
|
77
|
+
export function layerRigGain(frame, { viewerDistance = null, gain = null } = {}) {
|
|
78
|
+
if (gain !== null) return gain;
|
|
79
|
+
const n = viewerDistance !== null ? viewerDistance : DEFAULT_VIEWER_DISTANCE_M;
|
|
80
|
+
return frame.D / (frame.m * n);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The window-fixing shear for one eye, as a column-major 4×4 (and its inverse), in the space the
|
|
85
|
+
* declared rig and the views share.
|
|
86
|
+
*
|
|
87
|
+
* M(X) = X + v · (ẑ·X − c), ẑ = −fwd, c = ẑ·Wc, v = (E − E') / h(E'), h(X) = ẑ·X − c
|
|
88
|
+
* M⁻¹ = the same form with v' = −v / (1 + ẑ·v)
|
|
89
|
+
*
|
|
90
|
+
* @param {number[]} E the runtime's eye position (the view pose's translation).
|
|
91
|
+
* @returns {{ M: Float64Array, Minv: Float64Array, Eround: number[] }}
|
|
92
|
+
*/
|
|
93
|
+
export function windowShear(E, frame, k, M = new Float64Array(16), Minv = new Float64Array(16)) {
|
|
94
|
+
const { N0, fwd, D } = frame;
|
|
95
|
+
const z = [-fwd[0], -fwd[1], -fwd[2]];
|
|
96
|
+
const Wc = [N0[0] + D * fwd[0], N0[1] + D * fwd[1], N0[2] + D * fwd[2]];
|
|
97
|
+
const c = z[0] * Wc[0] + z[1] * Wc[1] + z[2] * Wc[2];
|
|
98
|
+
const Er = [N0[0] + k * (E[0] - N0[0]), N0[1] + k * (E[1] - N0[1]), N0[2] + k * (E[2] - N0[2])];
|
|
99
|
+
let h = z[0] * Er[0] + z[1] * Er[1] + z[2] * Er[2] - c;
|
|
100
|
+
const hMin = MIN_EYE_HEIGHT_FRAC * D;
|
|
101
|
+
if (!(h > hMin)) {
|
|
102
|
+
// Leaning into the glass with a large gain: keep the round eye in front of the window.
|
|
103
|
+
const dh = hMin - h;
|
|
104
|
+
Er[0] += dh * z[0];
|
|
105
|
+
Er[1] += dh * z[1];
|
|
106
|
+
Er[2] += dh * z[2];
|
|
107
|
+
h = hMin;
|
|
108
|
+
}
|
|
109
|
+
const v = [(E[0] - Er[0]) / h, (E[1] - Er[1]) / h, (E[2] - Er[2]) / h];
|
|
110
|
+
shearMatrix(v, z, c, M);
|
|
111
|
+
const s = 1 + z[0] * v[0] + z[1] * v[1] + z[2] * v[2];
|
|
112
|
+
shearMatrix([-v[0] / s, -v[1] / s, -v[2] / s], z, c, Minv);
|
|
113
|
+
return { M, Minv, Eround: Er };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** X ↦ X + v·(z·X − c), column-major. */
|
|
117
|
+
function shearMatrix(v, z, c, out) {
|
|
118
|
+
for (let col = 0; col < 3; col++) {
|
|
119
|
+
for (let row = 0; row < 3; row++) out[col * 4 + row] = (row === col ? 1 : 0) + v[row] * z[col];
|
|
120
|
+
out[col * 4 + 3] = 0;
|
|
121
|
+
}
|
|
122
|
+
out[12] = -v[0] * c;
|
|
123
|
+
out[13] = -v[1] * c;
|
|
124
|
+
out[14] = -v[2] * c;
|
|
125
|
+
out[15] = 1;
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Unit quaternion (xyzw) of a rigid column-major 4×4's rotation. */
|
|
130
|
+
function quatOf(m) {
|
|
131
|
+
const m11 = m[0], m12 = m[4], m13 = m[8], m21 = m[1], m22 = m[5], m23 = m[9], m31 = m[2], m32 = m[6], m33 = m[10];
|
|
132
|
+
const tr = m11 + m22 + m33;
|
|
133
|
+
if (tr > 0) {
|
|
134
|
+
const s = 0.5 / Math.sqrt(tr + 1);
|
|
135
|
+
return [(m32 - m23) * s, (m13 - m31) * s, (m21 - m12) * s, 0.25 / s];
|
|
136
|
+
}
|
|
137
|
+
if (m11 > m22 && m11 > m33) {
|
|
138
|
+
const s = 2 * Math.sqrt(1 + m11 - m22 - m33);
|
|
139
|
+
return [0.25 * s, (m12 + m21) / s, (m13 + m31) / s, (m32 - m23) / s];
|
|
140
|
+
}
|
|
141
|
+
if (m22 > m33) {
|
|
142
|
+
const s = 2 * Math.sqrt(1 + m22 - m11 - m33);
|
|
143
|
+
return [(m12 + m21) / s, 0.25 * s, (m23 + m32) / s, (m13 - m31) / s];
|
|
144
|
+
}
|
|
145
|
+
const s = 2 * Math.sqrt(1 + m33 - m11 - m22);
|
|
146
|
+
return [(m13 + m31) / s, (m23 + m32) / s, 0.25 * s, (m21 - m12) / s];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** a × b, column-major (local, so this module stays standalone). */
|
|
150
|
+
export function mul4(a, b, out = new Float64Array(16)) {
|
|
151
|
+
const r = new Float64Array(16);
|
|
152
|
+
for (let col = 0; col < 4; col++) {
|
|
153
|
+
for (let row = 0; row < 4; row++) {
|
|
154
|
+
r[col * 4 + row] = a[row] * b[col * 4] + a[4 + row] * b[col * 4 + 1] + a[8 + row] * b[col * 4 + 2] + a[12 + row] * b[col * 4 + 3];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
out.set(r);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Inverse of a rigid (rotation + translation) column-major 4×4. */
|
|
162
|
+
export function invertRigid(m, out = new Float64Array(16)) {
|
|
163
|
+
const r = [m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10]];
|
|
164
|
+
out[0] = r[0]; out[1] = r[3]; out[2] = r[6]; out[3] = 0;
|
|
165
|
+
out[4] = r[1]; out[5] = r[4]; out[6] = r[7]; out[7] = 0;
|
|
166
|
+
out[8] = r[2]; out[9] = r[5]; out[10] = r[8]; out[11] = 0;
|
|
167
|
+
const tx = m[12], ty = m[13], tz = m[14];
|
|
168
|
+
out[12] = -(out[0] * tx + out[4] * ty + out[8] * tz);
|
|
169
|
+
out[13] = -(out[1] * tx + out[5] * ty + out[9] * tz);
|
|
170
|
+
out[14] = -(out[2] * tx + out[6] * ty + out[10] * tz);
|
|
171
|
+
out[15] = 1;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The round views for one frame: for each runtime view pose P_i (camera → rig space, rigid),
|
|
177
|
+
* `viewInv = M_i⁻¹ · P_i` and `view = P_i⁻¹ · M_i` — what RenderView.setView takes. Null (the
|
|
178
|
+
* caller keeps the photo views) when there is nothing to round: no camera rig, fewer than two
|
|
179
|
+
* views, or a gain of exactly 1.
|
|
180
|
+
*/
|
|
181
|
+
export function roundViews(entries, frame, k, into = []) {
|
|
182
|
+
if (!frame || !entries || entries.length < 2 || !(k > 0) || k === 1) return null;
|
|
183
|
+
into.length = entries.length;
|
|
184
|
+
for (let i = 0; i < entries.length; i++) {
|
|
185
|
+
const P = entries[i].pose;
|
|
186
|
+
const o = (into[i] ||= { viewInv: new Float64Array(16), view: new Float64Array(16), M: new Float64Array(16), Minv: new Float64Array(16), eye: [0, 0, 0] });
|
|
187
|
+
const { Eround } = windowShear([P[12], P[13], P[14]], frame, k, o.M, o.Minv);
|
|
188
|
+
o.eye[0] = Eround[0];
|
|
189
|
+
o.eye[1] = Eround[1];
|
|
190
|
+
o.eye[2] = Eround[2];
|
|
191
|
+
mul4(o.Minv, P, o.viewInv);
|
|
192
|
+
mul4(invertRigid(P), o.M, o.view);
|
|
193
|
+
}
|
|
194
|
+
return into;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Split a layer composition into contiguous RUNS for per-rig cameras. `entries` is the
|
|
199
|
+
* composition's sublayer list as layer ids, in draw order (opaque and transparent sublayers both
|
|
200
|
+
* appear); `display` the set of display-rig layer ids; `eyeLayers` the ids the eye camera draws.
|
|
201
|
+
* PlayCanvas orders render actions by CAMERA, then layer — so keeping today's order with a second
|
|
202
|
+
* rig needs one camera per run:
|
|
203
|
+
* pre — eye-camera layers with any sublayer before the first display sublayer (stay put);
|
|
204
|
+
* display — the display layers the eye camera drew;
|
|
205
|
+
* post — eye-camera layers whose every sublayer comes after the last display sublayer.
|
|
206
|
+
* A non-display layer INTERLEAVED between display sublayers stays in `pre` (it would otherwise
|
|
207
|
+
* split a run) and is reported in `interleaved`.
|
|
208
|
+
*/
|
|
209
|
+
export function layerRuns(entries, display, eyeLayers) {
|
|
210
|
+
const eye = new Set(eyeLayers);
|
|
211
|
+
let first = -1;
|
|
212
|
+
let last = -1;
|
|
213
|
+
for (let i = 0; i < entries.length; i++) {
|
|
214
|
+
if (display.has(entries[i]) && eye.has(entries[i])) {
|
|
215
|
+
if (first < 0) first = i;
|
|
216
|
+
last = i;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const out = { pre: [], display: [], post: [], interleaved: [] };
|
|
220
|
+
if (first < 0) {
|
|
221
|
+
out.pre = [...eye];
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
for (const id of eyeLayers) {
|
|
226
|
+
if (seen.has(id)) continue;
|
|
227
|
+
seen.add(id);
|
|
228
|
+
if (display.has(id)) {
|
|
229
|
+
out.display.push(id);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const at = [];
|
|
233
|
+
for (let i = 0; i < entries.length; i++) if (entries[i] === id) at.push(i);
|
|
234
|
+
if (at.length && at.every((i) => i > last)) out.post.push(id);
|
|
235
|
+
else {
|
|
236
|
+
out.pre.push(id);
|
|
237
|
+
if (at.some((i) => i > first && i < last)) out.interleaved.push(id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Keep the run cameras' layer lists in draw order (the composition's, not the eye's list).
|
|
241
|
+
const order = (ids) => ids.sort((a, b) => entries.indexOf(a) - entries.indexOf(b));
|
|
242
|
+
order(out.display);
|
|
243
|
+
order(out.post);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── the engine side ──────────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The per-rig cameras of one tile. Owned by the viewer; `sync` + `frame` are called by its draw.
|
|
251
|
+
* The eye camera keeps its `layers` minus what moved; `restore()` puts them back.
|
|
252
|
+
*/
|
|
253
|
+
export class LayerRigCameras {
|
|
254
|
+
constructor(viewer) {
|
|
255
|
+
this.viewer = viewer;
|
|
256
|
+
/** What the page asked for: name or id → { viewerDistance, gain }. */
|
|
257
|
+
this.requests = new Map();
|
|
258
|
+
/** Those, resolved: layer id → options (the ones present in the composition right now). */
|
|
259
|
+
this.layers = new Map();
|
|
260
|
+
/** The gain options in force (the last setLayerRig call's — one gain per tile). */
|
|
261
|
+
this.opts = {};
|
|
262
|
+
this.disabled = false; // the kill switch (diag 'nolayerrig')
|
|
263
|
+
this.cams = { display: null, post: null };
|
|
264
|
+
this.moved = { display: [], post: [] };
|
|
265
|
+
this._key = '';
|
|
266
|
+
this._rvs = { display: [], post: [] };
|
|
267
|
+
this._round = [];
|
|
268
|
+
/** Diagnostics: what the last frame did. */
|
|
269
|
+
this.last = { rounded: false, gain: null, views: 0 };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
get active() {
|
|
273
|
+
return !this.disabled && this.requests.size > 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Resolve a name / id / pc.Layer to an id in this tile's composition (null if unknown). */
|
|
277
|
+
resolve(layer) {
|
|
278
|
+
const comp = this.viewer.app?.scene?.layers;
|
|
279
|
+
if (!comp) return null;
|
|
280
|
+
if (layer && typeof layer === 'object') return layer.id;
|
|
281
|
+
if (Number.isInteger(layer)) return layer;
|
|
282
|
+
const l = comp.getLayerByName?.(layer);
|
|
283
|
+
return l ? l.id : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Record a request by the caller's key (name, id): resolved against the composition on every
|
|
288
|
+
* sync, so a layer the page adds AFTER calling setLayerRig is picked up when it appears.
|
|
289
|
+
*/
|
|
290
|
+
set(key, rig, o) {
|
|
291
|
+
const k = key && typeof key === 'object' ? key.id : key;
|
|
292
|
+
if (rig === 'display') this.requests.set(k, o);
|
|
293
|
+
else this.requests.delete(k);
|
|
294
|
+
this.opts = o;
|
|
295
|
+
this._key = ''; // re-split on the next draw
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_resolveAll() {
|
|
299
|
+
this.layers.clear();
|
|
300
|
+
for (const [k, o] of this.requests) {
|
|
301
|
+
const id = this.resolve(k);
|
|
302
|
+
if (id !== null && id !== undefined) this.layers.set(id, o);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_makeCam(name, priority) {
|
|
307
|
+
const v = this.viewer;
|
|
308
|
+
const cam = v._makeCamera(name, null);
|
|
309
|
+
cam.camera.priority = priority;
|
|
310
|
+
cam.camera.clearColorBuffer = false;
|
|
311
|
+
cam.camera.clearDepthBuffer = false;
|
|
312
|
+
cam.camera.clearStencilBuffer = false;
|
|
313
|
+
cam.camera.frustumCulling = false;
|
|
314
|
+
cam.camera.layers = [];
|
|
315
|
+
return cam;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Re-split the composition when the display set, the eye's layers or the composition moved. */
|
|
319
|
+
sync() {
|
|
320
|
+
const v = this.viewer;
|
|
321
|
+
const eyeCam = v.eye?.camera;
|
|
322
|
+
const comp = v.app?.scene?.layers;
|
|
323
|
+
if (!eyeCam || !comp) return false;
|
|
324
|
+
if (!this.active) {
|
|
325
|
+
if (this.moved.display.length || this.moved.post.length) this.restore();
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
this._resolveAll();
|
|
329
|
+
const entries = (comp.layerList || []).map((l) => l.id);
|
|
330
|
+
const eyeLayers = [...(eyeCam.layers || []), ...this.moved.display, ...this.moved.post];
|
|
331
|
+
const key = `${[...this.layers.keys()].join(',')}|${entries.join(',')}|${(eyeCam.layers || []).join(',')}`;
|
|
332
|
+
if (key === this._key) return this.moved.display.length > 0;
|
|
333
|
+
const runs = layerRuns(entries, new Set(this.layers.keys()), eyeLayers);
|
|
334
|
+
if (runs.interleaved.length && !this._warnedInterleaved) {
|
|
335
|
+
this._warnedInterleaved = true;
|
|
336
|
+
console.warn(
|
|
337
|
+
`[inline3d/splat] setLayerRig: layer(s) ${runs.interleaved.join(', ')} sit BETWEEN display-rig ` +
|
|
338
|
+
'sublayers; they are drawn before them (with the eye camera). Move them before or after the display layers.',
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
this.moved.display = runs.display;
|
|
342
|
+
this.moved.post = runs.post;
|
|
343
|
+
eyeCam.layers = runs.pre;
|
|
344
|
+
if (runs.display.length) {
|
|
345
|
+
this.cams.display ||= this._makeCam('inline3d-eye-display', 1);
|
|
346
|
+
this.cams.display.camera.layers = runs.display;
|
|
347
|
+
}
|
|
348
|
+
if (runs.post.length) {
|
|
349
|
+
this.cams.post ||= this._makeCam('inline3d-eye-post', 2);
|
|
350
|
+
this.cams.post.camera.layers = runs.post;
|
|
351
|
+
}
|
|
352
|
+
if (this.cams.display) this.cams.display.enabled = runs.display.length > 0;
|
|
353
|
+
if (this.cams.post) this.cams.post.enabled = runs.post.length > 0;
|
|
354
|
+
this._key = `${[...this.layers.keys()].join(',')}|${entries.join(',')}|${eyeCam.layers.join(',')}`;
|
|
355
|
+
return runs.display.length > 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Every moved layer back on the eye camera, run cameras off (the kill switch / last layer off). */
|
|
359
|
+
restore() {
|
|
360
|
+
const eyeCam = this.viewer.eye?.camera;
|
|
361
|
+
if (eyeCam) {
|
|
362
|
+
const back = [...this.moved.display, ...this.moved.post];
|
|
363
|
+
eyeCam.layers = [...(eyeCam.layers || []), ...back.filter((id) => !(eyeCam.layers || []).includes(id))];
|
|
364
|
+
}
|
|
365
|
+
this.moved = { display: [], post: [] };
|
|
366
|
+
if (this.cams.display) this.cams.display.enabled = false;
|
|
367
|
+
if (this.cams.post) this.cams.post.enabled = false;
|
|
368
|
+
this._key = '';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Per drawn frame, after the eye camera's RenderViews are set. `rig` is the descriptor the
|
|
373
|
+
* runtime located these views with (the viewer snapshots it before the tick can declare anew).
|
|
374
|
+
*/
|
|
375
|
+
frame(entries, rect, f, rig) {
|
|
376
|
+
const pc = this.viewer.pc;
|
|
377
|
+
this.last.rounded = false;
|
|
378
|
+
this.last.views = entries.length;
|
|
379
|
+
const frame = cameraRigFrame(rig);
|
|
380
|
+
const k = frame ? layerRigGain(frame, this.opts || {}) : null;
|
|
381
|
+
const round = frame ? roundViews(entries, frame, k, this._round) : null;
|
|
382
|
+
this.last.gain = k;
|
|
383
|
+
for (const run of ['display', 'post']) {
|
|
384
|
+
const cam = this.cams[run];
|
|
385
|
+
if (!cam || !cam.enabled) continue;
|
|
386
|
+
const rvs = this._rvs[run];
|
|
387
|
+
if (rvs.length !== entries.length) {
|
|
388
|
+
rvs.length = 0;
|
|
389
|
+
for (let i = 0; i < entries.length; i++) rvs.push(new pc.RenderView());
|
|
390
|
+
cam.camera.camera.xrViews = rvs.slice();
|
|
391
|
+
}
|
|
392
|
+
const r = run === 'display' ? round : null;
|
|
393
|
+
for (let i = 0; i < entries.length; i++) {
|
|
394
|
+
const e = entries[i];
|
|
395
|
+
if (r) rvs[i].setView(e.proj, r[i].viewInv, r[i].view);
|
|
396
|
+
else rvs[i].setView(e.proj, e.pose);
|
|
397
|
+
const [x, y, w, h] = rect(e);
|
|
398
|
+
rvs[i].setViewport(x, y, w, h);
|
|
399
|
+
}
|
|
400
|
+
if (cam._frustumKey !== this.viewer._frustumKey) {
|
|
401
|
+
cam._frustumKey = this.viewer._frustumKey;
|
|
402
|
+
cam.camera.camera.setXrProperties({ ...f, horizontalFov: false });
|
|
403
|
+
}
|
|
404
|
+
// The node drives transparent sorting (the views ignore it): on the (round) first eye.
|
|
405
|
+
const p = entries[0].pose;
|
|
406
|
+
const q = quatOf(p);
|
|
407
|
+
cam.setLocalRotation(q[0], q[1], q[2], q[3]);
|
|
408
|
+
if (r) cam.setLocalPosition(r[0].eye[0], r[0].eye[1], r[0].eye[2]);
|
|
409
|
+
else cam.setLocalPosition(p[12], p[13], p[14]);
|
|
410
|
+
if (run === 'display') {
|
|
411
|
+
// Two camera spaces never share a depth test; in mono (and at gain 1) they are one space.
|
|
412
|
+
cam.camera.clearDepthBuffer = !!r;
|
|
413
|
+
this.last.rounded = !!r;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
destroy() {
|
|
419
|
+
this.restore();
|
|
420
|
+
for (const c of Object.values(this.cams)) c?.destroy?.();
|
|
421
|
+
this.cams = { display: null, post: null };
|
|
422
|
+
}
|
|
423
|
+
}
|