@preference-sl/pref-viewer 2.16.0-beta.9 → 2.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/package.json +6 -6
- package/src/babylonjs-controller.js +44 -208
- package/src/localization/translations.js +5 -17
- package/src/material-utils.js +72 -0
- package/src/pref-viewer-3d.js +24 -8
- package/src/pref-viewer-menu-3d.js +133 -260
- package/src/pref-viewer.js +3 -3
- package/src/render-math.js +127 -0
- package/src/styles.js +71 -162
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.16.0
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"index.d.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@babylonjs/core": "9.
|
|
39
|
-
"@babylonjs/loaders": "9.
|
|
40
|
-
"@babylonjs/serializers": "9.
|
|
41
|
-
"@panzoom/panzoom": "^4.6.
|
|
42
|
-
"babylonjs-gltf2interface": "9.
|
|
38
|
+
"@babylonjs/core": "^9.11.0",
|
|
39
|
+
"@babylonjs/loaders": "^9.11.0",
|
|
40
|
+
"@babylonjs/serializers": "^9.11.0",
|
|
41
|
+
"@panzoom/panzoom": "^4.6.2",
|
|
42
|
+
"babylonjs-gltf2interface": "^9.11.0",
|
|
43
43
|
"buffer": "^6.0.3",
|
|
44
44
|
"idb": "^8.0.3",
|
|
45
45
|
"is-svg": "^6.1.0",
|
|
@@ -10,104 +10,13 @@ import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
|
10
10
|
import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
11
11
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
12
12
|
import { translate } from "./localization/i18n.js";
|
|
13
|
+
import { PrefViewerStyleVariables } from "./styles.js";
|
|
14
|
+
import { clamp01, lerp, getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
|
|
15
|
+
import { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
|
|
13
16
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return safeBaseScaling / safePixelRatio;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function clamp01(value) {
|
|
21
|
-
if (!Number.isFinite(value)) {
|
|
22
|
-
return 0.5;
|
|
23
|
-
}
|
|
24
|
-
return Math.min(1, Math.max(0, value));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function lerp(from, to, amount) {
|
|
28
|
-
const t = clamp01(amount);
|
|
29
|
-
return from + (to - from) * t;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function smoothstep(edge0, edge1, x) {
|
|
33
|
-
if (edge0 === edge1) {
|
|
34
|
-
return x >= edge1 ? 1 : 0;
|
|
35
|
-
}
|
|
36
|
-
const t = clamp01((x - edge0) / (edge1 - edge0));
|
|
37
|
-
return t * t * (3 - 2 * t);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function bellCurve(x) {
|
|
41
|
-
const clamped = clamp01(x);
|
|
42
|
-
return Math.sin(clamped * Math.PI);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function normalizeDirection(x, y, z) {
|
|
46
|
-
const length = Math.hypot(x, y, z) || 1;
|
|
47
|
-
return {
|
|
48
|
-
x: x / length,
|
|
49
|
-
y: y / length,
|
|
50
|
-
z: z / length,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY = "prefViewerSimplifiedTransmission";
|
|
55
|
-
|
|
56
|
-
export function markPrefViewerSimplifiedTransmissionMaterial(mat) {
|
|
57
|
-
if (!mat) return mat;
|
|
58
|
-
mat.metadata = {
|
|
59
|
-
...(mat.metadata ?? {}),
|
|
60
|
-
[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY]: true,
|
|
61
|
-
};
|
|
62
|
-
return mat;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function isPrefViewerTransparentRenderOrderMaterial(mat) {
|
|
66
|
-
if (!mat) return false;
|
|
67
|
-
const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
|
|
68
|
-
const isSimplifiedTransmission = mat.metadata?.[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY] === true;
|
|
69
|
-
const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
|
|
70
|
-
const isAlphaBlended =
|
|
71
|
-
alpha < 1 ||
|
|
72
|
-
mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
|
|
73
|
-
mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
|
|
74
|
-
mat.needAlphaBlending?.() === true ||
|
|
75
|
-
mat.needAlphaTesting?.() === true;
|
|
76
|
-
|
|
77
|
-
return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isPrefViewerPhysicalGlassMaterial(mat) {
|
|
81
|
-
return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
85
|
-
const normalizedTime = clamp01(timeOfDay);
|
|
86
|
-
const timeHours = normalizedTime * 24;
|
|
87
|
-
const sunriseHour = 6.25;
|
|
88
|
-
const sunsetHour = 20.25;
|
|
89
|
-
const daylightProgress = clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
|
|
90
|
-
const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(bellCurve(daylightProgress), 0.88) : 0;
|
|
91
|
-
const nightFactor = 1 - dayFactor;
|
|
92
|
-
const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
|
|
93
|
-
const solarAzimuth = lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
|
|
94
|
-
const solarElevation = lerp(-0.18, 1.18, solarArc);
|
|
95
|
-
const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
|
|
96
|
-
const sunDirection = normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
|
|
97
|
-
const moonDirection = normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
timeOfDay: normalizedTime,
|
|
101
|
-
timeHours,
|
|
102
|
-
sunPhase,
|
|
103
|
-
solarAzimuth,
|
|
104
|
-
solarElevation,
|
|
105
|
-
dayFactor,
|
|
106
|
-
nightFactor,
|
|
107
|
-
sunDirection,
|
|
108
|
-
moonDirection,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
17
|
+
// Re-export so existing consumers (tests, other modules) can keep importing from this file.
|
|
18
|
+
export { getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
|
|
19
|
+
export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
|
|
111
20
|
|
|
112
21
|
/**
|
|
113
22
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
@@ -130,9 +39,10 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
|
130
39
|
*
|
|
131
40
|
* Runtime Flow
|
|
132
41
|
* 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
|
|
133
|
-
* 2. Call `enable()` to configure Draco, spin up the engine/scene, attach interaction + XR hooks
|
|
42
|
+
* 2. Call `enable()` to configure Draco, spin up the engine/scene, and attach interaction + XR hooks. The render loop
|
|
43
|
+
* does not start here; it starts automatically after the first `load()` completes.
|
|
134
44
|
* 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
|
|
135
|
-
* fetches sources, rebuilds containers, and
|
|
45
|
+
* fetches sources, rebuilds containers, reattaches pipelines, and starts the render loop.
|
|
136
46
|
* 4. Use `scheduleRenderSettingsReload()` to merge/persist toggles; when it reports `changed: true`, call
|
|
137
47
|
* `reloadWithCurrentSettings()` to apply the staged settings.
|
|
138
48
|
* 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
|
|
@@ -149,10 +59,13 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
|
149
59
|
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
150
60
|
* - setContainerVisibility(name, show)
|
|
151
61
|
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
62
|
+
* - setIlluminationConfig(config)
|
|
63
|
+
* - getAnimations() / playAnimation(name, action) / setAnimationProgress(name, progress) / setAnimationLoop(name, loop)
|
|
152
64
|
*
|
|
153
65
|
* Key Invariants
|
|
154
|
-
* - Asset containers must expose `setPendingWithCurrentStorage
|
|
155
|
-
*
|
|
66
|
+
* - Asset containers must expose `setPendingWithCurrentStorage` before calling load/reload; the controller reads that
|
|
67
|
+
* flag to resolve fresh sources. Option objects (camera, materials, IBL) expose `setPendingWithCurrent` for the same
|
|
68
|
+
* purpose. Neither set of flags touches the DOM — they only gate resolver calls.
|
|
156
69
|
* - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
|
|
157
70
|
* only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
|
|
158
71
|
* - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
|
|
@@ -186,7 +99,7 @@ export default class BabylonJSController {
|
|
|
186
99
|
lightingTimeOfDay: 0.5,
|
|
187
100
|
// Highlight settings
|
|
188
101
|
highlightEnabled: true,
|
|
189
|
-
highlightColor:
|
|
102
|
+
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
190
103
|
};
|
|
191
104
|
|
|
192
105
|
// Canvas HTML element
|
|
@@ -208,7 +121,7 @@ export default class BabylonJSController {
|
|
|
208
121
|
#XRExperience = null;
|
|
209
122
|
#canvasResizeObserver = null;
|
|
210
123
|
|
|
211
|
-
#hdrTexture = null; //
|
|
124
|
+
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
212
125
|
|
|
213
126
|
#containers = {};
|
|
214
127
|
#options = {};
|
|
@@ -261,30 +174,30 @@ export default class BabylonJSController {
|
|
|
261
174
|
// Runtime configuration constants (tuning knobs, not per-frame state).
|
|
262
175
|
#config = {
|
|
263
176
|
pointerPicking: {
|
|
264
|
-
movePickIntervalMs: 50, //
|
|
265
|
-
movePickMinDistancePx: 2, //
|
|
177
|
+
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
178
|
+
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
266
179
|
},
|
|
267
180
|
render: {
|
|
268
181
|
burstFramesBase: 2,
|
|
269
|
-
burstFramesEnhanced: 32, //
|
|
182
|
+
burstFramesEnhanced: 32, // When AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
270
183
|
interactionMs: 250,
|
|
271
184
|
animationMs: 200,
|
|
272
185
|
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
273
186
|
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
274
187
|
},
|
|
275
188
|
resize: {
|
|
276
|
-
throttleMs: 50, //
|
|
189
|
+
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
277
190
|
},
|
|
278
191
|
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
279
192
|
quality: {
|
|
280
193
|
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
281
194
|
// Applied automatically based on GPU detected capabilities
|
|
282
|
-
tier: 'medium',
|
|
283
|
-
shadowMapSize: 1024,
|
|
284
|
-
iblShadowResolution: 1,
|
|
285
|
-
iblSampleDirections: 4,
|
|
286
|
-
shadowBlurKernel: 16,
|
|
287
|
-
ssaoEnabled: true,
|
|
195
|
+
tier: 'medium', // Default, will be updated after hardware detection
|
|
196
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
197
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
198
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
199
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
200
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
288
201
|
},
|
|
289
202
|
look: {
|
|
290
203
|
showroom: {
|
|
@@ -408,23 +321,23 @@ export default class BabylonJSController {
|
|
|
408
321
|
// High-end mobile patterns (recent flagship devices)
|
|
409
322
|
const highEndPatterns = [
|
|
410
323
|
/iPhone (1[4-9]|SE)/i, // iPhone 14+
|
|
411
|
-
/iPad Pro/i,
|
|
412
|
-
/SM-S91[0-9]/i,
|
|
413
|
-
/SM-S9[0-9]/i,
|
|
414
|
-
/Pixel [7-9]/i,
|
|
415
|
-
/OnePlus [8-9|10|11]/i,
|
|
324
|
+
/iPad Pro/i, // iPad Pro
|
|
325
|
+
/SM-S91[0-9]/i, // Samsung S21+
|
|
326
|
+
/SM-S9[0-9]/i, // Samsung S20+
|
|
327
|
+
/Pixel [7-9]/i, // Pixel 7+
|
|
328
|
+
/OnePlus [8-9|10|11]/i, // OnePlus 8+
|
|
416
329
|
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
417
|
-
/Huawei (P50|Mate 4|)/i,
|
|
330
|
+
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
418
331
|
];
|
|
419
332
|
|
|
420
333
|
// Mid-range patterns
|
|
421
334
|
const midRangePatterns = [
|
|
422
|
-
/iPhone (1[0-3]|SE)/i,
|
|
335
|
+
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
423
336
|
/iPad Air/i, // iPad Air
|
|
424
|
-
/SM-A[0-9]/i,
|
|
425
|
-
/Redmi Note/i,
|
|
426
|
-
/Pixel [4-6]/i,
|
|
427
|
-
/Moto G/i,
|
|
337
|
+
/SM-A[0-9]/i, // Samsung A series
|
|
338
|
+
/Redmi Note/i, // Redmi Note
|
|
339
|
+
/Pixel [4-6]/i, // Pixel 4-6
|
|
340
|
+
/Moto G/i, // Moto G
|
|
428
341
|
];
|
|
429
342
|
|
|
430
343
|
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
@@ -476,13 +389,13 @@ export default class BabylonJSController {
|
|
|
476
389
|
// Mobile low-end (old phones, budget devices)
|
|
477
390
|
low: {
|
|
478
391
|
shadowMapSize: 256, // Very small for speed
|
|
479
|
-
iblShadowResolution: -1,
|
|
392
|
+
iblShadowResolution: -1, // Disable IBL shadows
|
|
480
393
|
iblSampleDirections: 1, // Minimal sampling
|
|
481
394
|
shadowBlurKernel: 0, // No blur
|
|
482
395
|
ssaoEnabled: false,
|
|
483
396
|
antialiasingEnabled: false,
|
|
484
|
-
hardwareScaling: 2.5,
|
|
485
|
-
maxModelResolution: 512,
|
|
397
|
+
hardwareScaling: 2.5, // Aggressive downscaling
|
|
398
|
+
maxModelResolution: 512, // Low-res model loading
|
|
486
399
|
environmentTextureSize: 512,
|
|
487
400
|
},
|
|
488
401
|
// Mobile high-end / Tablet / Desktop low
|
|
@@ -500,7 +413,7 @@ export default class BabylonJSController {
|
|
|
500
413
|
// Desktop mid-range / High-end mobile
|
|
501
414
|
high: {
|
|
502
415
|
shadowMapSize: 1024,
|
|
503
|
-
iblShadowResolution: 0,
|
|
416
|
+
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
504
417
|
iblSampleDirections: 4, // Quality sampling
|
|
505
418
|
shadowBlurKernel: 8, // Sharper shadows for product clarity
|
|
506
419
|
ssaoEnabled: false,
|
|
@@ -512,9 +425,9 @@ export default class BabylonJSController {
|
|
|
512
425
|
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
513
426
|
ultra: {
|
|
514
427
|
shadowMapSize: 2048,
|
|
515
|
-
iblShadowResolution: 1,
|
|
428
|
+
iblShadowResolution: 1, // 2048x2048
|
|
516
429
|
iblSampleDirections: 8, // Maximum quality
|
|
517
|
-
shadowBlurKernel: 16,
|
|
430
|
+
shadowBlurKernel: 16, // Preserve clarity over softness
|
|
518
431
|
ssaoEnabled: false,
|
|
519
432
|
antialiasingEnabled: true,
|
|
520
433
|
hardwareScaling: 1.0, // Native resolution
|
|
@@ -1657,82 +1570,7 @@ export default class BabylonJSController {
|
|
|
1657
1570
|
}
|
|
1658
1571
|
});
|
|
1659
1572
|
}
|
|
1660
|
-
|
|
1661
|
-
/**
|
|
1662
|
-
* Fixes cross-GLTF transparency ordering. Detects transparent/transmissive meshes
|
|
1663
|
-
* (alpha-blended, alpha-tested, simplified environment transmission, or
|
|
1664
|
-
* KHR_materials_transmission) across all loaded containers and moves them to
|
|
1665
|
-
* renderingGroupId=1 with depth-write disabled.
|
|
1666
|
-
* This ensures opaque scene geometry (e.g. lamp cups behind a model window) is
|
|
1667
|
-
* written to the depth buffer first (group 0) so the glass layer composites on top
|
|
1668
|
-
* without occluding it.
|
|
1669
|
-
* @private
|
|
1670
|
-
* @returns {void}
|
|
1671
|
-
*/
|
|
1672
|
-
#applyTransmittanceRenderOrder() {
|
|
1673
|
-
if (!this.#scene) return;
|
|
1674
|
-
|
|
1675
|
-
const envMeshIds = new Set(
|
|
1676
|
-
this.#containers.environment?.assetContainer?.meshes.map((m) => m.uniqueId) ?? []
|
|
1677
|
-
);
|
|
1678
|
-
const modelMeshIds = new Set(
|
|
1679
|
-
this.#containers.model?.assetContainer?.meshes.map((m) => m.uniqueId) ?? []
|
|
1680
|
-
);
|
|
1681
|
-
|
|
1682
|
-
// Map each material to the model meshes using it — needed to detect shared materials
|
|
1683
|
-
// before modifying an env mesh's material (must clone to avoid breaking model glass).
|
|
1684
|
-
const matToModelMeshes = new Map();
|
|
1685
|
-
this.#scene.meshes.forEach((mesh) => {
|
|
1686
|
-
if (!modelMeshIds.has(mesh.uniqueId) || !mesh.material) return;
|
|
1687
|
-
if (!matToModelMeshes.has(mesh.material)) matToModelMeshes.set(mesh.material, new Set());
|
|
1688
|
-
matToModelMeshes.get(mesh.material).add(mesh.uniqueId);
|
|
1689
|
-
});
|
|
1690
|
-
|
|
1691
|
-
this.#scene.meshes.forEach((mesh) => {
|
|
1692
|
-
const mat = mesh.material;
|
|
1693
|
-
if (!mat) return;
|
|
1694
|
-
const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
|
|
1695
|
-
const isTransparentLike = isPrefViewerTransparentRenderOrderMaterial(mat);
|
|
1696
|
-
if (!isTransparentLike) return;
|
|
1697
|
-
|
|
1698
|
-
if (envMeshIds.has(mesh.uniqueId)) {
|
|
1699
|
-
// Environment mesh carrying a transmissive material introduced by a material swap
|
|
1700
|
-
// (initial-load materials are pre-simplified in #assetContainer_simplifyTransmissiveMaterials).
|
|
1701
|
-
// Clone if shared with a model mesh so the model's physical glass is not affected.
|
|
1702
|
-
let targetMat = mat;
|
|
1703
|
-
if (matToModelMeshes.has(mat)) {
|
|
1704
|
-
targetMat = mat.clone(`${mat.name}_env_simplified`);
|
|
1705
|
-
mesh.material = targetMat;
|
|
1706
|
-
}
|
|
1707
|
-
if (isTransmissive) {
|
|
1708
|
-
this.#simplifyTransmissiveMaterial(targetMat);
|
|
1709
|
-
}
|
|
1710
|
-
mesh.renderingGroupId = 1;
|
|
1711
|
-
targetMat.disableDepthWrite = true;
|
|
1712
|
-
targetMat.alphaIndex = 0;
|
|
1713
|
-
} else if (modelMeshIds.has(mesh.uniqueId)) {
|
|
1714
|
-
// Model transparent materials render after all group-0 geometry.
|
|
1715
|
-
mesh.renderingGroupId = 1;
|
|
1716
|
-
mat.disableDepthWrite = true;
|
|
1717
|
-
|
|
1718
|
-
// Physical model glass keeps transmission, but strips volume absorption (tintColor)
|
|
1719
|
-
// and roughness because those blur/darken transmitted geometry. Do not apply this
|
|
1720
|
-
// tuning to plain alpha-blended model materials: render-order and physical glass
|
|
1721
|
-
// material tuning are separate concerns.
|
|
1722
|
-
if (isTransmissive && mat.subSurface) {
|
|
1723
|
-
mat.subSurface.tintColor = Color3.White();
|
|
1724
|
-
mat.roughness = 0;
|
|
1725
|
-
}
|
|
1726
|
-
mat.alphaIndex = 1;
|
|
1727
|
-
} else {
|
|
1728
|
-
// Unknown container (future-proof safe default).
|
|
1729
|
-
mesh.renderingGroupId = 1;
|
|
1730
|
-
mat.disableDepthWrite = true;
|
|
1731
|
-
mat.alphaIndex = 1;
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1573
|
+
|
|
1736
1574
|
/**
|
|
1737
1575
|
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1738
1576
|
* @private
|
|
@@ -2863,7 +2701,6 @@ export default class BabylonJSController {
|
|
|
2863
2701
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2864
2702
|
this.#setMaxSimultaneousLights();
|
|
2865
2703
|
this.#enhanceTextureQuality();
|
|
2866
|
-
this.#applyTransmittanceRenderOrder();
|
|
2867
2704
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2868
2705
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2869
2706
|
this.#setHighlightConfig({
|
|
@@ -3346,7 +3183,6 @@ export default class BabylonJSController {
|
|
|
3346
3183
|
setMaterialOptions() {
|
|
3347
3184
|
this.#stopRender();
|
|
3348
3185
|
const materialsOptionsSetted = this.#setOptions_Materials();
|
|
3349
|
-
this.#applyTransmittanceRenderOrder();
|
|
3350
3186
|
this.#startRender();
|
|
3351
3187
|
return materialsOptionsSetted;
|
|
3352
3188
|
}
|
|
@@ -3,18 +3,12 @@ export const translations = {
|
|
|
3
3
|
prefViewer: {
|
|
4
4
|
menu3d: {
|
|
5
5
|
title: "3D render settings",
|
|
6
|
-
subtitle: "
|
|
7
|
-
pendingLabel: "Pending changes",
|
|
6
|
+
subtitle: "Modify the visual effects.",
|
|
8
7
|
actions: {
|
|
9
|
-
apply: "Apply",
|
|
10
|
-
applying: "Applying...",
|
|
11
8
|
play: "Play",
|
|
12
9
|
pause: "Pause",
|
|
13
10
|
},
|
|
14
|
-
|
|
15
|
-
error: "Couldn't apply the changes.",
|
|
16
|
-
},
|
|
17
|
-
switches: {
|
|
11
|
+
features: {
|
|
18
12
|
antiAliasingEnabled: {
|
|
19
13
|
label: "Antialiasing",
|
|
20
14
|
helper: "Softens jagged edges and lines.",
|
|
@@ -70,18 +64,12 @@ export const translations = {
|
|
|
70
64
|
prefViewer: {
|
|
71
65
|
menu3d: {
|
|
72
66
|
title: "Ajustes de render 3D",
|
|
73
|
-
subtitle: "
|
|
74
|
-
pendingLabel: "Pendiente de aplicar",
|
|
67
|
+
subtitle: "Modifica los efectos visuales.",
|
|
75
68
|
actions: {
|
|
76
|
-
apply: "Aplicar",
|
|
77
|
-
applying: "Aplicando...",
|
|
78
69
|
play: "Reproducir",
|
|
79
70
|
pause: "Pausar",
|
|
80
71
|
},
|
|
81
|
-
|
|
82
|
-
error: "No se pudo aplicar los cambios.",
|
|
83
|
-
},
|
|
84
|
-
switches: {
|
|
72
|
+
features: {
|
|
85
73
|
antiAliasingEnabled: {
|
|
86
74
|
label: "Antialiasing",
|
|
87
75
|
helper: "Suaviza aristas y líneas.",
|
|
@@ -100,7 +88,7 @@ export const translations = {
|
|
|
100
88
|
},
|
|
101
89
|
highlightEnabled: {
|
|
102
90
|
label: "Resaltado",
|
|
103
|
-
helper: "
|
|
91
|
+
helper: "Resalta elementos animados al pasar el ratón.",
|
|
104
92
|
},
|
|
105
93
|
highlightColor: {
|
|
106
94
|
label: "Color de resaltado",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PBRMaterial } from "@babylonjs/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utilities for classifying and tagging PBR materials by their transparency/transmission mode.
|
|
5
|
+
* Used by BabylonJSController at load time and by pref-viewer-3d for render-order decisions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY = "prefViewerSimplifiedTransmission";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tags a PBR material as having been converted from physical KHR_materials_transmission to simple
|
|
12
|
+
* alpha blending. The tag is stored in `mat.metadata` under an internal key so that downstream
|
|
13
|
+
* render-order passes can still identify these materials as transparent candidates even after
|
|
14
|
+
* their `subSurface.isRefractionEnabled` flag has been cleared.
|
|
15
|
+
*
|
|
16
|
+
* Returns the same material instance (or the original value when `mat` is falsy) so calls can be
|
|
17
|
+
* chained.
|
|
18
|
+
* @param {import("@babylonjs/core").PBRMaterial|null|undefined} mat - Material to tag.
|
|
19
|
+
* @returns {import("@babylonjs/core").PBRMaterial|null|undefined} The same `mat` value that was passed in.
|
|
20
|
+
*/
|
|
21
|
+
export function markPrefViewerSimplifiedTransmissionMaterial(mat) {
|
|
22
|
+
if (!mat) {
|
|
23
|
+
return mat;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
mat.metadata = {
|
|
27
|
+
...(mat.metadata ?? {}),
|
|
28
|
+
[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY]: true,
|
|
29
|
+
};
|
|
30
|
+
return mat;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns true when the material uses Babylon.js physical glass rendering — i.e. either
|
|
35
|
+
* KHR_materials_transmission (refraction) or KHR_materials_translucency is active.
|
|
36
|
+
*
|
|
37
|
+
* This is intentionally narrower than `isPrefViewerTransparentRenderOrderMaterial`: it only
|
|
38
|
+
* matches full physical transmission, not plain alpha blending or simplified environment glass.
|
|
39
|
+
* @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isPrefViewerPhysicalGlassMaterial(mat) {
|
|
43
|
+
return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns true when a material should be placed in the transparent rendering group (renderingGroupId=1).
|
|
48
|
+
*
|
|
49
|
+
* A material qualifies when any of the following is true:
|
|
50
|
+
* - It is a physical glass material (KHR_materials_transmission / translucency).
|
|
51
|
+
* - It has been tagged as simplified environment transmission via `markPrefViewerSimplifiedTransmissionMaterial`.
|
|
52
|
+
* - Its effective alpha is below 1, its `transparencyMode` is alpha-blend or alpha-test-and-blend,
|
|
53
|
+
* or it self-reports as needing blending/testing through the Babylon.js material API.
|
|
54
|
+
* @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isPrefViewerTransparentRenderOrderMaterial(mat) {
|
|
58
|
+
if (!mat) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
|
|
62
|
+
const isSimplifiedTransmission = mat.metadata?.[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY] === true;
|
|
63
|
+
const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
|
|
64
|
+
const isAlphaBlended =
|
|
65
|
+
alpha < 1 ||
|
|
66
|
+
mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
|
|
67
|
+
mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
|
|
68
|
+
mat.needAlphaBlending?.() === true ||
|
|
69
|
+
mat.needAlphaTesting?.() === true;
|
|
70
|
+
|
|
71
|
+
return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
|
|
72
|
+
}
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -10,28 +10,32 @@ import FileStorage from "./file-storage.js";
|
|
|
10
10
|
* - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
|
|
11
11
|
* - Manages internal state for containers (model, environment, materials) and options (camera, materials, IBL, render toggles).
|
|
12
12
|
* - Handles asset loading, configuration, and option updates (camera, materials, IBL) through attributes and public methods.
|
|
13
|
-
* - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can
|
|
13
|
+
* - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can toggle AA/SSAO/IBL/shadows,
|
|
14
|
+
* hover highlight, and time-of-day lighting.
|
|
14
15
|
* - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
|
|
15
16
|
* - Emits custom events for loading, loaded, option-setting, and render-setting workflows.
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
18
19
|
* - Use as a custom HTML element: <pref-viewer-3d ...>
|
|
19
20
|
* - Configure via attributes (config, options, show-model, show-scene, visible).
|
|
20
|
-
* - Call getRenderSettings()/applyRenderSettings() to synchronize UI
|
|
21
|
+
* - Call getRenderSettings()/applyRenderSettings() to synchronize UI controls with BabylonJSController settings.
|
|
21
22
|
* - Control viewer state and assets via public methods.
|
|
22
23
|
*
|
|
23
24
|
* Observed Attributes:
|
|
24
25
|
* - show-model: Show or hide the 3D model ("true"/"false").
|
|
25
26
|
* - show-scene: Show or hide the 3D environment ("true"/"false").
|
|
26
27
|
* - visible: Show or hide the entire viewer ("true"/"false").
|
|
28
|
+
* - show-animation-menu: Show or hide animation controls in the 3D controller.
|
|
29
|
+
* - highlight-color: Hex color used for hover highlight on animated meshes.
|
|
30
|
+
* - highlight-enabled: Enable or disable hover highlight ("true"/"false").
|
|
27
31
|
*
|
|
28
32
|
* Public Methods:
|
|
29
33
|
* - show(): Shows the 3D viewer component.
|
|
30
34
|
* - hide(): Hides the 3D viewer component.
|
|
31
35
|
* - load(config): Loads the provided configuration into the viewer.
|
|
32
36
|
* - setOptions(options): Sets viewer options such as camera, materials, and image-based lighting (IBL).
|
|
33
|
-
* - getRenderSettings(): Returns the current
|
|
34
|
-
* - applyRenderSettings(settings):
|
|
37
|
+
* - getRenderSettings(): Returns the current render settings snapshot, including highlight and lighting values.
|
|
38
|
+
* - applyRenderSettings(settings): Applies render settings, reloading only when a setting requires it.
|
|
35
39
|
* - showModel(): Shows the 3D model.
|
|
36
40
|
* - hideModel(): Hides the 3D model.
|
|
37
41
|
* - showEnvironment(): Shows the 3D environment/scene.
|
|
@@ -110,6 +114,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
110
114
|
* - "show-model": shows or hides the model according to the attribute value ("true"/"false").
|
|
111
115
|
* - "show-scene": shows or hides the scene according to the attribute value ("true"/"false").
|
|
112
116
|
* - "visible": shows or hides the component according to the attribute value ("true"/"false").
|
|
117
|
+
* - "show-animation-menu", "highlight-color", and "highlight-enabled": forward runtime UI/highlight settings to BabylonJSController.
|
|
113
118
|
*/
|
|
114
119
|
attributeChangedCallback(name, _old, value) {
|
|
115
120
|
switch (name) {
|
|
@@ -874,8 +879,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
874
879
|
|
|
875
880
|
/**
|
|
876
881
|
* Returns the current render settings used by the BabylonJSController.
|
|
882
|
+
* The snapshot is suitable for hydrating PrefViewerMenu3D controls.
|
|
877
883
|
* @public
|
|
878
|
-
* @returns {object|null} A shallow copy of the render settings or null if unavailable.
|
|
884
|
+
* @returns {object|null} A shallow copy of the render settings, including quality toggles, highlight state, and lighting time, or null if unavailable.
|
|
879
885
|
*/
|
|
880
886
|
getRenderSettings() {
|
|
881
887
|
if (!this.#babylonJSController) {
|
|
@@ -944,10 +950,20 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
944
950
|
}
|
|
945
951
|
|
|
946
952
|
/**
|
|
947
|
-
* Applies render settings
|
|
953
|
+
* Applies render settings received from PrefViewerMenu3D or another UI.
|
|
954
|
+
* Highlight and time-of-day lighting values are pushed directly to BabylonJSController; AA/SSAO/IBL/shadow changes
|
|
955
|
+
* are scheduled through the render-settings reload path when needed.
|
|
948
956
|
* @public
|
|
949
|
-
* @param {object} settings - Partial settings
|
|
950
|
-
* @
|
|
957
|
+
* @param {object} settings - Partial render settings payload.
|
|
958
|
+
* @param {boolean} [settings.antiAliasingEnabled] - Enables or disables anti-aliasing.
|
|
959
|
+
* @param {boolean} [settings.ambientOcclusionEnabled] - Enables or disables SSAO.
|
|
960
|
+
* @param {boolean} [settings.iblEnabled] - Enables or disables image-based lighting.
|
|
961
|
+
* @param {boolean} [settings.shadowsEnabled] - Enables or disables scene shadows.
|
|
962
|
+
* @param {boolean|string} [settings.highlightEnabled] - Enables or disables hover highlight; string "false" is normalized to false.
|
|
963
|
+
* @param {string} [settings.highlightColor] - Hex color used for hover highlight.
|
|
964
|
+
* @param {number} [settings.lightingTimeOfDay] - Normalized 0..1 time-of-day value.
|
|
965
|
+
* @param {boolean} [settings.persist=true] - False for transient lighting updates such as playback frames.
|
|
966
|
+
* @returns {Promise<{changed: boolean, success: boolean, detail?: object, controller?: object}>} Resolves with the apply/reload summary.
|
|
951
967
|
*/
|
|
952
968
|
async applyRenderSettings(settings) {
|
|
953
969
|
if (!this.#babylonJSController) {
|