@preference-sl/pref-viewer 2.14.0-beta.5 → 2.14.0-beta.7
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 +388 -216
- package/src/localization/translations.js +18 -0
- package/src/pref-viewer-3d.js +61 -1
- package/src/pref-viewer-menu-3d.js +206 -3
- package/src/pref-viewer.js +5 -1
- package/src/styles.js +117 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, Material, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, 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";
|
|
@@ -11,6 +11,74 @@ 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
13
|
|
|
14
|
+
export function getAdaptiveHardwareScalingLevel(baseScaling = 1, pixelRatio = 1) {
|
|
15
|
+
const safeBaseScaling = Number.isFinite(baseScaling) && baseScaling > 0 ? baseScaling : 1;
|
|
16
|
+
const safePixelRatio = Number.isFinite(pixelRatio) && pixelRatio > 0 ? pixelRatio : 1;
|
|
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
|
+
export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
55
|
+
const normalizedTime = clamp01(timeOfDay);
|
|
56
|
+
const timeHours = normalizedTime * 24;
|
|
57
|
+
const sunriseHour = 6.25;
|
|
58
|
+
const sunsetHour = 20.25;
|
|
59
|
+
const daylightProgress = clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
|
|
60
|
+
const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(bellCurve(daylightProgress), 0.88) : 0;
|
|
61
|
+
const nightFactor = 1 - dayFactor;
|
|
62
|
+
const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
|
|
63
|
+
const solarAzimuth = lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
|
|
64
|
+
const solarElevation = lerp(-0.18, 1.18, solarArc);
|
|
65
|
+
const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
|
|
66
|
+
const sunDirection = normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
|
|
67
|
+
const moonDirection = normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
timeOfDay: normalizedTime,
|
|
71
|
+
timeHours,
|
|
72
|
+
sunPhase,
|
|
73
|
+
solarAzimuth,
|
|
74
|
+
solarElevation,
|
|
75
|
+
dayFactor,
|
|
76
|
+
nightFactor,
|
|
77
|
+
sunDirection,
|
|
78
|
+
moonDirection,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
14
82
|
/**
|
|
15
83
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
16
84
|
* rebuilds camera-dependent pipelines once textures are ready, brokers XR/download interactions, and persists
|
|
@@ -85,6 +153,7 @@ export default class BabylonJSController {
|
|
|
85
153
|
ambientOcclusionEnabled: true,
|
|
86
154
|
iblEnabled: true,
|
|
87
155
|
shadowsEnabled: false,
|
|
156
|
+
lightingTimeOfDay: 0.5,
|
|
88
157
|
// Highlight settings
|
|
89
158
|
highlightEnabled: true,
|
|
90
159
|
highlightColor: "#ff6700",
|
|
@@ -103,6 +172,7 @@ export default class BabylonJSController {
|
|
|
103
172
|
#camera = null;
|
|
104
173
|
#hemiLight = null;
|
|
105
174
|
#dirLight = null;
|
|
175
|
+
#moonLight = null;
|
|
106
176
|
#cameraLight = null;
|
|
107
177
|
#shadowGen = [];
|
|
108
178
|
#XRExperience = null;
|
|
@@ -114,7 +184,6 @@ export default class BabylonJSController {
|
|
|
114
184
|
#options = {};
|
|
115
185
|
|
|
116
186
|
#gltfResolver = null; // GLTFResolver instance
|
|
117
|
-
#loadGeneration = 0; // incremented per #loadContainers call to discard stale deferred results
|
|
118
187
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
119
188
|
|
|
120
189
|
#renderPipelines = {
|
|
@@ -187,6 +256,18 @@ export default class BabylonJSController {
|
|
|
187
256
|
shadowBlurKernel: 16, // Shadow blur amount
|
|
188
257
|
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
189
258
|
},
|
|
259
|
+
look: {
|
|
260
|
+
showroom: {
|
|
261
|
+
exposure: 0.92,
|
|
262
|
+
contrast: 1.02,
|
|
263
|
+
toneMappingEnabled: true,
|
|
264
|
+
toneMappingType: ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0,
|
|
265
|
+
environmentIntensity: 1.08,
|
|
266
|
+
hemiLightIntensity: 0.52,
|
|
267
|
+
dirLightIntensity: 0.56,
|
|
268
|
+
cameraLightIntensity: 0.16,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
190
271
|
};
|
|
191
272
|
|
|
192
273
|
/**
|
|
@@ -201,6 +282,7 @@ export default class BabylonJSController {
|
|
|
201
282
|
|
|
202
283
|
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
203
284
|
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
285
|
+
const isAngleRenderer = /^ANGLE\s*\(/i.test(rendererName);
|
|
204
286
|
|
|
205
287
|
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
206
288
|
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
@@ -211,6 +293,10 @@ export default class BabylonJSController {
|
|
|
211
293
|
|
|
212
294
|
// If we detect a desktop GPU, go directly to desktop detection
|
|
213
295
|
if (isDesktopGPU && !isMobileGPU) {
|
|
296
|
+
if (isAngleRenderer) {
|
|
297
|
+
console.log(`[PrefViewer] ANGLE-backed renderer detected: ${rendererName}`);
|
|
298
|
+
return this.#detectDesktopTier(gl, rendererName, true);
|
|
299
|
+
}
|
|
214
300
|
console.log(`[PrefViewer] Desktop GPU detected: ${rendererName}`);
|
|
215
301
|
return this.#detectDesktopTier(gl, rendererName);
|
|
216
302
|
}
|
|
@@ -247,7 +333,7 @@ export default class BabylonJSController {
|
|
|
247
333
|
* @param {string} rendererName - GPU renderer name
|
|
248
334
|
* @returns {string} Hardware tier
|
|
249
335
|
*/
|
|
250
|
-
#detectDesktopTier(gl, rendererName) {
|
|
336
|
+
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
251
337
|
// Check WebGL capabilities
|
|
252
338
|
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
253
339
|
|
|
@@ -257,12 +343,14 @@ export default class BabylonJSController {
|
|
|
257
343
|
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
258
344
|
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
259
345
|
|
|
260
|
-
let tier = 'high'; // Default to
|
|
346
|
+
let tier = conservative ? 'medium' : 'high'; // Default to conservative desktop when ANGLE is in the path
|
|
261
347
|
|
|
262
348
|
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
263
349
|
tier = 'medium';
|
|
264
|
-
} else if (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384) {
|
|
350
|
+
} else if (!conservative && (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384)) {
|
|
265
351
|
tier = 'ultra';
|
|
352
|
+
} else if (conservative && maxTextureSize >= 16384) {
|
|
353
|
+
tier = 'high';
|
|
266
354
|
}
|
|
267
355
|
|
|
268
356
|
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
@@ -365,6 +453,7 @@ export default class BabylonJSController {
|
|
|
365
453
|
antialiasingEnabled: false,
|
|
366
454
|
hardwareScaling: 2.5, // Aggressive downscaling
|
|
367
455
|
maxModelResolution: 512, // Low-res model loading
|
|
456
|
+
environmentTextureSize: 512,
|
|
368
457
|
},
|
|
369
458
|
// Mobile high-end / Tablet / Desktop low
|
|
370
459
|
medium: {
|
|
@@ -376,28 +465,31 @@ export default class BabylonJSController {
|
|
|
376
465
|
antialiasingEnabled: true,
|
|
377
466
|
hardwareScaling: 2.0,
|
|
378
467
|
maxModelResolution: 1024,
|
|
468
|
+
environmentTextureSize: 1024,
|
|
379
469
|
},
|
|
380
470
|
// Desktop mid-range / High-end mobile
|
|
381
471
|
high: {
|
|
382
472
|
shadowMapSize: 1024,
|
|
383
473
|
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
384
474
|
iblSampleDirections: 4, // Quality sampling
|
|
385
|
-
shadowBlurKernel:
|
|
386
|
-
ssaoEnabled:
|
|
475
|
+
shadowBlurKernel: 8, // Sharper shadows for product clarity
|
|
476
|
+
ssaoEnabled: false,
|
|
387
477
|
antialiasingEnabled: true,
|
|
388
|
-
hardwareScaling: 1.
|
|
478
|
+
hardwareScaling: 1.0,
|
|
389
479
|
maxModelResolution: 2048,
|
|
480
|
+
environmentTextureSize: 2048,
|
|
390
481
|
},
|
|
391
482
|
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
392
483
|
ultra: {
|
|
393
484
|
shadowMapSize: 2048,
|
|
394
485
|
iblShadowResolution: 1, // 2048x2048
|
|
395
486
|
iblSampleDirections: 8, // Maximum quality
|
|
396
|
-
shadowBlurKernel:
|
|
397
|
-
ssaoEnabled:
|
|
487
|
+
shadowBlurKernel: 16, // Preserve clarity over softness
|
|
488
|
+
ssaoEnabled: false,
|
|
398
489
|
antialiasingEnabled: true,
|
|
399
490
|
hardwareScaling: 1.0, // Native resolution
|
|
400
491
|
maxModelResolution: 4096,
|
|
492
|
+
environmentTextureSize: 4096,
|
|
401
493
|
},
|
|
402
494
|
};
|
|
403
495
|
|
|
@@ -510,6 +602,11 @@ export default class BabylonJSController {
|
|
|
510
602
|
this.#settings[key] = settings[key];
|
|
511
603
|
changed = true;
|
|
512
604
|
}
|
|
605
|
+
// Handle numeric settings such as lightingTimeOfDay
|
|
606
|
+
if (typeof settings[key] === "number" && Number.isFinite(settings[key]) && this.#settings[key] !== settings[key]) {
|
|
607
|
+
this.#settings[key] = settings[key];
|
|
608
|
+
changed = true;
|
|
609
|
+
}
|
|
513
610
|
});
|
|
514
611
|
|
|
515
612
|
if (changed) {
|
|
@@ -544,6 +641,10 @@ export default class BabylonJSController {
|
|
|
544
641
|
if (typeof parsed?.[key] === "string") {
|
|
545
642
|
this.#settings[key] = parsed[key];
|
|
546
643
|
}
|
|
644
|
+
// Handle numeric settings such as lightingTimeOfDay
|
|
645
|
+
if (typeof parsed?.[key] === "number" && Number.isFinite(parsed[key])) {
|
|
646
|
+
this.#settings[key] = parsed[key];
|
|
647
|
+
}
|
|
547
648
|
});
|
|
548
649
|
} catch (error) {
|
|
549
650
|
console.warn("PrefViewer: unable to load render settings", error);
|
|
@@ -896,6 +997,7 @@ export default class BabylonJSController {
|
|
|
896
997
|
this.#camera.lowerRadiusLimit = 5;
|
|
897
998
|
this.#camera.upperRadiusLimit = 20;
|
|
898
999
|
this.#camera.metadata = { locked: false };
|
|
1000
|
+
this.#applyShowroomCameraFraming(this.#camera);
|
|
899
1001
|
this.#camera.attachControl(this.#canvas, true);
|
|
900
1002
|
this.#scene.activeCamera = this.#camera;
|
|
901
1003
|
}
|
|
@@ -917,25 +1019,11 @@ export default class BabylonJSController {
|
|
|
917
1019
|
const cameraLightName = "PrefViewerCameraLight";
|
|
918
1020
|
const dirLightName = "PrefViewerDirLight";
|
|
919
1021
|
|
|
920
|
-
const hemiLight = this.#scene.getLightByName(hemiLightName);
|
|
921
|
-
const cameraLight = this.#scene.getLightByName(cameraLightName);
|
|
922
|
-
const dirLight = this.#scene.getLightByName(dirLightName);
|
|
923
|
-
|
|
924
1022
|
let lightsChanged = false;
|
|
925
1023
|
|
|
926
1024
|
const iblEnabled = this.#settings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
927
1025
|
|
|
928
1026
|
if (iblEnabled) {
|
|
929
|
-
if (hemiLight) {
|
|
930
|
-
hemiLight.dispose();
|
|
931
|
-
}
|
|
932
|
-
if (cameraLight) {
|
|
933
|
-
cameraLight.dispose();
|
|
934
|
-
}
|
|
935
|
-
if (dirLight) {
|
|
936
|
-
dirLight.dispose();
|
|
937
|
-
}
|
|
938
|
-
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
939
1027
|
lightsChanged = await this.#initializeEnvironmentTexture();
|
|
940
1028
|
} else {
|
|
941
1029
|
// If IBL is disabled but an environment texture exists, dispose it to save resources and ensure it doesn't affect the lighting
|
|
@@ -944,27 +1032,22 @@ export default class BabylonJSController {
|
|
|
944
1032
|
this.#scene.environmentTexture = null;
|
|
945
1033
|
lightsChanged = true;
|
|
946
1034
|
}
|
|
947
|
-
|
|
948
|
-
// Add a hemispheric light for basic ambient illumination
|
|
949
|
-
if (!this.#hemiLight) {
|
|
950
|
-
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
951
|
-
this.#hemiLight.intensity = 0.6;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
955
|
-
if (!this.#dirLight) {
|
|
956
|
-
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
957
|
-
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
958
|
-
this.#dirLight.intensity = 0.6;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
962
|
-
if (!this.#cameraLight) {
|
|
963
|
-
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
964
|
-
this.#cameraLight.parent = this.#camera;
|
|
965
|
-
this.#cameraLight.intensity = 0.3;
|
|
966
|
-
}
|
|
967
1035
|
}
|
|
1036
|
+
if (!this.#hemiLight) {
|
|
1037
|
+
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
1038
|
+
}
|
|
1039
|
+
if (!this.#dirLight) {
|
|
1040
|
+
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
1041
|
+
this.#dirLight.position = new Vector3(5, 4, 5);
|
|
1042
|
+
}
|
|
1043
|
+
if (!this.#moonLight) {
|
|
1044
|
+
this.#moonLight = new DirectionalLight("PrefViewerMoonLight", new Vector3(10, -10, 8), this.#scene);
|
|
1045
|
+
}
|
|
1046
|
+
if (!this.#cameraLight) {
|
|
1047
|
+
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
1048
|
+
this.#cameraLight.parent = this.#camera;
|
|
1049
|
+
}
|
|
1050
|
+
lightsChanged = this.#applyLightingConfig() || lightsChanged;
|
|
968
1051
|
return lightsChanged;
|
|
969
1052
|
}
|
|
970
1053
|
|
|
@@ -1141,34 +1224,13 @@ export default class BabylonJSController {
|
|
|
1141
1224
|
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
1142
1225
|
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
1143
1226
|
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
1144
|
-
// FXAA
|
|
1145
|
-
defaultPipeline.fxaaEnabled =
|
|
1146
|
-
defaultPipeline.fxaa.samples = 8;
|
|
1147
|
-
defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
|
|
1148
|
-
if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
|
|
1149
|
-
defaultPipeline.fxaa.edgeThreshold = 0.125;
|
|
1150
|
-
}
|
|
1151
|
-
if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
|
|
1152
|
-
defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
|
|
1153
|
-
}
|
|
1154
|
-
if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
|
|
1155
|
-
defaultPipeline.fxaa.subPixelQuality = 0.75;
|
|
1156
|
-
}
|
|
1227
|
+
// FXAA can soften product surfaces too much; keep MSAA only for crisper materials.
|
|
1228
|
+
defaultPipeline.fxaaEnabled = false;
|
|
1157
1229
|
|
|
1158
|
-
// Grain
|
|
1159
|
-
defaultPipeline.grainEnabled =
|
|
1160
|
-
defaultPipeline.grain.adaptScaleToCurrentViewport = true;
|
|
1161
|
-
defaultPipeline.grain.animated = false;
|
|
1162
|
-
defaultPipeline.grain.intensity = 3;
|
|
1230
|
+
// Grain is disabled to keep product surfaces clean and avoid visible noise.
|
|
1231
|
+
defaultPipeline.grainEnabled = false;
|
|
1163
1232
|
|
|
1164
1233
|
// Configure post-processes to calculate only once instead of every frame for better performance
|
|
1165
|
-
if (defaultPipeline.fxaa?._postProcess) {
|
|
1166
|
-
defaultPipeline.fxaa._postProcess.autoClear = false;
|
|
1167
|
-
}
|
|
1168
|
-
if (defaultPipeline.grain?._postProcess) {
|
|
1169
|
-
defaultPipeline.grain._postProcess.autoClear = false;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
1234
|
this.#renderPipelines.default = defaultPipeline;
|
|
1173
1235
|
pipelineManager.update();
|
|
1174
1236
|
return true;
|
|
@@ -1202,7 +1264,8 @@ export default class BabylonJSController {
|
|
|
1202
1264
|
let hdrTexture = null;
|
|
1203
1265
|
if (this.#options.ibl?.cachedUrl) {
|
|
1204
1266
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1205
|
-
|
|
1267
|
+
const envTextureSize = this.#config.quality.environmentTextureSize || 1024;
|
|
1268
|
+
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
|
|
1206
1269
|
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
1207
1270
|
hdrTexture = this.#hdrTexture.clone();
|
|
1208
1271
|
} else {
|
|
@@ -1219,8 +1282,10 @@ export default class BabylonJSController {
|
|
|
1219
1282
|
this.#options.ibl?.consumeCachedUrl?.(true);
|
|
1220
1283
|
}
|
|
1221
1284
|
|
|
1222
|
-
hdrTexture.level = this.#options.ibl.intensity;
|
|
1285
|
+
hdrTexture.level = this.#options.ibl.intensity ?? 1;
|
|
1223
1286
|
this.#scene.environmentTexture = hdrTexture;
|
|
1287
|
+
this.#applyShowroomLook();
|
|
1288
|
+
this.#applyLightingConfig();
|
|
1224
1289
|
this.#scene.markAllMaterialsAsDirty(Material.TextureDirtyFlag);
|
|
1225
1290
|
return true;
|
|
1226
1291
|
}
|
|
@@ -1306,7 +1371,7 @@ export default class BabylonJSController {
|
|
|
1306
1371
|
});
|
|
1307
1372
|
const materialsForReceivingShadows = this.#scene.materials.filter((material) => {
|
|
1308
1373
|
if (material instanceof PBRMaterial) {
|
|
1309
|
-
material.enableSpecularAntiAliasing =
|
|
1374
|
+
material.enableSpecularAntiAliasing = true;
|
|
1310
1375
|
}
|
|
1311
1376
|
return true;
|
|
1312
1377
|
});
|
|
@@ -1531,6 +1596,184 @@ export default class BabylonJSController {
|
|
|
1531
1596
|
}
|
|
1532
1597
|
}
|
|
1533
1598
|
|
|
1599
|
+
/**
|
|
1600
|
+
* Improves texture sharpness by enabling trilinear filtering and anisotropy.
|
|
1601
|
+
* @private
|
|
1602
|
+
* @returns {void}
|
|
1603
|
+
*/
|
|
1604
|
+
#enhanceTextureQuality() {
|
|
1605
|
+
if (!this.#scene) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
1610
|
+
const maxAnisotropy = Math.max(1, Math.min(16, caps.maxAnisotropy || 1));
|
|
1611
|
+
|
|
1612
|
+
this.#scene.textures?.forEach((texture) => {
|
|
1613
|
+
if (!texture || texture.isDisposed?.()) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if ("anisotropicFilteringLevel" in texture) {
|
|
1618
|
+
texture.anisotropicFilteringLevel = maxAnisotropy;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (typeof texture.updateSamplingMode === "function") {
|
|
1622
|
+
try {
|
|
1623
|
+
texture.updateSamplingMode(Texture.TRILINEAR_SAMPLINGMODE);
|
|
1624
|
+
} catch {
|
|
1625
|
+
// Ignore texture subclasses that do not support sampling changes.
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1633
|
+
* @private
|
|
1634
|
+
* @returns {void}
|
|
1635
|
+
*/
|
|
1636
|
+
#applyShowroomLook() {
|
|
1637
|
+
if (!this.#scene?.imageProcessingConfiguration) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const showroom = this.#config.look?.showroom || {};
|
|
1642
|
+
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1643
|
+
|
|
1644
|
+
this.#scene.clearColor = new Color4(0.98, 0.98, 0.98, 1).toLinearSpace();
|
|
1645
|
+
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1646
|
+
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1647
|
+
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1648
|
+
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1649
|
+
imageProcessing.vignetteEnabled = false;
|
|
1650
|
+
imageProcessing.colorCurvesEnabled = false;
|
|
1651
|
+
|
|
1652
|
+
if ("environmentIntensity" in this.#scene) {
|
|
1653
|
+
this.#scene.environmentIntensity = showroom.environmentIntensity ?? 1.15;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Applies the current time-of-day cycle to the scene lighting, environment intensity, and tonemapping bias.
|
|
1659
|
+
* @private
|
|
1660
|
+
* @returns {boolean} True when a scene existed and was updated.
|
|
1661
|
+
*/
|
|
1662
|
+
#applyLightingConfig() {
|
|
1663
|
+
if (!this.#scene) {
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const showroom = this.#config.look?.showroom || {};
|
|
1668
|
+
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ?? 0.5);
|
|
1669
|
+
const profile = getLightingCycleProfile(timeOfDay);
|
|
1670
|
+
const daylight = profile.dayFactor;
|
|
1671
|
+
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
1672
|
+
const twilight = 1 - peakLight;
|
|
1673
|
+
|
|
1674
|
+
if (this.#scene.imageProcessingConfiguration) {
|
|
1675
|
+
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1676
|
+
imageProcessing.exposure = lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1677
|
+
imageProcessing.contrast = lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1678
|
+
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1679
|
+
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1680
|
+
imageProcessing.vignetteEnabled = false;
|
|
1681
|
+
imageProcessing.colorCurvesEnabled = false;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
this.#scene.clearColor = new Color4(
|
|
1685
|
+
lerp(0.035, 0.98, daylight),
|
|
1686
|
+
lerp(0.045, 0.98, daylight),
|
|
1687
|
+
lerp(0.085, 0.98, daylight),
|
|
1688
|
+
1,
|
|
1689
|
+
).toLinearSpace();
|
|
1690
|
+
|
|
1691
|
+
if ("environmentIntensity" in this.#scene) {
|
|
1692
|
+
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (this.#hemiLight) {
|
|
1696
|
+
this.#hemiLight.direction = new Vector3(
|
|
1697
|
+
lerp(-0.45, -0.8, peakLight),
|
|
1698
|
+
lerp(0.45, 0.95, peakLight),
|
|
1699
|
+
lerp(-0.75, -0.35, peakLight),
|
|
1700
|
+
);
|
|
1701
|
+
this.#hemiLight.intensity = lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1702
|
+
this.#hemiLight.diffuse = new Color3(
|
|
1703
|
+
lerp(0.42, 0.92, peakLight),
|
|
1704
|
+
lerp(0.48, 0.96, peakLight),
|
|
1705
|
+
lerp(0.62, 0.98, peakLight),
|
|
1706
|
+
);
|
|
1707
|
+
this.#hemiLight.groundColor = new Color3(
|
|
1708
|
+
lerp(0.08, 0.3, twilight),
|
|
1709
|
+
lerp(0.08, 0.28, twilight),
|
|
1710
|
+
lerp(0.1, 0.22, twilight),
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (this.#dirLight) {
|
|
1715
|
+
this.#dirLight.direction = new Vector3(profile.sunDirection.x, profile.sunDirection.y, profile.sunDirection.z);
|
|
1716
|
+
this.#dirLight.intensity = lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1717
|
+
this.#dirLight.diffuse = new Color3(
|
|
1718
|
+
lerp(0.28, 1.0, peakLight),
|
|
1719
|
+
lerp(0.3, 0.97, peakLight),
|
|
1720
|
+
lerp(0.34, 0.9, peakLight),
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (this.#moonLight) {
|
|
1725
|
+
this.#moonLight.direction = new Vector3(profile.moonDirection.x, profile.moonDirection.y, profile.moonDirection.z);
|
|
1726
|
+
this.#moonLight.intensity = lerp(0.44, 0.03, peakLight);
|
|
1727
|
+
this.#moonLight.diffuse = new Color3(0.58, 0.7, 1.0);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (this.#cameraLight) {
|
|
1731
|
+
this.#cameraLight.intensity = lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1732
|
+
this.#cameraLight.diffuse = new Color3(
|
|
1733
|
+
lerp(0.35, 1.0, peakLight),
|
|
1734
|
+
lerp(0.35, 0.97, peakLight),
|
|
1735
|
+
lerp(0.36, 0.92, peakLight),
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
this.#scene.markAllMaterialsAsDirty?.(Material.LightDirtyFlag ?? Material.TextureDirtyFlag);
|
|
1740
|
+
this.#requestRender({ frames: 2, continuousMs: this.#config.render.interactionMs });
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Recomputes the effective hardware scaling from the current device pixel ratio.
|
|
1746
|
+
* This keeps the 3D output crisp when the viewport changes or the browser zoom/DPR shifts.
|
|
1747
|
+
* @private
|
|
1748
|
+
* @returns {void}
|
|
1749
|
+
*/
|
|
1750
|
+
#applyViewportHardwareScaling() {
|
|
1751
|
+
if (!this.#engine) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1756
|
+
const baseScaling = this.#config.quality.hardwareScaling || 1;
|
|
1757
|
+
const scalingLevel = getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1758
|
+
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Keeps the showroom camera framing stable when the viewport aspect ratio changes.
|
|
1763
|
+
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
1764
|
+
* field of view on ArcRotate cameras to avoid the background appearing undersized on wide layouts.
|
|
1765
|
+
* @private
|
|
1766
|
+
* @param {Camera|null|undefined} camera - Active camera to adjust.
|
|
1767
|
+
* @returns {void}
|
|
1768
|
+
*/
|
|
1769
|
+
#applyShowroomCameraFraming(camera = this.#scene?.activeCamera) {
|
|
1770
|
+
if (!(camera instanceof ArcRotateCamera)) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
camera.fovMode = Camera?.FOVMODE_HORIZONTAL_FIXED ?? 1;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1534
1777
|
/**
|
|
1535
1778
|
* Resets pointer-picking sampling state.
|
|
1536
1779
|
* @private
|
|
@@ -1697,7 +1940,7 @@ export default class BabylonJSController {
|
|
|
1697
1940
|
}
|
|
1698
1941
|
this.#engine.dispose();
|
|
1699
1942
|
this.#engine = this.#scene = this.#camera = null;
|
|
1700
|
-
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
1943
|
+
this.#hemiLight = this.#dirLight = this.#moonLight = this.#cameraLight = null;
|
|
1701
1944
|
}
|
|
1702
1945
|
|
|
1703
1946
|
/**
|
|
@@ -1932,8 +2175,11 @@ export default class BabylonJSController {
|
|
|
1932
2175
|
}
|
|
1933
2176
|
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1934
2177
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2178
|
+
this.#applyShowroomCameraFraming();
|
|
1935
2179
|
this.#engine.resize();
|
|
1936
|
-
this.#
|
|
2180
|
+
const needsEnhancedBurst = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled || this.#settings.shadowsEnabled;
|
|
2181
|
+
const frames = needsEnhancedBurst ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2182
|
+
this.#requestRender({ frames, continuousMs: this.#config.render.interactionMs });
|
|
1937
2183
|
};
|
|
1938
2184
|
|
|
1939
2185
|
if (elapsed >= this.#config.resize.throttleMs) {
|
|
@@ -2085,6 +2331,7 @@ export default class BabylonJSController {
|
|
|
2085
2331
|
cameraState.setSuccess(true);
|
|
2086
2332
|
}
|
|
2087
2333
|
}
|
|
2334
|
+
this.#applyShowroomCameraFraming(camera);
|
|
2088
2335
|
this.#scene.activeCamera?.detachControl();
|
|
2089
2336
|
camera.detachControl();
|
|
2090
2337
|
if (!cameraState.locked) {
|
|
@@ -2401,51 +2648,23 @@ export default class BabylonJSController {
|
|
|
2401
2648
|
|
|
2402
2649
|
/**
|
|
2403
2650
|
* Loads all asset containers (model, environment, materials, etc.) and adds them to the scene.
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2410
|
-
* camera/IBL/visibility options, and restart.
|
|
2411
|
-
*
|
|
2412
|
-
* When the environment is *not* pending (e.g. only the model changed), the original
|
|
2413
|
-
* single-phase behaviour is preserved — all containers settle immediately and are applied
|
|
2414
|
-
* together.
|
|
2415
|
-
*
|
|
2416
|
-
* A generation counter (`#loadGeneration`) guards Phase 2: if a newer `load()` call starts
|
|
2417
|
-
* while the environment is still downloading, the stale result is disposed instead of applied.
|
|
2418
|
-
*
|
|
2419
|
-
* @private
|
|
2420
|
-
* @param {boolean} [force=false] - Bypass cached size/timestamp checks.
|
|
2421
|
-
* @returns {Promise<{success: boolean, error: any}>}
|
|
2651
|
+
* @private
|
|
2652
|
+
* @returns {Promise<{success: boolean, error: any}>} Resolves to an object indicating if loading succeeded and any error encountered.
|
|
2653
|
+
* @description
|
|
2654
|
+
* Waits for all containers to load in parallel, then replaces or adds them to the scene as needed.
|
|
2655
|
+
* Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
|
|
2656
|
+
* Returns an object with success status and error details.
|
|
2422
2657
|
*/
|
|
2423
2658
|
async #loadContainers(force = false) {
|
|
2424
|
-
const generation = ++this.#loadGeneration;
|
|
2425
2659
|
this.#detachAnimationChangedListener();
|
|
2426
2660
|
await this.#stopRender();
|
|
2427
2661
|
|
|
2428
2662
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
2429
2663
|
let newModelMetadata = {};
|
|
2430
2664
|
|
|
2431
|
-
|
|
2432
|
-
const allLoadPromises = new Map();
|
|
2665
|
+
const promiseArray = [];
|
|
2433
2666
|
Object.values(this.#containers).forEach((container) => {
|
|
2434
|
-
|
|
2435
|
-
});
|
|
2436
|
-
|
|
2437
|
-
// When the environment is pending (heavy scene geometry) we defer it to Phase 2 so
|
|
2438
|
-
// the model can render first. Non-pending environments resolve immediately with
|
|
2439
|
-
// [container, false] and are handled in Phase 1 like any other container.
|
|
2440
|
-
const environmentPending = this.#containers.environment?.state?.isPending === true;
|
|
2441
|
-
const priorityPromises = [];
|
|
2442
|
-
let deferredPromise = null;
|
|
2443
|
-
allLoadPromises.forEach((promise, name) => {
|
|
2444
|
-
if (name === "environment" && environmentPending) {
|
|
2445
|
-
deferredPromise = promise;
|
|
2446
|
-
} else {
|
|
2447
|
-
priorityPromises.push(promise);
|
|
2448
|
-
}
|
|
2667
|
+
promiseArray.push(this.#loadAssetContainer(container, force));
|
|
2449
2668
|
});
|
|
2450
2669
|
|
|
2451
2670
|
let detail = {
|
|
@@ -2453,18 +2672,13 @@ export default class BabylonJSController {
|
|
|
2453
2672
|
error: null,
|
|
2454
2673
|
};
|
|
2455
2674
|
|
|
2456
|
-
|
|
2457
|
-
await Promise.allSettled(priorityPromises)
|
|
2675
|
+
await Promise.allSettled(promiseArray)
|
|
2458
2676
|
.then(async (values) => {
|
|
2459
2677
|
// Scene may have been disposed (disconnectedCallback) while async loading was in
|
|
2460
2678
|
// progress. Abort cleanly: #replaceContainer already guards the GPU calls, but
|
|
2461
2679
|
// we skip the post-load option/visibility calls too to avoid further null-derefs.
|
|
2462
2680
|
if (!this.#scene) {
|
|
2463
2681
|
values.forEach((result) => { result.value?.[1]?.dispose(); });
|
|
2464
|
-
if (deferredPromise) {
|
|
2465
|
-
deferredPromise.then((r) => { r?.[1]?.dispose?.(); }).catch(() => {});
|
|
2466
|
-
deferredPromise = null;
|
|
2467
|
-
}
|
|
2468
2682
|
return;
|
|
2469
2683
|
}
|
|
2470
2684
|
this.#disposeAnimationController();
|
|
@@ -2503,11 +2717,9 @@ export default class BabylonJSController {
|
|
|
2503
2717
|
detail.error = error;
|
|
2504
2718
|
})
|
|
2505
2719
|
.finally(async () => {
|
|
2506
|
-
|
|
2507
|
-
// No deferred work — single-phase path (original behaviour).
|
|
2508
|
-
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2509
|
-
}
|
|
2720
|
+
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2510
2721
|
this.#setMaxSimultaneousLights();
|
|
2722
|
+
this.#enhanceTextureQuality();
|
|
2511
2723
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2512
2724
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2513
2725
|
this.#setHighlightConfig({
|
|
@@ -2519,48 +2731,6 @@ export default class BabylonJSController {
|
|
|
2519
2731
|
}
|
|
2520
2732
|
await this.#startRender();
|
|
2521
2733
|
});
|
|
2522
|
-
|
|
2523
|
-
// ── Phase 2: Deferred environment (already downloading in parallel) ─────
|
|
2524
|
-
if (deferredPromise) {
|
|
2525
|
-
let deferredResult;
|
|
2526
|
-
try {
|
|
2527
|
-
deferredResult = await deferredPromise;
|
|
2528
|
-
} catch (error) {
|
|
2529
|
-
console.error("PrefViewer: failed to load environment progressively", error);
|
|
2530
|
-
deferredResult = [this.#containers.environment, null];
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
// A newer load() was triggered while we waited — discard stale results.
|
|
2534
|
-
if (this.#loadGeneration !== generation) {
|
|
2535
|
-
deferredResult?.[1]?.dispose?.();
|
|
2536
|
-
return detail;
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
if (this.#scene) {
|
|
2540
|
-
const [container, assetContainer] = deferredResult;
|
|
2541
|
-
if (assetContainer) {
|
|
2542
|
-
this.#detachAnimationChangedListener();
|
|
2543
|
-
await this.#stopRender();
|
|
2544
|
-
this.#assetContainer_retagCameras(assetContainer);
|
|
2545
|
-
this.#replaceContainer(container, assetContainer);
|
|
2546
|
-
container.state.setSuccess(true);
|
|
2547
|
-
this.#setOptions_Camera();
|
|
2548
|
-
await this.#setOptions_IBL();
|
|
2549
|
-
this.#setVisibilityOfWallAndFloorInModel();
|
|
2550
|
-
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2551
|
-
this.#setMaxSimultaneousLights();
|
|
2552
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2553
|
-
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2554
|
-
this.#attachAnimationChangedListener();
|
|
2555
|
-
}
|
|
2556
|
-
await this.#startRender();
|
|
2557
|
-
} else {
|
|
2558
|
-
container.state.setSuccess(false);
|
|
2559
|
-
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
2734
|
return detail;
|
|
2565
2735
|
}
|
|
2566
2736
|
|
|
@@ -2815,19 +2985,13 @@ export default class BabylonJSController {
|
|
|
2815
2985
|
*/
|
|
2816
2986
|
async enable() {
|
|
2817
2987
|
this.#configureDracoCompression();
|
|
2818
|
-
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
2988
|
+
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
2819
2989
|
this.#engine.disableUniformBuffers = true;
|
|
2820
2990
|
|
|
2821
2991
|
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
2822
2992
|
const detectedTier = this.#detectHardwareTier();
|
|
2823
2993
|
this.#applyQualitySettings(detectedTier);
|
|
2824
2994
|
|
|
2825
|
-
// OPTIMIZATION: Apply hardware scaling from quality preset
|
|
2826
|
-
const pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
|
|
2827
|
-
const baseScaling = this.#config.quality.hardwareScaling || 1;
|
|
2828
|
-
const scalingLevel = pixelRatio > 1 ? baseScaling * pixelRatio : baseScaling;
|
|
2829
|
-
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
2830
|
-
|
|
2831
2995
|
// OPTIMIZATION: Initialize idle tracking
|
|
2832
2996
|
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2833
2997
|
|
|
@@ -2843,14 +3007,7 @@ export default class BabylonJSController {
|
|
|
2843
3007
|
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
2844
3008
|
}
|
|
2845
3009
|
|
|
2846
|
-
this.#
|
|
2847
|
-
|
|
2848
|
-
// Lowered exposure to prevent scenes from looking blown out when the DefaultRenderingPipeline (Antialiasing) is enabled.
|
|
2849
|
-
this.#scene.imageProcessingConfiguration.exposure = 0.75;
|
|
2850
|
-
this.#scene.imageProcessingConfiguration.contrast = 1.0;
|
|
2851
|
-
this.#scene.imageProcessingConfiguration.toneMappingEnabled = false;
|
|
2852
|
-
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
2853
|
-
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
3010
|
+
this.#applyShowroomLook();
|
|
2854
3011
|
|
|
2855
3012
|
// Skip the built-in pointer picking logic since the controller implements its own optimized raycasting for interaction.
|
|
2856
3013
|
this.#scene.skipPointerMovePicking = true;
|
|
@@ -3064,6 +3221,60 @@ export default class BabylonJSController {
|
|
|
3064
3221
|
}
|
|
3065
3222
|
}
|
|
3066
3223
|
|
|
3224
|
+
/**
|
|
3225
|
+
* Applies lighting and highlight configuration without forcing a full scene reload.
|
|
3226
|
+
* @public
|
|
3227
|
+
* @param {{lightingTimeOfDay?:number, highlightColor?:string, highlightEnabled?:boolean, showAnimationMenu?:boolean}} config - Lighting/highlight configuration.
|
|
3228
|
+
* @returns {void}
|
|
3229
|
+
*/
|
|
3230
|
+
setIlluminationConfig(config = {}) {
|
|
3231
|
+
let shouldApplyLighting = false;
|
|
3232
|
+
|
|
3233
|
+
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3234
|
+
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3235
|
+
if (this.#settings.lightingTimeOfDay !== normalizedTime) {
|
|
3236
|
+
this.#settings.lightingTimeOfDay = normalizedTime;
|
|
3237
|
+
shouldApplyLighting = true;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
if (config.showAnimationMenu !== undefined) {
|
|
3242
|
+
// Handle show-animation-menu via the 3D component's parent
|
|
3243
|
+
// This is handled in pref-viewer-3d.js attributeChangedCallback
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// Handle highlight color for animation hover
|
|
3247
|
+
if (config.highlightColor !== undefined || config.highlightEnabled !== undefined) {
|
|
3248
|
+
// Normalize highlightEnabled: accept boolean, string "true"/"false", or null
|
|
3249
|
+
let enabled = config.highlightEnabled;
|
|
3250
|
+
if (typeof enabled === "string") {
|
|
3251
|
+
enabled = enabled.toLowerCase() !== "false";
|
|
3252
|
+
} else if (enabled === null) {
|
|
3253
|
+
enabled = undefined;
|
|
3254
|
+
}
|
|
3255
|
+
// Keep #settings in sync so getRenderSettings() always reflects the real state
|
|
3256
|
+
if (enabled !== undefined) {
|
|
3257
|
+
this.#settings.highlightEnabled = enabled;
|
|
3258
|
+
}
|
|
3259
|
+
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3260
|
+
this.#settings.highlightColor = config.highlightColor;
|
|
3261
|
+
}
|
|
3262
|
+
this.#setHighlightConfig({
|
|
3263
|
+
color: config.highlightColor ?? undefined,
|
|
3264
|
+
enabled,
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
if (shouldApplyLighting && this.#scene) {
|
|
3269
|
+
this.#applyLightingConfig();
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// Persist settings unless caller explicitly requests a non-persistent update (used by playback animation).
|
|
3273
|
+
if (config.persist !== false) {
|
|
3274
|
+
this.#storeRenderSettings();
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3067
3278
|
/**
|
|
3068
3279
|
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
3069
3280
|
* Adds or removes the container from the scene and updates wall/floor visibility.
|
|
@@ -3118,45 +3329,6 @@ export default class BabylonJSController {
|
|
|
3118
3329
|
return await this.#loadContainers();
|
|
3119
3330
|
}
|
|
3120
3331
|
|
|
3121
|
-
/**
|
|
3122
|
-
* Sets highlight configuration for animation hover effects.
|
|
3123
|
-
* @public
|
|
3124
|
-
* @param {{showAnimationMenu?:boolean, highlightColor?:string, highlightEnabled?:boolean}} config - Highlight configuration.
|
|
3125
|
-
* @returns {void}
|
|
3126
|
-
*/
|
|
3127
|
-
setIlluminationConfig(config = {}) {
|
|
3128
|
-
|
|
3129
|
-
if (config.showAnimationMenu !== undefined) {
|
|
3130
|
-
// Handle show-animation-menu via the 3D component's parent
|
|
3131
|
-
// This is handled in pref-viewer-3d.js attributeChangedCallback
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
// Handle highlight color for animation hover
|
|
3135
|
-
if (config.highlightColor !== undefined || config.highlightEnabled !== undefined) {
|
|
3136
|
-
// Normalize highlightEnabled: accept boolean, string "true"/"false", or null
|
|
3137
|
-
let enabled = config.highlightEnabled;
|
|
3138
|
-
if (typeof enabled === "string") {
|
|
3139
|
-
enabled = enabled.toLowerCase() !== "false";
|
|
3140
|
-
} else if (enabled === null) {
|
|
3141
|
-
enabled = undefined;
|
|
3142
|
-
}
|
|
3143
|
-
// Keep #settings in sync so getRenderSettings() always reflects the real state
|
|
3144
|
-
if (enabled !== undefined) {
|
|
3145
|
-
this.#settings.highlightEnabled = enabled;
|
|
3146
|
-
}
|
|
3147
|
-
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3148
|
-
this.#settings.highlightColor = config.highlightColor;
|
|
3149
|
-
}
|
|
3150
|
-
this.#setHighlightConfig({
|
|
3151
|
-
color: config.highlightColor ?? undefined,
|
|
3152
|
-
enabled,
|
|
3153
|
-
});
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
// Persist settings
|
|
3157
|
-
this.#storeRenderSettings();
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
3332
|
/**
|
|
3161
3333
|
* Returns a snapshot of all available animations and their current state.
|
|
3162
3334
|
* @public
|