@displayxr/inline3d 1.7.1 → 1.8.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 +61 -0
- package/README.md +13 -0
- package/js/inline3d-splat-perf.js +126 -0
- package/js/inline3d-splat-playcanvas.js +1908 -0
- package/js/inline3d-splat-rig.js +268 -3
- package/js/inline3d-splat-shared.js +269 -0
- package/js/inline3d-splat.js +174 -118
- package/js/inline3d-three.js +43 -9
- package/js/inline3d-viewer.js +27 -41
- package/package.json +8 -2
- package/splat.d.ts +137 -7
- package/three.d.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,67 @@ 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.8.0 — 2026-09-22
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./splat`, plus one additive `./three` export). **Existing callers get
|
|
11
|
+
the same engine and the same code path**: with no `engine` option, or `engine: 'spark'`, `addSplat`
|
|
12
|
+
renders with three.js + Spark as in 1.7.1. The PlayCanvas engine is **opt-in** with
|
|
13
|
+
`engine: 'playcanvas'`.
|
|
14
|
+
|
|
15
|
+
**One behaviour change, for one class of file.** The camera-rig focus waterfall gains a rung (see
|
|
16
|
+
*Changed*). A `.sog` whose `camera` block has **no `focus`**, or a `focus` its converter computed
|
|
17
|
+
as the whole-cloud median (`focus.source: "cloud-median"`), now converges on the **nearest
|
|
18
|
+
substantial clump** in the middle of the frame instead of the whole-scene median. On an open
|
|
19
|
+
landscape that is 2.4 m (the tree trunk), not 46.9 m, so that file's 3D will look different, as
|
|
20
|
+
intended. Blocks with a considered focus (`convergence`, `manual`, …, or no `source`), assets with
|
|
21
|
+
no block, and a caller's `focus` / `convergence` are unaffected. Both engines.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`addSplat(…, { engine: 'playcanvas' })`: the PlayCanvas engine as a second splat backend**
|
|
26
|
+
(epic #36).
|
|
27
|
+
- Same handle and the same rig/lens/focus waterfall as Spark. One camera renders every view
|
|
28
|
+
through the engine's `RenderView` list, so one sort serves all eyes.
|
|
29
|
+
- A shader-chunk fix for the engine's square-pixel footprint assumption, which a side-by-side
|
|
30
|
+
buffer breaks.
|
|
31
|
+
- `perf` presets map onto engine knobs, and `perf: false` leaves the engine alone.
|
|
32
|
+
- A **URL** `.sog` yields its `camera` block too.
|
|
33
|
+
- Needs the new **optional** peer `playcanvas >=2.22.3 <3`, loaded by a dynamic `import()` only
|
|
34
|
+
when asked for.
|
|
35
|
+
- Reads `.sog`, `.ply` and a Streamed-SOG `lod-meta.json`. With `engine: 'playcanvas'`, a
|
|
36
|
+
`.spz` / `.splat` / `.ksplat` source (or a Spark-only `fileType`) **throws at call time**.
|
|
37
|
+
Those stay Spark formats.
|
|
38
|
+
- Differences from the Spark path: [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md).
|
|
39
|
+
- On the PlayCanvas backend:
|
|
40
|
+
- **`handle.setSource(src, { fadeMs, resetPose })`**: a crossfading in-place asset swap that
|
|
41
|
+
re-runs the rig waterfall for the new file. The pose is kept unless `resetPose`. It throws on
|
|
42
|
+
Spark.
|
|
43
|
+
- **`feather`**: the per-eye edge fade, matching `EdgeFeather`.
|
|
44
|
+
- **Tilt-and-relax `orbit`**: the drag is a fraction of the tile, capped at `orbitMaxDeg` (15°),
|
|
45
|
+
and relaxes back on release (`orbitEase` `{ drag: 0.2, rest: 0.6 }` s). Spark keeps its
|
|
46
|
+
turntable drag.
|
|
47
|
+
- **Exact `pick`**: the nearest gaussian centre over the full centre set, with haze skipped.
|
|
48
|
+
- **`captureFit: 'height' | 'cover'`** on the camera rig, both backends. `'height'` (default) is
|
|
49
|
+
the 1.7 window. `'cover'` fills the tile with photograph (a 4:3 capture in a 16:9 tile crops
|
|
50
|
+
top/bottom).
|
|
51
|
+
- **`handle.engine`** (advanced, not semver-covered): `{ app, root, camera }` on PlayCanvas, and
|
|
52
|
+
`{ renderer, scene, camera }` on Spark. **`handle.backend`**: `'playcanvas'` or `'spark'`.
|
|
53
|
+
- **`handle.getFocus()`** and **`handle.onFocusChange(point, { focusSource })`**, in the splat's
|
|
54
|
+
own space, on both backends. `handle.rig` gains `blockFocusSource` and `clumpMassFrac`.
|
|
55
|
+
- **`cameraRigFromPose(pose, opts)`** in `./three`. It builds the camera-rig descriptor from a
|
|
56
|
+
plain `{position, orientation, fov}`, bit-identical to `cameraRigFromCamera`, which is unchanged.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **Focus waterfall: `nearest-clump`** (the behaviour change above). The order is now: caller ›
|
|
61
|
+
caller convergence › block focus (any source except `cloud-median` / `median-disparity`) ›
|
|
62
|
+
**nearest-clump** (needs the block's or caller's lens) › `block-cloud-median` ›
|
|
63
|
+
`median-disparity` › 2 m.
|
|
64
|
+
- **Viewer tuning constants are shared** (`inline3d-splat-shared.js`). SceneViewer reads the same
|
|
65
|
+
damping, idle, focus-ease, wheel and zoom numbers as before, now from one module the PlayCanvas
|
|
66
|
+
viewer also reads. The values are unchanged, and a side-by-side trace test pins the two viewers
|
|
67
|
+
bit-identical.
|
|
68
|
+
|
|
8
69
|
## 1.7.1 — 2026-09-20
|
|
9
70
|
|
|
10
71
|
Touches the **preview tier** (`./model`) only, and fixes exactly one thing: under a bundler, a
|
package/README.md
CHANGED
|
@@ -40,6 +40,17 @@ and only the `/three`, `/viewer` and `/splat` subpaths need them. The two viewer
|
|
|
40
40
|
call (auto-framing on the zero-disparity plane, orbit, idle turntable, mono fallback), but their
|
|
41
41
|
API is not yet covered by the semver promise below.
|
|
42
42
|
|
|
43
|
+
**A second splat engine (preview).** `addSplat(wall, canvas, src, { engine: 'playcanvas' })`
|
|
44
|
+
renders the same splat window with the [PlayCanvas](https://playcanvas.com/) engine instead of
|
|
45
|
+
Spark — same handle, same rig/focus/camera-block behaviour, same `perf` presets (mapped onto the
|
|
46
|
+
engine's knobs) — and adds `setSource(src, { fadeMs })` (crossfading asset swaps), `feather`, a
|
|
47
|
+
tilt-and-relax orbit and `handle.engine` (the engine objects, for advanced pages). It needs the
|
|
48
|
+
optional peer `playcanvas` (`>=2.22.3 <3`) and reads `.sog`, `.ply` and a Streamed-SOG
|
|
49
|
+
`lod-meta.json`. **Spark stays the default**; a page that never passes `engine` never loads
|
|
50
|
+
`playcanvas`. What differs, and why: [`docs/playcanvas-adapter.md`](docs/playcanvas-adapter.md).
|
|
51
|
+
Bundlers: the engine is a literal `import('playcanvas')`; its sort workers are Blob URLs (CSP
|
|
52
|
+
`worker-src blob:`), and esbuild needs `node:worker_threads` marked external.
|
|
53
|
+
|
|
43
54
|
Stability & what's covered by semver (and the deferred N-view / web-components / CSS-native roadmap
|
|
44
55
|
that is intentionally **not** in 1.0): [`docs/sdk-stability.md`](docs/sdk-stability.md).
|
|
45
56
|
|
|
@@ -114,6 +125,8 @@ js/
|
|
|
114
125
|
and the placement readback (getSubjectBounds / getPose / depthOffset)
|
|
115
126
|
inline3d-splat.js experimental: addSplat() — a Gaussian splat window via Spark
|
|
116
127
|
(`perf` cuts overdraw; a `.sog`'s `camera` block picks the view rig)
|
|
128
|
+
inline3d-splat-playcanvas.js
|
|
129
|
+
preview: the `engine: 'playcanvas'` backend of addSplat(), loaded on demand
|
|
117
130
|
inline3d-model.js experimental: addModel() — a glTF/GLB window; wires Draco / meshopt / KTX2
|
|
118
131
|
from what the asset declares (you serve the decoder files — see the guide)
|
|
119
132
|
docs/
|
|
@@ -253,3 +253,129 @@ export function splatPerfMeshOptions(perf) {
|
|
|
253
253
|
if (!profile || !profile.lod) return {};
|
|
254
254
|
return { lod: profile.lod === 'quality' ? 'quality' : true };
|
|
255
255
|
}
|
|
256
|
+
|
|
257
|
+
// ── the same presets on the PlayCanvas engine (`addSplat(…, { engine: 'playcanvas' })`) ──────
|
|
258
|
+
//
|
|
259
|
+
// Same contract as the Spark half above: nothing moves unless asked, and every knob can be
|
|
260
|
+
// switched off. The presets are Spark-shaped (they were measured on Spark), so this maps each
|
|
261
|
+
// knob onto the engine's nearest equivalent rather than inventing engine presets:
|
|
262
|
+
//
|
|
263
|
+
// | Spark knob | engine (2.22.3) | note |
|
|
264
|
+
// |---|---|---|
|
|
265
|
+
// | `alphaRadius` | always on | the engine ALREADY shrinks every quad to its own alpha radius (`clipCorner` in `gsplatCommonVS`: `min(1, sqrt(ln(a / alphaClip)) / 2)` of the √8σ quad — the same cut `patchAlphaRadius` puts into Spark). It cannot be turned off, so `alphaRadius: false` is reported as `'native'`. |
|
|
266
|
+
// | `minAlpha` | `scene.gsplat.alphaClipForward` | engine default is already 1/255 |
|
|
267
|
+
// | `maxStdDev` | chunk override of `gsplatCommonVS` | the engine's quad is a fixed √8σ; the override caps `clipCorner`'s scale at `maxStdDev/√8`, which truncates both the quad AND the gaussian's uv, i.e. Spark's semantics. Only ever SHRINKS (≤ √8σ). |
|
|
268
|
+
// | `minPixelRadius` | `scene.gsplat.minPixelSize` = 2 × radius | the engine compares a quad DIAMETER in px (`max(l1,l2)`) |
|
|
269
|
+
// | `lod`, `lodSplat*`, `maxPixelRadius`, `falloff`, `alphaFloor` | none | Spark-only; ignored with one warning. The engine's splat-count lever is `splatBudget` (below) |
|
|
270
|
+
//
|
|
271
|
+
// Engine-native keys may be passed in an options object as well and win over the mapping:
|
|
272
|
+
// `alphaClipForward`, `minPixelSize`, `splatBudget` (a global splat count, per app = per tile),
|
|
273
|
+
// `antiAlias` (only for AA-trained assets).
|
|
274
|
+
//
|
|
275
|
+
// `minPixelSize` IS THE ONE DEFAULT CHANGED. The engine drops every splat whose quad is under
|
|
276
|
+
// 2 px by default; Spark keeps them (its `minPixelRadius` default is 0). On a lifted photograph
|
|
277
|
+
// the sub-2px grain is real texture, so every `perf` value except `false` starts from 0 — the
|
|
278
|
+
// Spark-parity baseline — and only a preset or option that states a size moves it. `perf: false`
|
|
279
|
+
// is the kill switch: the engine's own defaults, untouched, 2 px included.
|
|
280
|
+
|
|
281
|
+
/** The engine's quad half-extent, in σ: `l = 2·sqrt(2λ)` over cornerUV ∈ [-1,1] (gsplatCorner). */
|
|
282
|
+
export const PLAYCANVAS_QUAD_SIGMA = Math.sqrt(8);
|
|
283
|
+
|
|
284
|
+
/** Knobs that are engine-native and pass straight through to `app.scene.gsplat`. */
|
|
285
|
+
const PC_NATIVE = ['alphaClipForward', 'minPixelSize', 'splatBudget'];
|
|
286
|
+
|
|
287
|
+
/** Spark knobs with no engine equivalent — named once in a warning, then ignored. */
|
|
288
|
+
const PC_UNMAPPED = ['lod', 'lodSplatCount', 'lodSplatScale', 'lodRenderScale', 'maxPixelRadius', 'falloff', 'alphaFloor'];
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resolve `perf` into engine settings. Pure: no engine, no GPU — the adapter applies the result.
|
|
292
|
+
*
|
|
293
|
+
* @param {undefined|null|false|true|string|object} perf the `addSplat` option, as given.
|
|
294
|
+
* @returns {{settings:object, quadExtent:number|null, applied:object|null, ignored:string[]}}
|
|
295
|
+
* `settings` go onto `app.scene.gsplat`; `quadExtent` (a fraction of the √8σ quad, or
|
|
296
|
+
* null for untouched) goes into the `gsplatCommonVS` override; `applied` is what the
|
|
297
|
+
* handle reports (null for `perf: false`).
|
|
298
|
+
*/
|
|
299
|
+
export function playcanvasPerfSettings(perf) {
|
|
300
|
+
const out = { settings: {}, quadExtent: null, applied: null, ignored: [] };
|
|
301
|
+
if (perf === false) return out; // kill switch: engine defaults, untouched
|
|
302
|
+
|
|
303
|
+
let profile = null;
|
|
304
|
+
let name = null;
|
|
305
|
+
if (perf === undefined || perf === null) name = 'default';
|
|
306
|
+
else if (perf === true) {
|
|
307
|
+
profile = SPLAT_PERF_PRESETS.balanced;
|
|
308
|
+
name = 'balanced';
|
|
309
|
+
} else if (typeof perf === 'string') {
|
|
310
|
+
profile = SPLAT_PERF_PRESETS[perf] || null;
|
|
311
|
+
name = profile ? perf : 'default';
|
|
312
|
+
if (!profile) {
|
|
313
|
+
console.warn(
|
|
314
|
+
`[inline3d/splat] unknown perf preset "${perf}" — ignored. ` +
|
|
315
|
+
`Known: ${Object.keys(SPLAT_PERF_PRESETS).join(', ')}, or an options object.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} else if (typeof perf === 'object') {
|
|
319
|
+
profile = perf;
|
|
320
|
+
name = 'custom';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const s = out.settings;
|
|
324
|
+
const applied = { preset: name };
|
|
325
|
+
// The Spark-parity baseline. See the block comment above.
|
|
326
|
+
s.minPixelSize = 0;
|
|
327
|
+
if (profile) {
|
|
328
|
+
if (isNum(profile.minAlpha)) s.alphaClipForward = profile.minAlpha;
|
|
329
|
+
if (isNum(profile.minPixelRadius)) s.minPixelSize = 2 * Math.max(0, profile.minPixelRadius);
|
|
330
|
+
if (isNum(profile.maxStdDev) && profile.maxStdDev > 0) {
|
|
331
|
+
const k = Math.min(1, profile.maxStdDev / PLAYCANVAS_QUAD_SIGMA);
|
|
332
|
+
if (k < 1) out.quadExtent = k;
|
|
333
|
+
}
|
|
334
|
+
if (profile.alphaRadius !== undefined) applied.alphaRadius = 'native';
|
|
335
|
+
for (const key of PC_NATIVE) if (isNum(profile[key])) s[key] = profile[key];
|
|
336
|
+
if (typeof profile.antiAlias === 'boolean') s.antiAlias = profile.antiAlias;
|
|
337
|
+
for (const key of PC_UNMAPPED) if (profile[key] !== undefined) out.ignored.push(key);
|
|
338
|
+
if (out.ignored.length) {
|
|
339
|
+
console.warn(
|
|
340
|
+
`[inline3d/splat] perf: ${out.ignored.join(', ')} ${out.ignored.length > 1 ? 'have' : 'has'} ` +
|
|
341
|
+
'no PlayCanvas equivalent and ' +
|
|
342
|
+
`${out.ignored.length > 1 ? 'are' : 'is'} ignored on engine:'playcanvas' — the engine's ` +
|
|
343
|
+
'splat-count lever is `splatBudget` (see js/inline3d-splat-perf.js).',
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
Object.assign(applied, s);
|
|
348
|
+
if (out.quadExtent !== null) applied.maxStdDev = out.quadExtent * PLAYCANVAS_QUAD_SIGMA;
|
|
349
|
+
out.applied = applied;
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
354
|
+
|
|
355
|
+
// The anchor the quad-extent override rewrites, from the engine's `gsplatCommonVS` (2.22.3). The
|
|
356
|
+
// npm build re-indents chunks with tabs and a later release may re-space them, so it is a
|
|
357
|
+
// whitespace-agnostic regex, not a string.
|
|
358
|
+
const PC_CLIP_ANCHOR = /float\s+clip\s*=\s*min\(\s*1\.0\s*,/;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Cap the engine's per-splat quad at `k` of its √8σ extent — Spark's `maxStdDev`, on PlayCanvas.
|
|
362
|
+
*
|
|
363
|
+
* Rewrites `clipCorner`'s `min(1.0, …)` to `min(k, …)`, which scales the corner offset AND the uv
|
|
364
|
+
* the fragment shader evaluates the gaussian at, so the tail is truncated rather than the
|
|
365
|
+
* profile squashed. Returns the source unchanged (and `ok:false`) when the anchor is missing;
|
|
366
|
+
* the caller warns once and renders at the engine's own extent.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} src the current `gsplatCommonVS` chunk.
|
|
369
|
+
* @param {number} k in (0, 1).
|
|
370
|
+
* @returns {{src:string, ok:boolean}}
|
|
371
|
+
*/
|
|
372
|
+
export function patchPlayCanvasQuadExtent(src, k) {
|
|
373
|
+
if (typeof src !== 'string' || !(k > 0 && k < 1)) return { src, ok: false };
|
|
374
|
+
if (src.includes('dxrQuadExtent')) return { src, ok: true }; // idempotent
|
|
375
|
+
if (!PC_CLIP_ANCHOR.test(src)) return { src, ok: false };
|
|
376
|
+
const lit = k.toFixed(7);
|
|
377
|
+
return {
|
|
378
|
+
src: src.replace(PC_CLIP_ANCHOR, `float clip = min(${lit} /* dxrQuadExtent */,`),
|
|
379
|
+
ok: true,
|
|
380
|
+
};
|
|
381
|
+
}
|