@displayxr/inline3d 1.14.0 → 1.16.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 +72 -0
- package/README.md +4 -1
- package/js/inline3d-model-playcanvas.js +1 -1
- package/js/inline3d-splat-effects.js +213 -5
- package/js/inline3d-splat-playcanvas.js +618 -31
- package/js/inline3d-splat.js +25 -0
- package/package.json +1 -1
- package/splat.d.ts +110 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,78 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
|
|
|
5
5
|
`./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
|
|
6
6
|
which tier they touch, because that is what tells you whether an upgrade can move your pixels.
|
|
7
7
|
|
|
8
|
+
## 1.16.0 — 2026-09-24
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive. A page that never
|
|
11
|
+
calls `setRig` renders the same pixels, except for the `setSource` fix below.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`handle.setRig(type, options?)`** (#36): a live, reversible rig switch on the PlayCanvas splat
|
|
16
|
+
handle. It applies on the next frame with no remount, reload or new session, and returns a
|
|
17
|
+
`Promise<handle>`.
|
|
18
|
+
- `setRig('display', { virtualDisplayHeight, fit, margin, frame, … })` frames the enabled meshes
|
|
19
|
+
under `handle.engine.root`, plus the splat when it is shown. The defaults are addModel's (0.24,
|
|
20
|
+
`contain`, 0.8, depthLimit 4, idleSpin 8), and it declares a display rig.
|
|
21
|
+
- While the splat is hidden, the page's meshes get addModel's tone mapping (`'neutral'`) and, if
|
|
22
|
+
the scene has no `envAtlas` of its own, addModel's neutral-studio IBL. Both are removed when the
|
|
23
|
+
rig switches away.
|
|
24
|
+
- `setRig('camera')` goes back to the asset's capture rig, re-resolved from the waterfall's own
|
|
25
|
+
inputs. `setRig('auto')` is the load-time result.
|
|
26
|
+
- The pose resets to the new rig's rest. `handle.rig.typeSource` reads `'setRig'`. The choice
|
|
27
|
+
sticks across `setSource` until `'auto'`, and `controls:'page'` throws.
|
|
28
|
+
- Gates, on a real GPU: a hidden splat plus `setRig('display')` plus DamagedHelmet or Fox renders
|
|
29
|
+
**MAE 0.0000** against `addModel(url, { engine: 'playcanvas' })`, in mono and fake stereo, with
|
|
30
|
+
`antialias: true`. With the tile's default MSAA off the residual is 0.11–0.44, silhouette edges
|
|
31
|
+
only. The round trip `setRig('display')` → `setRig('camera')` returns to the exact pre-switch
|
|
32
|
+
render (**MAE 0.0000**) and the same camera-rig declaration.
|
|
33
|
+
- See [`docs/playcanvas-adapter.md` § setRig](docs/playcanvas-adapter.md#setrig--switching-between-the-display-rig-and-the-camera-rig-36).
|
|
34
|
+
- **`addSplat(…, { engine: 'playcanvas', antialias: true })`** creates the tile's WebGL context with
|
|
35
|
+
MSAA. The default stays off. Use it when a page draws meshes under `handle.engine.root`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **`setSource` from a camera-rig asset to a display-rig asset** now declares the display rig to the
|
|
40
|
+
runtime. The camera rig used to stay declared. The new asset also orbits its frame centre again
|
|
41
|
+
(the camera rig's `recentre: false` was left behind).
|
|
42
|
+
|
|
43
|
+
## 1.15.0 — 2026-09-24
|
|
44
|
+
|
|
45
|
+
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive; the existing transitions
|
|
46
|
+
and reveals render the same pixels.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Particle transitions for `setSource`** (#36): `transition: 'swarm' | 'burst' | 'shimmer-cross' | 'dust'`.
|
|
51
|
+
They play the particle reveals across a slide change, with both photos live, in 3D and in 2D.
|
|
52
|
+
The old photo plays its effect backwards while the new one plays it forwards, on overlapping
|
|
53
|
+
spans of one clock (`overlap`).
|
|
54
|
+
- `swarm`: the old photo disperses into a curl-noise swarm and the new one assembles out of one.
|
|
55
|
+
- `burst`: the old photo collapses into its focus and the new one bursts out of its own.
|
|
56
|
+
- `shimmer-cross`: twinkling points out and twinkling points in; nothing moves.
|
|
57
|
+
- `dust`: drifting dust out, gathering dust in.
|
|
58
|
+
|
|
59
|
+
Defaults are 2.6–2.8 s on a linear shared clock; each particle eases on its own path. The end
|
|
60
|
+
state is a `cut`, MAE 0.000 in colour and alpha in both eyes, and VRAM goes back to baseline.
|
|
61
|
+
A hidden tab gets the crossfade.
|
|
62
|
+
See [`docs/splat-effects.md` § Particle transitions](docs/splat-effects.md#particle-transitions).
|
|
63
|
+
- They render at RENDER TIME. One tile-scope chunk serves both photos, and each photo's values
|
|
64
|
+
sit on the mesh instance of the gsplat manager that draws it. A first cut used entity-scope
|
|
65
|
+
work-buffer modifiers, which rewrote both 1.18M work buffers and re-sorted both every frame.
|
|
66
|
+
Measured in a visible Chrome, that cost 2D pacing its 120 Hz: p95 went from 16.7 ms to 9.8–10.1.
|
|
67
|
+
A photo with nothing to draw skips its draw.
|
|
68
|
+
- **`prepareSource(src, { transition, … })`**: with a particle transition, this also compiles and
|
|
69
|
+
links its shaders in the dwell. Otherwise the first transition of each kind blocks its first
|
|
70
|
+
frame on the link: 35–50 ms, and 190 + 260 ms on a variant the machine has never compiled.
|
|
71
|
+
- Particle options `vanish` (fade over the first part of the flight) and `density` (the share
|
|
72
|
+
drawn in flight). Both are off for the reveals.
|
|
73
|
+
|
|
74
|
+
### Not changed
|
|
75
|
+
|
|
76
|
+
- The live `crossfade` / `wavefront` still run at about 40 Hz in stereo on an M1 (median 25.6 ms).
|
|
77
|
+
They draw both photos in full for the whole window. The same skip-what-is-not-visible idea may
|
|
78
|
+
apply there; that is a follow-up.
|
|
79
|
+
|
|
8
80
|
## 1.14.0 — 2026-09-24
|
|
9
81
|
|
|
10
82
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). New particle reveals are additive. One default changes pixels
|
package/README.md
CHANGED
|
@@ -45,7 +45,10 @@ API is not yet covered by the semver promise below.
|
|
|
45
45
|
renders the same splat window with the [PlayCanvas](https://playcanvas.com/) engine instead of
|
|
46
46
|
Spark — same handle, same rig/focus/camera-block behaviour, same `perf` presets (mapped onto the
|
|
47
47
|
engine's knobs) — and adds `setSource(src, { fadeMs })` (crossfading asset swaps), `feather`, a
|
|
48
|
-
tilt-and-relax orbit
|
|
48
|
+
tilt-and-relax orbit, `handle.engine` (the engine objects, for advanced pages) and
|
|
49
|
+
`handle.setRig('display' | 'camera' | 'auto')`: a live, reversible rig switch, so one persistent tile
|
|
50
|
+
can show a glTF under `handle.engine.root` exactly as `addModel` would and then go back to the photo's
|
|
51
|
+
capture rig ([§setRig](docs/playcanvas-adapter.md#setrig--switching-between-the-display-rig-and-the-camera-rig-36)). It needs the
|
|
49
52
|
optional peer `playcanvas` (`>=2.22.3 <3`) and reads `.sog`, `.ply` and a Streamed-SOG
|
|
50
53
|
`lod-meta.json`. **Spark stays the default**; a page that never passes `engine` never loads
|
|
51
54
|
`playcanvas`. What differs, and why: [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md).
|
|
@@ -346,7 +346,7 @@ export function neutralStudioRGBE(width = 256, height = 128, env = NEUTRAL_STUDI
|
|
|
346
346
|
export const ENV_YAW_DEG = 0;
|
|
347
347
|
|
|
348
348
|
/** Build the default environment on this app and set it as the scene's envAtlas. */
|
|
349
|
-
function useNeutralStudio(pc, app, yawDeg) {
|
|
349
|
+
export function useNeutralStudio(pc, app, yawDeg) {
|
|
350
350
|
const W = 256;
|
|
351
351
|
const H = 128;
|
|
352
352
|
const tex = new pc.Texture(app.graphicsDevice, {
|
|
@@ -626,6 +626,8 @@ uniform float ${P}grow;
|
|
|
626
626
|
uniform float ${P}tx;
|
|
627
627
|
uniform vec3 ${P}glow;
|
|
628
628
|
uniform float ${P}falpha;
|
|
629
|
+
uniform float ${P}van;
|
|
630
|
+
uniform float ${P}dens;
|
|
629
631
|
float ${P}lp = -1.0;
|
|
630
632
|
float ${P}h(vec3 c, float s) { return dxrFxHash(c + vec3(s * 17.13, s * 31.71, s * 7.31)); }
|
|
631
633
|
// where a point sits in the PICTURE (image x, y in half-view-widths) and its log depth: noise is
|
|
@@ -683,6 +685,12 @@ void ${P}color(vec3 c, inout vec4 col) {
|
|
|
683
685
|
float g = smoothstep(${P}grow, 1.0, lp);
|
|
684
686
|
col.rgb += ${P}glow * (1.0 - g);
|
|
685
687
|
col.a *= mix(${P}falpha, 1.0, g);
|
|
688
|
+
// setSource's particle transitions: a particle fades over the first "vanish" of its flight, so a
|
|
689
|
+
// swarm gathers out of nothing and a leaving photo's swarm thins out to nothing (0 = off)
|
|
690
|
+
if (${P}van > 0.0) col.a *= smoothstep(0.0, ${P}van, lp);
|
|
691
|
+
// "density" < 1: only that share of the gaussians is drawn in flight (the rest appear as they
|
|
692
|
+
// grow home), so a million-point swarm reads as a swarm, not as snow
|
|
693
|
+
if (${P}dens < 1.0 && ${P}h(c, 9.0) > ${P}dens) col.a *= g;
|
|
686
694
|
${extra}
|
|
687
695
|
}
|
|
688
696
|
`;
|
|
@@ -705,6 +713,8 @@ const PARTICLE_VALIDATE = (what, o) => {
|
|
|
705
713
|
if (o.color !== undefined) vec3Of(o.color, `${what} color`);
|
|
706
714
|
if (o.noiseScale !== undefined) num(o.noiseScale, `${what} noiseScale`, 1e-3, 1e3);
|
|
707
715
|
if (o.maxDisparity !== undefined) num(o.maxDisparity, `${what} maxDisparity`, 0, 0.05);
|
|
716
|
+
if (o.vanish !== undefined) num(o.vanish, `${what} vanish`, 0, 1);
|
|
717
|
+
if (o.density !== undefined) num(o.density, `${what} density`, 0, 1);
|
|
708
718
|
if (o.layerSize !== undefined && !(Number.isInteger(o.layerSize) && o.layerSize > 0)) {
|
|
709
719
|
throw new RangeError(`@displayxr/inline3d/splat: ${what} layerSize must be a positive integer.`);
|
|
710
720
|
}
|
|
@@ -773,6 +783,8 @@ const particleUniforms = (ctx, inst, amount, tMs) => {
|
|
|
773
783
|
tx: s.tanX,
|
|
774
784
|
glow: o.color.map((x) => x * o.glow),
|
|
775
785
|
falpha: o.flightAlpha,
|
|
786
|
+
van: o.vanish,
|
|
787
|
+
dens: o.density,
|
|
776
788
|
};
|
|
777
789
|
};
|
|
778
790
|
|
|
@@ -785,11 +797,18 @@ const PARTICLE_DEFAULTS = {
|
|
|
785
797
|
noiseScale: 2,
|
|
786
798
|
dotSize: 0.0007,
|
|
787
799
|
grow: 0.6,
|
|
800
|
+
vanish: 0,
|
|
801
|
+
density: 1,
|
|
788
802
|
};
|
|
789
803
|
|
|
790
|
-
/**
|
|
791
|
-
|
|
804
|
+
/**
|
|
805
|
+
* A particle reveal's registry entry: `body(P)` adds the centre/rs stages (and extra uniforms).
|
|
806
|
+
* `hiddenAtZero(opts)`: nothing is drawn at amount 0 (converge / shimmer: not yet launched; the
|
|
807
|
+
* others with `vanish` > 0) — a setSource transition then skips that photo's draw.
|
|
808
|
+
*/
|
|
809
|
+
function particleEffect(name, defaults, { body, color = particleColor, uniforms: extra = () => ({}), validate, hiddenAtZero = (o) => o.vanish > 0 }) {
|
|
792
810
|
return {
|
|
811
|
+
hiddenAtZero,
|
|
793
812
|
stage: 'reveal',
|
|
794
813
|
kind: 'transition',
|
|
795
814
|
particle: true,
|
|
@@ -967,6 +986,7 @@ void ${P}rs(vec3 oc, vec3 mc, inout vec4 r, inout vec3 sc) {
|
|
|
967
986
|
}
|
|
968
987
|
`,
|
|
969
988
|
color: (P) => particleColor(P, 'col.a *= smoothstep(0.0, 0.15, lp);'),
|
|
989
|
+
hiddenAtZero: () => true,
|
|
970
990
|
},
|
|
971
991
|
),
|
|
972
992
|
|
|
@@ -1009,10 +1029,68 @@ void ${P}rs(vec3 oc, vec3 mc, inout vec4 r, inout vec3 sc) {
|
|
|
1009
1029
|
col.rgb = mix(col.rgb, ${P}sc, min(1.0, tw * ${P}sp) * (1.0 - g));
|
|
1010
1030
|
col.a *= smoothstep(0.0, 0.12, lp) * mix(0.55 + 0.45 * tw, 1.0, g);`,
|
|
1011
1031
|
),
|
|
1032
|
+
hiddenAtZero: () => true,
|
|
1012
1033
|
},
|
|
1013
1034
|
),
|
|
1014
1035
|
});
|
|
1015
1036
|
|
|
1037
|
+
// ── setSource's particle transitions ─────────────────────────────────────────────────────────
|
|
1038
|
+
//
|
|
1039
|
+
// swarm / burst / shimmer-cross / dust: the OUTGOING photo plays a particle reveal backwards (its
|
|
1040
|
+
// gaussians leave home as dots and thin out to nothing, `vanish`) while the INCOMING one plays it
|
|
1041
|
+
// forwards, on overlapping spans of one clock. Both run as entity-scope effects driven by the
|
|
1042
|
+
// adapter (SplatEffects.drive); the outgoing one is drawn by the LIVE outgoing camera, and the
|
|
1043
|
+
// adapter composites the two live images per eye, the old one OVER the new one.
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* The transitions and their defaults. `out` / `in` = the particle effect each side plays (out: in
|
|
1047
|
+
* reverse) with its option overrides; `overlap` = how much of the clock the two spans share (0 =
|
|
1048
|
+
* one after the other, 1 = both over the whole clock): out over [0, (1 + overlap)/2], in over
|
|
1049
|
+
* [(1 − overlap)/2, 1] of the eased clock.
|
|
1050
|
+
*/
|
|
1051
|
+
export const PARTICLE_TRANSITIONS = Object.freeze({
|
|
1052
|
+
swarm: {
|
|
1053
|
+
durationMs: 2800,
|
|
1054
|
+
easing: 'linear',
|
|
1055
|
+
overlap: 0.45,
|
|
1056
|
+
out: { effect: 'assemble', opts: { stagger: 0.65, jitter: 0.35, spread: 0.3, swirl: 1.8, density: 0.2, dotSize: 0.0009, vanish: 0.45, glow: 0.1 } },
|
|
1057
|
+
in: { effect: 'assemble', opts: { stagger: 0.65, spread: 0.3, swirl: 1.8, density: 0.2, dotSize: 0.0009, vanish: 0.35, glow: 0.1 } },
|
|
1058
|
+
},
|
|
1059
|
+
burst: {
|
|
1060
|
+
durationMs: 2600,
|
|
1061
|
+
easing: 'linear',
|
|
1062
|
+
overlap: 0.3,
|
|
1063
|
+
out: { effect: 'converge', opts: { stagger: 0.5, density: 0.5, glow: 0.25 } },
|
|
1064
|
+
in: { effect: 'converge', opts: { stagger: 0.55, density: 0.5 } },
|
|
1065
|
+
},
|
|
1066
|
+
'shimmer-cross': {
|
|
1067
|
+
durationMs: 2600,
|
|
1068
|
+
easing: 'linear',
|
|
1069
|
+
overlap: 0.45,
|
|
1070
|
+
out: { effect: 'shimmer', opts: { stagger: 0.75 } },
|
|
1071
|
+
in: { effect: 'shimmer', opts: { stagger: 0.75 } },
|
|
1072
|
+
},
|
|
1073
|
+
dust: {
|
|
1074
|
+
durationMs: 2800,
|
|
1075
|
+
easing: 'linear',
|
|
1076
|
+
overlap: 0.4,
|
|
1077
|
+
out: { effect: 'dissolve-in', opts: { density: 0.4, vanish: 0.4, drift: 0.45 } },
|
|
1078
|
+
in: { effect: 'dissolve-in', opts: { density: 0.4, vanish: 0.3 } },
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
/** Where a side stands at eased clock t: 0..1 over its span. */
|
|
1083
|
+
export function particleSpan(t, overlap, side) {
|
|
1084
|
+
const v = Math.min(1, Math.max(0, overlap));
|
|
1085
|
+
const a = side === 'out' ? 0 : (1 - v) / 2;
|
|
1086
|
+
const b = side === 'out' ? (1 + v) / 2 : 1;
|
|
1087
|
+
return Math.min(1, Math.max(0, (t - a) / Math.max(b - a, 1e-6)));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Options a page may pass through setSource to both sides of a particle transition. */
|
|
1091
|
+
export const PARTICLE_TRANSITION_OPTIONS = Object.freeze(['order', 'stagger', 'jitter', 'maxDisparity', 'dotSize', 'noiseScale', 'layerSize', 'origin']);
|
|
1092
|
+
|
|
1093
|
+
|
|
1016
1094
|
/**
|
|
1017
1095
|
* The wavefront's per-column commit, lt ∈ [0, 1], for eased progress `t` at normalised u — the
|
|
1018
1096
|
* prototype's formula: column u starts at t = u·(1 − band) and has committed band later, so
|
|
@@ -1243,7 +1321,8 @@ export class SplatEffects {
|
|
|
1243
1321
|
resolve: null,
|
|
1244
1322
|
promise: null,
|
|
1245
1323
|
originPoint: (ctx, eyes) => this._origin(inst, ctx, eyes),
|
|
1246
|
-
|
|
1324
|
+
// a driven instance (drive()) is handed its time by the adapter's clock
|
|
1325
|
+
elapsedS: (tMs) => (inst.timeS !== undefined ? inst.timeS : inst.startedAt === null ? 0 : Math.max(0, (tMs - inst.startedAt) / 1000)),
|
|
1247
1326
|
};
|
|
1248
1327
|
inst.promise = new Promise((r) => (inst.resolve = r));
|
|
1249
1328
|
return inst;
|
|
@@ -1326,6 +1405,134 @@ export class SplatEffects {
|
|
|
1326
1405
|
this._install(entity);
|
|
1327
1406
|
}
|
|
1328
1407
|
|
|
1408
|
+
/**
|
|
1409
|
+
* An effect on one entity driven by the ADAPTER's clock, not the runner's (setSource's particle
|
|
1410
|
+
* transitions: two effects on two assets, on overlapping spans of one transition clock). Hidden
|
|
1411
|
+
* from effects() and from a page's stopEffect(). Returns { set(amount, timeS), restart(),
|
|
1412
|
+
* remove() }: `amount` as the GLSL reads it (1 = the untouched asset), `restart()` re-takes the
|
|
1413
|
+
* effect's frame (start()) — the incoming asset's, once its rig and first frames are in.
|
|
1414
|
+
*/
|
|
1415
|
+
drive(entity, name, opts) {
|
|
1416
|
+
const o = resolveEffectOptions(name, { ...opts, scope: 'entity', direction: 'in', progress: 0 }, 'set', { internal: true });
|
|
1417
|
+
o.entity = entity;
|
|
1418
|
+
const inst = this._makeInstance(name, o, 'set');
|
|
1419
|
+
inst.hidden = true;
|
|
1420
|
+
inst.timeS = 0;
|
|
1421
|
+
this._replace(entity, name, inst);
|
|
1422
|
+
this._setupInstance(inst);
|
|
1423
|
+
this._apply(entity, inst, this.ctx.now());
|
|
1424
|
+
this._install(entity);
|
|
1425
|
+
const alive = () => !this._disposed && this.scopes.get(entity)?.get(name) === inst;
|
|
1426
|
+
return {
|
|
1427
|
+
set: (amount, timeS = inst.timeS) => {
|
|
1428
|
+
if (!alive()) return;
|
|
1429
|
+
inst.opts.progress = Math.min(1, Math.max(0, amount));
|
|
1430
|
+
inst.timeS = timeS;
|
|
1431
|
+
this._apply(entity, inst, this.ctx.now());
|
|
1432
|
+
},
|
|
1433
|
+
restart: () => {
|
|
1434
|
+
if (!alive()) return;
|
|
1435
|
+
inst.state = {};
|
|
1436
|
+
this._setupInstance(inst);
|
|
1437
|
+
this._apply(entity, inst, this.ctx.now());
|
|
1438
|
+
},
|
|
1439
|
+
remove: () => {
|
|
1440
|
+
if (!alive()) return;
|
|
1441
|
+
this.scopes.get(entity).delete(name);
|
|
1442
|
+
inst.resolve({ finished: true });
|
|
1443
|
+
this._install(entity);
|
|
1444
|
+
},
|
|
1445
|
+
get alive() {
|
|
1446
|
+
return alive();
|
|
1447
|
+
},
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* ONE tile-scope body for the two photos of a setSource particle transition, with each photo's
|
|
1453
|
+
* values on its OWN mesh instance (the engine draws each photo through its own gsplat manager and
|
|
1454
|
+
* renderer: the eye camera's for the incoming photo, the live camera's for the outgoing one).
|
|
1455
|
+
* Render-time only: no work-buffer rewrite, no per-frame re-sort (an entity-scope modifier forces
|
|
1456
|
+
* both, every frame, for each 1.18M asset — measured as the transitions' hitches). The chunk's
|
|
1457
|
+
* material values are the untouched asset's (amount 1). Returns { side(getMeshInstance, opts),
|
|
1458
|
+
* remove() }; a side is { set(amount, timeS), restart() }. `opts` of the two sides share what
|
|
1459
|
+
* the GLSL is compiled from (the `order`).
|
|
1460
|
+
*/
|
|
1461
|
+
driveShared(name, effect, opts) {
|
|
1462
|
+
const def = EFFECTS[effect];
|
|
1463
|
+
const o = resolveEffectOptions(effect, { ...opts, scope: 'tile', direction: 'in', progress: 1 }, 'set', { internal: true });
|
|
1464
|
+
const inst = this._makeInstance(name, o, 'set');
|
|
1465
|
+
inst.def = def;
|
|
1466
|
+
inst.hidden = true;
|
|
1467
|
+
inst.fixed = true; // its material values never change: no per-frame upload
|
|
1468
|
+
this._replace('tile', name, inst);
|
|
1469
|
+
this._setupInstance(inst);
|
|
1470
|
+
this._apply('tile', inst, this.ctx.now());
|
|
1471
|
+
this._install('tile');
|
|
1472
|
+
const P = prefixOf(name);
|
|
1473
|
+
const alive = () => !this._disposed && this.scopes.get('tile')?.get(name) === inst;
|
|
1474
|
+
const touched = new Set();
|
|
1475
|
+
const hidden = new Set(); // mesh instances this transition turned off (a photo with nothing to draw)
|
|
1476
|
+
return {
|
|
1477
|
+
side: (getMeshInstance, sideOpts) => {
|
|
1478
|
+
const s = { opts: resolveEffectOptions(effect, { ...sideOpts, order: o.order, scope: 'tile', direction: 'in' }, 'set', { internal: true }), state: {}, timeS: 0 };
|
|
1479
|
+
s.originPoint = (ctx, eyes) => this._origin(s, ctx, eyes);
|
|
1480
|
+
s.elapsedS = () => s.timeS;
|
|
1481
|
+
def.start?.(this.ctx, s);
|
|
1482
|
+
return {
|
|
1483
|
+
/** Upload this photo's values; false when its mesh instance is not there (yet). */
|
|
1484
|
+
set: (amount, timeS = s.timeS) => {
|
|
1485
|
+
if (!alive()) return false;
|
|
1486
|
+
const mi = getMeshInstance();
|
|
1487
|
+
if (!mi?.setParameter) return false;
|
|
1488
|
+
s.timeS = timeS;
|
|
1489
|
+
touched.add(mi);
|
|
1490
|
+
// the values first, always: should the engine show the mesh again on its own (a
|
|
1491
|
+
// rebuild sets its visibility), it draws this state, never the untouched photo
|
|
1492
|
+
const values = def.uniforms(this.ctx, s, Math.min(1, Math.max(0, amount)), this.ctx.now());
|
|
1493
|
+
for (const [k, v] of Object.entries(values)) mi.setParameter(P + k, v);
|
|
1494
|
+
// Nothing of this photo is drawn at amount 0 (every particle hidden before its flight):
|
|
1495
|
+
// skip its draw call rather than run the vertex stage for 1.18M invisible gaussians —
|
|
1496
|
+
// the two photos then share the GPU only while their spans overlap.
|
|
1497
|
+
if (amount <= 0 && def.hiddenAtZero?.(s.opts)) {
|
|
1498
|
+
if (mi.visible !== false) {
|
|
1499
|
+
mi.visible = false;
|
|
1500
|
+
hidden.add(mi);
|
|
1501
|
+
}
|
|
1502
|
+
} else if (hidden.delete(mi)) mi.visible = true;
|
|
1503
|
+
return true;
|
|
1504
|
+
},
|
|
1505
|
+
restart: () => {
|
|
1506
|
+
s.state = {};
|
|
1507
|
+
def.start?.(this.ctx, s);
|
|
1508
|
+
},
|
|
1509
|
+
};
|
|
1510
|
+
},
|
|
1511
|
+
remove: () => {
|
|
1512
|
+
for (const mi of hidden) mi.visible = true;
|
|
1513
|
+
hidden.clear();
|
|
1514
|
+
for (const mi of touched) {
|
|
1515
|
+
for (const k of Object.keys(mi.parameters || {})) if (k.startsWith(P)) mi.deleteParameter?.(k);
|
|
1516
|
+
}
|
|
1517
|
+
touched.clear();
|
|
1518
|
+
if (!alive()) return;
|
|
1519
|
+
this.scopes.get('tile').delete(name);
|
|
1520
|
+
inst.resolve({ finished: true });
|
|
1521
|
+
this._install('tile');
|
|
1522
|
+
},
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* The tile chunk driveShared(name, effect, opts) WOULD install, without installing it — what
|
|
1528
|
+
* setSource's shader pre-warm compiles ahead of the transition.
|
|
1529
|
+
*/
|
|
1530
|
+
sharedChunkCode(name, effect, opts) {
|
|
1531
|
+
const o = resolveEffectOptions(effect, { ...opts, scope: 'tile', direction: 'in', progress: 1 }, 'set', { internal: true });
|
|
1532
|
+
const insts = [...(this.scopes.get('tile')?.values() ?? [])].filter((i) => i.name !== name);
|
|
1533
|
+
return composeModifier([...insts, { name, def: EFFECTS[effect], opts: o }]).code;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1329
1536
|
_setupInstance(inst) {
|
|
1330
1537
|
// Also for a gated one (its held START state needs the geometry); re-run when the gate opens,
|
|
1331
1538
|
// since the rig/framing may have changed meanwhile (setSource's flip adopts a new asset).
|
|
@@ -1346,7 +1553,7 @@ export class SplatEffects {
|
|
|
1346
1553
|
*/
|
|
1347
1554
|
stop(name, { finish = false, entity = null } = {}) {
|
|
1348
1555
|
for (const [key, n, inst] of [...this._all()]) {
|
|
1349
|
-
if (name ? n !== name : inst.def.internal) continue;
|
|
1556
|
+
if (inst.hidden || (name ? n !== name : inst.def.internal)) continue;
|
|
1350
1557
|
if (entity && key !== entity) continue;
|
|
1351
1558
|
const m = this.scopes.get(key);
|
|
1352
1559
|
if (finish && inst.mode === 'play' && inst.opts.direction === 'out') {
|
|
@@ -1366,7 +1573,7 @@ export class SplatEffects {
|
|
|
1366
1573
|
list() {
|
|
1367
1574
|
const out = [];
|
|
1368
1575
|
for (const [key, name, inst] of this._all()) {
|
|
1369
|
-
if (inst.def.internal) continue;
|
|
1576
|
+
if (inst.def.internal || inst.hidden) continue;
|
|
1370
1577
|
out.push({
|
|
1371
1578
|
name,
|
|
1372
1579
|
scope: key === 'tile' ? 'tile' : 'entity',
|
|
@@ -1398,6 +1605,7 @@ export class SplatEffects {
|
|
|
1398
1605
|
tick(tMs) {
|
|
1399
1606
|
if (this._disposed) return;
|
|
1400
1607
|
for (const [key, name, inst] of [...this._all()]) {
|
|
1608
|
+
if (inst.fixed) continue;
|
|
1401
1609
|
if (inst.mode === 'play') {
|
|
1402
1610
|
if (inst.startGate) {
|
|
1403
1611
|
this._apply(key, inst, tMs); // hold the start state
|