@displayxr/inline3d 1.12.0 → 1.13.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 +55 -0
- package/README.md +11 -0
- package/THIRD_PARTY_NOTICES.md +76 -0
- package/js/inline3d-playcanvas-engine.js +5 -0
- package/js/inline3d-splat-effects.js +1116 -0
- package/js/inline3d-splat-perf.js +3 -0
- package/js/inline3d-splat-playcanvas.js +517 -82
- package/js/inline3d-splat-shared.js +33 -0
- package/js/inline3d-splat.js +28 -0
- package/package.json +4 -2
- package/splat.d.ts +152 -5
|
@@ -437,3 +437,36 @@ export function normalizeCameraPose(matrixWorld, o = {}) {
|
|
|
437
437
|
}
|
|
438
438
|
return { matrixWorld: m, verticalFovDeg: fov, near, far, convergence };
|
|
439
439
|
}
|
|
440
|
+
|
|
441
|
+
// ── coverage-linear alpha (setSource's crossfade, the `fade` effect) ────────────────────────────
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Nominal optical depth of a dense photo's splat stack, per pixel: −ln(transmittance) through the
|
|
445
|
+
* whole stack at rest. The remap below is exact for a pixel of optical depth `L`. Real pictures
|
|
446
|
+
* spread widely around any single value (measured 13–25 on SHARP photos), which is why the
|
|
447
|
+
* crossfade blends IMAGES (FRAME_SNAPSHOT in ./inline3d-splat-playcanvas.js) and uses this only
|
|
448
|
+
* to shape ramps — never as the thing that makes a fade linear.
|
|
449
|
+
*/
|
|
450
|
+
export const FADE_OPTICAL_DEPTH = 3;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Smallest per-splat transmittance the remap works from. A fully opaque splat (SOG alpha 255/255
|
|
454
|
+
* decodes to exactly 1, common in photos) has transmittance 0, and 0^k is 0 for every k > 0 — so
|
|
455
|
+
* without a floor it could never fade at all. 0.02 caps one splat's optical depth at ~3.9.
|
|
456
|
+
*/
|
|
457
|
+
export const FADE_TRANSMITTANCE_FLOOR = 0.02;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The per-splat alpha exponent `k` that puts a stack of nominal optical depth `L` at screen
|
|
461
|
+
* coverage `c` (0..1).
|
|
462
|
+
*
|
|
463
|
+
* Scaling every splat's alpha by `c` saturates: a pixel under n near-opaque splats is covered
|
|
464
|
+
* `1 − (1 − c·α)^n`. Raising each splat's transmittance to a power instead, `α' = 1 − (1 − α)^k`,
|
|
465
|
+
* multiplies the stack's optical depth by k: coverage is `1 − e^{−kL}`. Solving for coverage =
|
|
466
|
+
* c·(1 − e^{−L}) gives this k. Exact ends: k(0) = 0 (α' = 0, invisible) and k(1) = 1 (untouched).
|
|
467
|
+
*/
|
|
468
|
+
export function coverageExponent(c, L = FADE_OPTICAL_DEPTH) {
|
|
469
|
+
if (!(c > 0)) return 0;
|
|
470
|
+
if (c >= 1) return 1;
|
|
471
|
+
return -Math.log(1 - c * (1 - Math.exp(-L))) / L;
|
|
472
|
+
}
|
package/js/inline3d-splat.js
CHANGED
|
@@ -23,6 +23,7 @@ import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';
|
|
|
23
23
|
import { EyeCamera, EdgeFeather, cameraRigFromCamera } from './inline3d-three.js';
|
|
24
24
|
import { SceneViewer, boundsFromPositions } from './inline3d-viewer.js';
|
|
25
25
|
import { readSogCamera } from './inline3d-sog.js';
|
|
26
|
+
import { resolveRevealOption, effectsNotOnSpark, validateEffectCall } from './inline3d-splat-effects.js';
|
|
26
27
|
import { applySplatPerf, splatPerfMeshOptions } from './inline3d-splat-perf.js';
|
|
27
28
|
import {
|
|
28
29
|
toArray3,
|
|
@@ -172,6 +173,8 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
172
173
|
// ./inline3d-splat-playcanvas.js, imported DYNAMICALLY so a page that never asks for it never
|
|
173
174
|
// resolves `playcanvas` — see addSplatDeferred.
|
|
174
175
|
if (resolveSplatEngine(opts) === 'playcanvas') {
|
|
176
|
+
// A bad `reveal` is a page bug true of every call: throw at the call.
|
|
177
|
+
resolveRevealOption(opts.reveal);
|
|
175
178
|
// A format that engine provably cannot read (a .spz URL, gzip bytes, a Spark-only fileType)
|
|
176
179
|
// is a page bug: say so NOW rather than fail inside a loader later.
|
|
177
180
|
const why = playcanvasCannotRead(src, opts);
|
|
@@ -182,6 +185,7 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
182
185
|
// controls:'page' is a PlayCanvas-backend feature (docs/playcanvas-adapter.md §controls:'page'):
|
|
183
186
|
// on Spark it would need SceneViewer — which ./viewer and ./model share — to take an external
|
|
184
187
|
// camera in both its mono and eye paths. Refused by name rather than half-supported.
|
|
188
|
+
if (opts.reveal !== undefined && opts.reveal !== false) throw effectsNotOnSpark('reveal');
|
|
185
189
|
if (pageControls) {
|
|
186
190
|
throw new Error(
|
|
187
191
|
"@displayxr/inline3d/splat: controls:'page' is not supported on Spark (the default engine) — " +
|
|
@@ -365,6 +369,17 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
365
369
|
"only; with the Spark backend, remove() this handle and addSplat() the new asset.",
|
|
366
370
|
);
|
|
367
371
|
},
|
|
372
|
+
/** Not on this backend (splat effects are PlayCanvas-only in this version). */
|
|
373
|
+
playEffect() {
|
|
374
|
+
throw effectsNotOnSpark('playEffect()');
|
|
375
|
+
},
|
|
376
|
+
setEffect() {
|
|
377
|
+
throw effectsNotOnSpark('setEffect()');
|
|
378
|
+
},
|
|
379
|
+
stopEffect() {
|
|
380
|
+
throw effectsNotOnSpark('stopEffect()');
|
|
381
|
+
},
|
|
382
|
+
effects: () => [],
|
|
368
383
|
};
|
|
369
384
|
|
|
370
385
|
// `src` may be a URL or the bytes themselves.
|
|
@@ -691,6 +706,19 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
691
706
|
// A swap requested before the first asset has landed runs once it has (the adapter's own
|
|
692
707
|
// setSource replaces this stub on the same object by then).
|
|
693
708
|
setSource: (...args) => out.ready.then(() => out.setSource(...args)),
|
|
709
|
+
// Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
|
|
710
|
+
// then run once the first asset is on screen.
|
|
711
|
+
playEffect: (name, o) => {
|
|
712
|
+
validateEffectCall(name, o, 'play');
|
|
713
|
+
return out.ready.then(() => out.playEffect(name, o));
|
|
714
|
+
},
|
|
715
|
+
setEffect: (name, params) => {
|
|
716
|
+
validateEffectCall(name, params, 'set');
|
|
717
|
+
pending.push(['setEffect', [name, params]]);
|
|
718
|
+
return out;
|
|
719
|
+
},
|
|
720
|
+
stopEffect: queue('stopEffect'),
|
|
721
|
+
effects: () => [],
|
|
694
722
|
getFocus: () => null,
|
|
695
723
|
// A plain data slot the adapter reads at call time, so a callback assigned on the very next
|
|
696
724
|
// line after addSplat — before the module has loaded — is the one that fires.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"js/inline3d-splat-rig.js",
|
|
51
51
|
"js/inline3d-splat-playcanvas.js",
|
|
52
52
|
"js/inline3d-splat-shared.js",
|
|
53
|
+
"js/inline3d-splat-effects.js",
|
|
53
54
|
"js/inline3d-model.js",
|
|
54
55
|
"js/inline3d-model-entry.js",
|
|
55
56
|
"js/inline3d-model-playcanvas.js",
|
|
@@ -61,7 +62,8 @@
|
|
|
61
62
|
"model.d.ts",
|
|
62
63
|
"CHANGELOG.md",
|
|
63
64
|
"README.md",
|
|
64
|
-
"LICENSE"
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"THIRD_PARTY_NOTICES.md"
|
|
65
67
|
],
|
|
66
68
|
"keywords": [
|
|
67
69
|
"displayxr",
|
package/splat.d.ts
CHANGED
|
@@ -327,6 +327,133 @@ export interface SplatOptions {
|
|
|
327
327
|
* caught and warned once; frames keep rendering.
|
|
328
328
|
*/
|
|
329
329
|
onBeforeFrame?: (frame: SplatFrameInfo) => void;
|
|
330
|
+
/**
|
|
331
|
+
* `engine: 'playcanvas'` only (throws on Spark): play a reveal on the first asset. Installed at
|
|
332
|
+
* its start state before the first frame, played once `firstWoven` settles (at once in 2D) —
|
|
333
|
+
* docs/splat-effects.md. A name uses that effect's defaults; an object overrides them.
|
|
334
|
+
*/
|
|
335
|
+
reveal?: SplatRevealName | false | SplatRevealSpec;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** The transition effects `reveal` accepts. */
|
|
339
|
+
export type SplatRevealName = 'inflate' | 'sweep' | 'dissolve' | 'fade';
|
|
340
|
+
|
|
341
|
+
/** Named easings; a function `(x) => y` on [0, 1] also works. */
|
|
342
|
+
export type SplatEasing =
|
|
343
|
+
| 'linear'
|
|
344
|
+
| 'easeInQuad'
|
|
345
|
+
| 'easeOutQuad'
|
|
346
|
+
| 'easeInOutQuad'
|
|
347
|
+
| 'easeInCubic'
|
|
348
|
+
| 'easeOutCubic'
|
|
349
|
+
| 'easeInOutCubic'
|
|
350
|
+
| 'easeInOutSine'
|
|
351
|
+
| ((x: number) => number);
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Where an effect is centred: `'focus'` (the focus point), `'eyes'` (the eyes' midpoint — the
|
|
355
|
+
* inflate default), a point in the splat's OWN space `[x, y, z]`, or a canvas point
|
|
356
|
+
* `[clientX, clientY]` / `{ clientX, clientY }` resolved with `pick()` when the effect starts.
|
|
357
|
+
* All effects are keyed on world position and time only, so both eyes agree.
|
|
358
|
+
*/
|
|
359
|
+
export type SplatEffectOrigin = 'focus' | 'eyes' | [number, number, number] | [number, number] | { clientX: number; clientY: number };
|
|
360
|
+
|
|
361
|
+
/** Options every played effect takes. */
|
|
362
|
+
export interface SplatEffectTiming {
|
|
363
|
+
durationMs?: number;
|
|
364
|
+
/** Wait this long at the start state before the clock runs. */
|
|
365
|
+
holdMs?: number;
|
|
366
|
+
easing?: SplatEasing;
|
|
367
|
+
origin?: SplatEffectOrigin;
|
|
368
|
+
/** `'in'` (default): arrive, then the effect is removed. `'out'`: leave; the end state is held until `stopEffect`. */
|
|
369
|
+
direction?: 'in' | 'out';
|
|
370
|
+
/** `'tile'` (default, every splat of the tile, render time) or `'entity'` (the current asset only, work buffer). */
|
|
371
|
+
scope?: 'tile' | 'entity';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** `reveal: { type, … }`. */
|
|
375
|
+
export interface SplatRevealSpec extends Omit<SplatEffectTiming, 'direction' | 'scope'> {
|
|
376
|
+
type: SplatRevealName;
|
|
377
|
+
[param: string]: unknown;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** A custom effect's GLSL (PlayCanvas-shaped) — docs/splat-effects.md §custom. */
|
|
381
|
+
export interface SplatCustomEffect extends SplatEffectTiming {
|
|
382
|
+
/**
|
|
383
|
+
* Any of `void modifySplatCenter(inout vec3 center)`, `void modifySplatRotationScale(vec3
|
|
384
|
+
* originalCenter, vec3 modifiedCenter, inout vec4 rotation, inout vec3 scale)`, `void
|
|
385
|
+
* modifySplatColor(vec3 center, inout vec4 color)` — world-space centres. `dxrProgress` (0..1)
|
|
386
|
+
* and `dxrTime` (s) are defined; `splat.index` / `splat.uv` are the asset's own file index in
|
|
387
|
+
* `scope: 'entity'`. Screen-space inputs are refused.
|
|
388
|
+
*/
|
|
389
|
+
glsl: string;
|
|
390
|
+
/** Tile scope only: `void modifySplatColor(vec2 gaussianUV, inout vec4 color)` per fragment. */
|
|
391
|
+
fragmentGlsl?: string;
|
|
392
|
+
/** Uniform values, set every frame; a function gets the frame time in ms. */
|
|
393
|
+
uniforms?: Record<string, number | number[] | ((tMs: number) => number | number[])>;
|
|
394
|
+
/** `setEffect` only: hold `dxrProgress` here (default 1). */
|
|
395
|
+
progress?: number;
|
|
396
|
+
/** `playEffect` only: keep the effect at its end instead of removing it. */
|
|
397
|
+
hold?: boolean;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export type SplatEffectName = 'inflate' | 'deflate' | 'sweep' | 'dissolve' | 'fade' | 'pulse' | 'grade' | 'clip' | 'custom' | `custom:${string}`;
|
|
401
|
+
|
|
402
|
+
/** `setEffect('grade', …)`. */
|
|
403
|
+
export interface SplatGradeParams {
|
|
404
|
+
/** Stops; default 0. */
|
|
405
|
+
exposure?: number;
|
|
406
|
+
contrast?: number;
|
|
407
|
+
saturation?: number;
|
|
408
|
+
tint?: [number, number, number];
|
|
409
|
+
scope?: 'tile' | 'entity';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `setEffect('clip', …)` — in the splat's own space, like setFocus. Exactly one shape. */
|
|
413
|
+
export interface SplatClipParams {
|
|
414
|
+
box?: { min: [number, number, number]; max: [number, number, number] };
|
|
415
|
+
sphere?: { center: [number, number, number]; radius: number };
|
|
416
|
+
/** Keep the outside instead. */
|
|
417
|
+
invert?: boolean;
|
|
418
|
+
scope?: 'tile' | 'entity';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** One row of `handle.effects()`. */
|
|
422
|
+
export interface SplatEffectState {
|
|
423
|
+
name: string;
|
|
424
|
+
scope: 'tile' | 'entity';
|
|
425
|
+
stage: 'grade' | 'clip' | 'reveal' | 'pulse' | 'custom';
|
|
426
|
+
/** Played (has a clock) vs set (held). */
|
|
427
|
+
playing: boolean;
|
|
428
|
+
/** Held at its start state until a gate settles (a reveal waiting for `firstWoven`). */
|
|
429
|
+
waiting: boolean;
|
|
430
|
+
/** 0..1, before easing. */
|
|
431
|
+
progress: number;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** `setSource`'s options. */
|
|
435
|
+
export interface SplatSourceOptions {
|
|
436
|
+
/** > 0 = a crossfade of this length (the 1.10 option; same as `transition: 'crossfade'`). */
|
|
437
|
+
fadeMs?: number;
|
|
438
|
+
resetPose?: boolean;
|
|
439
|
+
/**
|
|
440
|
+
* `'cut'` (default), `'crossfade'` (images lerp, see setSource), `'flip'` (the old photo
|
|
441
|
+
* flattens to its convergence plane — zero disparity — the swap happens there, the new one
|
|
442
|
+
* inflates out of its own; default 2200 ms), `'wavefront'` (a soft front crosses left → right
|
|
443
|
+
* with a thin depth ridge riding it; default 2000 ms, ease-in-out; falls back to the crossfade
|
|
444
|
+
* in a hidden tab). For photo slideshows: `crossfade` or `wavefront`.
|
|
445
|
+
*/
|
|
446
|
+
transition?: 'cut' | 'crossfade' | 'flip' | 'wavefront';
|
|
447
|
+
durationMs?: number;
|
|
448
|
+
easing?: SplatEasing;
|
|
449
|
+
/** `cut`/`crossfade` only: reveal the INCOMING asset (entity scope) while the old one fades. */
|
|
450
|
+
reveal?: SplatRevealName | false | SplatRevealSpec;
|
|
451
|
+
/** `wavefront`: the soft band, a fraction of the picture width (default 0.18). */
|
|
452
|
+
band?: number;
|
|
453
|
+
/** `wavefront`: the ridge's pull toward the eyes, world units (default 0.03 — 3 cm on a metric photo). */
|
|
454
|
+
ridge?: number;
|
|
455
|
+
/** `wavefront`: the ridge's disparity cap, a fraction of the eye view's width (default 0.004, max 0.05). */
|
|
456
|
+
ridgeMaxDisparity?: number;
|
|
330
457
|
}
|
|
331
458
|
|
|
332
459
|
/** What `onBeforeFrame` receives. */
|
|
@@ -466,15 +593,35 @@ export interface SplatHandle {
|
|
|
466
593
|
* Swap the asset (URL or bytes) in place. PlayCanvas backend only — throws on Spark.
|
|
467
594
|
*
|
|
468
595
|
* The new file loads BEHIND the current one; then the two crossfade over `fadeMs` (0 = a cut)
|
|
469
|
-
* and the old one is released. The
|
|
596
|
+
* and the old one is released. The crossfade blends the two IMAGES linearly: the last frame of
|
|
597
|
+
* the old asset is frozen (both eyes) and lerped with the live new one, so the mix is exactly
|
|
598
|
+
* `t` whatever the two photos' depth order (the old asset's own motion stops during the fade). The rig waterfall re-runs for the new file (rig, lens, focus and
|
|
470
599
|
* frame update; `onFocusChange` fires). The pose (yaw/pitch/zoom/depth) is kept unless
|
|
471
600
|
* `resetPose`. A newer call supersedes an older one still loading. Resolves once the fade has
|
|
472
601
|
* finished; rejects if the new asset cannot be loaded (the current one stays on screen).
|
|
473
602
|
*/
|
|
474
|
-
setSource(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
603
|
+
setSource(src: string | Blob | ArrayBuffer | Uint8Array, opts?: SplatSourceOptions): Promise<SplatHandle>;
|
|
604
|
+
/**
|
|
605
|
+
* `engine: 'playcanvas'` only (throws on Spark). Play a transition/pulse/custom effect;
|
|
606
|
+
* validated at the call, run once the first asset is on screen. Resolves `{ finished }` —
|
|
607
|
+
* false when stopped or replaced. docs/splat-effects.md.
|
|
608
|
+
*/
|
|
609
|
+
playEffect(
|
|
610
|
+
name: Exclude<SplatEffectName, 'grade' | 'clip'>,
|
|
611
|
+
opts?: SplatEffectTiming & Record<string, unknown>,
|
|
612
|
+
): Promise<{ finished: boolean }>;
|
|
613
|
+
/**
|
|
614
|
+
* `engine: 'playcanvas'` only. Set a persistent effect (`grade`, `clip`, `custom`), hold a
|
|
615
|
+
* transition at `{ progress }`, or pass `null` to remove it (the exact baseline).
|
|
616
|
+
*/
|
|
617
|
+
setEffect(name: 'grade', params: SplatGradeParams | null): SplatHandle;
|
|
618
|
+
setEffect(name: 'clip', params: SplatClipParams | null): SplatHandle;
|
|
619
|
+
setEffect(name: 'custom' | `custom:${string}`, params: SplatCustomEffect | null): SplatHandle;
|
|
620
|
+
setEffect(name: SplatEffectName, params: (SplatEffectTiming & { progress?: number } & Record<string, unknown>) | null): SplatHandle;
|
|
621
|
+
/** Stop one effect (all with no name): `finish: true` jumps to its end state, else removes it. */
|
|
622
|
+
stopEffect(name?: SplatEffectName, opts?: { finish?: boolean }): SplatHandle;
|
|
623
|
+
/** What is on right now. */
|
|
624
|
+
effects(): SplatEffectState[];
|
|
478
625
|
/**
|
|
479
626
|
* Called with the live focus (the splat's own space) whenever it moves — easing included — and
|
|
480
627
|
* which waterfall step it came from. PlayCanvas backend; assign any time, even before `ready`.
|