@preference-sl/pref-viewer 2.16.0-beta.10 → 2.16.0-beta.12
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/README.md +125 -102
- package/package.json +1 -1
- package/src/babylonjs-animation-opening.js +11 -5
- package/src/babylonjs-controller.js +337 -60
- package/src/pref-viewer-2d.js +14 -21
- package/src/pref-viewer-3d.js +140 -85
- package/src/pref-viewer-menu-3d.js +84 -40
- package/src/pref-viewer-task.js +7 -2
- package/src/pref-viewer.js +547 -185
- package/src/styles.js +105 -29
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
2
2
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
3
|
import "@babylonjs/loaders";
|
|
4
4
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
@@ -54,9 +54,10 @@ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlass
|
|
|
54
54
|
* Public API Highlights
|
|
55
55
|
* - constructor(canvas, containers, options)
|
|
56
56
|
* - enable() / disable()
|
|
57
|
-
* - load() / reloadWithCurrentSettings()
|
|
57
|
+
* - load(force?) / reloadWithCurrentSettings()
|
|
58
58
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
59
59
|
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
60
|
+
* - getDebugInfo()
|
|
60
61
|
* - setContainerVisibility(name, show)
|
|
61
62
|
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
62
63
|
* - setIlluminationConfig(config)
|
|
@@ -96,10 +97,13 @@ export default class BabylonJSController {
|
|
|
96
97
|
ambientOcclusionEnabled: true,
|
|
97
98
|
iblEnabled: true,
|
|
98
99
|
shadowsEnabled: false,
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
// Animation menu and highlight settings
|
|
101
|
+
showAnimationMenu: true,
|
|
101
102
|
highlightEnabled: true,
|
|
102
103
|
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
104
|
+
// Lighting time of day (0=midnight, 0.5=noon, 1=midnight)
|
|
105
|
+
showLightingTimeOfDay: true,
|
|
106
|
+
lightingTimeOfDay: 0.5,
|
|
103
107
|
};
|
|
104
108
|
|
|
105
109
|
// Canvas HTML element
|
|
@@ -120,7 +124,7 @@ export default class BabylonJSController {
|
|
|
120
124
|
#shadowGen = [];
|
|
121
125
|
#XRExperience = null;
|
|
122
126
|
#canvasResizeObserver = null;
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
125
129
|
|
|
126
130
|
#containers = {};
|
|
@@ -222,18 +226,18 @@ export default class BabylonJSController {
|
|
|
222
226
|
#detectHardwareTier() {
|
|
223
227
|
// First, try to get WebGL info to help identify the device
|
|
224
228
|
const gl = this.#engine?.getRenderingCanvas()?.getContext?.('webgl2') || this.#engine?.getRenderingCanvas()?.getContext?.('webgl');
|
|
225
|
-
|
|
229
|
+
|
|
226
230
|
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
227
231
|
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
228
232
|
const isAngleRenderer = /^ANGLE\s*\(/i.test(rendererName);
|
|
229
|
-
|
|
233
|
+
|
|
230
234
|
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
231
235
|
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
232
236
|
const isDesktopGPU = /NVIDIA|AMD|Intel|Radeon|GeForce|Quadro/i.test(rendererName);
|
|
233
|
-
|
|
237
|
+
|
|
234
238
|
// Also check if it's explicitly a mobile GPU
|
|
235
239
|
const isMobileGPU = /Adreno|Mali|Apple GPU/i.test(rendererName);
|
|
236
|
-
|
|
240
|
+
|
|
237
241
|
// If we detect a desktop GPU, go directly to desktop detection
|
|
238
242
|
if (isDesktopGPU && !isMobileGPU) {
|
|
239
243
|
if (isAngleRenderer) {
|
|
@@ -252,7 +256,7 @@ export default class BabylonJSController {
|
|
|
252
256
|
|
|
253
257
|
// If no WebGL info or unclear, fall back to user agent detection
|
|
254
258
|
const mobileInfo = this.#detectMobileInfo();
|
|
255
|
-
|
|
259
|
+
|
|
256
260
|
// Only treat as mobile if we have strong mobile indicators
|
|
257
261
|
if (mobileInfo.isMobile && !mobileInfo.isDesktopLike) {
|
|
258
262
|
if (mobileInfo.isHighEnd) {
|
|
@@ -279,15 +283,15 @@ export default class BabylonJSController {
|
|
|
279
283
|
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
280
284
|
// Check WebGL capabilities
|
|
281
285
|
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
282
|
-
|
|
286
|
+
|
|
283
287
|
// Detect high-end GPUs
|
|
284
288
|
const isAppleSilicon = /Apple|M1|M2|M3|M4/i.test(rendererName);
|
|
285
289
|
const isNvidiaHighEnd = /RTX|GeForce (3|4|5|6|7|8|9|40|50|60|70|80|90)|RTX A[5-9]|RTX Pro|Quadro/i.test(rendererName);
|
|
286
290
|
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
287
291
|
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
288
|
-
|
|
292
|
+
|
|
289
293
|
let tier = conservative ? 'medium' : 'high'; // Default to conservative desktop when ANGLE is in the path
|
|
290
|
-
|
|
294
|
+
|
|
291
295
|
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
292
296
|
tier = 'medium';
|
|
293
297
|
} else if (!conservative && (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384)) {
|
|
@@ -295,7 +299,7 @@ export default class BabylonJSController {
|
|
|
295
299
|
} else if (conservative && maxTextureSize >= 16384) {
|
|
296
300
|
tier = 'high';
|
|
297
301
|
}
|
|
298
|
-
|
|
302
|
+
|
|
299
303
|
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
300
304
|
return tier;
|
|
301
305
|
}
|
|
@@ -311,13 +315,13 @@ export default class BabylonJSController {
|
|
|
311
315
|
const screenWidth = window.screen?.width || 0;
|
|
312
316
|
const screenHeight = window.screen?.height || 0;
|
|
313
317
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
314
|
-
|
|
318
|
+
|
|
315
319
|
// Detect device type
|
|
316
320
|
const isIPhone = /iPhone/i.test(userAgent);
|
|
317
321
|
const isIPad = /iPad/i.test(userAgent);
|
|
318
322
|
const isAndroid = /Android/i.test(userAgent);
|
|
319
323
|
const isMobile = /Mobile/i.test(userAgent);
|
|
320
|
-
|
|
324
|
+
|
|
321
325
|
// High-end mobile patterns (recent flagship devices)
|
|
322
326
|
const highEndPatterns = [
|
|
323
327
|
/iPhone (1[4-9]|SE)/i, // iPhone 14+
|
|
@@ -329,7 +333,7 @@ export default class BabylonJSController {
|
|
|
329
333
|
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
330
334
|
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
331
335
|
];
|
|
332
|
-
|
|
336
|
+
|
|
333
337
|
// Mid-range patterns
|
|
334
338
|
const midRangePatterns = [
|
|
335
339
|
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
@@ -339,20 +343,20 @@ export default class BabylonJSController {
|
|
|
339
343
|
/Pixel [4-6]/i, // Pixel 4-6
|
|
340
344
|
/Moto G/i, // Moto G
|
|
341
345
|
];
|
|
342
|
-
|
|
346
|
+
|
|
343
347
|
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
344
348
|
const isMidRange = midRangePatterns.some(p => p.test(userAgent));
|
|
345
|
-
|
|
349
|
+
|
|
346
350
|
// Detect tablet ONLY if explicitly in user agent (not just screen size)
|
|
347
351
|
// Many desktops have large screens and would incorrectly be detected as tablets
|
|
348
352
|
const isTablet = /iPad|Android.*Tablet|Nexus 9|Nexus 10/i.test(userAgent);
|
|
349
|
-
|
|
353
|
+
|
|
350
354
|
// Detect if mobile (phone form factor) - must have explicit mobile indicators
|
|
351
355
|
const isMobilePhone = /Android.*Mobile|iPhone|iPod/i.test(userAgent);
|
|
352
|
-
|
|
356
|
+
|
|
353
357
|
// Check if it looks like a desktop/laptop (has "Windows", "Mac", "Linux" but no mobile indicators)
|
|
354
358
|
const isDesktopLike = /Windows NT|Macintosh|Linux|X11/i.test(userAgent) && !isMobilePhone && !isTablet;
|
|
355
|
-
|
|
359
|
+
|
|
356
360
|
// Device name for logging
|
|
357
361
|
let deviceName = 'Unknown';
|
|
358
362
|
if (isIPhone) {
|
|
@@ -364,7 +368,7 @@ export default class BabylonJSController {
|
|
|
364
368
|
const match = userAgent.match(/Android.*;.*(.+?)\)/);
|
|
365
369
|
deviceName = match ? match[1].trim() : 'Android';
|
|
366
370
|
}
|
|
367
|
-
|
|
371
|
+
|
|
368
372
|
return {
|
|
369
373
|
isMobile: isMobilePhone && !isDesktopLike,
|
|
370
374
|
isTablet: isTablet && !isDesktopLike,
|
|
@@ -445,10 +449,10 @@ export default class BabylonJSController {
|
|
|
445
449
|
};
|
|
446
450
|
|
|
447
451
|
const preset = presetMap[tier] || qualityPresets.medium;
|
|
448
|
-
|
|
452
|
+
|
|
449
453
|
// Apply preset to config
|
|
450
|
-
this.#config.quality = {
|
|
451
|
-
tier,
|
|
454
|
+
this.#config.quality = {
|
|
455
|
+
tier,
|
|
452
456
|
...preset,
|
|
453
457
|
// Legacy compatibility
|
|
454
458
|
shadowMapSize: preset.shadowMapSize,
|
|
@@ -642,6 +646,55 @@ export default class BabylonJSController {
|
|
|
642
646
|
}
|
|
643
647
|
}
|
|
644
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Preloads Babylon's internal BRDF lookup texture required by PBR materials.
|
|
651
|
+
* In render-on-demand hosts the RGBD expansion can stall unless a few renders are kept alive.
|
|
652
|
+
* @private
|
|
653
|
+
* @returns {Promise<boolean>} True when the texture is ready, false when it timed out or is unavailable.
|
|
654
|
+
*/
|
|
655
|
+
async #ensureEnvironmentBRDFTextureReady() {
|
|
656
|
+
if (!this.#scene || !this.#engine) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const brdfTexture = BRDFTextureTools.GetEnvironmentBRDFTexture(this.#scene);
|
|
661
|
+
if (!brdfTexture || brdfTexture.isReady?.()) {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const timeoutMs = 2000;
|
|
666
|
+
const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
667
|
+
this.#requestRender({ frames: this.#config.render.burstFramesEnhanced, continuousMs: timeoutMs });
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
await new Promise((resolve) => {
|
|
671
|
+
const tick = () => {
|
|
672
|
+
if (!this.#scene || !this.#engine || brdfTexture.isDisposed?.()) {
|
|
673
|
+
resolve(false);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (brdfTexture.isReady?.()) {
|
|
677
|
+
resolve(true);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
681
|
+
if (now - startedAt >= timeoutMs) {
|
|
682
|
+
resolve(false);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
this.#requestRender({ frames: 1, continuousMs: 100 });
|
|
686
|
+
setTimeout(tick, 16);
|
|
687
|
+
};
|
|
688
|
+
tick();
|
|
689
|
+
});
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.warn("PrefViewer: Error preparing Babylon BRDF texture:", error);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
this.#requestRender({ frames: 2 });
|
|
695
|
+
return brdfTexture.isReady?.() ?? false;
|
|
696
|
+
}
|
|
697
|
+
|
|
645
698
|
/**
|
|
646
699
|
* Starts Babylon's engine render loop if it is not already running.
|
|
647
700
|
* @private
|
|
@@ -669,7 +722,7 @@ export default class BabylonJSController {
|
|
|
669
722
|
this.#state.render.isLoopRunning = false;
|
|
670
723
|
return true;
|
|
671
724
|
}
|
|
672
|
-
|
|
725
|
+
|
|
673
726
|
/**
|
|
674
727
|
* Resets transient render-loop bookkeeping so the next render request starts from a clean baseline.
|
|
675
728
|
* Clears queued burst frames, ends any active continuous-render window, and drops the idle-throttle timestamp.
|
|
@@ -804,12 +857,12 @@ export default class BabylonJSController {
|
|
|
804
857
|
}
|
|
805
858
|
|
|
806
859
|
const shouldRender = animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
807
|
-
|
|
860
|
+
|
|
808
861
|
// Reset idle flag if we need to render
|
|
809
862
|
if (shouldRender) {
|
|
810
863
|
this.#state.render.isIdle = false;
|
|
811
864
|
}
|
|
812
|
-
|
|
865
|
+
|
|
813
866
|
return shouldRender;
|
|
814
867
|
}
|
|
815
868
|
|
|
@@ -845,7 +898,7 @@ export default class BabylonJSController {
|
|
|
845
898
|
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
846
899
|
return;
|
|
847
900
|
}
|
|
848
|
-
|
|
901
|
+
|
|
849
902
|
this.#scene.render();
|
|
850
903
|
this.#state.render.lastRenderAt = now;
|
|
851
904
|
|
|
@@ -1570,7 +1623,7 @@ export default class BabylonJSController {
|
|
|
1570
1623
|
}
|
|
1571
1624
|
});
|
|
1572
1625
|
}
|
|
1573
|
-
|
|
1626
|
+
|
|
1574
1627
|
/**
|
|
1575
1628
|
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1576
1629
|
* @private
|
|
@@ -1608,7 +1661,7 @@ export default class BabylonJSController {
|
|
|
1608
1661
|
}
|
|
1609
1662
|
|
|
1610
1663
|
const showroom = this.#config.look?.showroom || {};
|
|
1611
|
-
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ?? 0.5);
|
|
1664
|
+
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ? this.#settings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1612
1665
|
const profile = getLightingCycleProfile(timeOfDay);
|
|
1613
1666
|
const daylight = profile.dayFactor;
|
|
1614
1667
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
@@ -1931,7 +1984,7 @@ export default class BabylonJSController {
|
|
|
1931
1984
|
}
|
|
1932
1985
|
|
|
1933
1986
|
/**
|
|
1934
|
-
* Attaches the `
|
|
1987
|
+
* Attaches the `pref-viewer-3d-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
1935
1988
|
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
1936
1989
|
* @private
|
|
1937
1990
|
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
@@ -1942,12 +1995,12 @@ export default class BabylonJSController {
|
|
|
1942
1995
|
return false;
|
|
1943
1996
|
}
|
|
1944
1997
|
this.#detachAnimationChangedListener();
|
|
1945
|
-
this.#prefViewer3D.addEventListener("
|
|
1998
|
+
this.#prefViewer3D.addEventListener("pref-viewer-3d-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1946
1999
|
return true;
|
|
1947
2000
|
}
|
|
1948
2001
|
|
|
1949
2002
|
/**
|
|
1950
|
-
* Detaches the `
|
|
2003
|
+
* Detaches the `pref-viewer-3d-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
1951
2004
|
* @private
|
|
1952
2005
|
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
1953
2006
|
*/
|
|
@@ -1955,7 +2008,7 @@ export default class BabylonJSController {
|
|
|
1955
2008
|
if (!this.#prefViewer3D) {
|
|
1956
2009
|
return false;
|
|
1957
2010
|
}
|
|
1958
|
-
this.#prefViewer3D.removeEventListener("
|
|
2011
|
+
this.#prefViewer3D.removeEventListener("pref-viewer-3d-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1959
2012
|
return true;
|
|
1960
2013
|
}
|
|
1961
2014
|
|
|
@@ -1988,12 +2041,12 @@ export default class BabylonJSController {
|
|
|
1988
2041
|
*/
|
|
1989
2042
|
#onMouseWheel(event, pickInfo) {
|
|
1990
2043
|
event.preventDefault();
|
|
1991
|
-
|
|
2044
|
+
|
|
1992
2045
|
// OPTIMIZATION: Mark activity for idle mode
|
|
1993
2046
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1994
2047
|
this.#state.render.lastActivityAt = now;
|
|
1995
2048
|
this.#state.render.isIdle = false;
|
|
1996
|
-
|
|
2049
|
+
|
|
1997
2050
|
const camera = this.#scene?.activeCamera;
|
|
1998
2051
|
if (!camera) {
|
|
1999
2052
|
return false;
|
|
@@ -2028,7 +2081,7 @@ export default class BabylonJSController {
|
|
|
2028
2081
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2029
2082
|
this.#state.render.lastActivityAt = now;
|
|
2030
2083
|
this.#state.render.isIdle = false;
|
|
2031
|
-
|
|
2084
|
+
|
|
2032
2085
|
if (this.#babylonJSAnimationController) {
|
|
2033
2086
|
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
2034
2087
|
this.#babylonJSAnimationController.hideMenu();
|
|
@@ -2053,7 +2106,7 @@ export default class BabylonJSController {
|
|
|
2053
2106
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2054
2107
|
this.#state.render.lastActivityAt = now;
|
|
2055
2108
|
this.#state.render.isIdle = false;
|
|
2056
|
-
|
|
2109
|
+
|
|
2057
2110
|
const camera = this.#scene?.activeCamera;
|
|
2058
2111
|
if (camera && !camera.metadata?.locked) {
|
|
2059
2112
|
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
@@ -2701,20 +2754,34 @@ export default class BabylonJSController {
|
|
|
2701
2754
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2702
2755
|
this.#setMaxSimultaneousLights();
|
|
2703
2756
|
this.#enhanceTextureQuality();
|
|
2704
|
-
this.#
|
|
2705
|
-
|
|
2706
|
-
this.#setHighlightConfig({
|
|
2707
|
-
enabled: this.#settings.highlightEnabled,
|
|
2708
|
-
color: this.#settings.highlightColor,
|
|
2709
|
-
});
|
|
2710
|
-
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2711
|
-
this.#attachAnimationChangedListener();
|
|
2712
|
-
}
|
|
2757
|
+
this.#syncAnimationControllerAfterModelLoad();
|
|
2758
|
+
|
|
2713
2759
|
await this.#startRender();
|
|
2714
2760
|
});
|
|
2715
2761
|
return detail;
|
|
2716
2762
|
}
|
|
2717
2763
|
|
|
2764
|
+
/**
|
|
2765
|
+
* Creates or disposes the animation controller after a model load according to current settings.
|
|
2766
|
+
* @private
|
|
2767
|
+
* @returns {void}
|
|
2768
|
+
*/
|
|
2769
|
+
#syncAnimationControllerAfterModelLoad() {
|
|
2770
|
+
if (this.#settings.showAnimationMenu && this.#containers.model.assetContainer) {
|
|
2771
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2772
|
+
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2773
|
+
this.#setHighlightConfig({
|
|
2774
|
+
enabled: this.#settings.highlightEnabled,
|
|
2775
|
+
color: this.#settings.highlightColor,
|
|
2776
|
+
});
|
|
2777
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2778
|
+
this.#attachAnimationChangedListener();
|
|
2779
|
+
}
|
|
2780
|
+
} else {
|
|
2781
|
+
this.#disposeAnimationController();
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2718
2785
|
/**
|
|
2719
2786
|
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
2720
2787
|
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
@@ -2968,14 +3035,14 @@ export default class BabylonJSController {
|
|
|
2968
3035
|
this.#configureDracoCompression();
|
|
2969
3036
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
2970
3037
|
this.#engine.disableUniformBuffers = true;
|
|
2971
|
-
|
|
3038
|
+
|
|
2972
3039
|
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
2973
3040
|
const detectedTier = this.#detectHardwareTier();
|
|
2974
3041
|
this.#applyQualitySettings(detectedTier);
|
|
2975
|
-
|
|
3042
|
+
|
|
2976
3043
|
// OPTIMIZATION: Initialize idle tracking
|
|
2977
3044
|
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2978
|
-
|
|
3045
|
+
|
|
2979
3046
|
this.#scene = new Scene(this.#engine);
|
|
2980
3047
|
|
|
2981
3048
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
@@ -2996,6 +3063,7 @@ export default class BabylonJSController {
|
|
|
2996
3063
|
this.#scene.skipPointerUpPicking = true;
|
|
2997
3064
|
|
|
2998
3065
|
this.#createCamera();
|
|
3066
|
+
await this.#ensureEnvironmentBRDFTextureReady();
|
|
2999
3067
|
this.#enableInteraction();
|
|
3000
3068
|
await this.#createXRExperience();
|
|
3001
3069
|
}
|
|
@@ -3151,6 +3219,182 @@ export default class BabylonJSController {
|
|
|
3151
3219
|
return { ...this.#settings };
|
|
3152
3220
|
}
|
|
3153
3221
|
|
|
3222
|
+
/**
|
|
3223
|
+
* Returns a diagnostic snapshot of the current Babylon.js runtime state.
|
|
3224
|
+
* Intended for integration troubleshooting and should not mutate scene state.
|
|
3225
|
+
* @public
|
|
3226
|
+
* @returns {object} Debug information for canvas, engine, scene, containers, render loop and settings.
|
|
3227
|
+
*/
|
|
3228
|
+
getDebugInfo() {
|
|
3229
|
+
const vectorToJSON = (value) => value ? { x: value.x, y: value.y, z: value.z } : null;
|
|
3230
|
+
const rect = this.#canvas?.getBoundingClientRect?.();
|
|
3231
|
+
const camera = this.#scene?.activeCamera ?? null;
|
|
3232
|
+
const sceneMeshes = this.#scene?.meshes ?? [];
|
|
3233
|
+
const sceneMaterials = this.#scene?.materials ?? [];
|
|
3234
|
+
const sceneTextures = this.#scene?.textures ?? [];
|
|
3235
|
+
const activeMeshes = this.#scene?.getActiveMeshes?.();
|
|
3236
|
+
const modelAssetContainer = this.#containers?.model?.assetContainer ?? null;
|
|
3237
|
+
const modelMeshes = modelAssetContainer?.meshes ?? [];
|
|
3238
|
+
const enabledModelMeshes = modelMeshes.filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false);
|
|
3239
|
+
const textureToJSON = (texture) => ({
|
|
3240
|
+
name: texture?.name ?? null,
|
|
3241
|
+
url: texture?.url ?? null,
|
|
3242
|
+
isReady: texture?.isReady?.() ?? null,
|
|
3243
|
+
hasTexture: Boolean(texture),
|
|
3244
|
+
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
3245
|
+
});
|
|
3246
|
+
const summarizeMaterial = (material) => {
|
|
3247
|
+
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
3248
|
+
const sampleMesh = relatedMeshes[0] ?? null;
|
|
3249
|
+
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
3250
|
+
let isReadyWithoutMesh = null;
|
|
3251
|
+
let isReadyWithMesh = null;
|
|
3252
|
+
try {
|
|
3253
|
+
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
3256
|
+
}
|
|
3257
|
+
try {
|
|
3258
|
+
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
return {
|
|
3264
|
+
name: material?.name ?? null,
|
|
3265
|
+
id: material?.id ?? null,
|
|
3266
|
+
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
3267
|
+
isReadyWithoutMesh,
|
|
3268
|
+
isReadyWithMesh,
|
|
3269
|
+
relatedMeshes: relatedMeshes.length,
|
|
3270
|
+
sampleMesh: sampleMesh ? {
|
|
3271
|
+
name: sampleMesh.name,
|
|
3272
|
+
id: sampleMesh.id,
|
|
3273
|
+
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
3274
|
+
isVisible: sampleMesh.isVisible ?? null,
|
|
3275
|
+
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
3276
|
+
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
3277
|
+
} : null,
|
|
3278
|
+
textures: activeTextures.map(textureToJSON),
|
|
3279
|
+
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(textureToJSON),
|
|
3280
|
+
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
3281
|
+
transparencyMode: material?.transparencyMode ?? null,
|
|
3282
|
+
alpha: material?.alpha ?? null,
|
|
3283
|
+
backFaceCulling: material?.backFaceCulling ?? null,
|
|
3284
|
+
disableLighting: material?.disableLighting ?? null,
|
|
3285
|
+
};
|
|
3286
|
+
};
|
|
3287
|
+
const materialDiagnostics = sceneMaterials.slice(0, 25).map(summarizeMaterial);
|
|
3288
|
+
const notReadyMaterials = materialDiagnostics
|
|
3289
|
+
.filter((material) => material.isReadyWithoutMesh === false || material.isReadyWithMesh === false || material.notReadyTextures.length > 0)
|
|
3290
|
+
.slice(0, 25);
|
|
3291
|
+
const notReadyTextures = sceneTextures
|
|
3292
|
+
.filter((texture) => texture?.isReady?.() === false)
|
|
3293
|
+
.slice(0, 25)
|
|
3294
|
+
.map(textureToJSON);
|
|
3295
|
+
const notReadyMeshes = sceneMeshes
|
|
3296
|
+
.filter((mesh) => typeof mesh.isReady === "function" && !mesh.isReady())
|
|
3297
|
+
.slice(0, 25)
|
|
3298
|
+
.map((mesh) => ({
|
|
3299
|
+
name: mesh.name,
|
|
3300
|
+
id: mesh.id,
|
|
3301
|
+
enabled: mesh.isEnabled?.() ?? null,
|
|
3302
|
+
isVisible: mesh.isVisible ?? null,
|
|
3303
|
+
material: mesh.material?.name ?? null,
|
|
3304
|
+
}));
|
|
3305
|
+
const modelBounds = enabledModelMeshes.length && this.#scene?.getWorldExtends
|
|
3306
|
+
? this.#scene.getWorldExtends((mesh) => enabledModelMeshes.includes(mesh))
|
|
3307
|
+
: null;
|
|
3308
|
+
const summarizeContainer = (container) => ({
|
|
3309
|
+
show: container?.state?.show ?? null,
|
|
3310
|
+
visible: container?.state?.visible ?? null,
|
|
3311
|
+
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
3312
|
+
isPending: container?.state?.isPending ?? null,
|
|
3313
|
+
isSuccess: container?.state?.isSuccess ?? null,
|
|
3314
|
+
hasAssetContainer: Boolean(container?.assetContainer),
|
|
3315
|
+
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
3316
|
+
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
3317
|
+
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
3318
|
+
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
3319
|
+
name: item.name,
|
|
3320
|
+
id: item.id,
|
|
3321
|
+
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
3322
|
+
position: vectorToJSON(item.position),
|
|
3323
|
+
target: vectorToJSON(item.target),
|
|
3324
|
+
})) ?? [],
|
|
3325
|
+
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
3326
|
+
});
|
|
3327
|
+
|
|
3328
|
+
return {
|
|
3329
|
+
canvas: {
|
|
3330
|
+
clientWidth: this.#canvas?.clientWidth ?? null,
|
|
3331
|
+
clientHeight: this.#canvas?.clientHeight ?? null,
|
|
3332
|
+
width: this.#canvas?.width ?? null,
|
|
3333
|
+
height: this.#canvas?.height ?? null,
|
|
3334
|
+
boundingWidth: rect?.width ?? null,
|
|
3335
|
+
boundingHeight: rect?.height ?? null,
|
|
3336
|
+
},
|
|
3337
|
+
engine: {
|
|
3338
|
+
renderWidth: this.#engine?.getRenderWidth?.() ?? null,
|
|
3339
|
+
renderHeight: this.#engine?.getRenderHeight?.() ?? null,
|
|
3340
|
+
hardwareScalingLevel: this.#engine?.getHardwareScalingLevel?.() ?? null,
|
|
3341
|
+
isPointerLock: this.#engine?.isPointerLock ?? null,
|
|
3342
|
+
isDisposed: this.#engine?.isDisposed ?? null,
|
|
3343
|
+
contextLost: this.#engine?.isContextLost?.() ?? null,
|
|
3344
|
+
},
|
|
3345
|
+
scene: {
|
|
3346
|
+
uid: this.#scene?.uid ?? null,
|
|
3347
|
+
isReady: this.#scene?.isReady?.() ?? null,
|
|
3348
|
+
meshes: sceneMeshes.length,
|
|
3349
|
+
visibleMeshes: activeMeshes?.length ?? activeMeshes?.data?.length ?? null,
|
|
3350
|
+
materials: sceneMaterials.length,
|
|
3351
|
+
textures: sceneTextures.length,
|
|
3352
|
+
lights: this.#scene?.lights?.length ?? 0,
|
|
3353
|
+
animationGroups: this.#scene?.animationGroups?.length ?? 0,
|
|
3354
|
+
activeCamera: camera ? {
|
|
3355
|
+
name: camera.name,
|
|
3356
|
+
id: camera.id,
|
|
3357
|
+
className: camera.getClassName?.() ?? camera.constructor?.name ?? null,
|
|
3358
|
+
position: vectorToJSON(camera.position),
|
|
3359
|
+
target: vectorToJSON(camera.target),
|
|
3360
|
+
radius: camera.radius ?? null,
|
|
3361
|
+
alpha: camera.alpha ?? null,
|
|
3362
|
+
beta: camera.beta ?? null,
|
|
3363
|
+
} : null,
|
|
3364
|
+
clearColor: this.#scene?.clearColor ? {
|
|
3365
|
+
r: this.#scene.clearColor.r,
|
|
3366
|
+
g: this.#scene.clearColor.g,
|
|
3367
|
+
b: this.#scene.clearColor.b,
|
|
3368
|
+
a: this.#scene.clearColor.a,
|
|
3369
|
+
} : null,
|
|
3370
|
+
environmentTexture: this.#scene?.environmentTexture ? {
|
|
3371
|
+
name: this.#scene.environmentTexture.name,
|
|
3372
|
+
isReady: this.#scene.environmentTexture.isReady?.() ?? null,
|
|
3373
|
+
} : null,
|
|
3374
|
+
notReadyMaterials,
|
|
3375
|
+
notReadyTextures,
|
|
3376
|
+
materialDiagnostics,
|
|
3377
|
+
notReadyMeshes,
|
|
3378
|
+
},
|
|
3379
|
+
containers: {
|
|
3380
|
+
model: summarizeContainer(this.#containers.model),
|
|
3381
|
+
environment: summarizeContainer(this.#containers.environment),
|
|
3382
|
+
materials: summarizeContainer(this.#containers.materials),
|
|
3383
|
+
},
|
|
3384
|
+
modelBounds: modelBounds ? {
|
|
3385
|
+
min: vectorToJSON(modelBounds.min),
|
|
3386
|
+
max: vectorToJSON(modelBounds.max),
|
|
3387
|
+
} : null,
|
|
3388
|
+
render: { ...this.#state.render },
|
|
3389
|
+
settings: { ...this.#settings },
|
|
3390
|
+
pipelines: {
|
|
3391
|
+
default: Boolean(this.#renderPipelines.default),
|
|
3392
|
+
ssao: Boolean(this.#renderPipelines.ssao),
|
|
3393
|
+
iblShadows: Boolean(this.#renderPipelines.iblShadows),
|
|
3394
|
+
},
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3154
3398
|
/**
|
|
3155
3399
|
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
3156
3400
|
* Applies material and camera options, sets visibility, and initializes lights and shadows.
|
|
@@ -3203,14 +3447,42 @@ export default class BabylonJSController {
|
|
|
3203
3447
|
}
|
|
3204
3448
|
|
|
3205
3449
|
/**
|
|
3206
|
-
* Applies
|
|
3450
|
+
* Applies animation-menu, highlight, and time-of-day lighting configuration without forcing a full scene reload.
|
|
3451
|
+
* Values are merged into the persisted render settings snapshot unless `config.persist === false`.
|
|
3452
|
+
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3453
|
+
* the active lighting time is reset to `DEFAULT_RENDER_SETTINGS.lightingTimeOfDay` (0.5).
|
|
3207
3454
|
* @public
|
|
3208
|
-
* @param {
|
|
3455
|
+
* @param {object} config - Immediate render/illumination configuration.
|
|
3456
|
+
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
3457
|
+
* @param {boolean|string|null} [config.highlightEnabled] - Enables/disables hover highlight; string "false" is normalized to false, null is ignored.
|
|
3458
|
+
* @param {string|null} [config.highlightColor] - Hex color used by hover highlight; null is ignored.
|
|
3459
|
+
* @param {boolean|string} [config.showLightingTimeOfDay] - Shows/hides the time-of-day control and can reset time to the default when hidden.
|
|
3460
|
+
* @param {number} [config.lightingTimeOfDay] - Normalized time of day in the 0..1 range.
|
|
3461
|
+
* @param {boolean} [config.persist=true] - False for transient updates such as lighting playback frames.
|
|
3209
3462
|
* @returns {void}
|
|
3210
3463
|
*/
|
|
3211
3464
|
setIlluminationConfig(config = {}) {
|
|
3212
3465
|
let shouldApplyLighting = false;
|
|
3213
3466
|
|
|
3467
|
+
if (config.showLightingTimeOfDay !== undefined) {
|
|
3468
|
+
const showLighting = typeof config.showLightingTimeOfDay === "string"
|
|
3469
|
+
? config.showLightingTimeOfDay.toLowerCase() !== "false"
|
|
3470
|
+
: Boolean(config.showLightingTimeOfDay);
|
|
3471
|
+
if (this.#settings.showLightingTimeOfDay !== showLighting) {
|
|
3472
|
+
shouldApplyLighting = true;
|
|
3473
|
+
}
|
|
3474
|
+
this.#settings.showLightingTimeOfDay = showLighting;
|
|
3475
|
+
|
|
3476
|
+
const hasExplicitLightingTime = typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay);
|
|
3477
|
+
if (!showLighting && !hasExplicitLightingTime) {
|
|
3478
|
+
const defaultTime = BabylonJSController.DEFAULT_RENDER_SETTINGS.lightingTimeOfDay;
|
|
3479
|
+
if (this.#settings.lightingTimeOfDay !== defaultTime) {
|
|
3480
|
+
this.#settings.lightingTimeOfDay = defaultTime;
|
|
3481
|
+
shouldApplyLighting = true;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3214
3486
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3215
3487
|
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3216
3488
|
if (this.#settings.lightingTimeOfDay !== normalizedTime) {
|
|
@@ -3220,8 +3492,11 @@ export default class BabylonJSController {
|
|
|
3220
3492
|
}
|
|
3221
3493
|
|
|
3222
3494
|
if (config.showAnimationMenu !== undefined) {
|
|
3223
|
-
|
|
3224
|
-
|
|
3495
|
+
const showMenu = typeof config.showAnimationMenu === "string"
|
|
3496
|
+
? config.showAnimationMenu.toLowerCase() !== "false"
|
|
3497
|
+
: Boolean(config.showAnimationMenu);
|
|
3498
|
+
this.#settings.showAnimationMenu = showMenu;
|
|
3499
|
+
this.#syncAnimationControllerAfterModelLoad();
|
|
3225
3500
|
}
|
|
3226
3501
|
|
|
3227
3502
|
// Handle highlight color for animation hover
|
|
@@ -3242,7 +3517,7 @@ export default class BabylonJSController {
|
|
|
3242
3517
|
}
|
|
3243
3518
|
this.#setHighlightConfig({
|
|
3244
3519
|
color: config.highlightColor ?? undefined,
|
|
3245
|
-
enabled,
|
|
3520
|
+
enabled: enabled,
|
|
3246
3521
|
});
|
|
3247
3522
|
}
|
|
3248
3523
|
|
|
@@ -3281,11 +3556,13 @@ export default class BabylonJSController {
|
|
|
3281
3556
|
}
|
|
3282
3557
|
|
|
3283
3558
|
/**
|
|
3284
|
-
* Merges
|
|
3559
|
+
* Merges reload-oriented render flags with the current configuration, persists them, and marks
|
|
3285
3560
|
* all dependent loaders/options as pending when something actually changed.
|
|
3561
|
+
* Immediate settings such as highlight, animation menu, and time-of-day lighting are handled by
|
|
3562
|
+
* `setIlluminationConfig()` before this method receives the remaining reload settings.
|
|
3286
3563
|
* @public
|
|
3287
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
3288
|
-
* @returns {{changed:boolean, settings:
|
|
3564
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of reload render toggles to apply.
|
|
3565
|
+
* @returns {{changed:boolean, settings:object}} Current full render settings snapshot and whether a reload is required.
|
|
3289
3566
|
* @description
|
|
3290
3567
|
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
3291
3568
|
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|