@displayxr/inline3d 1.30.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 +63 -0
- package/README.md +5 -5
- package/call.d.ts +53 -4
- package/dist/call.js +4 -4
- package/index.d.ts +56 -0
- package/js/call/signaling.js +66 -9
- package/js/inline3d-call.js +31 -1
- package/js/inline3d-model-playcanvas.js +1 -0
- package/js/inline3d-model.js +1 -0
- package/js/inline3d-splat-playcanvas.js +14 -3
- package/js/inline3d-splat.js +1 -0
- 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 +4 -2
- package/splat.d.ts +8 -1
- package/viewer.d.ts +9 -0
package/index.d.ts
CHANGED
|
@@ -555,7 +555,63 @@ export interface CreateInline3DOptions {
|
|
|
555
555
|
* `trackingstatechange` and do the same for a scene.
|
|
556
556
|
*/
|
|
557
557
|
untrackedFallback?: 'none' | 'mono';
|
|
558
|
+
/**
|
|
559
|
+
* The TRACKING EASE, as the default for every SDK renderer drawing through this session
|
|
560
|
+
* (`./viewer`'s SceneViewer, `./splat` and `./model` on both engines). When a viewer is
|
|
561
|
+
* acquired — the views jump from the runtime's nominal viewer to the tracked eyes — or lost,
|
|
562
|
+
* the eye cameras glide from where they were drawn to the new views over `durationMs` instead
|
|
563
|
+
* of snapping in one frame; everything parented to the views (parallax, display-rig layers)
|
|
564
|
+
* follows. Triggered by a jump in the views around a `trackingState` edge (or, where the browser
|
|
565
|
+
* reports no state, by a large one-frame jump); a no-op when the vendor already animates the eyes. On by default; `false` restores
|
|
566
|
+
* the snap; a renderer's own `viewerEase` option overrides this. A page rendering its own
|
|
567
|
+
* `addScene` can use the exported {@link ViewerEase}.
|
|
568
|
+
*/
|
|
569
|
+
viewerEase?: ViewerEaseOption;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** `true`/unset = on with defaults, `false` = off, or tune it. */
|
|
573
|
+
export type ViewerEaseOption =
|
|
574
|
+
| boolean
|
|
575
|
+
| {
|
|
576
|
+
/** Default true. */
|
|
577
|
+
enabled?: boolean;
|
|
578
|
+
/** The glide, in ms (default 300). 0 disables. */
|
|
579
|
+
durationMs?: number;
|
|
580
|
+
/** 'smoothstep' (default) | 'linear' | 'easeOutCubic'. */
|
|
581
|
+
easing?: 'smoothstep' | 'linear' | 'easeOutCubic' | string;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/** One frame's matrices as an SDK renderer copies them out of an XRView (column-major). */
|
|
585
|
+
export interface ViewerEaseEntry {
|
|
586
|
+
proj: Float32Array | Float64Array | number[];
|
|
587
|
+
pose: Float32Array | Float64Array | number[];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* The tracking ease on its own, for a page that renders its own `addScene`: copy each view's
|
|
592
|
+
* `projectionMatrix` and `transform.matrix` into an entry, call `apply(entries,
|
|
593
|
+
* frame.session.trackingState)` once per LIVE frame, and render from the entries (keep using the
|
|
594
|
+
* real XRView for `layer.getViewport`). It rewrites the entries only while an ease is running.
|
|
595
|
+
*/
|
|
596
|
+
export declare class ViewerEase {
|
|
597
|
+
constructor(opts?: ViewerEaseOption & object);
|
|
598
|
+
readonly enabled: boolean;
|
|
599
|
+
readonly durationMs: number;
|
|
600
|
+
readonly easing: string;
|
|
601
|
+
/** Is an ease running right now? */
|
|
602
|
+
readonly active: boolean;
|
|
603
|
+
/** What the last apply() did: the weight of the old views (0 = passed through), the trigger, the jump (eye separations). */
|
|
604
|
+
readonly last: { weight: number; reason: null | 'armed-jump' | 'jump' | 'easing'; jump: number };
|
|
605
|
+
apply(entries: ViewerEaseEntry[], trackingState?: XRTrackingState | string | null, timeMs?: number): number;
|
|
606
|
+
configure(opts?: ViewerEaseOption): void;
|
|
607
|
+
/** Forget the previous frame (call after a resize or a view-count change you handle yourself). */
|
|
608
|
+
reset(): void;
|
|
558
609
|
}
|
|
610
|
+
export function resolveViewerEaseOption(opt?: ViewerEaseOption): { enabled: boolean; durationMs: number; easing: string };
|
|
611
|
+
/** `frame.session.trackingState`, or null when the browser does not report one. */
|
|
612
|
+
export function frameTrackingState(frame: unknown): XRTrackingState | null;
|
|
613
|
+
/** 300. */
|
|
614
|
+
export const VIEWER_EASE_DEFAULT_MS: number;
|
|
559
615
|
|
|
560
616
|
/**
|
|
561
617
|
* The eased 2D<->3D transition — on by default, and the same sequencer (and the same defaults)
|
package/js/call/signaling.js
CHANGED
|
@@ -53,6 +53,18 @@ export async function roomKey(room) {
|
|
|
53
53
|
*/
|
|
54
54
|
export const DXR_SIGNAL_DEFAULT = 'wss://dxr-signal.displayxr.workers.dev';
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Every host the hosted server answers on. The SAME Worker serves all of them (rooms are shared),
|
|
58
|
+
* so the adapter fails over between them when one cannot be reached: the first entry is tried
|
|
59
|
+
* first. `signal.displayxr.org` is the target of the domain move (RFC 0003 §5g); the workers.dev
|
|
60
|
+
* host stays an alias through the transition. Only applies to the hosted default — a self-hosted
|
|
61
|
+
* URL is tried as given (pass `aliases` to add your own fallbacks).
|
|
62
|
+
*/
|
|
63
|
+
export const DXR_SIGNAL_ALIASES = Object.freeze([DXR_SIGNAL_DEFAULT, 'wss://signal.displayxr.org']);
|
|
64
|
+
|
|
65
|
+
/** Join-time error codes from the server that mean "do not retry with the same inputs". */
|
|
66
|
+
const FATAL_JOIN_CODES = ['bad-room', 'bad-id', 'bad-version', 'bad-key', 'origin-not-allowed', 'blocked'];
|
|
67
|
+
|
|
56
68
|
export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
57
69
|
if (!url || typeof url !== 'string') {
|
|
58
70
|
throw codedError(
|
|
@@ -60,16 +72,21 @@ export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
|
60
72
|
'dxrSignaling(url): url must be a ws:// or wss:// string (omit it for the hosted DisplayXR server)'
|
|
61
73
|
);
|
|
62
74
|
}
|
|
63
|
-
const
|
|
75
|
+
const strip = (u) => String(u).replace(/\/+$/, '');
|
|
76
|
+
const base = strip(url);
|
|
64
77
|
const WS = opts.WebSocket || globalThis.WebSocket;
|
|
65
78
|
const pingMs = opts.pingMs || 20000;
|
|
79
|
+
const extra = Array.isArray(opts.aliases) ? opts.aliases.filter((a) => typeof a === 'string' && a).map(strip) : DXR_SIGNAL_ALIASES.includes(base) ? [...DXR_SIGNAL_ALIASES] : [];
|
|
80
|
+
const bases = [base, ...extra.filter((b) => b !== base)];
|
|
81
|
+
let baseIdx = 0;
|
|
66
82
|
|
|
67
83
|
return {
|
|
68
84
|
name: 'dxr',
|
|
69
85
|
url: base,
|
|
70
86
|
async join(room, hooks) {
|
|
71
87
|
const key = await roomKey(room);
|
|
72
|
-
const
|
|
88
|
+
const query = `k=${key}${typeof opts.key === 'string' && opts.key ? `&key=${encodeURIComponent(opts.key)}` : ''}`;
|
|
89
|
+
const endpointAt = (i) => `${bases[i]}/v1/connect?${query}`;
|
|
73
90
|
let ws = null;
|
|
74
91
|
let left = false;
|
|
75
92
|
let attempt = 0;
|
|
@@ -77,11 +94,15 @@ export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
|
77
94
|
let known = new Set();
|
|
78
95
|
let queue = [];
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
// One open() tries every alias once (in order from the last one that worked) before it
|
|
98
|
+
// reports `signaling-unreachable`; a host that cannot be reached is skipped, not waited on.
|
|
99
|
+
const open = (isReconnect) => openAt(isReconnect, bases.length);
|
|
100
|
+
const openAt = (isReconnect, triesLeft) =>
|
|
81
101
|
new Promise((resolve, reject) => {
|
|
82
102
|
let settled = false;
|
|
83
103
|
let joined = false;
|
|
84
|
-
|
|
104
|
+
let opened = false;
|
|
105
|
+
const sock = new WS(endpointAt(baseIdx));
|
|
85
106
|
ws = sock;
|
|
86
107
|
const fail = (err) => {
|
|
87
108
|
if (!settled) {
|
|
@@ -95,6 +116,7 @@ export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
|
95
116
|
}
|
|
96
117
|
};
|
|
97
118
|
sock.onopen = () => {
|
|
119
|
+
opened = true;
|
|
98
120
|
sock.send(JSON.stringify({ t: 'join', v: 1, room, id: hooks.id, max: hooks.maxPeers }));
|
|
99
121
|
};
|
|
100
122
|
sock.onmessage = (ev) => {
|
|
@@ -121,17 +143,42 @@ export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
|
121
143
|
} else known = new Set(peers);
|
|
122
144
|
settled = true;
|
|
123
145
|
joined = true;
|
|
124
|
-
resolve({
|
|
146
|
+
resolve({
|
|
147
|
+
id: msg.id,
|
|
148
|
+
peers,
|
|
149
|
+
iceServers: Array.isArray(msg.iceServers) ? msg.iceServers : undefined,
|
|
150
|
+
max: msg.max,
|
|
151
|
+
// Hosted-service fields (RFC 0003 §5): the tier this session was admitted as,
|
|
152
|
+
// its key id, and the TURN decision — `turn.reason === 'cap'` is the SDK's
|
|
153
|
+
// `turn-cap`. A server that predates them sends none; nothing here is required.
|
|
154
|
+
tier: msg.tier === 'key' ? 'key' : 'anon',
|
|
155
|
+
key: typeof msg.key === 'string' ? msg.key : undefined,
|
|
156
|
+
turn: msg.turn && typeof msg.turn === 'object' ? msg.turn : undefined,
|
|
157
|
+
});
|
|
125
158
|
break;
|
|
126
159
|
}
|
|
127
160
|
case 'full':
|
|
128
161
|
left = true;
|
|
129
162
|
fail(codedError('room-full', `this call is full (${msg.max} participants)`));
|
|
130
163
|
break;
|
|
164
|
+
case 'expired': {
|
|
165
|
+
// The room's lifetime is up (hosted limits): no reconnect — the call reports it.
|
|
166
|
+
left = true;
|
|
167
|
+
clearInterval(pingTimer);
|
|
168
|
+
const e = codedError('expired', 'this room has reached its lifetime on the signalling server');
|
|
169
|
+
if (!settled) fail(e);
|
|
170
|
+
else hooks.onDisconnect?.(e);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
131
173
|
case 'error':
|
|
132
|
-
if (!settled &&
|
|
174
|
+
if (!settled && FATAL_JOIN_CODES.includes(msg.code)) {
|
|
133
175
|
left = true;
|
|
134
176
|
fail(codedError(msg.code, msg.message || msg.code));
|
|
177
|
+
} else if (!settled && (msg.code === 'rate-limited' || msg.code === 'quota')) {
|
|
178
|
+
// Refused by a limit, not by the inputs: a reconnect may retry after `retryMs`.
|
|
179
|
+
const e = codedError(msg.code, msg.message || msg.code);
|
|
180
|
+
/** @type {any} */ (e).retryMs = msg.retryMs > 0 ? msg.retryMs : 0;
|
|
181
|
+
fail(e);
|
|
135
182
|
} else if (!settled && msg.code === 'id-taken') {
|
|
136
183
|
// A reconnect raced the server noticing the old socket close: retry shortly.
|
|
137
184
|
fail(codedError('id-taken', 'peer id still held; retrying'));
|
|
@@ -154,7 +201,16 @@ export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
|
154
201
|
break;
|
|
155
202
|
}
|
|
156
203
|
};
|
|
157
|
-
sock.onerror = () =>
|
|
204
|
+
sock.onerror = () => {
|
|
205
|
+
if (!opened && !settled && triesLeft > 1) {
|
|
206
|
+
// This host is unreachable: fail over to the next alias right away.
|
|
207
|
+
settled = true;
|
|
208
|
+
baseIdx = (baseIdx + 1) % bases.length;
|
|
209
|
+
openAt(isReconnect, triesLeft - 1).then(resolve, reject);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
fail(codedError('signaling-unreachable', `cannot reach ${bases[baseIdx]}`));
|
|
213
|
+
};
|
|
158
214
|
sock.onclose = () => {
|
|
159
215
|
clearInterval(pingTimer);
|
|
160
216
|
fail(codedError('signaling-closed', 'signalling closed before joining'));
|
|
@@ -172,11 +228,12 @@ export function dxrSignaling(url = DXR_SIGNAL_DEFAULT, opts = {}) {
|
|
|
172
228
|
const tryAgain = () => {
|
|
173
229
|
if (left) return;
|
|
174
230
|
open(true).catch((err) => {
|
|
175
|
-
if (left || (err && err.code === 'room-full')) {
|
|
231
|
+
if (left || (err && (err.code === 'room-full' || FATAL_JOIN_CODES.includes(err.code)))) {
|
|
176
232
|
hooks.onDisconnect?.(err);
|
|
177
233
|
return;
|
|
178
234
|
}
|
|
179
|
-
|
|
235
|
+
// A `rate-limited` / `quota` refusal says when to come back; never sooner.
|
|
236
|
+
setTimeout(tryAgain, Math.max(backoffMs(attempt++, { baseMs: 500, maxMs: 15000 }), (err && err.retryMs) || 0));
|
|
180
237
|
});
|
|
181
238
|
};
|
|
182
239
|
setTimeout(tryAgain, backoffMs(attempt++, { baseMs: 300, maxMs: 15000 }));
|
package/js/inline3d-call.js
CHANGED
|
@@ -362,8 +362,9 @@ class Call {
|
|
|
362
362
|
log: (tag, obj) => this.log(tag, obj),
|
|
363
363
|
});
|
|
364
364
|
this.transport = t;
|
|
365
|
+
let session = null;
|
|
365
366
|
try {
|
|
366
|
-
await t.start({
|
|
367
|
+
session = await t.start({
|
|
367
368
|
room: this.room,
|
|
368
369
|
localStream: this.local.stream,
|
|
369
370
|
sendFormat: this.local.format,
|
|
@@ -410,9 +411,38 @@ class Call {
|
|
|
410
411
|
globalThis.addEventListener?.('pagehide', this._pagehide);
|
|
411
412
|
this.log('joined', { id: this.id, peers: this.tiles.size });
|
|
412
413
|
this.emit('joined', { room: this.room, id: this.id });
|
|
414
|
+
// The TURN decision is reported on the next task, after join() / mountCall() have resolved:
|
|
415
|
+
// a page (or <dxr-call>) subscribes once it has the handle, and an event raised mid-join
|
|
416
|
+
// would reach nobody.
|
|
417
|
+
setTimeout(() => {
|
|
418
|
+
if (this.transport === t) this._reportTurn(session);
|
|
419
|
+
}, 0);
|
|
413
420
|
return this.handle;
|
|
414
421
|
}
|
|
415
422
|
|
|
423
|
+
/**
|
|
424
|
+
* The signalling server's TURN decision for this session (hosted service, RFC 0003 §5c), once
|
|
425
|
+
* per join. `turn-cap` (an `error`): the service's monthly relay budget is spent, nobody gets
|
|
426
|
+
* TURN — direct connections still work. `turn-shed` (a `warning`): only THIS session has no
|
|
427
|
+
* TURN (an anonymous session near the budget, or too many credential requests from this
|
|
428
|
+
* address). A server with no TURN at all (self-hosted) says nothing here: that is a setup
|
|
429
|
+
* choice, documented in docs/call.md.
|
|
430
|
+
*/
|
|
431
|
+
_reportTurn(session) {
|
|
432
|
+
const turn = session && session.turn;
|
|
433
|
+
if (!turn || typeof turn !== 'object') return;
|
|
434
|
+
if (turn.status === 'off' && turn.reason === 'cap') {
|
|
435
|
+
this.error('turn-cap', 'relay (TURN) capacity for this month is used up; direct connections still work, but a participant behind a strict NAT may be unreachable', null);
|
|
436
|
+
} else if (turn.reason === 'budget' || turn.reason === 'rate') {
|
|
437
|
+
this.warning(
|
|
438
|
+
'turn-shed',
|
|
439
|
+
turn.reason === 'rate'
|
|
440
|
+
? 'no relay (TURN) for this session: too many credential requests from this address this hour'
|
|
441
|
+
: 'no relay (TURN) for this anonymous session: the hosted service is near its monthly relay budget (a publishable key keeps TURN)'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
416
446
|
_onPeer(id) {
|
|
417
447
|
if (this.tiles.has(id)) return;
|
|
418
448
|
const tile = new Tile(this, id);
|
|
@@ -464,6 +464,7 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
464
464
|
nearClip: opts.nearClip,
|
|
465
465
|
farClip: opts.farClip,
|
|
466
466
|
sky: false,
|
|
467
|
+
viewerEase: opts.viewerEase,
|
|
467
468
|
// Each environment's own look: 'room' is three's (untonemapped), the rest the Sample Viewer's.
|
|
468
469
|
toneMapping: envMap ? 'neutral' : ENVIRONMENT_TONE_MAPPING[environment] || 'neutral',
|
|
469
470
|
});
|
package/js/inline3d-model.js
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
sequenceSpans,
|
|
68
68
|
} from './inline3d-splat-effects.js';
|
|
69
69
|
import { LiveOutgoing, resolveOutgoingOption, defaultOutgoing, yieldIdle } from './inline3d-splat-live.js';
|
|
70
|
+
import { viewerEaseFor, frameTrackingState } from './inline3d-viewer-ease.js';
|
|
70
71
|
import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNIFORM, makeSbsMaterial } from './inline3d-splat-video.js';
|
|
71
72
|
import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
|
|
72
73
|
import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
|
|
@@ -817,8 +818,13 @@ export class PlayCanvasSplatViewer {
|
|
|
817
818
|
sky = false,
|
|
818
819
|
pageCamera = false,
|
|
819
820
|
toneMapping = 'none',
|
|
821
|
+
viewerEase,
|
|
820
822
|
} = opts;
|
|
821
823
|
this.canvas = canvas;
|
|
824
|
+
// The tracking-acquisition ease (./inline3d-viewer-ease.js): undefined = the session's
|
|
825
|
+
// createInline3D({ viewerEase }) default. Built on the first 3D frame, which knows the session.
|
|
826
|
+
this._viewerEaseOpt = viewerEase;
|
|
827
|
+
this.viewerEase = null;
|
|
822
828
|
// controls:'page': the PAGE owns the camera (setPageCamera). No orbit, no idle, no fit; the
|
|
823
829
|
// rig node carries the page's matrix instead of the inverse pivot.
|
|
824
830
|
this.pageCamera = pageCamera === true;
|
|
@@ -1194,7 +1200,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1194
1200
|
}
|
|
1195
1201
|
|
|
1196
1202
|
/** wall.addScene's frame callback. Validate BEFORE drawing; replay the last good frame else. */
|
|
1197
|
-
onFrame(views, layer) {
|
|
1203
|
+
onFrame(views, layer, frame) {
|
|
1198
1204
|
if (this._disposed) return;
|
|
1199
1205
|
if (this._mode !== '3d') this.stopMono();
|
|
1200
1206
|
// The rig these views were located with: Blink chained the rig declared BEFORE this callback,
|
|
@@ -1224,6 +1230,9 @@ export class PlayCanvasSplatViewer {
|
|
|
1224
1230
|
vps.push(vp);
|
|
1225
1231
|
}
|
|
1226
1232
|
this._cacheGood(views, vps);
|
|
1233
|
+
// Ease a tracking acquisition/loss on the copies (never the XRViews), before anything —
|
|
1234
|
+
// the rig map, the layer rig, the live outgoing photo — reads them.
|
|
1235
|
+
(this.viewerEase ||= viewerEaseFor(frame, this._viewerEaseOpt)).apply(this._lastGood.entries, frameTrackingState(frame));
|
|
1227
1236
|
this._drawEntries(this._lastGood.entries, this._lastGood);
|
|
1228
1237
|
}
|
|
1229
1238
|
|
|
@@ -2139,6 +2148,7 @@ export class PlayCanvasSplatViewer {
|
|
|
2139
2148
|
if (this._disposed) return;
|
|
2140
2149
|
const box = this.canvas.getBoundingClientRect();
|
|
2141
2150
|
if (box.width < 1 || box.height < 1) return;
|
|
2151
|
+
this.viewerEase?.reset(); // a new window size moves every projection: not a viewer jump
|
|
2142
2152
|
const dpr = Math.min((typeof window !== 'undefined' && window.devicePixelRatio) || 1, 2) * this.renderScale;
|
|
2143
2153
|
// Clamp to the device's GL limits BEFORE sizing (./inline3d-buffer-limit.js). Measured on an
|
|
2144
2154
|
// Adreno 740 tablet: MAX_TEXTURE_SIZE 4096, a 5120-wide request silently became a 4096-wide
|
|
@@ -3072,6 +3082,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3072
3082
|
nearClip: opts.nearClip,
|
|
3073
3083
|
farClip: opts.farClip,
|
|
3074
3084
|
sky: opts.sky,
|
|
3085
|
+
viewerEase: opts.viewerEase,
|
|
3075
3086
|
});
|
|
3076
3087
|
|
|
3077
3088
|
let handle = null;
|
|
@@ -3394,9 +3405,9 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3394
3405
|
let initialViewRig = null; // the rig addScene is created with (controls:'page'), else the shorthand
|
|
3395
3406
|
if (wall && wall.supported) {
|
|
3396
3407
|
const onFrame = diag
|
|
3397
|
-
? (views, layer) => {
|
|
3408
|
+
? (views, layer, frame) => {
|
|
3398
3409
|
const t = performance.now();
|
|
3399
|
-
viewer.onFrame(views, layer);
|
|
3410
|
+
viewer.onFrame(views, layer, frame);
|
|
3400
3411
|
diag.frame(views, t); // after the draw: the overlay state is what this frame showed
|
|
3401
3412
|
}
|
|
3402
3413
|
: viewer.onFrame;
|
package/js/inline3d-splat.js
CHANGED
|
@@ -246,6 +246,7 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
246
246
|
renderScale,
|
|
247
247
|
feather,
|
|
248
248
|
logTag: '[inline3d/splat]',
|
|
249
|
+
viewerEase: opts.viewerEase,
|
|
249
250
|
}).useEyeCamera(EyeCamera, EdgeFeather);
|
|
250
251
|
|
|
251
252
|
// Spark renders through the ordinary three.js pipeline, so splats and meshes co-exist and
|