@displayxr/inline3d 1.16.0 → 1.17.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/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/js/inline3d-model-playcanvas.js +351 -8
- package/js/inline3d-playcanvas-engine.js +1 -0
- package/js/inline3d-splat-playcanvas.js +65 -16
- package/model.d.ts +12 -6
- package/package.json +1 -1
- package/splat.d.ts +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ 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.17.0 — 2026-09-24
|
|
9
|
+
|
|
10
|
+
Touches the **preview tier** (`./model` and `./splat`, `engine: 'playcanvas'` only). The default look
|
|
11
|
+
does not move except on glTFs that use `KHR_materials_transmission`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`environment: 'room'` is now three's room on the PlayCanvas backend** (#36). `addModel(…, {
|
|
16
|
+
environment: 'room' })` and `setRig('display', { environment: 'room' })` light with three.js's
|
|
17
|
+
procedural RoomEnvironment, regenerated in memory (ray cast, no asset, ~16 ms once per page), and
|
|
18
|
+
default to tone mapping `'none'`, three's. Through 1.16 `'room'` was an alias of `'neutral'` on
|
|
19
|
+
this engine, so a page that passed it to keep its three look got the Sample-Viewer studio with PBR
|
|
20
|
+
Neutral. **The default is now spelled `'neutral'` and renders exactly as before** (Khronos gate
|
|
21
|
+
re-run: 16.7 / 6.4 / 2.7 / 9.4, unchanged). On a four-item product catalogue, object MAE against three r180
|
|
22
|
+
`room` goes from 20.5 / 54.4 / 29.5 / 23.3 (the default) to **10.2 / 11.0 / 7.1 / 6.3** (storm
|
|
23
|
+
lantern / handbag / boot / compass). Tone mapping and the environment explain the gap, and what is
|
|
24
|
+
left is where three departs from the Sample Viewer (PlayCanvas `room` is closer to the Sample
|
|
25
|
+
Viewer lit by the same room than three is: handbag 4.8 vs 13.4). `setRig('display')` with
|
|
26
|
+
`'room'` renders pixel-identical to `addModel` with it. See
|
|
27
|
+
[`docs/playcanvas-model-backend.md` § Environments](docs/playcanvas-model-backend.md#environments--neutral-default-and-room-three).
|
|
28
|
+
- `setRig('display')`'s `toneMapping` now defaults to the environment's (`'none'` for `'room'`,
|
|
29
|
+
`'neutral'` otherwise). A second `setRig('display')` with another environment swaps the IBL it
|
|
30
|
+
installed.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **`KHR_materials_transmission` / `_volume` on PlayCanvas** (#36), for `addModel` and for meshes
|
|
35
|
+
under root on `setRig('display')`:
|
|
36
|
+
- The scene-colour grab pass the material samples is now requested. Without it the storm lantern's
|
|
37
|
+
burner rendered as a magenta blob inside the globe.
|
|
38
|
+
- Transmissive draws sort ahead of blended ones, the pass order of the Sample Viewer and three.
|
|
39
|
+
The lantern's opaque-alpha globe no longer shows the burner through it.
|
|
40
|
+
- The grab UV is taken per view. The engine mapped each view's NDC over the whole target, so in
|
|
41
|
+
stereo each eye sampled across both. Mono renders are identical.
|
|
42
|
+
- Lantern object MAE vs the Sample Viewer (default look): 14.4 → 12.4.
|
|
43
|
+
- See [§ Transmission](docs/playcanvas-model-backend.md#transmission-khr_materials_transmission--_volume).
|
|
44
|
+
|
|
8
45
|
## 1.16.0 — 2026-09-24
|
|
9
46
|
|
|
10
47
|
Touches the **preview tier** (`./splat`, `engine: 'playcanvas'` only). Additive. A page that never
|
package/README.md
CHANGED
|
@@ -69,7 +69,8 @@ Splat and model rendering on the PlayCanvas backend uses the PlayCanvas engine (
|
|
|
69
69
|
**Models render with PlayCanvas by default (1.12).** `addModel(wall, canvas, 'chair.glb')` loads
|
|
70
70
|
the glTF with the PlayCanvas engine (optional peer `playcanvas`), lit to match the Khronos glTF
|
|
71
71
|
Sample Viewer: an in-memory neutral studio environment, Khronos PBR Neutral tone mapping,
|
|
72
|
-
exposure 1. `
|
|
72
|
+
exposure 1. `environment: 'room'` gives three's RoomEnvironment look instead (a page tuned on the
|
|
73
|
+
three path). `engine: 'three'` is the 1.11 three.js renderer, unchanged (also importable as
|
|
73
74
|
`@displayxr/inline3d/model/three`). Each engine is imported only when a tile asks for it. Draco
|
|
74
75
|
and KTX2 use the same served decoder folders on both engines. Design, the Sample-Viewer MAE table
|
|
75
76
|
and what throws: [`docs/playcanvas-model-backend.md`](docs/playcanvas-model-backend.md).
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -6,7 +6,7 @@ it are adapted from, or run on, the MIT-licensed projects below. Their notices f
|
|
|
6
6
|
| Project | Version | How it is used |
|
|
7
7
|
|---|---|---|
|
|
8
8
|
| [PlayCanvas engine](https://github.com/playcanvas/engine) | 2.22.3 | Optional peer (`playcanvas`): the default splat backend and the `./model` backend. Shader code in this package is **adapted** from it: the non-square-pixel footprint rewrite of `src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatCorner.js` (`js/inline3d-splat-playcanvas.js`), the quad-extent cap in `gsplatCommon.js` (`js/inline3d-splat-perf.js`), and the dissolve effect and its hash/noise/fbm helpers from `scripts/esm/gsplat/shader-effect-dissolve.mjs` (`js/inline3d-splat-effects.js`). Each adapted site carries a provenance comment. |
|
|
9
|
-
| [three.js](https://github.com/mrdoob/three.js) | ≥ 0.150 | Optional peer (`three`): the `./three` helpers, the Spark splat path, `./model`'s `engine: 'three'`. |
|
|
9
|
+
| [three.js](https://github.com/mrdoob/three.js) | ≥ 0.150 | Optional peer (`three`): the `./three` helpers, the Spark splat path, `./model`'s `engine: 'three'`. The PlayCanvas backend's `environment: 'room'` is **adapted** from `examples/jsm/environments/RoomEnvironment.js` (its scene numbers, re-rendered by ray casting in `js/inline3d-model-playcanvas.js`, `ROOM_ENVIRONMENT`). |
|
|
10
10
|
| [Spark](https://github.com/sparkjsdev/spark) | ≥ 2.0 | Optional peer (`@sparkjsdev/spark`): the Spark splat backend. |
|
|
11
11
|
|
|
12
12
|
## PlayCanvas engine — MIT
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
// and was fitted to the Sample Viewer's "Studio Neutral". three's path keeps RoomEnvironment
|
|
25
25
|
// and no tone mapping (unchanged since 1.11); the two backends therefore do not look
|
|
26
26
|
// identical, by design — docs/playcanvas-model-backend.md §Lighting has the MAE table.
|
|
27
|
+
// `environment: 'room'` is three's room on this engine (§ the room environment), untonemapped,
|
|
28
|
+
// for a page tuned on the three path (§Environments).
|
|
27
29
|
// · Decoders are the ENGINE's: Draco and Basis through the engine's own worker readers (fed the
|
|
28
30
|
// SAME files three's `libs/draco/` and `libs/basis/` folders hold), meshopt through the
|
|
29
31
|
// container's bufferView hook with meshoptimizer's `MeshoptDecoder` (the engine has no
|
|
@@ -347,10 +349,13 @@ export const ENV_YAW_DEG = 0;
|
|
|
347
349
|
|
|
348
350
|
/** Build the default environment on this app and set it as the scene's envAtlas. */
|
|
349
351
|
export function useNeutralStudio(pc, app, yawDeg) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
+
return useRgbeEquirect(pc, app, 'inline3d-neutral-studio', neutralStudioRGBE(256, 128), 256, 128, yawDeg);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** A grey RGBE equirect (row 0 = up) → prefiltered envAtlas on the scene. */
|
|
356
|
+
function useRgbeEquirect(pc, app, name, bytes, W, H, yawDeg) {
|
|
352
357
|
const tex = new pc.Texture(app.graphicsDevice, {
|
|
353
|
-
name
|
|
358
|
+
name,
|
|
354
359
|
width: W,
|
|
355
360
|
height: H,
|
|
356
361
|
format: pc.PIXELFORMAT_RGBA8,
|
|
@@ -359,7 +364,7 @@ export function useNeutralStudio(pc, app, yawDeg) {
|
|
|
359
364
|
mipmaps: false,
|
|
360
365
|
addressU: pc.ADDRESS_REPEAT,
|
|
361
366
|
addressV: pc.ADDRESS_CLAMP_TO_EDGE,
|
|
362
|
-
levels: [
|
|
367
|
+
levels: [bytes],
|
|
363
368
|
});
|
|
364
369
|
return useLightingSource(pc, app, tex, true, yawDeg);
|
|
365
370
|
}
|
|
@@ -373,7 +378,9 @@ function useLightingSource(pc, app, source, own, yawDeg = 0) {
|
|
|
373
378
|
app.scene.envAtlas = atlas;
|
|
374
379
|
app.scene.skyboxIntensity = 1;
|
|
375
380
|
app.scene.exposure = 1;
|
|
376
|
-
|
|
381
|
+
// Always written (identity at 0): a later environment on the same scene must not inherit the
|
|
382
|
+
// previous one's yaw (setRig switches room ↔ neutral on one app).
|
|
383
|
+
if (pc.Quat) app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(0, yawDeg || 0, 0);
|
|
377
384
|
return atlas;
|
|
378
385
|
}
|
|
379
386
|
|
|
@@ -388,6 +395,244 @@ function loadTexture(pc, app, url) {
|
|
|
388
395
|
});
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
// ── the room environment (environment: 'room') ─────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* `environment: 'room'` — three.js's procedural RoomEnvironment, regenerated here without three:
|
|
402
|
+
* the same scene (a white room lit by one point light, six grey boxes, six emissive panels), shaded
|
|
403
|
+
* the way three shades it when `PMREMGenerator.fromScene(new RoomEnvironment(), 0.04)` bakes it —
|
|
404
|
+
* which is exactly what `addModel(…, { engine: 'three', environment: 'room' })` lights with.
|
|
405
|
+
*
|
|
406
|
+
* It exists so a page that chose `'room'` on the three path gets that look on this backend. It is
|
|
407
|
+
* NOT the default: the default (`'neutral'`) is matched to the Khronos Sample Viewer, and three's
|
|
408
|
+
* room is a different, brighter room (mean radiance ~0.97 vs ~0.89, most of it from a 100-nit
|
|
409
|
+
* ceiling panel and two 50-nit wall panels).
|
|
410
|
+
*
|
|
411
|
+
* The scene's numbers (positions, rotations, scales, intensities) are three.js's
|
|
412
|
+
* examples/jsm/environments/RoomEnvironment.js (MIT, © three.js authors), itself after
|
|
413
|
+
* model-viewer's EnvironmentScene. No image is shipped: `roomRadiance` ray-casts the boxes per
|
|
414
|
+
* texel. Checked against three r180's own bake (its PMREM, read back through `textureCubeUV`):
|
|
415
|
+
* texel-to-texel ratio median 1.000 (p10 0.989, p90 1.006), solid-angle-weighted MAE 0.03 unblurred
|
|
416
|
+
* and 0.04 with the 0.04-rad blur. Solids are [cx, cy, cz, angleY, sx, sy, sz] (boxes) and
|
|
417
|
+
* [cx, cy, cz, sx, sy, sz, emissive] (panels), BoxGeometry(1) scaled — i.e. full extents.
|
|
418
|
+
*/
|
|
419
|
+
export const ROOM_ENVIRONMENT = Object.freeze({
|
|
420
|
+
light: [0.418, 16.199, 0.3, 900, 28, 2], // point light: position, intensity (cd), cutoff distance, decay
|
|
421
|
+
room: [-0.757, 13.219, 0.717, 31.713, 28.305, 28.591], // seen from inside
|
|
422
|
+
boxes: [
|
|
423
|
+
[-10.906, 2.009, 1.846, -0.195, 2.328, 7.905, 4.651],
|
|
424
|
+
[-5.607, -0.754, -0.758, 0.994, 1.97, 1.534, 3.955],
|
|
425
|
+
[6.167, 0.857, 7.803, 0.561, 3.927, 6.285, 3.687],
|
|
426
|
+
[-2.017, 0.018, 6.124, 0.333, 2.002, 4.566, 2.064],
|
|
427
|
+
[2.291, -0.756, -2.621, -0.286, 1.546, 1.552, 1.496],
|
|
428
|
+
[-2.193, -0.369, -5.547, 0.516, 3.875, 3.487, 2.986],
|
|
429
|
+
],
|
|
430
|
+
panels: [
|
|
431
|
+
[-16.116, 14.37, 8.208, 0.1, 2.428, 2.739, 50],
|
|
432
|
+
[-16.109, 18.021, -8.207, 0.1, 2.425, 2.751, 50],
|
|
433
|
+
[14.904, 12.198, -1.832, 0.15, 4.265, 6.331, 17],
|
|
434
|
+
[-0.462, 8.89, 14.52, 4.38, 5.441, 0.088, 43],
|
|
435
|
+
[3.235, 11.486, -12.541, 2.5, 2.0, 0.1, 20],
|
|
436
|
+
[0.0, 20.0, 0.0, 1.0, 0.1, 1.0, 100],
|
|
437
|
+
],
|
|
438
|
+
blur: 0.04, // radians — the sigma addModel's three path passes to fromScene
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/** Every solid as [cx, cy, cz, cosY, sinY, hx, hy, hz, kind (0 room, 1 lit box, 2 panel), emissive]. */
|
|
442
|
+
const _roomSolids = new WeakMap();
|
|
443
|
+
function roomSolids(env) {
|
|
444
|
+
let out = _roomSolids.get(env);
|
|
445
|
+
if (out) return out;
|
|
446
|
+
const [rx, ry, rz, sx, sy, sz] = env.room;
|
|
447
|
+
out = [[rx, ry, rz, 1, 0, sx / 2, sy / 2, sz / 2, 0, 0]];
|
|
448
|
+
for (const [px, py, pz, a, bx, by, bz] of env.boxes) out.push([px, py, pz, Math.cos(a), Math.sin(a), bx / 2, by / 2, bz / 2, 1, 0]);
|
|
449
|
+
for (const [px, py, pz, bx, by, bz, e] of env.panels) out.push([px, py, pz, 1, 0, bx / 2, by / 2, bz / 2, 2, e]);
|
|
450
|
+
_roomSolids.set(env, out);
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Radiance of the room seen from the origin toward unit direction (x, y, z): the nearest solid
|
|
456
|
+
* along the ray. A panel is its emissive value; a wall or box (white, roughness 1, non-metal) is
|
|
457
|
+
* three's direct physical shading from the one point light, unshadowed as in three (the scene casts
|
|
458
|
+
* no shadows) — Lambert plus GGX at alpha 1 (D = 1/π, V = 0.5/(NL+NV), Schlick F0 0.04), with
|
|
459
|
+
* three's inverse-square falloff and its (1 − (d/cutoff)⁴)² window.
|
|
460
|
+
*/
|
|
461
|
+
export function roomRadiance(x, y, z, env = ROOM_ENVIRONMENT) {
|
|
462
|
+
let best = Infinity;
|
|
463
|
+
let nx = 0;
|
|
464
|
+
let ny = 0;
|
|
465
|
+
let nz = 0;
|
|
466
|
+
let emit = -1;
|
|
467
|
+
let found = false;
|
|
468
|
+
for (const b of roomSolids(env)) {
|
|
469
|
+
const c = b[3];
|
|
470
|
+
const s = b[4];
|
|
471
|
+
// The ray in the solid's frame (a rotation about Y): local = Rᵀ(p − centre).
|
|
472
|
+
const ox = -b[0];
|
|
473
|
+
const oy = -b[1];
|
|
474
|
+
const oz = -b[2];
|
|
475
|
+
const o = [c * ox - s * oz, oy, s * ox + c * oz];
|
|
476
|
+
const d = [c * x - s * z, y, s * x + c * z];
|
|
477
|
+
let t0 = -Infinity;
|
|
478
|
+
let t1 = Infinity;
|
|
479
|
+
let a0 = 0;
|
|
480
|
+
let a1 = 0;
|
|
481
|
+
let miss = false;
|
|
482
|
+
for (let i = 0; i < 3; i++) {
|
|
483
|
+
const h = b[5 + i];
|
|
484
|
+
if (Math.abs(d[i]) < 1e-12) {
|
|
485
|
+
if (o[i] < -h || o[i] > h) {
|
|
486
|
+
miss = true;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
let n = (-h - o[i]) / d[i];
|
|
492
|
+
let f = (h - o[i]) / d[i];
|
|
493
|
+
let sn = -1; // the entry face's outward normal is −axis when the ray runs +axis
|
|
494
|
+
if (n > f) {
|
|
495
|
+
const t = n;
|
|
496
|
+
n = f;
|
|
497
|
+
f = t;
|
|
498
|
+
sn = 1;
|
|
499
|
+
}
|
|
500
|
+
if (n > t0) {
|
|
501
|
+
t0 = n;
|
|
502
|
+
a0 = (i + 1) * sn;
|
|
503
|
+
}
|
|
504
|
+
if (f < t1) {
|
|
505
|
+
t1 = f;
|
|
506
|
+
a1 = -(i + 1) * sn;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (miss || t0 > t1 || t1 < 0) continue;
|
|
510
|
+
const inside = b[8] === 0;
|
|
511
|
+
const t = inside ? t1 : t0;
|
|
512
|
+
if (!(t > 0) || t >= best) continue;
|
|
513
|
+
best = t;
|
|
514
|
+
found = true;
|
|
515
|
+
if (b[8] === 2) {
|
|
516
|
+
emit = b[9];
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
emit = -1;
|
|
520
|
+
const a = inside ? a1 : a0;
|
|
521
|
+
const sg = (inside ? -1 : 1) * Math.sign(a); // the room is seen from inside: its normals point in
|
|
522
|
+
const ax = Math.abs(a) - 1;
|
|
523
|
+
const l0 = ax === 0 ? sg : 0;
|
|
524
|
+
const l2 = ax === 2 ? sg : 0;
|
|
525
|
+
nx = c * l0 + s * l2;
|
|
526
|
+
ny = ax === 1 ? sg : 0;
|
|
527
|
+
nz = -s * l0 + c * l2;
|
|
528
|
+
}
|
|
529
|
+
if (!found) return 0;
|
|
530
|
+
if (emit >= 0) return emit;
|
|
531
|
+
const [lx, ly, lz, I, cutoff, decay] = env.light;
|
|
532
|
+
let tx = lx - x * best;
|
|
533
|
+
let ty = ly - y * best;
|
|
534
|
+
let tz = lz - z * best;
|
|
535
|
+
const dist = Math.hypot(tx, ty, tz);
|
|
536
|
+
tx /= dist;
|
|
537
|
+
ty /= dist;
|
|
538
|
+
tz /= dist;
|
|
539
|
+
const nl = nx * tx + ny * ty + nz * tz;
|
|
540
|
+
if (nl <= 0) return 0;
|
|
541
|
+
const win = Math.max(0, Math.min(1, 1 - Math.pow(dist / cutoff, 4)));
|
|
542
|
+
const E = (I / Math.max(Math.pow(dist, decay), 0.01)) * win * win * nl;
|
|
543
|
+
const nv = Math.max(1e-4, -(nx * x + ny * y + nz * z));
|
|
544
|
+
const hx = tx - x;
|
|
545
|
+
const hy = ty - y;
|
|
546
|
+
const hz = tz - z;
|
|
547
|
+
const vh = Math.max(0, (tx * hx + ty * hy + tz * hz) / Math.hypot(hx, hy, hz));
|
|
548
|
+
const F = 0.04 + 0.96 * Math.pow(1 - vh, 5);
|
|
549
|
+
return (E / Math.PI) * (1 + F * (0.5 / (nl + nv)));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* The room as a float equirect (row 0 = up, the same convention as neutralStudioRGBE), blurred by a
|
|
554
|
+
* Gaussian of `sigma` radians — separable on the sphere: σ in rows, σ/sinθ in columns (wrapping).
|
|
555
|
+
*/
|
|
556
|
+
export function roomEquirect(W = 256, H = 128, sigma = ROOM_ENVIRONMENT.blur, env = ROOM_ENVIRONMENT) {
|
|
557
|
+
const out = new Float32Array(W * H);
|
|
558
|
+
for (let r = 0; r < H; r++) {
|
|
559
|
+
const th = ((r + 0.5) / H) * Math.PI;
|
|
560
|
+
const st = Math.sin(th);
|
|
561
|
+
const y = Math.cos(th);
|
|
562
|
+
for (let c = 0; c < W; c++) {
|
|
563
|
+
const ph = ((c + 0.5) / W) * 2 * Math.PI;
|
|
564
|
+
out[r * W + c] = roomRadiance(st * Math.cos(ph), y, st * Math.sin(ph), env);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!(sigma > 0)) return out;
|
|
568
|
+
const perRow = Math.PI / H; // radians per row, and per column at the equator
|
|
569
|
+
const tmp = new Float32Array(W * H);
|
|
570
|
+
for (let r = 0; r < H; r++) {
|
|
571
|
+
const st = Math.max(Math.sin(((r + 0.5) / H) * Math.PI), 1e-3);
|
|
572
|
+
const sg = Math.min(sigma / perRow / st, W / 4);
|
|
573
|
+
const R = Math.ceil(3 * sg);
|
|
574
|
+
const k = [];
|
|
575
|
+
let ks = 0;
|
|
576
|
+
for (let i = -R; i <= R; i++) {
|
|
577
|
+
const v = Math.exp(-(i * i) / (2 * sg * sg));
|
|
578
|
+
k.push(v);
|
|
579
|
+
ks += v;
|
|
580
|
+
}
|
|
581
|
+
for (let c = 0; c < W; c++) {
|
|
582
|
+
let a = 0;
|
|
583
|
+
for (let i = -R; i <= R; i++) a += k[i + R] * out[r * W + (((c + i) % W) + W) % W];
|
|
584
|
+
tmp[r * W + c] = a / ks;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const sg = sigma / perRow;
|
|
588
|
+
const R = Math.ceil(3 * sg);
|
|
589
|
+
for (let r = 0; r < H; r++) {
|
|
590
|
+
for (let c = 0; c < W; c++) {
|
|
591
|
+
let a = 0;
|
|
592
|
+
let ks = 0;
|
|
593
|
+
for (let i = -R; i <= R; i++) {
|
|
594
|
+
const rr = r + i;
|
|
595
|
+
if (rr < 0 || rr >= H) continue;
|
|
596
|
+
const v = Math.exp(-(i * i) / (2 * sg * sg));
|
|
597
|
+
a += v * tmp[rr * W + c];
|
|
598
|
+
ks += v;
|
|
599
|
+
}
|
|
600
|
+
out[r * W + c] = a / ks;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** The room as RGBE bytes (cached per size: every tile on a page gets the same image). */
|
|
607
|
+
const _roomCache = new Map();
|
|
608
|
+
export function roomRGBE(W = 256, H = 128) {
|
|
609
|
+
const key = `${W}x${H}`;
|
|
610
|
+
let bytes = _roomCache.get(key);
|
|
611
|
+
if (!bytes) {
|
|
612
|
+
const f = roomEquirect(W, H);
|
|
613
|
+
bytes = new Uint8Array(W * H * 4);
|
|
614
|
+
for (let i = 0; i < f.length; i++) rgbe(f[i], bytes, i * 4);
|
|
615
|
+
_roomCache.set(key, bytes);
|
|
616
|
+
}
|
|
617
|
+
return bytes.slice();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Yaw that lines the generated room up with three's world (its +X panel on +X, the ceiling panel
|
|
622
|
+
* overhead). Calibrated headless against three r180's own `environment: 'room'` render on two
|
|
623
|
+
* catalogue meshes: a sweep over 0/90/180/270 × mirrored has a clean minimum at 90, unmirrored, on
|
|
624
|
+
* both. (The neutral studio's 0 is the Sample Viewer's orientation, a different frame.)
|
|
625
|
+
*/
|
|
626
|
+
export const ROOM_YAW_DEG = 90;
|
|
627
|
+
|
|
628
|
+
/** Build the room environment on this app and set it as the scene's envAtlas. */
|
|
629
|
+
export function useRoomEnvironment(pc, app, yawDeg = ROOM_YAW_DEG) {
|
|
630
|
+
return useRgbeEquirect(pc, app, 'inline3d-room', roomRGBE(256, 128), 256, 128, yawDeg);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Each environment's tone mapping: the look it was matched to. */
|
|
634
|
+
export const ENVIRONMENT_TONE_MAPPING = Object.freeze({ neutral: 'neutral', room: 'none', studio: 'neutral', none: 'neutral' });
|
|
635
|
+
|
|
391
636
|
/**
|
|
392
637
|
* `environment: 'studio'` — three's three-point rig, same directions and ratios. Engine lights
|
|
393
638
|
* shine along their local −Y, so each is aimed by rotation (never lookAt). three's punctual
|
|
@@ -430,6 +675,100 @@ function addStudioLights(pc, app, root) {
|
|
|
430
675
|
app.scene.ambientLight = new pc.Color(amb, amb, amb);
|
|
431
676
|
}
|
|
432
677
|
|
|
678
|
+
// ── KHR_materials_transmission / volume on this engine ─────────────────────────────────────
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The engine reads KHR_materials_transmission (and _volume) into `useDynamicRefraction` +
|
|
682
|
+
* `BLEND_NORMAL`, and a dynamic-refraction material samples the camera's SCENE COLOUR MAP. Three
|
|
683
|
+
* things the engine leaves to the app, all needed for a glass or transmissive glTF to look like
|
|
684
|
+
* it does in the Khronos Sample Viewer (and three):
|
|
685
|
+
*
|
|
686
|
+
* 1. THE GRAB PASS. Nothing renders the scene colour map unless a camera asks for it; without it
|
|
687
|
+
* the material samples an unbound texture — the storm lantern's burner rendered as a MAGENTA
|
|
688
|
+
* blob inside its globe (measured). `viewer.useSceneColor()` asks, on every eye camera.
|
|
689
|
+
* 2. PASS ORDER. Both references draw opaque → transmissive → blended. The engine puts a
|
|
690
|
+
* transmissive material in the ONE back-to-front transparent list with the blended ones, so a
|
|
691
|
+
* blended globe (depthWrite off) can be drawn before the transmissive body behind it, which
|
|
692
|
+
* then paints over it — the burner showed THROUGH the lantern's opaque-alpha globe. A sort
|
|
693
|
+
* distance that puts transmissive draws first (back-to-front among themselves) restores the
|
|
694
|
+
* order: object MAE 10.7 → 10.2 vs three, 11.2 → 10.8 vs the Sample Viewer.
|
|
695
|
+
* 3. STEREO. The engine turns the refracted point's clip position into a grab-texture UV by
|
|
696
|
+
* mapping the VIEW's NDC over the WHOLE target (`getGrabScreenPos`). A stereo tile draws two
|
|
697
|
+
* views side by side into one target, so each eye would sample across both. The patched chunk
|
|
698
|
+
* measures the offset from THIS fragment instead (NDC delta ÷ NDC-per-pixel, from the
|
|
699
|
+
* derivatives), which is exact for any viewport and identical in mono.
|
|
700
|
+
*/
|
|
701
|
+
const GRAB_UV_FN = `
|
|
702
|
+
vec2 inline3dGrabUV(vec4 clipPos) {
|
|
703
|
+
vec4 p0 = matrix_viewProjection * vec4(vPositionW, 1.0);
|
|
704
|
+
vec2 n0 = p0.xy / p0.w;
|
|
705
|
+
vec2 dn = clipPos.xy / clipPos.w - n0;
|
|
706
|
+
vec2 perPx = vec2(dFdx(n0.x), dFdy(n0.y));
|
|
707
|
+
vec2 off = vec2(abs(perPx.x) > 1e-9 ? dn.x / perPx.x : 0.0, abs(perPx.y) > 1e-9 ? dn.y / perPx.y : 0.0);
|
|
708
|
+
return (gl_FragCoord.xy + off) * uScreenSize.zw;
|
|
709
|
+
}
|
|
710
|
+
`;
|
|
711
|
+
const _grabChunk = new WeakMap();
|
|
712
|
+
let _warnedGrabChunk = false;
|
|
713
|
+
|
|
714
|
+
/** The engine's refractionDynamicPS with the per-view grab UV (null if its shape is not the expected one). */
|
|
715
|
+
function stereoRefractionChunk(pc, device) {
|
|
716
|
+
if (_grabChunk.has(device)) return _grabChunk.get(device);
|
|
717
|
+
let out = null;
|
|
718
|
+
try {
|
|
719
|
+
const src = pc.ShaderChunks?.get?.(device, pc.SHADERLANGUAGE_GLSL)?.get?.('refractionDynamicPS');
|
|
720
|
+
const call = /getGrabScreenPos\(\s*projectionPoint\s*\)/;
|
|
721
|
+
const def = /vec3\s+evalRefractionColor\s*\(/;
|
|
722
|
+
if (typeof src === 'string' && call.test(src) && def.test(src)) {
|
|
723
|
+
out = src.replace(def, (m) => `${GRAB_UV_FN}\n${m}`).replace(call, 'inline3dGrabUV(projectionPoint)');
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
out = null;
|
|
727
|
+
}
|
|
728
|
+
if (!out && !_warnedGrabChunk) {
|
|
729
|
+
_warnedGrabChunk = true;
|
|
730
|
+
console.warn(`${TAG} this engine's refraction shader has an unexpected shape; transmissive materials keep the engine's grab UV (correct in mono, not per eye in stereo).`);
|
|
731
|
+
}
|
|
732
|
+
_grabChunk.set(device, out);
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Transmissive draws first, back-to-front among themselves (the engine's own key + an offset). */
|
|
737
|
+
function transmissiveFirst(mi, camPos, camFwd) {
|
|
738
|
+
const c = mi.aabb.center;
|
|
739
|
+
return 1e6 + (c.x - camPos.x) * camFwd.x + (c.y - camPos.y) * camFwd.y + (c.z - camPos.z) * camFwd.z;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Apply 1–3 above to every dynamic-refraction mesh under `entity` not already handled (`seen`).
|
|
744
|
+
* Returns how many transmissive mesh instances there are under it (handled now or before) —
|
|
745
|
+
* the caller turns the grab pass on when that is > 0. Cheap to call again: handled ones are skipped.
|
|
746
|
+
*/
|
|
747
|
+
export function prepareTransmission(pc, entity, device, seen = new WeakSet()) {
|
|
748
|
+
const renders = entity?.findComponents ? entity.findComponents('render') : [];
|
|
749
|
+
let n = 0;
|
|
750
|
+
for (const r of renders) {
|
|
751
|
+
for (const mi of r.meshInstances || []) {
|
|
752
|
+
const m = mi.material;
|
|
753
|
+
if (!m?.useDynamicRefraction) continue;
|
|
754
|
+
n++;
|
|
755
|
+
if (seen.has(mi)) continue;
|
|
756
|
+
seen.add(mi);
|
|
757
|
+
mi.calculateSortDistance = transmissiveFirst;
|
|
758
|
+
if (!seen.has(m)) {
|
|
759
|
+
seen.add(m);
|
|
760
|
+
const chunk = device ? stereoRefractionChunk(pc, device) : null;
|
|
761
|
+
if (chunk && typeof m.getShaderChunks === 'function') {
|
|
762
|
+
m.getShaderChunks(pc.SHADERLANGUAGE_GLSL).set('refractionDynamicPS', chunk);
|
|
763
|
+
m.shaderChunksVersion = pc.CHUNKAPI_2_8 ?? '2.8';
|
|
764
|
+
m.update?.();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return n;
|
|
770
|
+
}
|
|
771
|
+
|
|
433
772
|
// ── framing ─────────────────────────────────────────────────────────────────────────────────
|
|
434
773
|
|
|
435
774
|
/** Union of the render mesh instances' world AABBs (content space, the file's own units). */
|
|
@@ -535,7 +874,7 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
535
874
|
fitSweep = true,
|
|
536
875
|
renderScale = 1,
|
|
537
876
|
feather = 0,
|
|
538
|
-
environment = '
|
|
877
|
+
environment = 'neutral',
|
|
539
878
|
envMap = null,
|
|
540
879
|
decoderPath = null,
|
|
541
880
|
meshoptDecoder = null,
|
|
@@ -568,7 +907,8 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
568
907
|
nearClip: opts.nearClip,
|
|
569
908
|
farClip: opts.farClip,
|
|
570
909
|
sky: false,
|
|
571
|
-
|
|
910
|
+
// Each environment's own look: 'room' is three's (untonemapped), the rest the Sample Viewer's.
|
|
911
|
+
toneMapping: envMap ? 'neutral' : ENVIRONMENT_TONE_MAPPING[environment] || 'neutral',
|
|
572
912
|
});
|
|
573
913
|
|
|
574
914
|
let handle = null;
|
|
@@ -701,7 +1041,8 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
701
1041
|
const yaw = Number.isFinite(opts.environmentRotation) ? opts.environmentRotation : 0;
|
|
702
1042
|
if (typeof envMap === 'string') useLightingSource(pc, app, await loadTexture(pc, app, envMap), true, yaw);
|
|
703
1043
|
else if (envMap && typeof envMap === 'object') useLightingSource(pc, app, envMap, false, yaw);
|
|
704
|
-
else if (environment === 'room'
|
|
1044
|
+
else if (environment === 'room') useRoomEnvironment(pc, app, ROOM_YAW_DEG + yaw);
|
|
1045
|
+
else if (environment === 'neutral') useNeutralStudio(pc, app, ENV_YAW_DEG + yaw);
|
|
705
1046
|
else if (environment === 'studio') addStudioLights(pc, app, viewer.content);
|
|
706
1047
|
if (removed) return out;
|
|
707
1048
|
|
|
@@ -736,6 +1077,8 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
|
|
|
736
1077
|
if (removed) return out;
|
|
737
1078
|
|
|
738
1079
|
const entity = asset.resource.instantiateRenderEntity();
|
|
1080
|
+
// KHR_materials_transmission / volume: grab pass, pass order, per-eye grab UV (§ prepareTransmission).
|
|
1081
|
+
if (prepareTransmission(pc, entity, app.graphicsDevice) > 0) viewer.useSceneColor(true);
|
|
739
1082
|
viewer.content.addChild(entity);
|
|
740
1083
|
out.model = entity;
|
|
741
1084
|
out.container = asset.resource; // animations, materials, textures (engine types)
|
|
@@ -652,6 +652,7 @@ export class PlayCanvasSplatViewer {
|
|
|
652
652
|
// loaded). 'none' for splats — their colours are already display-referred; ./model's mesh
|
|
653
653
|
// tiles pass 'neutral' (Khronos PBR Neutral), the glTF Sample Viewer's default.
|
|
654
654
|
this.toneMapping = toneMapping;
|
|
655
|
+
this.sceneColor = false;
|
|
655
656
|
// Depth range for a MIXED scene (meshes under handle.engine.root depth-test against each
|
|
656
657
|
// other; splats only test against them). The projections' own near/far stay the adapter's —
|
|
657
658
|
// these only raise the near (floor) and lower the far (cap). Unset: untouched.
|
|
@@ -927,6 +928,18 @@ export class PlayCanvasSplatViewer {
|
|
|
927
928
|
if (this._viewPath === 'cameras') for (const c of this._views) if (c?.camera) c.camera.toneMapping = v;
|
|
928
929
|
}
|
|
929
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Turn the scene-colour grab pass on (true) or off on every eye camera — what a
|
|
933
|
+
* KHR_materials_transmission material samples (./model's prepareTransmission). Idempotent: the
|
|
934
|
+
* engine's `renderSceneColorMap` setter counts one request per camera. Cameras made later (a
|
|
935
|
+
* view-path switch) inherit it via `_makeCamera`.
|
|
936
|
+
*/
|
|
937
|
+
useSceneColor(on = true) {
|
|
938
|
+
this.sceneColor = !!on;
|
|
939
|
+
const cams = [this.eye, ...(this._viewPath === 'cameras' ? this._views : [])];
|
|
940
|
+
for (const c of cams) if (c?.camera && c.camera.renderSceneColorMap !== undefined) c.camera.renderSceneColorMap = this.sceneColor;
|
|
941
|
+
}
|
|
942
|
+
|
|
930
943
|
/** Forget the auto-fit (scale 1, no subject box) — the camera rig's framing. */
|
|
931
944
|
resetFit() {
|
|
932
945
|
this._fitScale = 1;
|
|
@@ -1490,6 +1503,7 @@ export class PlayCanvasSplatViewer {
|
|
|
1490
1503
|
// decodeGamma → toneMap → gammaCorrectOutput. Spark writes the stored colour straight out;
|
|
1491
1504
|
// NONE is the same thing here (GAMMA_SRGB alone leaves a gamma-space colour untouched).
|
|
1492
1505
|
e.camera.toneMapping = TONE_MAPPINGS[this.toneMapping] ? pc[TONE_MAPPINGS[this.toneMapping]] ?? pc.TONEMAP_NONE : pc.TONEMAP_NONE;
|
|
1506
|
+
if (this.sceneColor) e.camera.renderSceneColorMap = true;
|
|
1493
1507
|
this.rigNode.addChild(e);
|
|
1494
1508
|
return e;
|
|
1495
1509
|
}
|
|
@@ -2170,8 +2184,10 @@ export function describeResource(res) {
|
|
|
2170
2184
|
* handle.setRig's DISPLAY-rig defaults: addModel's, so a mesh framed through setRig('display')
|
|
2171
2185
|
* sits exactly where `addModel(url, { engine: 'playcanvas' })` puts it (docs/playcanvas-adapter.md
|
|
2172
2186
|
* §setRig). `toneMapping` applies to the page's meshes while the splat is hidden (the splat keeps
|
|
2173
|
-
* 'none' whenever it is shown); `environment
|
|
2174
|
-
*
|
|
2187
|
+
* 'none' whenever it is shown); `environment` installs addModel's IBL of the same name when the
|
|
2188
|
+
* page has no `scene.envAtlas` of its own, and removes it again when the rig changes back —
|
|
2189
|
+
* `'neutral'` (default, the Sample Viewer's studio) or `'room'` (three's RoomEnvironment; its
|
|
2190
|
+
* tone mapping defaults to `'none'`, three's, unless `toneMapping` is passed).
|
|
2175
2191
|
*/
|
|
2176
2192
|
export const SET_RIG_DISPLAY_DEFAULTS = Object.freeze({
|
|
2177
2193
|
virtualDisplayHeight: 0.24,
|
|
@@ -2184,9 +2200,12 @@ export const SET_RIG_DISPLAY_DEFAULTS = Object.freeze({
|
|
|
2184
2200
|
parallaxFactor: 1,
|
|
2185
2201
|
perspectiveFactor: 1,
|
|
2186
2202
|
toneMapping: 'neutral',
|
|
2187
|
-
environment: '
|
|
2203
|
+
environment: 'neutral',
|
|
2188
2204
|
});
|
|
2189
2205
|
|
|
2206
|
+
/** The tone mapping each setRig environment defaults to (addModel's ENVIRONMENT_TONE_MAPPING). */
|
|
2207
|
+
const SET_RIG_ENV_TONE_MAPPING = Object.freeze({ neutral: 'neutral', room: 'none', none: 'neutral' });
|
|
2208
|
+
|
|
2190
2209
|
export const SET_RIG_TYPES = Object.freeze(['display', 'camera', 'auto']);
|
|
2191
2210
|
const SET_RIG_FITS = ['contain', 'cover', 'height', 'none'];
|
|
2192
2211
|
let warnedSetRigKeys = false;
|
|
@@ -2224,14 +2243,14 @@ export function validateSetRig(type, o = {}, pageMode = false) {
|
|
|
2224
2243
|
};
|
|
2225
2244
|
const fit = o.fit === undefined ? D.fit : o.fit;
|
|
2226
2245
|
if (!SET_RIG_FITS.includes(fit)) throw new Error(`@displayxr/inline3d/splat: setRig('display') — fit "${fit}", expected ${SET_RIG_FITS.join(' | ')}.`);
|
|
2227
|
-
const toneMapping = o.toneMapping === undefined ? D.toneMapping : o.toneMapping;
|
|
2228
|
-
if (!TONE_MAPPINGS[toneMapping]) {
|
|
2229
|
-
throw new Error(`@displayxr/inline3d/splat: setRig('display') — toneMapping "${toneMapping}", expected ${Object.keys(TONE_MAPPINGS).join(' | ')}.`);
|
|
2230
|
-
}
|
|
2231
2246
|
const environment = o.environment === undefined ? D.environment : o.environment;
|
|
2232
2247
|
if (!['room', 'neutral', 'none'].includes(environment)) {
|
|
2233
2248
|
throw new Error(`@displayxr/inline3d/splat: setRig('display') — environment "${environment}", expected 'room' | 'neutral' | 'none'.`);
|
|
2234
2249
|
}
|
|
2250
|
+
const toneMapping = o.toneMapping === undefined ? SET_RIG_ENV_TONE_MAPPING[environment] : o.toneMapping;
|
|
2251
|
+
if (!TONE_MAPPINGS[toneMapping]) {
|
|
2252
|
+
throw new Error(`@displayxr/inline3d/splat: setRig('display') — toneMapping "${toneMapping}", expected ${Object.keys(TONE_MAPPINGS).join(' | ')}.`);
|
|
2253
|
+
}
|
|
2235
2254
|
let frame = null;
|
|
2236
2255
|
if (o.frame != null) {
|
|
2237
2256
|
const c = o.frame.center;
|
|
@@ -3080,8 +3099,14 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3080
3099
|
/** null = 'auto' (the per-asset waterfall); else { type: 'camera' } | { type: 'display', o }. */
|
|
3081
3100
|
let rigOverride = null;
|
|
3082
3101
|
let rigSeq = 0;
|
|
3083
|
-
/** The
|
|
3102
|
+
/** The IBL setRig('display') installed ({ kind, atlas, prev }), and what it replaced — or null. */
|
|
3084
3103
|
let rigEnv = null;
|
|
3104
|
+
/** ./inline3d-model-playcanvas.js once a display rig has loaded it (transmission, environments). */
|
|
3105
|
+
let rigModelModule = null;
|
|
3106
|
+
/** Mesh instances / materials prepareTransmission has handled, and whether it turned the grab on. */
|
|
3107
|
+
const rigTransmissionSeen = new WeakSet();
|
|
3108
|
+
let rigGrab = false;
|
|
3109
|
+
let rigTick = 0;
|
|
3085
3110
|
const baseToneMapping = viewer.toneMapping;
|
|
3086
3111
|
|
|
3087
3112
|
/** Is the splat on screen: its entity (and every ancestor) enabled. */
|
|
@@ -3155,21 +3180,39 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3155
3180
|
viewer.onTick = pageMode
|
|
3156
3181
|
? viewer.onTick
|
|
3157
3182
|
: () => {
|
|
3158
|
-
const
|
|
3183
|
+
const display = rigOverride?.type === 'display';
|
|
3184
|
+
const want = display && !splatShown() ? rigOverride.o.toneMapping : baseToneMapping;
|
|
3159
3185
|
if (want !== viewer.toneMapping) viewer.setToneMapping(want);
|
|
3186
|
+
// A transmissive glTF the page hangs under root on the display rig (a glass
|
|
3187
|
+
// lantern): addModel's grab pass + pass order + per-eye grab UV. Re-checked every 30
|
|
3188
|
+
// frames, since a page adds its meshes whenever they load — before or after setRig.
|
|
3189
|
+
if (display && rigModelModule && pcModule && (rigTick++ % 30 === 0)) syncRigTransmission();
|
|
3160
3190
|
};
|
|
3161
3191
|
|
|
3162
|
-
/**
|
|
3192
|
+
/** prepareTransmission over root; the grab pass on while a transmissive mesh is there. */
|
|
3193
|
+
function syncRigTransmission() {
|
|
3194
|
+
const n = rigModelModule.prepareTransmission(pcModule, viewer.content, viewer.app?.graphicsDevice, rigTransmissionSeen);
|
|
3195
|
+
if ((n > 0) !== rigGrab) {
|
|
3196
|
+
rigGrab = n > 0;
|
|
3197
|
+
viewer.useSceneColor(rigGrab);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
/** Install addModel's IBL (`disp.environment`) for the display rig, if the page has none of its own. */
|
|
3163
3202
|
async function ensureRigEnvironment(disp, seq) {
|
|
3164
|
-
|
|
3203
|
+
const m = (rigModelModule ||= await import('./inline3d-model-playcanvas.js'));
|
|
3204
|
+
if (removed || seq !== rigSeq || rigOverride?.type !== 'display') return;
|
|
3205
|
+
rigTick = 0; // transmission check on the next tick
|
|
3206
|
+
const kind = disp.environment;
|
|
3207
|
+
if (rigEnv && rigEnv.kind === kind) return;
|
|
3208
|
+
dropRigEnvironment(); // a different environment of ours (setRig('display') called again)
|
|
3209
|
+
if (kind === 'none' || viewer.sky) return;
|
|
3165
3210
|
const app = viewer.app;
|
|
3166
3211
|
const scene = app?.scene;
|
|
3167
3212
|
if (!scene || scene.envAtlas) return; // the page lights its own meshes: leave it alone
|
|
3168
|
-
const m = await import('./inline3d-model-playcanvas.js');
|
|
3169
|
-
if (removed || seq !== rigSeq || rigOverride?.type !== 'display' || scene.envAtlas || rigEnv) return;
|
|
3170
3213
|
const prev = { skyboxIntensity: scene.skyboxIntensity, exposure: scene.exposure, skyboxRotation: scene.skyboxRotation };
|
|
3171
|
-
const atlas = m.useNeutralStudio(pcModule, app, m.ENV_YAW_DEG);
|
|
3172
|
-
rigEnv = { atlas, prev };
|
|
3214
|
+
const atlas = kind === 'room' ? m.useRoomEnvironment(pcModule, app, m.ROOM_YAW_DEG) : m.useNeutralStudio(pcModule, app, m.ENV_YAW_DEG);
|
|
3215
|
+
rigEnv = { kind, atlas, prev };
|
|
3173
3216
|
}
|
|
3174
3217
|
|
|
3175
3218
|
/** Undo ensureRigEnvironment (only what it installed, only if the page has not replaced it). */
|
|
@@ -3200,7 +3243,13 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
|
|
|
3200
3243
|
return first.then(async () => {
|
|
3201
3244
|
if (removed || seq !== rigSeq || !current) return out;
|
|
3202
3245
|
rigOverride = req.type === 'auto' ? null : req;
|
|
3203
|
-
if (req.type !== 'display')
|
|
3246
|
+
if (req.type !== 'display') {
|
|
3247
|
+
dropRigEnvironment();
|
|
3248
|
+
if (rigGrab) {
|
|
3249
|
+
rigGrab = false;
|
|
3250
|
+
viewer.useSceneColor(false);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3204
3253
|
const resolved = resolveFor(current.rigIn, req.type);
|
|
3205
3254
|
out.rig = resolved;
|
|
3206
3255
|
applyRig(resolved, current.rigIn.bounds, true);
|
package/model.d.ts
CHANGED
|
@@ -40,18 +40,24 @@ export interface ModelOptions {
|
|
|
40
40
|
/**
|
|
41
41
|
* How the mesh is lit.
|
|
42
42
|
*
|
|
43
|
-
* PlayCanvas (default engine):
|
|
44
|
-
* from an in-memory neutral photo studio fitted to
|
|
45
|
-
* Neutral", with Khronos PBR Neutral tone mapping and
|
|
46
|
-
*
|
|
43
|
+
* PlayCanvas (default engine):
|
|
44
|
+
* - `neutral` (default): image-based lighting from an in-memory neutral photo studio fitted to
|
|
45
|
+
* the Khronos glTF Sample Viewer's "Studio Neutral", with Khronos PBR Neutral tone mapping and
|
|
46
|
+
* exposure 1 — the Sample Viewer's look.
|
|
47
|
+
* - `room`: three.js's procedural RoomEnvironment, regenerated in memory (no asset), untonemapped
|
|
48
|
+
* — the look `engine: 'three'` gives `room`. Pass it when a page was tuned on the three path.
|
|
49
|
+
* (Through 1.16 `room` was an alias of `neutral` here.)
|
|
50
|
+
*
|
|
51
|
+
* three: `room` (default) bakes three's procedural RoomEnvironment, untonemapped (unchanged since
|
|
52
|
+
* 1.11); `neutral` is not offered.
|
|
47
53
|
*
|
|
48
54
|
* Both: `studio` is the three-point punctual rig (metal renders dark under it — nothing to
|
|
49
55
|
* reflect); `none` adds no light at all. Anything else throws on PlayCanvas.
|
|
50
56
|
*/
|
|
51
57
|
environment?: 'room' | 'neutral' | 'studio' | 'none';
|
|
52
58
|
/**
|
|
53
|
-
* PlayCanvas only: yaw of the environment about +Y, degrees
|
|
54
|
-
* default orientation).
|
|
59
|
+
* PlayCanvas only: yaw of the environment about +Y, degrees, on top of its own calibrated
|
|
60
|
+
* orientation (default 0 = the Sample Viewer's orientation for `neutral`, three's for `room`).
|
|
55
61
|
*/
|
|
56
62
|
environmentRotation?: number;
|
|
57
63
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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",
|
package/splat.d.ts
CHANGED
|
@@ -529,13 +529,18 @@ export interface SplatSetRigDisplayOptions {
|
|
|
529
529
|
perspectiveFactor?: number;
|
|
530
530
|
/**
|
|
531
531
|
* The eye camera's tone mapping WHILE THE SPLAT IS HIDDEN (the splat's display-referred colours
|
|
532
|
-
* keep `'none'` whenever it is shown). Default `'neutral'`
|
|
532
|
+
* keep `'none'` whenever it is shown). Default: the environment's own — `'neutral'` (Khronos PBR
|
|
533
|
+
* Neutral) for `environment: 'neutral'`, `'none'` (three's) for `environment: 'room'`.
|
|
533
534
|
*/
|
|
534
535
|
toneMapping?: 'none' | 'linear' | 'neutral' | 'aces' | 'aces2' | 'filmic' | 'hejl';
|
|
535
536
|
/**
|
|
536
|
-
*
|
|
537
|
-
* when the
|
|
538
|
-
*
|
|
537
|
+
* addModel's IBL of the same name, installed when the scene has no `envAtlas` of its own and
|
|
538
|
+
* removed again when the rig switches away (a page that lights its own meshes is never touched):
|
|
539
|
+
* `'neutral'` (default) the Sample-Viewer-matched studio; `'room'` three's RoomEnvironment (the
|
|
540
|
+
* look `addModel(…, { engine: 'three', environment: 'room' })` gives), with tone mapping
|
|
541
|
+
* `'none'` unless `toneMapping` is passed; `'none'` installs nothing. (Through 1.16 `'room'` was
|
|
542
|
+
* the default and an alias of `'neutral'`.) A transmissive glTF under root (KHR_materials_
|
|
543
|
+
* transmission) also gets the scene-colour grab pass while the display rig is active.
|
|
539
544
|
*/
|
|
540
545
|
environment?: 'room' | 'neutral' | 'none';
|
|
541
546
|
/**
|