@preference-sl/pref-viewer 2.13.0 → 2.14.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/babylonjs-controller.js +336 -8
package/package.json
CHANGED
|
@@ -141,9 +141,11 @@ export default class BabylonJSController {
|
|
|
141
141
|
// Render loop state balances performance with responsiveness.
|
|
142
142
|
render: {
|
|
143
143
|
isLoopRunning: false,
|
|
144
|
+
isIdle: false,
|
|
144
145
|
dirtyFrames: 0,
|
|
145
146
|
continuousUntil: 0,
|
|
146
147
|
lastRenderAt: 0,
|
|
148
|
+
lastActivityAt: 0,
|
|
147
149
|
},
|
|
148
150
|
// Resize state batches frequent ResizeObserver notifications.
|
|
149
151
|
resize: {
|
|
@@ -164,13 +166,259 @@ export default class BabylonJSController {
|
|
|
164
166
|
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
165
167
|
interactionMs: 250,
|
|
166
168
|
animationMs: 200,
|
|
167
|
-
idleThrottleMs: 1000 / 15
|
|
169
|
+
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
170
|
+
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
168
171
|
},
|
|
169
172
|
resize: {
|
|
170
173
|
throttleMs: 50, // cap resize work to ~20 Hz while dragging/resizing containers
|
|
171
174
|
},
|
|
175
|
+
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
176
|
+
quality: {
|
|
177
|
+
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
178
|
+
// Applied automatically based on GPU detected capabilities
|
|
179
|
+
tier: 'medium', // default, will be updated after hardware detection
|
|
180
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
181
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
182
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
183
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
184
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
185
|
+
},
|
|
172
186
|
};
|
|
173
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Detects hardware capabilities and determines optimal quality tier.
|
|
190
|
+
* Uses WebGL renderer info, device type, and battery status to classify hardware.
|
|
191
|
+
* @private
|
|
192
|
+
* @returns {string} Hardware tier: 'low', 'medium', 'high', or 'ultra'
|
|
193
|
+
*/
|
|
194
|
+
#detectHardwareTier() {
|
|
195
|
+
// First, try to get WebGL info to help identify the device
|
|
196
|
+
const gl = this.#engine?.getRenderingCanvas()?.getContext?.('webgl2') || this.#engine?.getRenderingCanvas()?.getContext?.('webgl');
|
|
197
|
+
|
|
198
|
+
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
199
|
+
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
200
|
+
|
|
201
|
+
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
202
|
+
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
203
|
+
const isDesktopGPU = /NVIDIA|AMD|Intel|Radeon|GeForce|Quadro/i.test(rendererName);
|
|
204
|
+
|
|
205
|
+
// Also check if it's explicitly a mobile GPU
|
|
206
|
+
const isMobileGPU = /Adreno|Mali|Apple GPU/i.test(rendererName);
|
|
207
|
+
|
|
208
|
+
// If we detect a desktop GPU, go directly to desktop detection
|
|
209
|
+
if (isDesktopGPU && !isMobileGPU) {
|
|
210
|
+
console.log(`[PrefViewer] Desktop GPU detected: ${rendererName}`);
|
|
211
|
+
return this.#detectDesktopTier(gl, rendererName);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If we detect a mobile GPU, it's definitely mobile
|
|
215
|
+
if (isMobileGPU) {
|
|
216
|
+
console.log(`[PrefViewer] Mobile GPU detected: ${rendererName} - Quality tier: low`);
|
|
217
|
+
return 'low';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If no WebGL info or unclear, fall back to user agent detection
|
|
221
|
+
const mobileInfo = this.#detectMobileInfo();
|
|
222
|
+
|
|
223
|
+
// Only treat as mobile if we have strong mobile indicators
|
|
224
|
+
if (mobileInfo.isMobile && !mobileInfo.isDesktopLike) {
|
|
225
|
+
if (mobileInfo.isHighEnd) {
|
|
226
|
+
console.log(`[PrefViewer] High-end mobile detected: ${mobileInfo.deviceName} - Quality tier: medium`);
|
|
227
|
+
return 'medium';
|
|
228
|
+
} else {
|
|
229
|
+
console.log(`[PrefViewer] Mobile detected: ${mobileInfo.deviceName} - Quality tier: low`);
|
|
230
|
+
return 'low';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If still unclear, default to desktop detection
|
|
235
|
+
console.log(`[PrefViewer] Using desktop GPU detection (unknown renderer: ${rendererName || 'none'})`);
|
|
236
|
+
return this.#detectDesktopTier(gl, rendererName);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Detects hardware tier for desktop GPUs based on renderer name and WebGL capabilities.
|
|
241
|
+
* @private
|
|
242
|
+
* @param {WebGLRenderingContext} gl - WebGL context
|
|
243
|
+
* @param {string} rendererName - GPU renderer name
|
|
244
|
+
* @returns {string} Hardware tier
|
|
245
|
+
*/
|
|
246
|
+
#detectDesktopTier(gl, rendererName) {
|
|
247
|
+
// Check WebGL capabilities
|
|
248
|
+
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
249
|
+
|
|
250
|
+
// Detect high-end GPUs
|
|
251
|
+
const isAppleSilicon = /Apple|M1|M2|M3|M4/i.test(rendererName);
|
|
252
|
+
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);
|
|
253
|
+
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
254
|
+
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
255
|
+
|
|
256
|
+
let tier = 'high'; // Default to high for desktop
|
|
257
|
+
|
|
258
|
+
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
259
|
+
tier = 'medium';
|
|
260
|
+
} else if (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384) {
|
|
261
|
+
tier = 'ultra';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
265
|
+
return tier;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Detects mobile device type and capabilities.
|
|
270
|
+
* Analyzes user agent, screen specs, and device performance indicators.
|
|
271
|
+
* @private
|
|
272
|
+
* @returns {Object} Mobile device info with tier classification
|
|
273
|
+
*/
|
|
274
|
+
#detectMobileInfo() {
|
|
275
|
+
const userAgent = navigator.userAgent || '';
|
|
276
|
+
const screenWidth = window.screen?.width || 0;
|
|
277
|
+
const screenHeight = window.screen?.height || 0;
|
|
278
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
279
|
+
|
|
280
|
+
// Detect device type
|
|
281
|
+
const isIPhone = /iPhone/i.test(userAgent);
|
|
282
|
+
const isIPad = /iPad/i.test(userAgent);
|
|
283
|
+
const isAndroid = /Android/i.test(userAgent);
|
|
284
|
+
const isMobile = /Mobile/i.test(userAgent);
|
|
285
|
+
|
|
286
|
+
// High-end mobile patterns (recent flagship devices)
|
|
287
|
+
const highEndPatterns = [
|
|
288
|
+
/iPhone (1[4-9]|SE)/i, // iPhone 14+
|
|
289
|
+
/iPad Pro/i, // iPad Pro
|
|
290
|
+
/SM-S91[0-9]/i, // Samsung S21+
|
|
291
|
+
/SM-S9[0-9]/i, // Samsung S20+
|
|
292
|
+
/Pixel [7-9]/i, // Pixel 7+
|
|
293
|
+
/OnePlus [8-9|10|11]/i, // OnePlus 8+
|
|
294
|
+
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
295
|
+
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
// Mid-range patterns
|
|
299
|
+
const midRangePatterns = [
|
|
300
|
+
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
301
|
+
/iPad Air/i, // iPad Air
|
|
302
|
+
/SM-A[0-9]/i, // Samsung A series
|
|
303
|
+
/Redmi Note/i, // Redmi Note
|
|
304
|
+
/Pixel [4-6]/i, // Pixel 4-6
|
|
305
|
+
/Moto G/i, // Moto G
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
309
|
+
const isMidRange = midRangePatterns.some(p => p.test(userAgent));
|
|
310
|
+
|
|
311
|
+
// Detect tablet ONLY if explicitly in user agent (not just screen size)
|
|
312
|
+
// Many desktops have large screens and would incorrectly be detected as tablets
|
|
313
|
+
const isTablet = /iPad|Android.*Tablet|Nexus 9|Nexus 10/i.test(userAgent);
|
|
314
|
+
|
|
315
|
+
// Detect if mobile (phone form factor) - must have explicit mobile indicators
|
|
316
|
+
const isMobilePhone = /Android.*Mobile|iPhone|iPod/i.test(userAgent);
|
|
317
|
+
|
|
318
|
+
// Check if it looks like a desktop/laptop (has "Windows", "Mac", "Linux" but no mobile indicators)
|
|
319
|
+
const isDesktopLike = /Windows NT|Macintosh|Linux|X11/i.test(userAgent) && !isMobilePhone && !isTablet;
|
|
320
|
+
|
|
321
|
+
// Device name for logging
|
|
322
|
+
let deviceName = 'Unknown';
|
|
323
|
+
if (isIPhone) {
|
|
324
|
+
const match = userAgent.match(/iPhone.*OS (\d+)/);
|
|
325
|
+
deviceName = match ? `iPhone OS ${match[1]}` : 'iPhone';
|
|
326
|
+
} else if (isIPad) {
|
|
327
|
+
deviceName = 'iPad';
|
|
328
|
+
} else if (isAndroid) {
|
|
329
|
+
const match = userAgent.match(/Android.*;.*(.+?)\)/);
|
|
330
|
+
deviceName = match ? match[1].trim() : 'Android';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
isMobile: isMobilePhone && !isDesktopLike,
|
|
335
|
+
isTablet: isTablet && !isDesktopLike,
|
|
336
|
+
isDesktopLike,
|
|
337
|
+
isHighEnd: isHighEnd || (isTablet && pixelRatio >= 2),
|
|
338
|
+
isMidRange: isMidRange,
|
|
339
|
+
isLowEnd: !isHighEnd && !isMidRange,
|
|
340
|
+
deviceName,
|
|
341
|
+
screenWidth,
|
|
342
|
+
screenHeight,
|
|
343
|
+
pixelRatio,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Applies quality settings based on detected hardware tier.
|
|
349
|
+
* @private
|
|
350
|
+
* @param {string} tier - Hardware tier: 'low', 'medium', or 'high'
|
|
351
|
+
*/
|
|
352
|
+
#applyQualitySettings(tier) {
|
|
353
|
+
const qualityPresets = {
|
|
354
|
+
// Mobile low-end (old phones, budget devices)
|
|
355
|
+
low: {
|
|
356
|
+
shadowMapSize: 256, // Very small for speed
|
|
357
|
+
iblShadowResolution: -1, // Disable IBL shadows
|
|
358
|
+
iblSampleDirections: 1, // Minimal sampling
|
|
359
|
+
shadowBlurKernel: 0, // No blur
|
|
360
|
+
ssaoEnabled: false,
|
|
361
|
+
antialiasingEnabled: false,
|
|
362
|
+
hardwareScaling: 2.5, // Aggressive downscaling
|
|
363
|
+
maxModelResolution: 512, // Low-res model loading
|
|
364
|
+
},
|
|
365
|
+
// Mobile high-end / Tablet / Desktop low
|
|
366
|
+
medium: {
|
|
367
|
+
shadowMapSize: 512,
|
|
368
|
+
iblShadowResolution: 0, // 1024x1024
|
|
369
|
+
iblSampleDirections: 2, // Fast sampling
|
|
370
|
+
shadowBlurKernel: 4, // Light blur
|
|
371
|
+
ssaoEnabled: false, // Disable for mobile battery
|
|
372
|
+
antialiasingEnabled: true,
|
|
373
|
+
hardwareScaling: 2.0,
|
|
374
|
+
maxModelResolution: 1024,
|
|
375
|
+
},
|
|
376
|
+
// Desktop mid-range / High-end mobile
|
|
377
|
+
high: {
|
|
378
|
+
shadowMapSize: 1024,
|
|
379
|
+
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
380
|
+
iblSampleDirections: 4, // Quality sampling
|
|
381
|
+
shadowBlurKernel: 16, // Smooth shadows
|
|
382
|
+
ssaoEnabled: true,
|
|
383
|
+
antialiasingEnabled: true,
|
|
384
|
+
hardwareScaling: 1.5,
|
|
385
|
+
maxModelResolution: 2048,
|
|
386
|
+
},
|
|
387
|
+
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
388
|
+
ultra: {
|
|
389
|
+
shadowMapSize: 2048,
|
|
390
|
+
iblShadowResolution: 1, // 2048x2048
|
|
391
|
+
iblSampleDirections: 8, // Maximum quality
|
|
392
|
+
shadowBlurKernel: 32, // Ultra smooth
|
|
393
|
+
ssaoEnabled: true,
|
|
394
|
+
antialiasingEnabled: true,
|
|
395
|
+
hardwareScaling: 1.0, // Native resolution
|
|
396
|
+
maxModelResolution: 4096,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Map tier to preset with fallback
|
|
401
|
+
const presetMap = {
|
|
402
|
+
'low': qualityPresets.low,
|
|
403
|
+
'medium': qualityPresets.medium,
|
|
404
|
+
'high': qualityPresets.high,
|
|
405
|
+
'ultra': qualityPresets.ultra,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const preset = presetMap[tier] || qualityPresets.medium;
|
|
409
|
+
|
|
410
|
+
// Apply preset to config
|
|
411
|
+
this.#config.quality = {
|
|
412
|
+
tier,
|
|
413
|
+
...preset,
|
|
414
|
+
// Legacy compatibility
|
|
415
|
+
shadowMapSize: preset.shadowMapSize,
|
|
416
|
+
iblShadowResolution: preset.iblShadowResolution,
|
|
417
|
+
iblSampleDirections: preset.iblSampleDirections,
|
|
418
|
+
shadowBlurKernel: preset.shadowBlurKernel,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
174
422
|
// Promises to track async disable() lifecycle when XR and general teardown may run concurrently; ensures idempotent disable calls are safe and callers can await full teardown completion.
|
|
175
423
|
#disablingPromises = {
|
|
176
424
|
xr: null,
|
|
@@ -463,6 +711,7 @@ export default class BabylonJSController {
|
|
|
463
711
|
* Evaluates whether the renderer should stay in continuous mode.
|
|
464
712
|
* XR always forces continuous rendering; animation/camera motion also extends the
|
|
465
713
|
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
714
|
+
* OPTIMIZATION: Added true idle mode - stops rendering completely after idleThresholdMs of inactivity.
|
|
466
715
|
* @private
|
|
467
716
|
* @param {number} now - Current high-resolution timestamp.
|
|
468
717
|
* @returns {boolean} True when continuous rendering should remain active.
|
|
@@ -470,6 +719,8 @@ export default class BabylonJSController {
|
|
|
470
719
|
#shouldRenderContinuously(now) {
|
|
471
720
|
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
472
721
|
if (inXR) {
|
|
722
|
+
this.#state.render.isIdle = false;
|
|
723
|
+
this.#state.render.lastActivityAt = now;
|
|
473
724
|
return true;
|
|
474
725
|
}
|
|
475
726
|
|
|
@@ -478,18 +729,36 @@ export default class BabylonJSController {
|
|
|
478
729
|
|
|
479
730
|
if (animationRunning) {
|
|
480
731
|
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
732
|
+
this.#state.render.isIdle = false;
|
|
733
|
+
this.#state.render.lastActivityAt = now;
|
|
481
734
|
}
|
|
482
735
|
if (cameraInMotion) {
|
|
483
736
|
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
737
|
+
this.#state.render.isIdle = false;
|
|
738
|
+
this.#state.render.lastActivityAt = now;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// OPTIMIZATION: True idle mode - if no activity for idleThresholdMs, stop rendering completely
|
|
742
|
+
const timeSinceActivity = now - this.#state.render.lastActivityAt;
|
|
743
|
+
if (timeSinceActivity > this.#config.render.idleThresholdMs) {
|
|
744
|
+
this.#state.render.isIdle = true;
|
|
484
745
|
}
|
|
485
746
|
|
|
486
|
-
|
|
747
|
+
const shouldRender = animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
748
|
+
|
|
749
|
+
// Reset idle flag if we need to render
|
|
750
|
+
if (shouldRender) {
|
|
751
|
+
this.#state.render.isIdle = false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return shouldRender;
|
|
487
755
|
}
|
|
488
756
|
|
|
489
757
|
/**
|
|
490
758
|
* Render loop callback for Babylon.js.
|
|
491
759
|
* Runs only while scene state is dirty, interactive motion is active, animations are running, or XR is active.
|
|
492
|
-
* It self-stops when the scene becomes idle.
|
|
760
|
+
* It self-stops when the scene becomes idle (true idle mode - no renders at all).
|
|
761
|
+
* OPTIMIZATION: Added true idle mode that completely stops rendering after idleThresholdMs.
|
|
493
762
|
* @private
|
|
494
763
|
* @returns {void}
|
|
495
764
|
*/
|
|
@@ -503,6 +772,12 @@ export default class BabylonJSController {
|
|
|
503
772
|
const continuous = this.#shouldRenderContinuously(now);
|
|
504
773
|
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
505
774
|
|
|
775
|
+
// OPTIMIZATION: True idle mode - stop rendering completely when idle
|
|
776
|
+
if (this.#state.render.isIdle && !needsRender) {
|
|
777
|
+
this.#stopEngineRenderLoop();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
506
781
|
if (!needsRender) {
|
|
507
782
|
this.#stopEngineRenderLoop();
|
|
508
783
|
return;
|
|
@@ -1035,9 +1310,10 @@ export default class BabylonJSController {
|
|
|
1035
1310
|
|
|
1036
1311
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
1037
1312
|
|
|
1313
|
+
// Use hardware-adaptive quality settings
|
|
1038
1314
|
const pipelineOptions = {
|
|
1039
|
-
resolutionExp:
|
|
1040
|
-
sampleDirections:
|
|
1315
|
+
resolutionExp: this.#config.quality.iblShadowResolution, // Adaptive: 0=1024, 1=2048, 2=4096
|
|
1316
|
+
sampleDirections: this.#config.quality.iblSampleDirections, // Adaptive: 2=fast, 4=quality
|
|
1041
1317
|
ssShadowsEnabled: true,
|
|
1042
1318
|
shadowRemanence: 0.85,
|
|
1043
1319
|
triPlanarVoxelization: true,
|
|
@@ -1108,6 +1384,7 @@ export default class BabylonJSController {
|
|
|
1108
1384
|
|
|
1109
1385
|
/**
|
|
1110
1386
|
* Configures soft shadows for the built-in directional light used when no HDR environment is present.
|
|
1387
|
+
* Uses hardware-adaptive quality settings.
|
|
1111
1388
|
* @private
|
|
1112
1389
|
* @returns {void}
|
|
1113
1390
|
*/
|
|
@@ -1116,9 +1393,12 @@ export default class BabylonJSController {
|
|
|
1116
1393
|
return;
|
|
1117
1394
|
}
|
|
1118
1395
|
this.#dirLight.autoUpdateExtends = false;
|
|
1119
|
-
|
|
1396
|
+
// Use quality-adaptive shadow map size
|
|
1397
|
+
const shadowMapSize = this.#config.quality.shadowMapSize;
|
|
1398
|
+
const shadowGenerator = new ShadowGenerator(shadowMapSize, this.#dirLight);
|
|
1120
1399
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
1121
|
-
|
|
1400
|
+
// Use quality-adaptive blur kernel
|
|
1401
|
+
shadowGenerator.blurKernel = this.#config.quality.shadowBlurKernel;
|
|
1122
1402
|
shadowGenerator.darkness = 0.5;
|
|
1123
1403
|
shadowGenerator.bias = 0.0005;
|
|
1124
1404
|
shadowGenerator.normalBias = 0.02;
|
|
@@ -1224,6 +1504,7 @@ export default class BabylonJSController {
|
|
|
1224
1504
|
* @returns {void}
|
|
1225
1505
|
*/
|
|
1226
1506
|
#setMaxSimultaneousLights() {
|
|
1507
|
+
if (!this.#scene) return;
|
|
1227
1508
|
let lightsNumber = 1; // At least one light coming from the environment texture contribution
|
|
1228
1509
|
this.#scene.lights.forEach((light) => {
|
|
1229
1510
|
if (light.isEnabled()) {
|
|
@@ -1498,6 +1779,7 @@ export default class BabylonJSController {
|
|
|
1498
1779
|
|
|
1499
1780
|
/**
|
|
1500
1781
|
* Handles mouse wheel events on the Babylon.js canvas for zooming the camera.
|
|
1782
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
1501
1783
|
* @private
|
|
1502
1784
|
* @param {WheelEvent} event - The mouse wheel event.
|
|
1503
1785
|
* @param {Object} pickInfo - The result of the scene pick operation (not used in this method).
|
|
@@ -1505,6 +1787,12 @@ export default class BabylonJSController {
|
|
|
1505
1787
|
*/
|
|
1506
1788
|
#onMouseWheel(event, pickInfo) {
|
|
1507
1789
|
event.preventDefault();
|
|
1790
|
+
|
|
1791
|
+
// OPTIMIZATION: Mark activity for idle mode
|
|
1792
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1793
|
+
this.#state.render.lastActivityAt = now;
|
|
1794
|
+
this.#state.render.isIdle = false;
|
|
1795
|
+
|
|
1508
1796
|
const camera = this.#scene?.activeCamera;
|
|
1509
1797
|
if (!camera) {
|
|
1510
1798
|
return false;
|
|
@@ -1526,14 +1814,20 @@ export default class BabylonJSController {
|
|
|
1526
1814
|
}
|
|
1527
1815
|
}
|
|
1528
1816
|
|
|
1529
|
-
|
|
1817
|
+
/**
|
|
1530
1818
|
* Handles pointer up events on the Babylon.js scene.
|
|
1819
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
1531
1820
|
* @private
|
|
1532
1821
|
* @param {PointerEvent} event - The pointer up event.
|
|
1533
1822
|
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
1534
1823
|
* @returns {void}
|
|
1535
1824
|
*/
|
|
1536
1825
|
#onPointerUp(event, pickInfo) {
|
|
1826
|
+
// OPTIMIZATION: Mark activity for idle mode
|
|
1827
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1828
|
+
this.#state.render.lastActivityAt = now;
|
|
1829
|
+
this.#state.render.isIdle = false;
|
|
1830
|
+
|
|
1537
1831
|
if (this.#babylonJSAnimationController) {
|
|
1538
1832
|
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
1539
1833
|
this.#babylonJSAnimationController.hideMenu();
|
|
@@ -1547,12 +1841,18 @@ export default class BabylonJSController {
|
|
|
1547
1841
|
|
|
1548
1842
|
/**
|
|
1549
1843
|
* Handles pointer move events on the Babylon.js scene.
|
|
1844
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
1550
1845
|
* @private
|
|
1551
1846
|
* @param {PointerEvent} event - The pointer move event.
|
|
1552
1847
|
* @param {PickInfo|null} pickInfo - The sampled result of the scene pick operation (may be null when sampling skips a raycast).
|
|
1553
1848
|
* @returns {void}
|
|
1554
1849
|
*/
|
|
1555
1850
|
#onPointerMove(event, pickInfo) {
|
|
1851
|
+
// OPTIMIZATION: Mark activity for idle mode
|
|
1852
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1853
|
+
this.#state.render.lastActivityAt = now;
|
|
1854
|
+
this.#state.render.isIdle = false;
|
|
1855
|
+
|
|
1556
1856
|
const camera = this.#scene?.activeCamera;
|
|
1557
1857
|
if (camera && !camera.metadata?.locked) {
|
|
1558
1858
|
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
@@ -1917,6 +2217,12 @@ export default class BabylonJSController {
|
|
|
1917
2217
|
container.assetContainer.dispose();
|
|
1918
2218
|
container.assetContainer = null;
|
|
1919
2219
|
}
|
|
2220
|
+
if (!this.#scene) {
|
|
2221
|
+
// Scene was disposed (disconnect) while async loading was in progress; discard the
|
|
2222
|
+
// newly loaded container so we don't leak GPU resources and avoid the null-deref below.
|
|
2223
|
+
newAssetContainer?.dispose();
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
1920
2226
|
this.#scene.getEngine().releaseEffects();
|
|
1921
2227
|
this.#scene.getEngine().releaseComputeEffects();
|
|
1922
2228
|
|
|
@@ -2004,6 +2310,7 @@ export default class BabylonJSController {
|
|
|
2004
2310
|
* @returns {Promise<void>}
|
|
2005
2311
|
*/
|
|
2006
2312
|
async #startRender() {
|
|
2313
|
+
if (!this.#scene) return;
|
|
2007
2314
|
await this.#loadCameraDependentEffects();
|
|
2008
2315
|
await this.#scene.whenReadyAsync();
|
|
2009
2316
|
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
@@ -2105,6 +2412,13 @@ export default class BabylonJSController {
|
|
|
2105
2412
|
|
|
2106
2413
|
await Promise.allSettled(promiseArray)
|
|
2107
2414
|
.then(async (values) => {
|
|
2415
|
+
// Scene may have been disposed (disconnectedCallback) while async loading was in
|
|
2416
|
+
// progress. Abort cleanly: #replaceContainer already guards the GPU calls, but
|
|
2417
|
+
// we skip the post-load option/visibility calls too to avoid further null-derefs.
|
|
2418
|
+
if (!this.#scene) {
|
|
2419
|
+
values.forEach((result) => { result.value?.[1]?.dispose(); });
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2108
2422
|
this.#disposeAnimationController();
|
|
2109
2423
|
values.forEach((result) => {
|
|
2110
2424
|
const container = result.value ? result.value[0] : null;
|
|
@@ -2405,6 +2719,20 @@ export default class BabylonJSController {
|
|
|
2405
2719
|
this.#configureDracoCompression();
|
|
2406
2720
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
2407
2721
|
this.#engine.disableUniformBuffers = true;
|
|
2722
|
+
|
|
2723
|
+
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
2724
|
+
const detectedTier = this.#detectHardwareTier();
|
|
2725
|
+
this.#applyQualitySettings(detectedTier);
|
|
2726
|
+
|
|
2727
|
+
// OPTIMIZATION: Apply hardware scaling from quality preset
|
|
2728
|
+
const pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
|
|
2729
|
+
const baseScaling = this.#config.quality.hardwareScaling || 1;
|
|
2730
|
+
const scalingLevel = pixelRatio > 1 ? baseScaling * pixelRatio : baseScaling;
|
|
2731
|
+
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
2732
|
+
|
|
2733
|
+
// OPTIMIZATION: Initialize idle tracking
|
|
2734
|
+
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2735
|
+
|
|
2408
2736
|
this.#scene = new Scene(this.#engine);
|
|
2409
2737
|
|
|
2410
2738
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|