@preference-sl/pref-viewer 2.12.0-beta.4 → 2.12.0-beta.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.12.0-beta.4",
3
+ "version": "2.12.0-beta.6",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import { Color3, HighlightLayer, Mesh, PickingInfo, Scene } from "@babylonjs/core";
1
+ import { AssetContainer, Color3, HighlightLayer, InstancedMesh, Mesh, PickingInfo, Quaternion, Scene, UtilityLayerRenderer, Vector3 } from "@babylonjs/core";
2
2
  import { PrefViewerColors } from "./styles.js";
3
3
  import OpeningAnimation from "./babylonjs-animation-opening.js";
4
4
 
@@ -18,27 +18,36 @@ import OpeningAnimation from "./babylonjs-animation-opening.js";
18
18
  *
19
19
  * Public Methods:
20
20
  * - dispose(): Disposes all resources managed by the animation controller.
21
- * - hightlightMeshes(pickingInfo): Highlights meshes that are children of an animated node when hovered.
21
+ * - highlightMeshes(pickingInfo): Highlights meshes that are children of an animated node when hovered.
22
22
  * - hideMenu(): Hides and disposes the animation control menu if it exists.
23
23
  * - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
24
24
  *
25
25
  * @class
26
26
  */
27
27
  export default class BabylonJSAnimationController {
28
- #scene = null;
29
28
  #canvas = null;
29
+ #scene = null;
30
+ #assetContainer = null;
31
+
30
32
  #animatedNodes = [];
31
- #highlightLayer = null;
32
- #highlightColor = Color3.FromHexString(PrefViewerColors.primary);
33
33
  #openingAnimations = [];
34
34
 
35
+ #highlightColor = Color3.FromHexString(PrefViewerColors.primary);
36
+ #highlightLayer = null;
37
+ #overlayLayer = null;
38
+ #useHighlightLayer = false; // Set to true to use HighlightLayer (better performance) and false to use overlay meshes (UtilityLayerRenderer - always on top)
39
+ #lastHighlightedMeshId = null; // Cache to avoid redundant highlight updates
40
+
35
41
  /**
36
42
  * Creates a new BabylonJSAnimationController for a Babylon.js scene.
37
- * @param {Scene} scene - The Babylon.js scene instance.
43
+ * @param {AssetContainer|Scene} assetContainer - The Babylon.js asset container or scene instance.
38
44
  */
39
- constructor(scene) {
40
- this.#scene = scene;
41
- this.#canvas = this.#scene._engine._renderingCanvas;
45
+ constructor(assetContainer) {
46
+ if (assetContainer instanceof AssetContainer || assetContainer instanceof Scene) {
47
+ this.#scene = assetContainer.scene ? assetContainer.scene : assetContainer;
48
+ this.#assetContainer = assetContainer;
49
+ this.#canvas = this.#scene._engine._renderingCanvas;
50
+ }
42
51
  this.#initializeAnimations();
43
52
  }
44
53
 
@@ -48,7 +57,7 @@ export default class BabylonJSAnimationController {
48
57
  */
49
58
  #initializeAnimations() {
50
59
  this.hideMenu(); // Clean up any existing menus
51
- if (!this.#scene.animationGroups.length) {
60
+ if (!this.#assetContainer?.animationGroups?.length) {
52
61
  return;
53
62
  }
54
63
  this.#getAnimatedNodes();
@@ -60,7 +69,7 @@ export default class BabylonJSAnimationController {
60
69
  * @private
61
70
  */
62
71
  #getAnimatedNodes() {
63
- this.#scene.animationGroups.forEach((animationGroup) => {
72
+ this.#assetContainer.animationGroups.forEach((animationGroup) => {
64
73
  animationGroup.stop();
65
74
  if (!animationGroup._targetedAnimations.length) {
66
75
  return;
@@ -81,7 +90,7 @@ export default class BabylonJSAnimationController {
81
90
  */
82
91
  #getOpeningAnimations() {
83
92
  const openings = {};
84
- this.#scene.animationGroups.forEach((animationGroup) => {
93
+ this.#assetContainer.animationGroups.forEach((animationGroup) => {
85
94
  const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
86
95
  if (!match) {
87
96
  return;
@@ -118,7 +127,7 @@ export default class BabylonJSAnimationController {
118
127
  * @param {Mesh} mesh - The mesh to check.
119
128
  * @returns {Array<string>} Array of animated node IDs associated with the mesh.
120
129
  */
121
- #getNodesAnimatedByMesh = function (mesh) {
130
+ #getNodesAnimatedByMesh(mesh) {
122
131
  let nodeId = [];
123
132
  let node = mesh;
124
133
  while (node.parent !== null) {
@@ -128,7 +137,176 @@ export default class BabylonJSAnimationController {
128
137
  }
129
138
  }
130
139
  return nodeId;
131
- };
140
+ }
141
+
142
+ /**
143
+ * Creates overlay meshes with highlight rendering for a group of meshes.
144
+ * Handles both regular meshes and instanced meshes by creating clones in the utility layer.
145
+ * Synchronizes world matrix transformations between original and overlay meshes only when transforms change.
146
+ * @private
147
+ * @param {Mesh[]} meshes - Array of meshes to highlight with overlay outlines.
148
+ * @returns {Object} Cleanup API object with a dispose() method to release all overlay resources.
149
+ * @description
150
+ * - Uses UtilityLayerRenderer for rendering
151
+ * - Only syncs transforms when original mesh's world matrix changes (performance optimized)
152
+ * - Handles InstancedMeshes by creating instances of a shared overlay base
153
+ * - Removes overlay when mouse moves away from highlighted meshes
154
+ */
155
+ #addGroupOutlineOverlayForInstances(meshes) {
156
+ // Configuration
157
+ const OUTLINE_ENABLED = false; // Draw outline on individual meshes
158
+ const OUTLINE_COLOR = this.#highlightColor;
159
+ const OUTLINE_WIDTH = 1.5;
160
+ const OVERLAY_ENABLED = true; // Draw semi-transparent fill
161
+ const OVERLAY_COLOR = this.#highlightColor;
162
+ const OVERLAY_ALPHA = 0.25;
163
+ const RENDERING_GROUP_ID = 0; // MUST be > 0 to render on top of main scene
164
+
165
+ // Use UtilityLayerRenderer for rendering
166
+ const uScene = UtilityLayerRenderer.DefaultKeepDepthUtilityLayer.utilityLayerScene;
167
+
168
+ // Cache map: baseMesh -> overlayBaseMesh to reuse for InstancedMeshes
169
+ const baseToOverlayBase = new Map();
170
+
171
+ // Pairs of { original mesh, overlay mesh } for tracking
172
+ const pairs = [];
173
+
174
+ /**
175
+ * Gets the base mesh from either a regular mesh or an InstancedMesh
176
+ * @param {Mesh} mesh - The mesh to check
177
+ * @returns {Mesh} The base mesh or the mesh itself
178
+ */
179
+ function getBaseMesh(mesh) {
180
+ return mesh instanceof InstancedMesh ? mesh.sourceMesh : mesh;
181
+ }
182
+
183
+ /**
184
+ * Creates or retrieves an overlay base mesh in the utility scene
185
+ * Reuses the same base for all instances of the same source mesh
186
+ * @param {Mesh} baseMesh - The base mesh to clone
187
+ * @returns {Mesh} The overlay base mesh
188
+ */
189
+ function ensureOverlayBase(baseMesh) {
190
+ let overlayBase = baseToOverlayBase.get(baseMesh);
191
+ if (overlayBase) {
192
+ return overlayBase;
193
+ }
194
+
195
+ // Clone the source mesh into the utility scene (deep clone materials/geometry)
196
+ overlayBase = baseMesh.clone(baseMesh.name + "_overlayBase", null, true, false);
197
+ overlayBase._scene = uScene;
198
+ overlayBase.isPickable = false;
199
+ overlayBase.setEnabled(true);
200
+
201
+ // Configure visual appearance for the overlay
202
+ overlayBase.renderOutline = OUTLINE_ENABLED; // Only draw outline if enabled
203
+ overlayBase.outlineColor = OUTLINE_COLOR;
204
+ overlayBase.outlineWidth = OUTLINE_WIDTH;
205
+
206
+ // Overlay color and transparency (semi-transparent colored fill)
207
+ overlayBase.renderOverlay = OVERLAY_ENABLED;
208
+ overlayBase.overlayColor = OVERLAY_COLOR;
209
+ overlayBase.overlayAlpha = OVERLAY_ALPHA;
210
+
211
+ overlayBase.visibility = 1;
212
+
213
+ // Ensure always-on-top rendering or not
214
+ overlayBase.renderingGroupId = RENDERING_GROUP_ID;
215
+
216
+ baseToOverlayBase.set(baseMesh, overlayBase);
217
+ return overlayBase;
218
+ }
219
+
220
+ // Create overlay meshes for each input mesh
221
+ meshes.forEach((mesh) => {
222
+ // Skip meshes without geometry
223
+ if (!mesh.geometry && !mesh.getChildMeshes) {
224
+ return;
225
+ }
226
+
227
+ const baseMesh = getBaseMesh(mesh);
228
+ const overlayBase = ensureOverlayBase(baseMesh);
229
+
230
+ let overlay;
231
+ if (mesh instanceof InstancedMesh) {
232
+ // For instances, create a new instance of the overlay base
233
+ overlay = overlayBase.createInstance(mesh.name + "_overlayInst");
234
+ overlay._scene = uScene;
235
+ overlay.isPickable = false;
236
+ } else {
237
+ // For regular meshes, use the overlay base directly (avoids double cloning)
238
+ overlay = overlayBase;
239
+ }
240
+
241
+ // Remove parent to allow independent world matrix control
242
+ overlay.parent = null;
243
+ overlay.renderingGroupId = RENDERING_GROUP_ID;
244
+
245
+ pairs.push({ original: mesh, overlay });
246
+ });
247
+
248
+ // Sync world matrix transformations
249
+ const pos = new Vector3();
250
+ const scl = new Vector3();
251
+ const rot = new Quaternion();
252
+
253
+ // Track all observers for cleanup
254
+ const observers = [];
255
+
256
+ // Subscribe to world matrix update events
257
+ // This is more efficient than updating every frame
258
+ pairs.forEach(({ original, overlay }) => {
259
+ // Skip if mesh has no geometry to sync
260
+ if (!original.getWorldMatrix) {
261
+ return;
262
+ }
263
+
264
+ // Initial sync: copy the current world matrix
265
+ original.computeWorldMatrix(true);
266
+ original.getWorldMatrix().decompose(scl, rot, pos);
267
+
268
+ overlay.position.copyFrom(pos);
269
+ overlay.scaling.copyFrom(scl);
270
+ overlay.rotationQuaternion = overlay.rotationQuaternion || new Quaternion();
271
+ overlay.rotationQuaternion.copyFrom(rot);
272
+ overlay.computeWorldMatrix(true);
273
+
274
+ // Subscribe to world matrix changes
275
+ // Only updates when the original mesh's transformation actually changes
276
+ const obs = original.onAfterWorldMatrixUpdateObservable?.add(() => {
277
+ original.getWorldMatrix().decompose(scl, rot, pos);
278
+
279
+ overlay.position.copyFrom(pos);
280
+ overlay.scaling.copyFrom(scl);
281
+ overlay.rotationQuaternion.copyFrom(rot);
282
+ overlay.computeWorldMatrix(true);
283
+ });
284
+
285
+ if (obs) {
286
+ observers.push({ mesh: original, observer: obs });
287
+ }
288
+ });
289
+
290
+ // Cleanup API
291
+ return {
292
+ dispose() {
293
+ // Remove world matrix update observers
294
+ observers.forEach(({ mesh, observer }) => {
295
+ mesh.onAfterWorldMatrixUpdateObservable?.remove(observer);
296
+ });
297
+
298
+ // Dispose overlay instances and meshes
299
+ pairs.forEach(({ original, overlay }) => {
300
+ overlay.dispose();
301
+ });
302
+
303
+ // Dispose overlay base meshes
304
+ baseToOverlayBase.forEach((overlayBase) => {
305
+ overlayBase.dispose();
306
+ });
307
+ },
308
+ };
309
+ }
132
310
 
133
311
  /**
134
312
  * ---------------------------
@@ -148,6 +326,10 @@ export default class BabylonJSAnimationController {
148
326
  this.#highlightLayer.dispose();
149
327
  this.#highlightLayer = null;
150
328
  }
329
+ if (this.#overlayLayer) {
330
+ this.#overlayLayer.dispose();
331
+ this.#overlayLayer = null;
332
+ }
151
333
  this.hideMenu();
152
334
  this.#animatedNodes = [];
153
335
  this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
@@ -159,39 +341,74 @@ export default class BabylonJSAnimationController {
159
341
  * @public
160
342
  * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
161
343
  */
162
- hightlightMeshes(pickingInfo) {
163
- if (!this.#highlightLayer) {
164
- this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
344
+ highlightMeshes(pickingInfo) {
345
+ // Check if we're hovering the same mesh to avoid recreating overlays/highlights
346
+ const pickedMeshId = pickingInfo?.pickedMesh?.id;
347
+ if (this.#lastHighlightedMeshId === pickedMeshId) {
348
+ return; // No need to update if hovering the same mesh
165
349
  }
350
+ this.#lastHighlightedMeshId = pickedMeshId;
166
351
 
167
- this.#highlightLayer.removeAllMeshes();
168
- if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
169
- return;
170
- }
352
+ if (this.#useHighlightLayer) {
353
+ if (!this.#highlightLayer) {
354
+ this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
355
+ }
171
356
 
172
- const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
173
- if (!nodeIds.length) {
174
- return;
175
- }
357
+ this.#highlightLayer.removeAllMeshes();
176
358
 
177
- const transformNodes = [];
178
- nodeIds.forEach((nodeId) => {
179
- const transformNode = this.#scene.getTransformNodeByID(nodeId);
180
- if (transformNode) {
181
- transformNodes.push(transformNode);
359
+ if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
360
+ return;
182
361
  }
183
- });
184
362
 
185
- transformNodes.forEach((transformNode) => {
186
- const nodeMeshes = transformNode.getChildMeshes();
187
- if (nodeMeshes.length) {
188
- nodeMeshes.forEach((mesh) => {
189
- if (!this.#highlightLayer.hasMesh(mesh)) {
190
- this.#highlightLayer.addMesh(mesh, this.#highlightColor);
191
- }
192
- });
363
+ const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
364
+ if (!nodeIds.length) {
365
+ return;
193
366
  }
194
- });
367
+
368
+ const transformNodes = [];
369
+ nodeIds.forEach((nodeId) => {
370
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
371
+ if (transformNode) {
372
+ transformNodes.push(transformNode);
373
+ }
374
+ });
375
+
376
+ transformNodes.forEach((transformNode) => {
377
+ const transformNodeMeshes = transformNode.getChildMeshes();
378
+ if (transformNodeMeshes.length) {
379
+ transformNodeMeshes.forEach((mesh) => {
380
+ if (!this.#highlightLayer.hasMesh(mesh)) {
381
+ this.#highlightLayer.addMesh(mesh, this.#highlightColor);
382
+ }
383
+ });
384
+ }
385
+ });
386
+ } else {
387
+ if (this.#overlayLayer) {
388
+ this.#overlayLayer.dispose();
389
+ this.#overlayLayer = null;
390
+ }
391
+
392
+ if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
393
+ return;
394
+ }
395
+
396
+ const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
397
+ if (!nodeIds.length) {
398
+ return;
399
+ }
400
+
401
+ const meshes = [];
402
+ nodeIds.forEach((nodeId) => {
403
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
404
+ if (transformNode) {
405
+ const transformNodeMeshes = transformNode.getChildMeshes(false);
406
+ meshes.push(...transformNodeMeshes);
407
+ }
408
+ });
409
+
410
+ this.#overlayLayer = this.#addGroupOutlineOverlayForInstances(meshes);
411
+ }
195
412
  }
196
413
 
197
414
  /**
@@ -201,7 +418,7 @@ export default class BabylonJSAnimationController {
201
418
  */
202
419
  hideMenu() {
203
420
  this.#openingAnimations.forEach((openingAnimation) => openingAnimation.hideControls());
204
- this.#canvas.parentElement.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
421
+ this.#canvas?.parentElement?.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
205
422
  }
206
423
 
207
424
  /**
@@ -95,6 +95,8 @@ export default class OpeningAnimation {
95
95
  this.#bindHandlers();
96
96
  this.#openAnimation.onAnimationGroupEndObservable.add(this.#handlers.onOpened);
97
97
  this.#closeAnimation.onAnimationGroupEndObservable.add(this.#handlers.onClosed);
98
+
99
+ this.#goToClosed();
98
100
  }
99
101
 
100
102
  #bindHandlers() {
@@ -1,4 +1,4 @@
1
- import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
1
+ import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } 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";
@@ -118,20 +118,25 @@ export default class BabylonJSController {
118
118
  #shadowGen = [];
119
119
  #XRExperience = null;
120
120
  #canvasResizeObserver = null;
121
-
121
+
122
122
  #containers = {};
123
123
  #options = {};
124
-
124
+
125
125
  #gltfResolver = null; // GLTFResolver instance
126
126
  #babylonJSAnimationController = null; // AnimationController instance
127
-
127
+
128
128
  #handlers = {
129
129
  onKeyUp: null,
130
130
  onPointerObservable: null,
131
131
  renderLoop: null,
132
132
  };
133
-
134
- #shadowsEnabled = false;
133
+
134
+ #settings = {
135
+ antiAliasingEnabled: true,
136
+ ambientOcclusionEnabled: true,
137
+ iblEnabled: true,
138
+ shadowsEnabled: false,
139
+ };
135
140
 
136
141
  /**
137
142
  * Constructs a new BabylonJSController instance.
@@ -298,7 +303,7 @@ export default class BabylonJSController {
298
303
  * @returns {void}
299
304
  */
300
305
  #createLights() {
301
- if (this.#options.ibl && this.#options.ibl.url) {
306
+ if (this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.url) {
302
307
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
303
308
  this.#initializeEnvironmentTexture();
304
309
  }
@@ -308,18 +313,27 @@ export default class BabylonJSController {
308
313
  }
309
314
 
310
315
  // 1) Stronger ambient fill
311
- this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
312
- this.#hemiLight.intensity = 0.6;
316
+ this.#hemiLight = this.#scene.getLightByName("PrefViewerHemiLight");
317
+ if (!this.#hemiLight) {
318
+ this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
319
+ this.#hemiLight.intensity = 0.6;
320
+ }
313
321
 
314
322
  // 2) Directional light from the front-right, angled slightly down
315
- this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
316
- this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
317
- this.#dirLight.intensity = 0.6;
323
+ this.#dirLight = this.#scene.getLightByName("PrefViewerDirLight");
324
+ if (!this.#dirLight) {
325
+ this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
326
+ this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
327
+ this.#dirLight.intensity = 0.6;
328
+ }
318
329
 
319
330
  // 3) Camera‐attached headlight
320
- this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
321
- this.#cameraLight.parent = this.#camera;
322
- this.#cameraLight.intensity = 0.3;
331
+ this.#cameraLight = this.#scene.getLightByName("PrefViewerCameraLight");
332
+ if (!this.#cameraLight) {
333
+ this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
334
+ this.#cameraLight.parent = this.#camera;
335
+ this.#cameraLight.intensity = 0.3;
336
+ }
323
337
  }
324
338
 
325
339
  /**
@@ -334,6 +348,10 @@ export default class BabylonJSController {
334
348
  return false;
335
349
  }
336
350
 
351
+ if (!this.#settings.ambientOcclusionEnabled) {
352
+ return false;
353
+ }
354
+
337
355
  const pipelineName = "PrefViewerSSAORenderingPipeline";
338
356
 
339
357
  const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
@@ -366,6 +384,13 @@ export default class BabylonJSController {
366
384
  ssaoPipeline.radius = 0.0001;
367
385
  ssaoPipeline.totalStrength = 1;
368
386
  ssaoPipeline.base = 0.6;
387
+
388
+ // Configure SSAO to calculate only once instead of every frame for better performance
389
+ if (ssaoPipeline._ssaoPostProcess) {
390
+ ssaoPipeline._ssaoPostProcess.autoClear = false;
391
+ ssaoPipeline._ssaoPostProcess.samples = 1;
392
+ }
393
+
369
394
  this.#scene.postProcessRenderPipelineManager.update();
370
395
  return true;
371
396
  } else {
@@ -402,6 +427,10 @@ export default class BabylonJSController {
402
427
  this.#scene.postProcessRenderPipelineManager.update();
403
428
  }
404
429
 
430
+ if (!this.#settings.antiAliasingEnabled) {
431
+ return false;
432
+ }
433
+
405
434
  const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
406
435
 
407
436
  if (!defaultPipeline){
@@ -434,6 +463,14 @@ export default class BabylonJSController {
434
463
  defaultPipeline.grain.animated = false;
435
464
  defaultPipeline.grain.intensity = 3;
436
465
 
466
+ // Configure post-processes to calculate only once instead of every frame for better performance
467
+ if (defaultPipeline.fxaa?._postProcess) {
468
+ defaultPipeline.fxaa._postProcess.autoClear = false;
469
+ }
470
+ if (defaultPipeline.grain?._postProcess) {
471
+ defaultPipeline.grain._postProcess.autoClear = false;
472
+ }
473
+
437
474
  this.#scene.postProcessRenderPipelineManager.update();
438
475
  return true;
439
476
  } else {
@@ -520,8 +557,15 @@ export default class BabylonJSController {
520
557
 
521
558
  Object.assign(iblShadowsRenderPipeline, pipelineProps);
522
559
 
560
+ if (iblShadowsRenderPipeline._ssaoPostProcess) {
561
+ iblShadowsRenderPipeline._ssaoPostProcess.autoClear = false;
562
+ iblShadowsRenderPipeline._ssaoPostProcess.samples = 1;
563
+ }
564
+
523
565
  this.#scene.meshes.forEach((mesh) => {
524
- if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
566
+ const isRootMesh = mesh.id.startsWith("__root__");
567
+ const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
568
+ if (isRootMesh || isHDRIMesh) {
525
569
  return false;
526
570
  }
527
571
  iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
@@ -553,15 +597,19 @@ export default class BabylonJSController {
553
597
  */
554
598
  #initializeDefaultLightShadows() {
555
599
  this.#shadowGen = [];
600
+ this.#dirLight.autoUpdateExtends = false;
556
601
  const shadowGenerator = new ShadowGenerator(1024, this.#dirLight);
557
602
  shadowGenerator.useBlurExponentialShadowMap = true;
558
603
  shadowGenerator.blurKernel = 16;
559
604
  shadowGenerator.darkness = 0.5;
605
+ shadowGenerator.bias = 0.0005;
606
+ shadowGenerator.normalBias = 0.02;
607
+ shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
560
608
  this.#scene.meshes.forEach((mesh) => {
561
609
  if (mesh.id.startsWith("__root__")) {
562
610
  return false;
563
611
  }
564
- if (mesh.name !== "hdri") {
612
+ if (mesh.name?.toLowerCase() !== "hdri") {
565
613
  shadowGenerator.addShadowCaster(mesh, true);
566
614
  }
567
615
  mesh.receiveShadows = true;
@@ -589,15 +637,19 @@ export default class BabylonJSController {
589
637
  if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
590
638
  return;
591
639
  }
640
+ light.autoUpdateExtends = false;
592
641
  const shadowGenerator = new ShadowGenerator(1024, light);
593
642
  shadowGenerator.useBlurExponentialShadowMap = true;
594
643
  shadowGenerator.blurKernel = 16;
595
644
  shadowGenerator.darkness = 0.5;
645
+ shadowGenerator.bias = 0.0005;
646
+ shadowGenerator.normalBias = 0.02;
647
+ shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
596
648
  this.#scene.meshes.forEach((mesh) => {
597
649
  if (mesh.id.startsWith("__root__")) {
598
650
  return false;
599
651
  }
600
- if (mesh.name !== "hdri") {
652
+ if (mesh.name?.toLowerCase() !== "hdri") {
601
653
  shadowGenerator.addShadowCaster(mesh, true);
602
654
  }
603
655
  });
@@ -614,7 +666,7 @@ export default class BabylonJSController {
614
666
  * Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
615
667
  */
616
668
  #initializeShadows() {
617
- if (!this.#shadowsEnabled) {
669
+ if (!this.#settings.shadowsEnabled) {
618
670
  return false;
619
671
  }
620
672
  if (this.#scene.environmentTexture) {
@@ -815,7 +867,7 @@ export default class BabylonJSController {
815
867
  */
816
868
  #onPointerMove(event, pickInfo) {
817
869
  if (this.#babylonJSAnimationController) {
818
- this.#babylonJSAnimationController.hightlightMeshes(pickInfo);
870
+ this.#babylonJSAnimationController.highlightMeshes(pickInfo);
819
871
  }
820
872
  }
821
873
 
@@ -954,11 +1006,12 @@ export default class BabylonJSController {
954
1006
  * @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
955
1007
  */
956
1008
  #setOptions_IBL() {
957
- if (this.#options.ibl.isPending) {
1009
+ if (this.#options.ibl.isPending && this.#settings.iblEnabled) {
958
1010
  this.#options.ibl.setSuccess(true);
959
1011
  this.#createLights();
960
1012
  return true;
961
1013
  }
1014
+ this.#createLights();
962
1015
  return false;
963
1016
  }
964
1017
 
@@ -1224,7 +1277,8 @@ export default class BabylonJSController {
1224
1277
  this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
1225
1278
  this.#setMaxSimultaneousLights();
1226
1279
  this.#loadCameraDepentEffects();
1227
- this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
1280
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
1281
+ this.#forceReflectionsInModelGlasses();
1228
1282
  this.#startRender();
1229
1283
  });
1230
1284
  return detail;
@@ -1318,6 +1372,28 @@ export default class BabylonJSController {
1318
1372
  }
1319
1373
  }
1320
1374
 
1375
+ /**
1376
+ * Configures realistic glass material properties for all materials whose names include "Glass".
1377
+ * @private
1378
+ * @param {AssetContainer} [assetContainer] - The asset container with materials to process. If undefined, uses the model container.
1379
+ * @returns {void}
1380
+ * @note This method assumes that glass materials are named with the substring "Glass".
1381
+ */
1382
+ #forceReflectionsInModelGlasses(assetContainer) {
1383
+ if (assetContainer === undefined) {
1384
+ assetContainer = this.#containers.model.assetContainer;
1385
+ }
1386
+ if (!this.#scene || !assetContainer) {
1387
+ return;
1388
+ }
1389
+ assetContainer.materials?.forEach(material => {
1390
+ if (material && material.name && material.name.includes("Glass")) {
1391
+ material.metallic = 0.25;
1392
+ material.environmentIntensity = 1.0;
1393
+ }
1394
+ });
1395
+ }
1396
+
1321
1397
  /**
1322
1398
  * Translates a node along the scene's vertical (Y) axis by the provided value.
1323
1399
  * @private
@@ -1483,7 +1559,6 @@ export default class BabylonJSController {
1483
1559
 
1484
1560
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
1485
1561
  this.#createCamera();
1486
- this.#createLights();
1487
1562
  this.#enableInteraction();
1488
1563
  await this.#createXRExperience();
1489
1564
  this.#startRender();
@@ -169,7 +169,7 @@ export class FileStorage {
169
169
  let file = undefined;
170
170
  return new Promise((resolve) => {
171
171
  const xhr = new XMLHttpRequest();
172
- xhr.open("GET", uri, true);
172
+ xhr.open("GET", encodeURI(uri), true);
173
173
  xhr.responseType = "blob";
174
174
  xhr.onload = () => {
175
175
  if (xhr.status === 200) {
@@ -205,7 +205,7 @@ export class FileStorage {
205
205
  let timeStamp = null;
206
206
  return new Promise((resolve) => {
207
207
  const xhr = new XMLHttpRequest();
208
- xhr.open("HEAD", uri, true);
208
+ xhr.open("HEAD", encodeURI(uri), true);
209
209
  xhr.responseType = "blob";
210
210
  xhr.onload = () => {
211
211
  if (xhr.status === 200) {
@@ -238,7 +238,7 @@ export class FileStorage {
238
238
  let size = 0;
239
239
  return new Promise((resolve) => {
240
240
  const xhr = new XMLHttpRequest();
241
- xhr.open("HEAD", uri, true);
241
+ xhr.open("HEAD", encodeURI(uri), true);
242
242
  xhr.responseType = "blob";
243
243
  xhr.onload = () => {
244
244
  if (xhr.status === 200) {
@@ -336,12 +336,14 @@ export default class PrefViewer3D extends HTMLElement {
336
336
  let intensity = undefined;
337
337
 
338
338
  if (options.ibl.url) {
339
- const fileStorage = new FileStorage("PrefViewer", "Files");
340
- const newURL = await fileStorage.getURL(options.ibl.url);
341
- if (newURL) {
342
- url = newURL;
343
- timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
344
- }
339
+ url = options.ibl.url;
340
+ // TEMPORARY: Disable FileStorage usage due to efficiency problems
341
+ // const fileStorage = new FileStorage("PrefViewer", "Files");
342
+ // const newURL = await fileStorage.getURL(options.ibl.url);
343
+ // if (newURL) {
344
+ // url = newURL;
345
+ // timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
346
+ // }
345
347
  }
346
348
  if (options.ibl.shadows !== undefined) {
347
349
  shadows = options.ibl.shadows;