@preference-sl/pref-viewer 2.16.0-beta.9 → 2.16.1
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 +129 -102
- package/index.d.ts +373 -12
- package/package.json +7 -6
- package/src/babylonjs-animation-opening.js +11 -5
- package/src/babylonjs-controller.js +484 -271
- package/src/localization/translations.js +5 -17
- package/src/material-utils.js +72 -0
- package/src/panzoom-controller.js +36 -17
- package/src/pref-viewer-2d.js +395 -21
- package/src/pref-viewer-3d.js +160 -89
- package/src/pref-viewer-menu-3d.js +185 -268
- package/src/pref-viewer-task.js +7 -2
- package/src/pref-viewer.js +618 -188
- package/src/render-math.js +127 -0
- package/src/styles.js +169 -178
|
@@ -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";
|
|
@@ -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
|
|
@@ -144,15 +54,19 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
|
144
54
|
* Public API Highlights
|
|
145
55
|
* - constructor(canvas, containers, options)
|
|
146
56
|
* - enable() / disable()
|
|
147
|
-
* - load() / reloadWithCurrentSettings()
|
|
57
|
+
* - load(force?) / reloadWithCurrentSettings()
|
|
148
58
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
149
59
|
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
60
|
+
* - getDebugInfo()
|
|
150
61
|
* - setContainerVisibility(name, show)
|
|
151
62
|
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
63
|
+
* - setIlluminationConfig(config)
|
|
64
|
+
* - getAnimations() / playAnimation(name, action) / setAnimationProgress(name, progress) / setAnimationLoop(name, loop)
|
|
152
65
|
*
|
|
153
66
|
* Key Invariants
|
|
154
|
-
* - Asset containers must expose `setPendingWithCurrentStorage
|
|
155
|
-
*
|
|
67
|
+
* - Asset containers must expose `setPendingWithCurrentStorage` before calling load/reload; the controller reads that
|
|
68
|
+
* flag to resolve fresh sources. Option objects (camera, materials, IBL) expose `setPendingWithCurrent` for the same
|
|
69
|
+
* purpose. Neither set of flags touches the DOM — they only gate resolver calls.
|
|
156
70
|
* - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
|
|
157
71
|
* only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
|
|
158
72
|
* - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
|
|
@@ -183,10 +97,13 @@ export default class BabylonJSController {
|
|
|
183
97
|
ambientOcclusionEnabled: true,
|
|
184
98
|
iblEnabled: true,
|
|
185
99
|
shadowsEnabled: false,
|
|
186
|
-
|
|
187
|
-
|
|
100
|
+
// Animation menu and highlight settings
|
|
101
|
+
showAnimationMenu: true,
|
|
188
102
|
highlightEnabled: true,
|
|
189
|
-
highlightColor:
|
|
103
|
+
highlightColor: PrefViewerStyleVariables.colorPrimary,
|
|
104
|
+
// Lighting time of day (0=midnight, 0.5=noon, 1=midnight)
|
|
105
|
+
showLightingTimeOfDay: true,
|
|
106
|
+
lightingTimeOfDay: 0.5,
|
|
190
107
|
};
|
|
191
108
|
|
|
192
109
|
// Canvas HTML element
|
|
@@ -207,8 +124,8 @@ export default class BabylonJSController {
|
|
|
207
124
|
#shadowGen = [];
|
|
208
125
|
#XRExperience = null;
|
|
209
126
|
#canvasResizeObserver = null;
|
|
210
|
-
|
|
211
|
-
#hdrTexture = null; //
|
|
127
|
+
|
|
128
|
+
#hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
212
129
|
|
|
213
130
|
#containers = {};
|
|
214
131
|
#options = {};
|
|
@@ -261,30 +178,35 @@ export default class BabylonJSController {
|
|
|
261
178
|
// Runtime configuration constants (tuning knobs, not per-frame state).
|
|
262
179
|
#config = {
|
|
263
180
|
pointerPicking: {
|
|
264
|
-
movePickIntervalMs: 50, //
|
|
265
|
-
movePickMinDistancePx: 2, //
|
|
181
|
+
movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
182
|
+
movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
|
|
266
183
|
},
|
|
267
184
|
render: {
|
|
268
185
|
burstFramesBase: 2,
|
|
269
|
-
burstFramesEnhanced: 32, //
|
|
186
|
+
burstFramesEnhanced: 32, // When AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
270
187
|
interactionMs: 250,
|
|
271
188
|
animationMs: 200,
|
|
272
189
|
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
273
190
|
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
274
191
|
},
|
|
275
192
|
resize: {
|
|
276
|
-
throttleMs: 50, //
|
|
193
|
+
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
194
|
+
},
|
|
195
|
+
defaultCameraFraming: {
|
|
196
|
+
margin: 1.25, // Breathing room so the model doesn't touch the viewport edges
|
|
197
|
+
minRadiusFactor: 0.25, // Zoom in on detail
|
|
198
|
+
maxRadiusFactor: 4, // Pull back past the whole model
|
|
277
199
|
},
|
|
278
200
|
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
279
201
|
quality: {
|
|
280
202
|
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
281
203
|
// 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,
|
|
204
|
+
tier: 'medium', // Default, will be updated after hardware detection
|
|
205
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
206
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
207
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
208
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
209
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
288
210
|
},
|
|
289
211
|
look: {
|
|
290
212
|
showroom: {
|
|
@@ -296,6 +218,24 @@ export default class BabylonJSController {
|
|
|
296
218
|
hemiLightIntensity: 0.52,
|
|
297
219
|
dirLightIntensity: 0.56,
|
|
298
220
|
cameraLightIntensity: 0.16,
|
|
221
|
+
// Daylight background, in the LINEAR domain the tonemapper consumes — NOT sRGB.
|
|
222
|
+
//
|
|
223
|
+
// The clear colour goes through the same exposure + ACES + gamma chain as the
|
|
224
|
+
// render, so the value that reads correctly here is not the value that reaches the
|
|
225
|
+
// screen. The previous literal was an sRGB 0.98 ("almost white") and it arrived as
|
|
226
|
+
// #C0C0C0, a mid grey.
|
|
227
|
+
//
|
|
228
|
+
// This one is solved against the ACES fit at exposure 0.92 and verified on screen:
|
|
229
|
+
// a light studio grey, #D9D9D9. Other targets, if this needs revisiting —
|
|
230
|
+
// #F4F3F0 warm off-white (matches the host app's 2D/PR surface) -> 4.289 3.991 3.304
|
|
231
|
+
// #D9D9D9 light studio (current) -> 1.424
|
|
232
|
+
// #CCCCCC neutral studio -> 1.112
|
|
233
|
+
// #B3B3B3 darker -> 0.688
|
|
234
|
+
// Note how steeply the required input climbs near white: that is the ACES shoulder,
|
|
235
|
+
// and it is why a near-white background needs a value well above 1.
|
|
236
|
+
//
|
|
237
|
+
// Re-solve these if exposure, contrast or the tonemapping curve change.
|
|
238
|
+
backgroundDay: [1.424, 1.424, 1.424],
|
|
299
239
|
},
|
|
300
240
|
},
|
|
301
241
|
};
|
|
@@ -309,18 +249,18 @@ export default class BabylonJSController {
|
|
|
309
249
|
#detectHardwareTier() {
|
|
310
250
|
// First, try to get WebGL info to help identify the device
|
|
311
251
|
const gl = this.#engine?.getRenderingCanvas()?.getContext?.('webgl2') || this.#engine?.getRenderingCanvas()?.getContext?.('webgl');
|
|
312
|
-
|
|
252
|
+
|
|
313
253
|
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
314
254
|
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
315
255
|
const isAngleRenderer = /^ANGLE\s*\(/i.test(rendererName);
|
|
316
|
-
|
|
256
|
+
|
|
317
257
|
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
318
258
|
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
319
259
|
const isDesktopGPU = /NVIDIA|AMD|Intel|Radeon|GeForce|Quadro/i.test(rendererName);
|
|
320
|
-
|
|
260
|
+
|
|
321
261
|
// Also check if it's explicitly a mobile GPU
|
|
322
262
|
const isMobileGPU = /Adreno|Mali|Apple GPU/i.test(rendererName);
|
|
323
|
-
|
|
263
|
+
|
|
324
264
|
// If we detect a desktop GPU, go directly to desktop detection
|
|
325
265
|
if (isDesktopGPU && !isMobileGPU) {
|
|
326
266
|
if (isAngleRenderer) {
|
|
@@ -339,7 +279,7 @@ export default class BabylonJSController {
|
|
|
339
279
|
|
|
340
280
|
// If no WebGL info or unclear, fall back to user agent detection
|
|
341
281
|
const mobileInfo = this.#detectMobileInfo();
|
|
342
|
-
|
|
282
|
+
|
|
343
283
|
// Only treat as mobile if we have strong mobile indicators
|
|
344
284
|
if (mobileInfo.isMobile && !mobileInfo.isDesktopLike) {
|
|
345
285
|
if (mobileInfo.isHighEnd) {
|
|
@@ -366,15 +306,15 @@ export default class BabylonJSController {
|
|
|
366
306
|
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
367
307
|
// Check WebGL capabilities
|
|
368
308
|
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
369
|
-
|
|
309
|
+
|
|
370
310
|
// Detect high-end GPUs
|
|
371
311
|
const isAppleSilicon = /Apple|M1|M2|M3|M4/i.test(rendererName);
|
|
372
312
|
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);
|
|
373
313
|
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
374
314
|
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
375
|
-
|
|
315
|
+
|
|
376
316
|
let tier = conservative ? 'medium' : 'high'; // Default to conservative desktop when ANGLE is in the path
|
|
377
|
-
|
|
317
|
+
|
|
378
318
|
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
379
319
|
tier = 'medium';
|
|
380
320
|
} else if (!conservative && (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384)) {
|
|
@@ -382,7 +322,7 @@ export default class BabylonJSController {
|
|
|
382
322
|
} else if (conservative && maxTextureSize >= 16384) {
|
|
383
323
|
tier = 'high';
|
|
384
324
|
}
|
|
385
|
-
|
|
325
|
+
|
|
386
326
|
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
387
327
|
return tier;
|
|
388
328
|
}
|
|
@@ -398,48 +338,48 @@ export default class BabylonJSController {
|
|
|
398
338
|
const screenWidth = window.screen?.width || 0;
|
|
399
339
|
const screenHeight = window.screen?.height || 0;
|
|
400
340
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
401
|
-
|
|
341
|
+
|
|
402
342
|
// Detect device type
|
|
403
343
|
const isIPhone = /iPhone/i.test(userAgent);
|
|
404
344
|
const isIPad = /iPad/i.test(userAgent);
|
|
405
345
|
const isAndroid = /Android/i.test(userAgent);
|
|
406
346
|
const isMobile = /Mobile/i.test(userAgent);
|
|
407
|
-
|
|
347
|
+
|
|
408
348
|
// High-end mobile patterns (recent flagship devices)
|
|
409
349
|
const highEndPatterns = [
|
|
410
350
|
/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,
|
|
351
|
+
/iPad Pro/i, // iPad Pro
|
|
352
|
+
/SM-S91[0-9]/i, // Samsung S21+
|
|
353
|
+
/SM-S9[0-9]/i, // Samsung S20+
|
|
354
|
+
/Pixel [7-9]/i, // Pixel 7+
|
|
355
|
+
/OnePlus [8-9|10|11]/i, // OnePlus 8+
|
|
416
356
|
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
417
|
-
/Huawei (P50|Mate 4|)/i,
|
|
357
|
+
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
418
358
|
];
|
|
419
|
-
|
|
359
|
+
|
|
420
360
|
// Mid-range patterns
|
|
421
361
|
const midRangePatterns = [
|
|
422
|
-
/iPhone (1[0-3]|SE)/i,
|
|
362
|
+
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
423
363
|
/iPad Air/i, // iPad Air
|
|
424
|
-
/SM-A[0-9]/i,
|
|
425
|
-
/Redmi Note/i,
|
|
426
|
-
/Pixel [4-6]/i,
|
|
427
|
-
/Moto G/i,
|
|
364
|
+
/SM-A[0-9]/i, // Samsung A series
|
|
365
|
+
/Redmi Note/i, // Redmi Note
|
|
366
|
+
/Pixel [4-6]/i, // Pixel 4-6
|
|
367
|
+
/Moto G/i, // Moto G
|
|
428
368
|
];
|
|
429
|
-
|
|
369
|
+
|
|
430
370
|
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
431
371
|
const isMidRange = midRangePatterns.some(p => p.test(userAgent));
|
|
432
|
-
|
|
372
|
+
|
|
433
373
|
// Detect tablet ONLY if explicitly in user agent (not just screen size)
|
|
434
374
|
// Many desktops have large screens and would incorrectly be detected as tablets
|
|
435
375
|
const isTablet = /iPad|Android.*Tablet|Nexus 9|Nexus 10/i.test(userAgent);
|
|
436
|
-
|
|
376
|
+
|
|
437
377
|
// Detect if mobile (phone form factor) - must have explicit mobile indicators
|
|
438
378
|
const isMobilePhone = /Android.*Mobile|iPhone|iPod/i.test(userAgent);
|
|
439
|
-
|
|
379
|
+
|
|
440
380
|
// Check if it looks like a desktop/laptop (has "Windows", "Mac", "Linux" but no mobile indicators)
|
|
441
381
|
const isDesktopLike = /Windows NT|Macintosh|Linux|X11/i.test(userAgent) && !isMobilePhone && !isTablet;
|
|
442
|
-
|
|
382
|
+
|
|
443
383
|
// Device name for logging
|
|
444
384
|
let deviceName = 'Unknown';
|
|
445
385
|
if (isIPhone) {
|
|
@@ -451,7 +391,7 @@ export default class BabylonJSController {
|
|
|
451
391
|
const match = userAgent.match(/Android.*;.*(.+?)\)/);
|
|
452
392
|
deviceName = match ? match[1].trim() : 'Android';
|
|
453
393
|
}
|
|
454
|
-
|
|
394
|
+
|
|
455
395
|
return {
|
|
456
396
|
isMobile: isMobilePhone && !isDesktopLike,
|
|
457
397
|
isTablet: isTablet && !isDesktopLike,
|
|
@@ -476,13 +416,13 @@ export default class BabylonJSController {
|
|
|
476
416
|
// Mobile low-end (old phones, budget devices)
|
|
477
417
|
low: {
|
|
478
418
|
shadowMapSize: 256, // Very small for speed
|
|
479
|
-
iblShadowResolution: -1,
|
|
419
|
+
iblShadowResolution: -1, // Disable IBL shadows
|
|
480
420
|
iblSampleDirections: 1, // Minimal sampling
|
|
481
421
|
shadowBlurKernel: 0, // No blur
|
|
482
422
|
ssaoEnabled: false,
|
|
483
423
|
antialiasingEnabled: false,
|
|
484
|
-
hardwareScaling: 2.5,
|
|
485
|
-
maxModelResolution: 512,
|
|
424
|
+
hardwareScaling: 2.5, // Aggressive downscaling
|
|
425
|
+
maxModelResolution: 512, // Low-res model loading
|
|
486
426
|
environmentTextureSize: 512,
|
|
487
427
|
},
|
|
488
428
|
// Mobile high-end / Tablet / Desktop low
|
|
@@ -500,7 +440,7 @@ export default class BabylonJSController {
|
|
|
500
440
|
// Desktop mid-range / High-end mobile
|
|
501
441
|
high: {
|
|
502
442
|
shadowMapSize: 1024,
|
|
503
|
-
iblShadowResolution: 0,
|
|
443
|
+
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
504
444
|
iblSampleDirections: 4, // Quality sampling
|
|
505
445
|
shadowBlurKernel: 8, // Sharper shadows for product clarity
|
|
506
446
|
ssaoEnabled: false,
|
|
@@ -512,9 +452,9 @@ export default class BabylonJSController {
|
|
|
512
452
|
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
513
453
|
ultra: {
|
|
514
454
|
shadowMapSize: 2048,
|
|
515
|
-
iblShadowResolution: 1,
|
|
455
|
+
iblShadowResolution: 1, // 2048x2048
|
|
516
456
|
iblSampleDirections: 8, // Maximum quality
|
|
517
|
-
shadowBlurKernel: 16,
|
|
457
|
+
shadowBlurKernel: 16, // Preserve clarity over softness
|
|
518
458
|
ssaoEnabled: false,
|
|
519
459
|
antialiasingEnabled: true,
|
|
520
460
|
hardwareScaling: 1.0, // Native resolution
|
|
@@ -532,10 +472,10 @@ export default class BabylonJSController {
|
|
|
532
472
|
};
|
|
533
473
|
|
|
534
474
|
const preset = presetMap[tier] || qualityPresets.medium;
|
|
535
|
-
|
|
475
|
+
|
|
536
476
|
// Apply preset to config
|
|
537
|
-
this.#config.quality = {
|
|
538
|
-
tier,
|
|
477
|
+
this.#config.quality = {
|
|
478
|
+
tier,
|
|
539
479
|
...preset,
|
|
540
480
|
// Legacy compatibility
|
|
541
481
|
shadowMapSize: preset.shadowMapSize,
|
|
@@ -729,6 +669,55 @@ export default class BabylonJSController {
|
|
|
729
669
|
}
|
|
730
670
|
}
|
|
731
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Preloads Babylon's internal BRDF lookup texture required by PBR materials.
|
|
674
|
+
* In render-on-demand hosts the RGBD expansion can stall unless a few renders are kept alive.
|
|
675
|
+
* @private
|
|
676
|
+
* @returns {Promise<boolean>} True when the texture is ready, false when it timed out or is unavailable.
|
|
677
|
+
*/
|
|
678
|
+
async #ensureEnvironmentBRDFTextureReady() {
|
|
679
|
+
if (!this.#scene || !this.#engine) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const brdfTexture = BRDFTextureTools.GetEnvironmentBRDFTexture(this.#scene);
|
|
684
|
+
if (!brdfTexture || brdfTexture.isReady?.()) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const timeoutMs = 2000;
|
|
689
|
+
const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
690
|
+
this.#requestRender({ frames: this.#config.render.burstFramesEnhanced, continuousMs: timeoutMs });
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
await new Promise((resolve) => {
|
|
694
|
+
const tick = () => {
|
|
695
|
+
if (!this.#scene || !this.#engine || brdfTexture.isDisposed?.()) {
|
|
696
|
+
resolve(false);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (brdfTexture.isReady?.()) {
|
|
700
|
+
resolve(true);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
704
|
+
if (now - startedAt >= timeoutMs) {
|
|
705
|
+
resolve(false);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.#requestRender({ frames: 1, continuousMs: 100 });
|
|
709
|
+
setTimeout(tick, 16);
|
|
710
|
+
};
|
|
711
|
+
tick();
|
|
712
|
+
});
|
|
713
|
+
} catch (error) {
|
|
714
|
+
console.warn("PrefViewer: Error preparing Babylon BRDF texture:", error);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
this.#requestRender({ frames: 2 });
|
|
718
|
+
return brdfTexture.isReady?.() ?? false;
|
|
719
|
+
}
|
|
720
|
+
|
|
732
721
|
/**
|
|
733
722
|
* Starts Babylon's engine render loop if it is not already running.
|
|
734
723
|
* @private
|
|
@@ -756,7 +745,7 @@ export default class BabylonJSController {
|
|
|
756
745
|
this.#state.render.isLoopRunning = false;
|
|
757
746
|
return true;
|
|
758
747
|
}
|
|
759
|
-
|
|
748
|
+
|
|
760
749
|
/**
|
|
761
750
|
* Resets transient render-loop bookkeeping so the next render request starts from a clean baseline.
|
|
762
751
|
* Clears queued burst frames, ends any active continuous-render window, and drops the idle-throttle timestamp.
|
|
@@ -891,12 +880,12 @@ export default class BabylonJSController {
|
|
|
891
880
|
}
|
|
892
881
|
|
|
893
882
|
const shouldRender = animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
894
|
-
|
|
883
|
+
|
|
895
884
|
// Reset idle flag if we need to render
|
|
896
885
|
if (shouldRender) {
|
|
897
886
|
this.#state.render.isIdle = false;
|
|
898
887
|
}
|
|
899
|
-
|
|
888
|
+
|
|
900
889
|
return shouldRender;
|
|
901
890
|
}
|
|
902
891
|
|
|
@@ -932,7 +921,7 @@ export default class BabylonJSController {
|
|
|
932
921
|
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
933
922
|
return;
|
|
934
923
|
}
|
|
935
|
-
|
|
924
|
+
|
|
936
925
|
this.#scene.render();
|
|
937
926
|
this.#state.render.lastRenderAt = now;
|
|
938
927
|
|
|
@@ -1658,81 +1647,6 @@ export default class BabylonJSController {
|
|
|
1658
1647
|
});
|
|
1659
1648
|
}
|
|
1660
1649
|
|
|
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
|
-
|
|
1736
1650
|
/**
|
|
1737
1651
|
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1738
1652
|
* @private
|
|
@@ -1746,7 +1660,8 @@ export default class BabylonJSController {
|
|
|
1746
1660
|
const showroom = this.#config.look?.showroom || {};
|
|
1747
1661
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1748
1662
|
|
|
1749
|
-
|
|
1663
|
+
const bg = showroom.backgroundDay ?? this.#config.look.showroom.backgroundDay;
|
|
1664
|
+
this.#scene.clearColor = new Color4(bg[0], bg[1], bg[2], 1);
|
|
1750
1665
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1751
1666
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1752
1667
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
@@ -1770,7 +1685,7 @@ export default class BabylonJSController {
|
|
|
1770
1685
|
}
|
|
1771
1686
|
|
|
1772
1687
|
const showroom = this.#config.look?.showroom || {};
|
|
1773
|
-
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ?? 0.5);
|
|
1688
|
+
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ? this.#settings.lightingTimeOfDay ?? 0.5 : 0.5);
|
|
1774
1689
|
const profile = getLightingCycleProfile(timeOfDay);
|
|
1775
1690
|
const daylight = profile.dayFactor;
|
|
1776
1691
|
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
@@ -1786,12 +1701,15 @@ export default class BabylonJSController {
|
|
|
1786
1701
|
imageProcessing.colorCurvesEnabled = false;
|
|
1787
1702
|
}
|
|
1788
1703
|
|
|
1704
|
+
// Night end stays in low linear values (a dark blue sky); the day end is the calibrated
|
|
1705
|
+
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1706
|
+
const bgDay = showroom.backgroundDay ?? this.#config.look.showroom.backgroundDay;
|
|
1789
1707
|
this.#scene.clearColor = new Color4(
|
|
1790
|
-
lerp(0.
|
|
1791
|
-
lerp(0.
|
|
1792
|
-
lerp(0.
|
|
1708
|
+
lerp(0.000627, bgDay[0], daylight),
|
|
1709
|
+
lerp(0.001089, bgDay[1], daylight),
|
|
1710
|
+
lerp(0.004413, bgDay[2], daylight),
|
|
1793
1711
|
1,
|
|
1794
|
-
)
|
|
1712
|
+
);
|
|
1795
1713
|
|
|
1796
1714
|
if ("environmentIntensity" in this.#scene) {
|
|
1797
1715
|
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
@@ -1863,6 +1781,78 @@ export default class BabylonJSController {
|
|
|
1863
1781
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1864
1782
|
}
|
|
1865
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* World-space bounds of the loaded model's renderable geometry.
|
|
1786
|
+
* @private
|
|
1787
|
+
* @returns {{center: Vector3, radius: number}|null} Centre and bounding-sphere radius, or null when nothing renderable is loaded.
|
|
1788
|
+
*/
|
|
1789
|
+
#getModelBounds() {
|
|
1790
|
+
const meshes = (this.#containers?.model?.assetContainer?.meshes ?? []).filter((mesh) => (mesh?.getTotalVertices?.() ?? 0) > 0);
|
|
1791
|
+
if (meshes.length === 0) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
let min = null;
|
|
1796
|
+
let max = null;
|
|
1797
|
+
meshes.forEach((mesh) => {
|
|
1798
|
+
mesh.computeWorldMatrix(true);
|
|
1799
|
+
const box = mesh.getBoundingInfo?.()?.boundingBox;
|
|
1800
|
+
if (!box) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
min = min ? Vector3.Minimize(min, box.minimumWorld) : box.minimumWorld.clone();
|
|
1804
|
+
max = max ? Vector3.Maximize(max, box.maximumWorld) : box.maximumWorld.clone();
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
if (!min || !max) {
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const radius = Vector3.Distance(min, max) / 2;
|
|
1812
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return { center: Vector3.Center(min, max), radius };
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* Frames the default camera on the loaded model: aims it at the model's centre and pulls it
|
|
1821
|
+
* back far enough for the whole model to fit the viewport.
|
|
1822
|
+
*
|
|
1823
|
+
* Only ever applied to the viewer's own camera — a camera authored in the model or the scene
|
|
1824
|
+
* is a composition choice and is left untouched. Orbit angles are preserved so a model reload
|
|
1825
|
+
* (an option or dimension change) re-centres without snapping the user's viewpoint back.
|
|
1826
|
+
* @private
|
|
1827
|
+
* @param {Camera|null|undefined} camera - The camera about to become active.
|
|
1828
|
+
* @returns {void}
|
|
1829
|
+
*/
|
|
1830
|
+
#fitDefaultCameraToModel(camera) {
|
|
1831
|
+
if (camera !== this.#camera || !(camera instanceof ArcRotateCamera)) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const bounds = this.#getModelBounds();
|
|
1836
|
+
if (!bounds) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// fovMode is FOVMODE_HORIZONTAL_FIXED (see #applyShowroomCameraFraming), so camera.fov is
|
|
1841
|
+
// the horizontal angle and the vertical one follows the aspect ratio. Fitting to the
|
|
1842
|
+
// horizontal angle alone clips the model top and bottom on any viewport wider than tall.
|
|
1843
|
+
const aspect = this.#engine?.getAspectRatio?.(camera) || 1;
|
|
1844
|
+
const horizontalFov = camera.fov;
|
|
1845
|
+
const verticalFov = 2 * Math.atan(Math.tan(horizontalFov / 2) / aspect);
|
|
1846
|
+
const limitingFov = Math.min(horizontalFov, verticalFov);
|
|
1847
|
+
const distance = (bounds.radius / Math.sin(limitingFov / 2)) * this.#config.defaultCameraFraming.margin;
|
|
1848
|
+
|
|
1849
|
+
camera.setTarget(bounds.center);
|
|
1850
|
+
camera.radius = distance;
|
|
1851
|
+
// The 5-20 m defaults were sized for a room; left in place they clamp the fit itself.
|
|
1852
|
+
camera.lowerRadiusLimit = distance * this.#config.defaultCameraFraming.minRadiusFactor;
|
|
1853
|
+
camera.upperRadiusLimit = distance * this.#config.defaultCameraFraming.maxRadiusFactor;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1866
1856
|
/**
|
|
1867
1857
|
* Keeps the showroom camera framing stable when the viewport aspect ratio changes.
|
|
1868
1858
|
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
@@ -2093,7 +2083,7 @@ export default class BabylonJSController {
|
|
|
2093
2083
|
}
|
|
2094
2084
|
|
|
2095
2085
|
/**
|
|
2096
|
-
* Attaches the `
|
|
2086
|
+
* Attaches the `pref-viewer-3d-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
2097
2087
|
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
2098
2088
|
* @private
|
|
2099
2089
|
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
@@ -2104,12 +2094,12 @@ export default class BabylonJSController {
|
|
|
2104
2094
|
return false;
|
|
2105
2095
|
}
|
|
2106
2096
|
this.#detachAnimationChangedListener();
|
|
2107
|
-
this.#prefViewer3D.addEventListener("
|
|
2097
|
+
this.#prefViewer3D.addEventListener("pref-viewer-3d-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
2108
2098
|
return true;
|
|
2109
2099
|
}
|
|
2110
2100
|
|
|
2111
2101
|
/**
|
|
2112
|
-
* Detaches the `
|
|
2102
|
+
* Detaches the `pref-viewer-3d-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
2113
2103
|
* @private
|
|
2114
2104
|
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
2115
2105
|
*/
|
|
@@ -2117,7 +2107,7 @@ export default class BabylonJSController {
|
|
|
2117
2107
|
if (!this.#prefViewer3D) {
|
|
2118
2108
|
return false;
|
|
2119
2109
|
}
|
|
2120
|
-
this.#prefViewer3D.removeEventListener("
|
|
2110
|
+
this.#prefViewer3D.removeEventListener("pref-viewer-3d-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
2121
2111
|
return true;
|
|
2122
2112
|
}
|
|
2123
2113
|
|
|
@@ -2150,12 +2140,12 @@ export default class BabylonJSController {
|
|
|
2150
2140
|
*/
|
|
2151
2141
|
#onMouseWheel(event, pickInfo) {
|
|
2152
2142
|
event.preventDefault();
|
|
2153
|
-
|
|
2143
|
+
|
|
2154
2144
|
// OPTIMIZATION: Mark activity for idle mode
|
|
2155
2145
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2156
2146
|
this.#state.render.lastActivityAt = now;
|
|
2157
2147
|
this.#state.render.isIdle = false;
|
|
2158
|
-
|
|
2148
|
+
|
|
2159
2149
|
const camera = this.#scene?.activeCamera;
|
|
2160
2150
|
if (!camera) {
|
|
2161
2151
|
return false;
|
|
@@ -2190,7 +2180,7 @@ export default class BabylonJSController {
|
|
|
2190
2180
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2191
2181
|
this.#state.render.lastActivityAt = now;
|
|
2192
2182
|
this.#state.render.isIdle = false;
|
|
2193
|
-
|
|
2183
|
+
|
|
2194
2184
|
if (this.#babylonJSAnimationController) {
|
|
2195
2185
|
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
2196
2186
|
this.#babylonJSAnimationController.hideMenu();
|
|
@@ -2215,7 +2205,7 @@ export default class BabylonJSController {
|
|
|
2215
2205
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2216
2206
|
this.#state.render.lastActivityAt = now;
|
|
2217
2207
|
this.#state.render.isIdle = false;
|
|
2218
|
-
|
|
2208
|
+
|
|
2219
2209
|
const camera = this.#scene?.activeCamera;
|
|
2220
2210
|
if (camera && !camera.metadata?.locked) {
|
|
2221
2211
|
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
@@ -2437,6 +2427,7 @@ export default class BabylonJSController {
|
|
|
2437
2427
|
}
|
|
2438
2428
|
}
|
|
2439
2429
|
this.#applyShowroomCameraFraming(camera);
|
|
2430
|
+
this.#fitDefaultCameraToModel(camera);
|
|
2440
2431
|
this.#scene.activeCamera?.detachControl();
|
|
2441
2432
|
camera.detachControl();
|
|
2442
2433
|
if (!cameraState.locked) {
|
|
@@ -2863,21 +2854,34 @@ export default class BabylonJSController {
|
|
|
2863
2854
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2864
2855
|
this.#setMaxSimultaneousLights();
|
|
2865
2856
|
this.#enhanceTextureQuality();
|
|
2866
|
-
this.#
|
|
2867
|
-
|
|
2868
|
-
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2869
|
-
this.#setHighlightConfig({
|
|
2870
|
-
enabled: this.#settings.highlightEnabled,
|
|
2871
|
-
color: this.#settings.highlightColor,
|
|
2872
|
-
});
|
|
2873
|
-
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2874
|
-
this.#attachAnimationChangedListener();
|
|
2875
|
-
}
|
|
2857
|
+
this.#syncAnimationControllerAfterModelLoad();
|
|
2858
|
+
|
|
2876
2859
|
await this.#startRender();
|
|
2877
2860
|
});
|
|
2878
2861
|
return detail;
|
|
2879
2862
|
}
|
|
2880
2863
|
|
|
2864
|
+
/**
|
|
2865
|
+
* Creates or disposes the animation controller after a model load according to current settings.
|
|
2866
|
+
* @private
|
|
2867
|
+
* @returns {void}
|
|
2868
|
+
*/
|
|
2869
|
+
#syncAnimationControllerAfterModelLoad() {
|
|
2870
|
+
if (this.#settings.showAnimationMenu && this.#containers.model.assetContainer) {
|
|
2871
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2872
|
+
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2873
|
+
this.#setHighlightConfig({
|
|
2874
|
+
enabled: this.#settings.highlightEnabled,
|
|
2875
|
+
color: this.#settings.highlightColor,
|
|
2876
|
+
});
|
|
2877
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2878
|
+
this.#attachAnimationChangedListener();
|
|
2879
|
+
}
|
|
2880
|
+
} else {
|
|
2881
|
+
this.#disposeAnimationController();
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2881
2885
|
/**
|
|
2882
2886
|
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
2883
2887
|
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
@@ -3131,14 +3135,14 @@ export default class BabylonJSController {
|
|
|
3131
3135
|
this.#configureDracoCompression();
|
|
3132
3136
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
3133
3137
|
this.#engine.disableUniformBuffers = true;
|
|
3134
|
-
|
|
3138
|
+
|
|
3135
3139
|
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
3136
3140
|
const detectedTier = this.#detectHardwareTier();
|
|
3137
3141
|
this.#applyQualitySettings(detectedTier);
|
|
3138
|
-
|
|
3142
|
+
|
|
3139
3143
|
// OPTIMIZATION: Initialize idle tracking
|
|
3140
3144
|
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
3141
|
-
|
|
3145
|
+
|
|
3142
3146
|
this.#scene = new Scene(this.#engine);
|
|
3143
3147
|
|
|
3144
3148
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
@@ -3159,6 +3163,7 @@ export default class BabylonJSController {
|
|
|
3159
3163
|
this.#scene.skipPointerUpPicking = true;
|
|
3160
3164
|
|
|
3161
3165
|
this.#createCamera();
|
|
3166
|
+
await this.#ensureEnvironmentBRDFTextureReady();
|
|
3162
3167
|
this.#enableInteraction();
|
|
3163
3168
|
await this.#createXRExperience();
|
|
3164
3169
|
}
|
|
@@ -3314,6 +3319,182 @@ export default class BabylonJSController {
|
|
|
3314
3319
|
return { ...this.#settings };
|
|
3315
3320
|
}
|
|
3316
3321
|
|
|
3322
|
+
/**
|
|
3323
|
+
* Returns a diagnostic snapshot of the current Babylon.js runtime state.
|
|
3324
|
+
* Intended for integration troubleshooting and should not mutate scene state.
|
|
3325
|
+
* @public
|
|
3326
|
+
* @returns {object} Debug information for canvas, engine, scene, containers, render loop and settings.
|
|
3327
|
+
*/
|
|
3328
|
+
getDebugInfo() {
|
|
3329
|
+
const vectorToJSON = (value) => value ? { x: value.x, y: value.y, z: value.z } : null;
|
|
3330
|
+
const rect = this.#canvas?.getBoundingClientRect?.();
|
|
3331
|
+
const camera = this.#scene?.activeCamera ?? null;
|
|
3332
|
+
const sceneMeshes = this.#scene?.meshes ?? [];
|
|
3333
|
+
const sceneMaterials = this.#scene?.materials ?? [];
|
|
3334
|
+
const sceneTextures = this.#scene?.textures ?? [];
|
|
3335
|
+
const activeMeshes = this.#scene?.getActiveMeshes?.();
|
|
3336
|
+
const modelAssetContainer = this.#containers?.model?.assetContainer ?? null;
|
|
3337
|
+
const modelMeshes = modelAssetContainer?.meshes ?? [];
|
|
3338
|
+
const enabledModelMeshes = modelMeshes.filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false);
|
|
3339
|
+
const textureToJSON = (texture) => ({
|
|
3340
|
+
name: texture?.name ?? null,
|
|
3341
|
+
url: texture?.url ?? null,
|
|
3342
|
+
isReady: texture?.isReady?.() ?? null,
|
|
3343
|
+
hasTexture: Boolean(texture),
|
|
3344
|
+
className: texture?.getClassName?.() ?? texture?.constructor?.name ?? null,
|
|
3345
|
+
});
|
|
3346
|
+
const summarizeMaterial = (material) => {
|
|
3347
|
+
const relatedMeshes = sceneMeshes.filter((mesh) => mesh.material === material);
|
|
3348
|
+
const sampleMesh = relatedMeshes[0] ?? null;
|
|
3349
|
+
const activeTextures = material?.getActiveTextures?.() ?? [];
|
|
3350
|
+
let isReadyWithoutMesh = null;
|
|
3351
|
+
let isReadyWithMesh = null;
|
|
3352
|
+
try {
|
|
3353
|
+
isReadyWithoutMesh = typeof material?.isReady === "function" ? material.isReady() : null;
|
|
3354
|
+
} catch (error) {
|
|
3355
|
+
isReadyWithoutMesh = `error: ${error?.message ?? error}`;
|
|
3356
|
+
}
|
|
3357
|
+
try {
|
|
3358
|
+
isReadyWithMesh = typeof material?.isReady === "function" && sampleMesh ? material.isReady(sampleMesh) : null;
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
isReadyWithMesh = `error: ${error?.message ?? error}`;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
return {
|
|
3364
|
+
name: material?.name ?? null,
|
|
3365
|
+
id: material?.id ?? null,
|
|
3366
|
+
className: material?.getClassName?.() ?? material?.constructor?.name ?? null,
|
|
3367
|
+
isReadyWithoutMesh,
|
|
3368
|
+
isReadyWithMesh,
|
|
3369
|
+
relatedMeshes: relatedMeshes.length,
|
|
3370
|
+
sampleMesh: sampleMesh ? {
|
|
3371
|
+
name: sampleMesh.name,
|
|
3372
|
+
id: sampleMesh.id,
|
|
3373
|
+
enabled: sampleMesh.isEnabled?.() ?? null,
|
|
3374
|
+
isVisible: sampleMesh.isVisible ?? null,
|
|
3375
|
+
hasVertices: sampleMesh.getTotalVertices?.() ?? null,
|
|
3376
|
+
hasIndices: sampleMesh.getTotalIndices?.() ?? null,
|
|
3377
|
+
} : null,
|
|
3378
|
+
textures: activeTextures.map(textureToJSON),
|
|
3379
|
+
notReadyTextures: activeTextures.filter((texture) => texture?.isReady?.() === false).map(textureToJSON),
|
|
3380
|
+
forceDepthWrite: material?.forceDepthWrite ?? null,
|
|
3381
|
+
transparencyMode: material?.transparencyMode ?? null,
|
|
3382
|
+
alpha: material?.alpha ?? null,
|
|
3383
|
+
backFaceCulling: material?.backFaceCulling ?? null,
|
|
3384
|
+
disableLighting: material?.disableLighting ?? null,
|
|
3385
|
+
};
|
|
3386
|
+
};
|
|
3387
|
+
const materialDiagnostics = sceneMaterials.slice(0, 25).map(summarizeMaterial);
|
|
3388
|
+
const notReadyMaterials = materialDiagnostics
|
|
3389
|
+
.filter((material) => material.isReadyWithoutMesh === false || material.isReadyWithMesh === false || material.notReadyTextures.length > 0)
|
|
3390
|
+
.slice(0, 25);
|
|
3391
|
+
const notReadyTextures = sceneTextures
|
|
3392
|
+
.filter((texture) => texture?.isReady?.() === false)
|
|
3393
|
+
.slice(0, 25)
|
|
3394
|
+
.map(textureToJSON);
|
|
3395
|
+
const notReadyMeshes = sceneMeshes
|
|
3396
|
+
.filter((mesh) => typeof mesh.isReady === "function" && !mesh.isReady())
|
|
3397
|
+
.slice(0, 25)
|
|
3398
|
+
.map((mesh) => ({
|
|
3399
|
+
name: mesh.name,
|
|
3400
|
+
id: mesh.id,
|
|
3401
|
+
enabled: mesh.isEnabled?.() ?? null,
|
|
3402
|
+
isVisible: mesh.isVisible ?? null,
|
|
3403
|
+
material: mesh.material?.name ?? null,
|
|
3404
|
+
}));
|
|
3405
|
+
const modelBounds = enabledModelMeshes.length && this.#scene?.getWorldExtends
|
|
3406
|
+
? this.#scene.getWorldExtends((mesh) => enabledModelMeshes.includes(mesh))
|
|
3407
|
+
: null;
|
|
3408
|
+
const summarizeContainer = (container) => ({
|
|
3409
|
+
show: container?.state?.show ?? null,
|
|
3410
|
+
visible: container?.state?.visible ?? null,
|
|
3411
|
+
mustBeShown: container?.state?.mustBeShown ?? null,
|
|
3412
|
+
isPending: container?.state?.isPending ?? null,
|
|
3413
|
+
isSuccess: container?.state?.isSuccess ?? null,
|
|
3414
|
+
hasAssetContainer: Boolean(container?.assetContainer),
|
|
3415
|
+
meshes: container?.assetContainer?.meshes?.length ?? 0,
|
|
3416
|
+
enabledMeshes: (container?.assetContainer?.meshes ?? []).filter((mesh) => mesh.isEnabled?.() !== false && mesh.isVisible !== false).length,
|
|
3417
|
+
materials: container?.assetContainer?.materials?.length ?? 0,
|
|
3418
|
+
cameras: container?.assetContainer?.cameras?.map((item) => ({
|
|
3419
|
+
name: item.name,
|
|
3420
|
+
id: item.id,
|
|
3421
|
+
className: item.getClassName?.() ?? item.constructor?.name ?? null,
|
|
3422
|
+
position: vectorToJSON(item.position),
|
|
3423
|
+
target: vectorToJSON(item.target),
|
|
3424
|
+
})) ?? [],
|
|
3425
|
+
animationGroups: container?.assetContainer?.animationGroups?.length ?? 0,
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3428
|
+
return {
|
|
3429
|
+
canvas: {
|
|
3430
|
+
clientWidth: this.#canvas?.clientWidth ?? null,
|
|
3431
|
+
clientHeight: this.#canvas?.clientHeight ?? null,
|
|
3432
|
+
width: this.#canvas?.width ?? null,
|
|
3433
|
+
height: this.#canvas?.height ?? null,
|
|
3434
|
+
boundingWidth: rect?.width ?? null,
|
|
3435
|
+
boundingHeight: rect?.height ?? null,
|
|
3436
|
+
},
|
|
3437
|
+
engine: {
|
|
3438
|
+
renderWidth: this.#engine?.getRenderWidth?.() ?? null,
|
|
3439
|
+
renderHeight: this.#engine?.getRenderHeight?.() ?? null,
|
|
3440
|
+
hardwareScalingLevel: this.#engine?.getHardwareScalingLevel?.() ?? null,
|
|
3441
|
+
isPointerLock: this.#engine?.isPointerLock ?? null,
|
|
3442
|
+
isDisposed: this.#engine?.isDisposed ?? null,
|
|
3443
|
+
contextLost: this.#engine?.isContextLost?.() ?? null,
|
|
3444
|
+
},
|
|
3445
|
+
scene: {
|
|
3446
|
+
uid: this.#scene?.uid ?? null,
|
|
3447
|
+
isReady: this.#scene?.isReady?.() ?? null,
|
|
3448
|
+
meshes: sceneMeshes.length,
|
|
3449
|
+
visibleMeshes: activeMeshes?.length ?? activeMeshes?.data?.length ?? null,
|
|
3450
|
+
materials: sceneMaterials.length,
|
|
3451
|
+
textures: sceneTextures.length,
|
|
3452
|
+
lights: this.#scene?.lights?.length ?? 0,
|
|
3453
|
+
animationGroups: this.#scene?.animationGroups?.length ?? 0,
|
|
3454
|
+
activeCamera: camera ? {
|
|
3455
|
+
name: camera.name,
|
|
3456
|
+
id: camera.id,
|
|
3457
|
+
className: camera.getClassName?.() ?? camera.constructor?.name ?? null,
|
|
3458
|
+
position: vectorToJSON(camera.position),
|
|
3459
|
+
target: vectorToJSON(camera.target),
|
|
3460
|
+
radius: camera.radius ?? null,
|
|
3461
|
+
alpha: camera.alpha ?? null,
|
|
3462
|
+
beta: camera.beta ?? null,
|
|
3463
|
+
} : null,
|
|
3464
|
+
clearColor: this.#scene?.clearColor ? {
|
|
3465
|
+
r: this.#scene.clearColor.r,
|
|
3466
|
+
g: this.#scene.clearColor.g,
|
|
3467
|
+
b: this.#scene.clearColor.b,
|
|
3468
|
+
a: this.#scene.clearColor.a,
|
|
3469
|
+
} : null,
|
|
3470
|
+
environmentTexture: this.#scene?.environmentTexture ? {
|
|
3471
|
+
name: this.#scene.environmentTexture.name,
|
|
3472
|
+
isReady: this.#scene.environmentTexture.isReady?.() ?? null,
|
|
3473
|
+
} : null,
|
|
3474
|
+
notReadyMaterials,
|
|
3475
|
+
notReadyTextures,
|
|
3476
|
+
materialDiagnostics,
|
|
3477
|
+
notReadyMeshes,
|
|
3478
|
+
},
|
|
3479
|
+
containers: {
|
|
3480
|
+
model: summarizeContainer(this.#containers.model),
|
|
3481
|
+
environment: summarizeContainer(this.#containers.environment),
|
|
3482
|
+
materials: summarizeContainer(this.#containers.materials),
|
|
3483
|
+
},
|
|
3484
|
+
modelBounds: modelBounds ? {
|
|
3485
|
+
min: vectorToJSON(modelBounds.min),
|
|
3486
|
+
max: vectorToJSON(modelBounds.max),
|
|
3487
|
+
} : null,
|
|
3488
|
+
render: { ...this.#state.render },
|
|
3489
|
+
settings: { ...this.#settings },
|
|
3490
|
+
pipelines: {
|
|
3491
|
+
default: Boolean(this.#renderPipelines.default),
|
|
3492
|
+
ssao: Boolean(this.#renderPipelines.ssao),
|
|
3493
|
+
iblShadows: Boolean(this.#renderPipelines.iblShadows),
|
|
3494
|
+
},
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3317
3498
|
/**
|
|
3318
3499
|
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
3319
3500
|
* Applies material and camera options, sets visibility, and initializes lights and shadows.
|
|
@@ -3346,7 +3527,6 @@ export default class BabylonJSController {
|
|
|
3346
3527
|
setMaterialOptions() {
|
|
3347
3528
|
this.#stopRender();
|
|
3348
3529
|
const materialsOptionsSetted = this.#setOptions_Materials();
|
|
3349
|
-
this.#applyTransmittanceRenderOrder();
|
|
3350
3530
|
this.#startRender();
|
|
3351
3531
|
return materialsOptionsSetted;
|
|
3352
3532
|
}
|
|
@@ -3367,14 +3547,42 @@ export default class BabylonJSController {
|
|
|
3367
3547
|
}
|
|
3368
3548
|
|
|
3369
3549
|
/**
|
|
3370
|
-
* Applies
|
|
3550
|
+
* Applies animation-menu, highlight, and time-of-day lighting configuration without forcing a full scene reload.
|
|
3551
|
+
* Values are merged into the persisted render settings snapshot unless `config.persist === false`.
|
|
3552
|
+
* When `showLightingTimeOfDay` is set to false and no explicit `lightingTimeOfDay` is supplied,
|
|
3553
|
+
* the active lighting time is reset to `DEFAULT_RENDER_SETTINGS.lightingTimeOfDay` (0.5).
|
|
3371
3554
|
* @public
|
|
3372
|
-
* @param {
|
|
3555
|
+
* @param {object} config - Immediate render/illumination configuration.
|
|
3556
|
+
* @param {boolean|string} [config.showAnimationMenu] - Enables/disables the animation controls when animations are available.
|
|
3557
|
+
* @param {boolean|string|null} [config.highlightEnabled] - Enables/disables hover highlight; string "false" is normalized to false, null is ignored.
|
|
3558
|
+
* @param {string|null} [config.highlightColor] - Hex color used by hover highlight; null is ignored.
|
|
3559
|
+
* @param {boolean|string} [config.showLightingTimeOfDay] - Shows/hides the time-of-day control and can reset time to the default when hidden.
|
|
3560
|
+
* @param {number} [config.lightingTimeOfDay] - Normalized time of day in the 0..1 range.
|
|
3561
|
+
* @param {boolean} [config.persist=true] - False for transient updates such as lighting playback frames.
|
|
3373
3562
|
* @returns {void}
|
|
3374
3563
|
*/
|
|
3375
3564
|
setIlluminationConfig(config = {}) {
|
|
3376
3565
|
let shouldApplyLighting = false;
|
|
3377
3566
|
|
|
3567
|
+
if (config.showLightingTimeOfDay !== undefined) {
|
|
3568
|
+
const showLighting = typeof config.showLightingTimeOfDay === "string"
|
|
3569
|
+
? config.showLightingTimeOfDay.toLowerCase() !== "false"
|
|
3570
|
+
: Boolean(config.showLightingTimeOfDay);
|
|
3571
|
+
if (this.#settings.showLightingTimeOfDay !== showLighting) {
|
|
3572
|
+
shouldApplyLighting = true;
|
|
3573
|
+
}
|
|
3574
|
+
this.#settings.showLightingTimeOfDay = showLighting;
|
|
3575
|
+
|
|
3576
|
+
const hasExplicitLightingTime = typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay);
|
|
3577
|
+
if (!showLighting && !hasExplicitLightingTime) {
|
|
3578
|
+
const defaultTime = BabylonJSController.DEFAULT_RENDER_SETTINGS.lightingTimeOfDay;
|
|
3579
|
+
if (this.#settings.lightingTimeOfDay !== defaultTime) {
|
|
3580
|
+
this.#settings.lightingTimeOfDay = defaultTime;
|
|
3581
|
+
shouldApplyLighting = true;
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3378
3586
|
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3379
3587
|
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3380
3588
|
if (this.#settings.lightingTimeOfDay !== normalizedTime) {
|
|
@@ -3384,8 +3592,11 @@ export default class BabylonJSController {
|
|
|
3384
3592
|
}
|
|
3385
3593
|
|
|
3386
3594
|
if (config.showAnimationMenu !== undefined) {
|
|
3387
|
-
|
|
3388
|
-
|
|
3595
|
+
const showMenu = typeof config.showAnimationMenu === "string"
|
|
3596
|
+
? config.showAnimationMenu.toLowerCase() !== "false"
|
|
3597
|
+
: Boolean(config.showAnimationMenu);
|
|
3598
|
+
this.#settings.showAnimationMenu = showMenu;
|
|
3599
|
+
this.#syncAnimationControllerAfterModelLoad();
|
|
3389
3600
|
}
|
|
3390
3601
|
|
|
3391
3602
|
// Handle highlight color for animation hover
|
|
@@ -3406,7 +3617,7 @@ export default class BabylonJSController {
|
|
|
3406
3617
|
}
|
|
3407
3618
|
this.#setHighlightConfig({
|
|
3408
3619
|
color: config.highlightColor ?? undefined,
|
|
3409
|
-
enabled,
|
|
3620
|
+
enabled: enabled,
|
|
3410
3621
|
});
|
|
3411
3622
|
}
|
|
3412
3623
|
|
|
@@ -3445,11 +3656,13 @@ export default class BabylonJSController {
|
|
|
3445
3656
|
}
|
|
3446
3657
|
|
|
3447
3658
|
/**
|
|
3448
|
-
* Merges
|
|
3659
|
+
* Merges reload-oriented render flags with the current configuration, persists them, and marks
|
|
3449
3660
|
* all dependent loaders/options as pending when something actually changed.
|
|
3661
|
+
* Immediate settings such as highlight, animation menu, and time-of-day lighting are handled by
|
|
3662
|
+
* `setIlluminationConfig()` before this method receives the remaining reload settings.
|
|
3450
3663
|
* @public
|
|
3451
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
3452
|
-
* @returns {{changed:boolean, settings:
|
|
3664
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of reload render toggles to apply.
|
|
3665
|
+
* @returns {{changed:boolean, settings:object}} Current full render settings snapshot and whether a reload is required.
|
|
3453
3666
|
* @description
|
|
3454
3667
|
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
3455
3668
|
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|