@preference-sl/pref-viewer 2.12.0-beta.3 → 2.12.0-beta.5

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.3",
3
+ "version": "2.12.0-beta.5",
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, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointerEventTypes, PointLight, Scene, ShadowGenerator, 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";
@@ -38,15 +38,17 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
38
38
  * - Disable rendering: controller.disable();
39
39
  *
40
40
  * Public Methods:
41
- * - enable(): Initializes engine, scene, camera, lights, XR, and starts rendering.
42
- * - disable(): Disposes engine and disconnects resize observer.
43
- * - load(): Loads all asset containers and adds them to the scene.
44
- * - setCameraOptions(): Applies camera options from configuration.
45
- * - setMaterialOptions(): Applies material options from configuration.
46
- * - setContainerVisibility(name, show): Shows or hides a container by name.
47
- * - downloadGLB(content): Downloads the current scene, model, or environment as a GLB file.
48
- * - downloadGLTF(content): Downloads the current scene, model, or environment as a glTF ZIP file.
49
- * - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
41
+ * - constructor(canvas, containers, options): Creates the controller, wires container state, and stores runtime options.
42
+ * - enable(): Boots the Babylon.js engine, scene, camera, baseline lights, XR support, and the render loop.
43
+ * - disable(): Stops rendering and disposes the engine, lights, XR experience, and observers.
44
+ * - load(): Reloads every pending asset container, re-applies options, and resolves with the loading summary { success, error }.
45
+ * - setCameraOptions(): Applies the pending camera selection, reinstalls dependent pipelines, and restarts rendering safely.
46
+ * - setMaterialOptions(): Re-applies all configured material overrides across visible containers and restarts rendering.
47
+ * - setIBLOptions(): Pushes pending HDR/IBL updates, refreshes dependent effects, and resumes the render loop.
48
+ * - setContainerVisibility(name, show): Toggles model/environment containers, syncing wall/floor helpers and component attributes.
49
+ * - downloadGLB(content): Exports the selected scope (scene/model/environment) into a time-stamped GLB and triggers the download.
50
+ * - downloadGLTF(content): Generates a glTF + BIN + textures ZIP for the requested scope, adding metadata comments for traceability.
51
+ * - downloadUSDZ(content): Builds an Apple USDZ archive for the requested scope and downloads it via blob streaming.
50
52
  *
51
53
  * Private Methods (using ECMAScript private fields):
52
54
  * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
@@ -56,9 +58,13 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
56
58
  * - #createXRExperience(): Initializes WebXR AR experience.
57
59
  * - #createCamera(): Creates and configures the main camera.
58
60
  * - #createLights(): Creates and configures scene lights and shadows.
61
+ * - #initializeAmbientOcclussion(): Rebuilds the SSAO pipeline for the active camera.
62
+ * - #initializeVisualImprovements(): Reinstalls the default rendering pipeline (MSAA/FXAA/grain).
59
63
  * - #initializeEnvironmentTexture(): Loads and sets the HDR environment texture.
60
64
  * - #initializeIBLShadows(): Sets up IBL shadow pipeline and assigns meshes/materials.
61
65
  * - #initializeShadows(): Sets up standard or IBL shadows for meshes.
66
+ * - #initializeDefaultLightShadows(): Configures soft shadows when no HDR environment exists.
67
+ * - #initializeEnvironmentShadows(): Rebuilds environment-provided shadow generators.
62
68
  * - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
63
69
  * - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
64
70
  * - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
@@ -73,6 +79,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
73
79
  * - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
74
80
  * - #setOptions_Materials(): Applies all material options from configuration.
75
81
  * - #setOptions_Camera(): Applies camera options from configuration.
82
+ * - #setOptions_IBL(): Applies pending HDR/IBL option updates and refreshes lights.
76
83
  * - #findContainerByName(name): Finds a container by its name.
77
84
  * - #addContainer(container, updateVisibility): Adds a container to the scene and updates visibility.
78
85
  * - #removeContainer(container, updateVisibility): Removes a container from the scene and updates visibility.
@@ -85,6 +92,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
85
92
  * - #startRender(): Starts the Babylon.js render loop.
86
93
  * - #loadAssetContainer(container): Loads an asset container asynchronously.
87
94
  * - #loadContainers(): Loads all asset containers and adds them to the scene.
95
+ * - #loadCameraDepentEffects(): Re-initializes camera-bound post-processes after reloads or option changes.
88
96
  * - #checkModelMetadata(oldMetadata, newMetadata): Processes metadata changes after loading.
89
97
  * - #checkInnerFloorTranslation(oldMetadata, newMetadata): Applies inner floor Y translations from metadata.
90
98
  * - #translateNodeY(name, deltaY): Adjusts the Y position of a scene node.
@@ -107,21 +115,28 @@ export default class BabylonJSController {
107
115
  #hemiLight = null;
108
116
  #dirLight = null;
109
117
  #cameraLight = null;
110
- #shadowGen = null;
118
+ #shadowGen = [];
111
119
  #XRExperience = null;
112
120
  #canvasResizeObserver = null;
113
-
121
+
114
122
  #containers = {};
115
123
  #options = {};
116
-
124
+
117
125
  #gltfResolver = null; // GLTFResolver instance
118
126
  #babylonJSAnimationController = null; // AnimationController instance
119
-
127
+
120
128
  #handlers = {
121
129
  onKeyUp: null,
122
130
  onPointerObservable: null,
123
131
  renderLoop: null,
124
132
  };
133
+
134
+ #settings = {
135
+ antiAliasingEnabled: true,
136
+ ambientOcclusionEnabled: true,
137
+ iblEnabled: true,
138
+ shadowsEnabled: false,
139
+ };
125
140
 
126
141
  /**
127
142
  * Constructs a new BabylonJSController instance.
@@ -288,33 +303,159 @@ export default class BabylonJSController {
288
303
  * @returns {void}
289
304
  */
290
305
  #createLights() {
291
- this.#initializeEnvironmentTexture();
306
+ if (this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.url) {
307
+ this.#hemiLight = this.#dirLight = this.#cameraLight = null;
308
+ this.#initializeEnvironmentTexture();
309
+ }
292
310
 
293
311
  if (this.#scene.environmentTexture) {
294
312
  return true;
295
313
  }
296
314
 
297
315
  // 1) Stronger ambient fill
298
- this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
316
+ this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
299
317
  this.#hemiLight.intensity = 0.6;
300
318
 
301
319
  // 2) Directional light from the front-right, angled slightly down
302
- this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
320
+ this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
303
321
  this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
304
322
  this.#dirLight.intensity = 0.6;
305
323
 
306
- // // 3) Soft shadows
307
- this.#shadowGen = new ShadowGenerator(1024, this.#dirLight);
308
- this.#shadowGen.useBlurExponentialShadowMap = true;
309
- this.#shadowGen.blurKernel = 16;
310
- this.#shadowGen.darkness = 0.5;
311
-
312
- // 4) Camera‐attached headlight
313
- this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
324
+ // 3) Camera‐attached headlight
325
+ this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
314
326
  this.#cameraLight.parent = this.#camera;
315
327
  this.#cameraLight.intensity = 0.3;
316
328
  }
317
329
 
330
+ /**
331
+ * Rebuilds the SSAO post-process pipeline to inject screenspace ambient occlusion on the active camera.
332
+ * Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
333
+ * current camera so contact shadows enhance depth perception once assets reload or the camera changes.
334
+ * @private
335
+ * @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
336
+ */
337
+ #initializeAmbientOcclussion() {
338
+ if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
339
+ return false;
340
+ }
341
+
342
+ if (!this.#settings.ambientOcclusionEnabled) {
343
+ return false;
344
+ }
345
+
346
+ const pipelineName = "PrefViewerSSAORenderingPipeline";
347
+
348
+ const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
349
+
350
+ if (!supportedPipelines) {
351
+ return false;
352
+ }
353
+
354
+ const oldSsaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
355
+
356
+ if (oldSsaoPipeline) {
357
+ oldSsaoPipeline.dispose();
358
+ this.#scene.postProcessRenderPipelineManager.update();
359
+ }
360
+
361
+ const ssaoRatio = {
362
+ ssaoRatio: 0.5,
363
+ combineRatio: 1.0
364
+ };
365
+
366
+ const ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
367
+
368
+ if (!ssaoPipeline){
369
+ return false;
370
+ }
371
+
372
+ if (ssaoPipeline.isSupported) {
373
+ ssaoPipeline.fallOff = 0.000001;
374
+ ssaoPipeline.area = 1;
375
+ ssaoPipeline.radius = 0.0001;
376
+ ssaoPipeline.totalStrength = 1;
377
+ ssaoPipeline.base = 0.6;
378
+ this.#scene.postProcessRenderPipelineManager.update();
379
+ return true;
380
+ } else {
381
+ ssaoPipeline.dispose();
382
+ this.#scene.postProcessRenderPipelineManager.update();
383
+ return false;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
389
+ * Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
390
+ * `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
391
+ * @private
392
+ * @returns {boolean} True when the pipeline is supported and active, otherwise false.
393
+ * @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
394
+ */
395
+ #initializeVisualImprovements() {
396
+ if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
397
+ return false;
398
+ }
399
+
400
+ const pipelineName = "PrefViewerDefaultRenderingPipeline";
401
+ const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
402
+
403
+ if (!supportedPipelines) {
404
+ return false;
405
+ }
406
+
407
+ const oldDefaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
408
+
409
+ if (oldDefaultPipeline) {
410
+ oldDefaultPipeline.dispose();
411
+ this.#scene.postProcessRenderPipelineManager.update();
412
+ }
413
+
414
+ if (!this.#settings.antiAliasingEnabled) {
415
+ return false;
416
+ }
417
+
418
+ const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
419
+
420
+ if (!defaultPipeline){
421
+ return false;
422
+ }
423
+
424
+ if (defaultPipeline.isSupported) {
425
+ // MSAA - Multisample Anti-Aliasing
426
+ const caps = this.#scene.getEngine()?.getCaps?.() || {};
427
+ const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
428
+ defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
429
+
430
+ // FXAA - Fast Approximate Anti-Aliasing
431
+ defaultPipeline.fxaaEnabled = true;
432
+ defaultPipeline.fxaa.samples = 8;
433
+ defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
434
+ if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
435
+ defaultPipeline.fxaa.edgeThreshold = 0.125;
436
+ }
437
+ if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
438
+ defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
439
+ }
440
+ if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
441
+ defaultPipeline.fxaa.subPixelQuality = 0.75;
442
+ }
443
+
444
+ // Grain
445
+ defaultPipeline.grainEnabled = true;
446
+ defaultPipeline.grain.adaptScaleToCurrentViewport = true;
447
+ defaultPipeline.grain.animated = false;
448
+ defaultPipeline.grain.intensity = 3;
449
+
450
+ this.#scene.postProcessRenderPipelineManager.update();
451
+ return true;
452
+ } else {
453
+ defaultPipeline.dispose();
454
+ this.#scene.postProcessRenderPipelineManager.update();
455
+ return false;
456
+ }
457
+ }
458
+
318
459
  /**
319
460
  * Initializes the environment texture for the Babylon.js scene.
320
461
  * Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
@@ -323,15 +464,9 @@ export default class BabylonJSController {
323
464
  * @returns {boolean}
324
465
  */
325
466
  #initializeEnvironmentTexture() {
326
- return false; // Environment texture disabled by the moment
327
- if (this.#scene.environmentTexture) {
328
- return false;
329
- }
330
- const hdrTextureURI = "../src/environments/noon_grass.hdr";
331
- const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
332
- hdrTexture.gammaSpace = true;
333
- hdrTexture._noMipmap = false;
334
- hdrTexture.level = 2.0;
467
+ const hdrTextureURI = this.#options.ibl.url;
468
+ const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128, false, false, false, true);
469
+ hdrTexture.level = this.#options.ibl.intensity;
335
470
  this.#scene.environmentTexture = hdrTexture;
336
471
  return true;
337
472
  }
@@ -345,61 +480,149 @@ export default class BabylonJSController {
345
480
  * @returns {void|false} Returns false if no environment texture is set; otherwise void.
346
481
  */
347
482
  #initializeIBLShadows() {
348
- if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
483
+ if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
349
484
  return false;
350
485
  }
351
486
 
352
- /**
353
- * Creates and configures the IBL shadow render pipeline for the Babylon.js scene.
354
- * Sets recommended options for resolution, sampling, opacity, and disables debug passes.
355
- * Accepts an optional camera array for pipeline targeting.
356
- * @private
357
- * @param {Scene} scene - The Babylon.js scene instance.
358
- * @param {Camera[]} [cameras] - Optional array of cameras to target with the pipeline.
359
- * @returns {IblShadowsRenderPipeline} The configured IBL shadow pipeline.
360
- */
361
- let createIBLShadowPipeline = function (scene, cameras = [scene.activeCamera]) {
362
- const pipeline = new IblShadowsRenderPipeline(
363
- "iblShadowsPipeline",
364
- scene,
365
- {
366
- resolutionExp: 8, // Higher resolution for better shadow quality
367
- sampleDirections: 4, // More sample directions for smoother shadows
368
- ssShadowsEnabled: true,
369
- shadowRemanence: 0.85,
370
- triPlanarVoxelization: true,
371
- shadowOpacity: 0.85,
372
- },
373
- cameras
374
- );
487
+ // if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
488
+ if (!this.#scene.environmentTexture) {
489
+ return false;
490
+ }
491
+ const pipelineName = "PrefViewerIblShadowsRenderPipeline";
492
+ const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
493
+
494
+ if (!supportedPipelines) {
495
+ return false;
496
+ }
497
+
498
+ const oldIblShadowsRenderPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
499
+
500
+ if (oldIblShadowsRenderPipeline) {
501
+ oldIblShadowsRenderPipeline.dispose();
502
+ this.#scene.postProcessRenderPipelineManager.update();
503
+ }
504
+
505
+ const pipelineOptions = {
506
+ resolutionExp: 8, // Higher resolution for better shadow quality
507
+ sampleDirections: 4, // More sample directions for smoother shadows
508
+ ssShadowsEnabled: true,
509
+ shadowRemanence: 0.85,
510
+ triPlanarVoxelization: true,
511
+ shadowOpacity: 0.85,
512
+ };
513
+
514
+ const iblShadowsRenderPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
515
+
516
+ if (!iblShadowsRenderPipeline) {
517
+ return false;
518
+ }
519
+
520
+ if (iblShadowsRenderPipeline.isSupported) {
375
521
  // Disable all debug passes for performance
376
522
  const pipelineProps = {
377
523
  allowDebugPasses: false,
378
524
  gbufferDebugEnabled: false,
379
525
  importanceSamplingDebugEnabled: false,
380
526
  voxelDebugEnabled: false,
381
- voxelDebugDisplayMip: 0,
527
+ voxelDebugDisplayMip: 1,
382
528
  voxelDebugAxis: 0,
383
529
  voxelTracingDebugEnabled: false,
384
530
  spatialBlurPassDebugEnabled: false,
385
531
  accumulationPassDebugEnabled: false,
386
532
  };
387
- Object.assign(pipeline, pipelineProps);
388
- return pipeline;
389
- };
390
533
 
391
- let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
534
+ Object.assign(iblShadowsRenderPipeline, pipelineProps);
392
535
 
536
+ this.#scene.meshes.forEach((mesh) => {
537
+ if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
538
+ return false;
539
+ }
540
+ iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
541
+ iblShadowsRenderPipeline.updateSceneBounds();
542
+ mesh.receiveShadows = true; // Not necessary for IBL shadows, but yes for standard shadows
543
+ });
544
+
545
+ this.#scene.materials.forEach((material) => {
546
+ if (material instanceof PBRMaterial) {
547
+ material.enableSpecularAntiAliasing = false;
548
+ }
549
+ iblShadowsRenderPipeline.addShadowReceivingMaterial(material);
550
+ });
551
+
552
+ iblShadowsRenderPipeline.updateVoxelization();
553
+ this.#scene.postProcessRenderPipelineManager.update();
554
+ return true;
555
+ } else {
556
+ iblShadowsRenderPipeline.dispose();
557
+ this.#scene.postProcessRenderPipelineManager.update();
558
+ return false;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Configures soft shadows for the built-in directional light used when no HDR environment is present.
564
+ * @private
565
+ * @returns {void}
566
+ */
567
+ #initializeDefaultLightShadows() {
568
+ this.#shadowGen = [];
569
+ this.#dirLight.autoUpdateExtends = false;
570
+ const shadowGenerator = new ShadowGenerator(1024, this.#dirLight);
571
+ shadowGenerator.useBlurExponentialShadowMap = true;
572
+ shadowGenerator.blurKernel = 16;
573
+ shadowGenerator.darkness = 0.5;
574
+ shadowGenerator.bias = 0.0005;
575
+ shadowGenerator.normalBias = 0.02;
576
+ shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
393
577
  this.#scene.meshes.forEach((mesh) => {
394
- if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
578
+ if (mesh.id.startsWith("__root__")) {
395
579
  return false;
396
580
  }
397
- iblShadowsPipeline.addShadowCastingMesh(mesh);
398
- iblShadowsPipeline.updateSceneBounds();
581
+ if (mesh.name !== "hdri") {
582
+ shadowGenerator.addShadowCaster(mesh, true);
583
+ }
584
+ mesh.receiveShadows = true;
399
585
  });
586
+ this.#shadowGen.push(shadowGenerator);
587
+ }
400
588
 
401
- this.#scene.materials.forEach((material) => {
402
- iblShadowsPipeline.addShadowReceivingMaterial(material);
589
+ /**
590
+ * Rebuilds the shadow generators contributed by the environment container.
591
+ * Keeps only the generator bound to the built-in `PrefViewerDirLight` so its shadows persist,
592
+ * then adds generators for every directional or spot light coming from the environment asset container.
593
+ * @private
594
+ * @returns {void}
595
+ */
596
+ #initializeEnvironmentShadows() {
597
+ this.#shadowGen = this.#shadowGen.filter((generator) => {
598
+ if (!generator || typeof generator.getLight !== "function") {
599
+ return false;
600
+ }
601
+ return generator.getLight()?.name === "PrefViewerDirLight";
602
+ });
603
+
604
+ this.#containers.environment?.AssetContainer?.lights.forEach((light) => {
605
+ // Only shadows for DirectionalLight and SpotLight types
606
+ if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
607
+ return;
608
+ }
609
+ light.autoUpdateExtends = false;
610
+ const shadowGenerator = new ShadowGenerator(1024, light);
611
+ shadowGenerator.useBlurExponentialShadowMap = true;
612
+ shadowGenerator.blurKernel = 16;
613
+ shadowGenerator.darkness = 0.5;
614
+ shadowGenerator.bias = 0.0005;
615
+ shadowGenerator.normalBias = 0.02;
616
+ shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
617
+ this.#scene.meshes.forEach((mesh) => {
618
+ if (mesh.id.startsWith("__root__")) {
619
+ return false;
620
+ }
621
+ if (mesh.name !== "hdri") {
622
+ shadowGenerator.addShadowCaster(mesh, true);
623
+ }
624
+ });
625
+ this.#shadowGen.push(shadowGenerator);
403
626
  });
404
627
  }
405
628
 
@@ -412,20 +635,24 @@ export default class BabylonJSController {
412
635
  * Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
413
636
  */
414
637
  #initializeShadows() {
415
- if (this.#scene.environmentTexture) {
416
- this.#initializeIBLShadows();
417
- return true;
638
+ if (!this.#settings.shadowsEnabled) {
639
+ return false;
418
640
  }
419
-
420
- this.#scene.meshes.forEach((mesh) => {
421
- if (mesh.id.startsWith("__root__")) {
422
- return false;
423
- }
424
- mesh.receiveShadows = true;
425
- if (mesh.name !== "hdri") {
426
- this.#shadowGen.addShadowCaster(mesh, true);
641
+ if (this.#scene.environmentTexture) {
642
+ if (this.#options.ibl.shadows) {
643
+ if (this.#scene.environmentTexture.isReady()) {
644
+ this.#initializeIBLShadows();
645
+ } else {
646
+ const self = this;
647
+ this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
648
+ self.#initializeIBLShadows();
649
+ });
650
+ }
427
651
  }
428
- });
652
+ } else {
653
+ this.#initializeDefaultLightShadows();
654
+ }
655
+ this.#initializeEnvironmentShadows();
429
656
  }
430
657
 
431
658
  /**
@@ -435,7 +662,7 @@ export default class BabylonJSController {
435
662
  * @returns {void}
436
663
  */
437
664
  #setMaxSimultaneousLights() {
438
- let lightsNumber = 1; // Como mínimo una luz correspondiente a la textura de environmentTexture
665
+ let lightsNumber = 1; // At least one light coming from the environment texture contribution
439
666
  this.#scene.lights.forEach((light) => {
440
667
  if (light.isEnabled()) {
441
668
  ++lightsNumber;
@@ -543,7 +770,7 @@ export default class BabylonJSController {
543
770
  this.#engine.dispose();
544
771
  this.#engine = this.#scene = this.#camera = null;
545
772
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
546
- this.#shadowGen = null;
773
+ this.#shadowGen = [];
547
774
  }
548
775
 
549
776
  /**
@@ -609,7 +836,7 @@ export default class BabylonJSController {
609
836
  */
610
837
  #onPointerMove(event, pickInfo) {
611
838
  if (this.#babylonJSAnimationController) {
612
- this.#babylonJSAnimationController.hightlightMeshes(pickInfo);
839
+ this.#babylonJSAnimationController.highlightMeshes(pickInfo);
613
840
  }
614
841
  }
615
842
 
@@ -741,6 +968,22 @@ export default class BabylonJSController {
741
968
  return true;
742
969
  }
743
970
 
971
+ /**
972
+ * Applies pending image-based lighting (IBL) option updates.
973
+ * Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
974
+ * @private
975
+ * @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
976
+ */
977
+ #setOptions_IBL() {
978
+ if (this.#options.ibl.isPending && this.#settings.iblEnabled) {
979
+ this.#options.ibl.setSuccess(true);
980
+ this.#createLights();
981
+ return true;
982
+ }
983
+ this.#createLights();
984
+ return false;
985
+ }
986
+
744
987
  /**
745
988
  * Finds and returns the asset container object by its name.
746
989
  * @private
@@ -951,6 +1194,7 @@ export default class BabylonJSController {
951
1194
  */
952
1195
  async #loadContainers() {
953
1196
  this.#stopRender();
1197
+ this.#scene.postProcessRenderPipelineManager?.dispose();
954
1198
 
955
1199
  let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
956
1200
  let newModelMetadata = {};
@@ -988,6 +1232,7 @@ export default class BabylonJSController {
988
1232
 
989
1233
  this.#setOptions_Materials();
990
1234
  this.#setOptions_Camera();
1235
+ this.#setOptions_IBL();
991
1236
  this.#setVisibilityOfWallAndFloorInModel();
992
1237
  detail.success = true;
993
1238
  })
@@ -1000,13 +1245,26 @@ export default class BabylonJSController {
1000
1245
  .finally(async () => {
1001
1246
  this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
1002
1247
  this.#setMaxSimultaneousLights();
1003
- this.#initializeShadows();
1004
- this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
1248
+ this.#loadCameraDepentEffects();
1249
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
1250
+ this.#forceReflectionsInModelGlasses();
1005
1251
  this.#startRender();
1006
1252
  });
1007
1253
  return detail;
1008
1254
  }
1009
1255
 
1256
+ /**
1257
+ * Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
1258
+ * Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
1259
+ * @private
1260
+ * @returns {void}
1261
+ */
1262
+ #loadCameraDepentEffects() {
1263
+ this.#initializeVisualImprovements();
1264
+ this.#initializeAmbientOcclussion();
1265
+ this.#initializeShadows();
1266
+ }
1267
+
1010
1268
  /**
1011
1269
  * Checks and applies model metadata changes after asset loading.
1012
1270
  * @private
@@ -1083,6 +1341,21 @@ export default class BabylonJSController {
1083
1341
  }
1084
1342
  }
1085
1343
 
1344
+ #forceReflectionsInModelGlasses(assetContainer) {
1345
+ if (assetContainer === undefined) {
1346
+ assetContainer = this.#containers.model.assetContainer;
1347
+ }
1348
+ if (!this.#scene || !assetContainer) {
1349
+ return;
1350
+ }
1351
+ assetContainer.materials?.forEach(material => {
1352
+ if (material && material.name && material.name.includes("Glass")) {
1353
+ material.metallic = 1.0;
1354
+ material.environmentIntensity = 1.0;
1355
+ }
1356
+ });
1357
+ }
1358
+
1086
1359
  /**
1087
1360
  * Translates a node along the scene's vertical (Y) axis by the provided value.
1088
1361
  * @private
@@ -1235,9 +1508,19 @@ export default class BabylonJSController {
1235
1508
  this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
1236
1509
  this.#engine.disableUniformBuffers = true;
1237
1510
  this.#scene = new Scene(this.#engine);
1511
+
1512
+ // Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
1513
+ // SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
1514
+ // different buffers (depth, normals, velocity) for later use in shaders.
1515
+ const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
1516
+ if (geometryBufferRenderer) {
1517
+ geometryBufferRenderer.enableScreenspaceDepth = true;
1518
+ geometryBufferRenderer.enableDepth = false;
1519
+ geometryBufferRenderer.generateNormalsInWorldSpace = true;
1520
+ }
1521
+
1238
1522
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
1239
1523
  this.#createCamera();
1240
- this.#createLights();
1241
1524
  this.#enableInteraction();
1242
1525
  await this.#createXRExperience();
1243
1526
  this.#startRender();
@@ -1278,6 +1561,7 @@ export default class BabylonJSController {
1278
1561
  setCameraOptions() {
1279
1562
  this.#stopRender();
1280
1563
  const cameraOptionsSetted = this.#setOptions_Camera();
1564
+ this.#loadCameraDepentEffects();
1281
1565
  this.#startRender();
1282
1566
  return cameraOptionsSetted;
1283
1567
  }
@@ -1295,6 +1579,20 @@ export default class BabylonJSController {
1295
1579
  return materialsOptionsSetted;
1296
1580
  }
1297
1581
 
1582
+ /**
1583
+ * Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
1584
+ * Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
1585
+ * @public
1586
+ * @returns {void}
1587
+ */
1588
+ setIBLOptions() {
1589
+ this.#stopRender();
1590
+ const IBLOptionsSetted = this.#setOptions_IBL();
1591
+ this.#loadCameraDepentEffects();
1592
+ this.#startRender();
1593
+ return IBLOptionsSetted;
1594
+ }
1595
+
1298
1596
  /**
1299
1597
  * Sets the visibility of a container (model, environment, etc.) by name.
1300
1598
  * Adds or removes the container from the scene and updates wall/floor visibility.
@@ -181,3 +181,53 @@ export class CameraData {
181
181
  return this.update.success === true;
182
182
  }
183
183
  }
184
+
185
+ /**
186
+ * IBLData - Tracks configurable settings for image-based lighting assets (IBL/HDR environments).
187
+ *
188
+ * Responsibilities:
189
+ * - Stores the HDR url, intensity scalar, whether environment-provided shadows should render, and cache timestamp.
190
+ * - Exposes a lightweight pending/success state machine so UI flows know when a new IBL selection is being fetched.
191
+ * - Provides helpers to update values atomically via `setValues`, toggle pending state, and mark completion.
192
+ *
193
+ * Usage:
194
+ * - Instantiate with defaults: `const ibl = new IBLData();`
195
+ * - Call `setPending()` before kicking off an async download, `setValues()` as metadata streams in, and `setSuccess(true)` once loading finishes.
196
+ * - Inspect `isPending`/`isSuccess` to drive UI or re-render logic.
197
+ */
198
+ export class IBLData {
199
+ constructor(url = null, intensity = 1.0, shadows = false, timeStamp = null) {
200
+ this.url = url;
201
+ this.intensity = intensity;
202
+ this.shadows = shadows;
203
+ this.timeStamp = timeStamp;
204
+ this.reset();
205
+ }
206
+ reset() {
207
+ this.pending = false;
208
+ this.success = false;
209
+ }
210
+ setValues(url, intensity, shadows, timeStamp) {
211
+ this.url = url !== undefined ? url : this.url;
212
+ this.intensity = intensity !== undefined ? intensity : this.intensity;
213
+ this.shadows = shadows !== undefined ? shadows : this.shadows;
214
+ this.timeStamp = timeStamp !== undefined ? timeStamp : this.timeStamp;
215
+ }
216
+ setSuccess(success = false) {
217
+ if (success) {
218
+ this.success = true;
219
+ } else {
220
+ this.success = false;
221
+ }
222
+ }
223
+ setPending() {
224
+ this.pending = true;
225
+ this.success = false;
226
+ }
227
+ get isPending() {
228
+ return this.pending === true;
229
+ }
230
+ get isSuccess() {
231
+ return this.success === true;
232
+ }
233
+ }
@@ -1,6 +1,7 @@
1
- import { CameraData, ContainerData, MaterialData } from "./pref-viewer-3d-data.js";
1
+ import { CameraData, ContainerData, MaterialData, IBLData } from "./pref-viewer-3d-data.js";
2
2
  import BabylonJSController from "./babylonjs-controller.js";
3
3
  import { PrefViewer3DStyles } from "./styles.js";
4
+ import { FileStorage } from "./file-storage.js";
4
5
 
5
6
  /**
6
7
  * PrefViewer3D - Custom Web Component for interactive 3D visualization and configuration.
@@ -8,7 +9,7 @@ import { PrefViewer3DStyles } from "./styles.js";
8
9
  * Overview:
9
10
  * - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
10
11
  * - Manages internal state for containers (model, environment, materials) and options (camera, materials).
11
- * - Handles asset loading, configuration, and option updates through attributes and public methods.
12
+ * - Handles asset loading, configuration, and option updates (camera, materials, IBL) through attributes and public methods.
12
13
  * - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
13
14
  * - Emits custom events for loading, loaded, and option-setting states.
14
15
  *
@@ -26,7 +27,7 @@ import { PrefViewer3DStyles } from "./styles.js";
26
27
  * - show(): Shows the 3D viewer component.
27
28
  * - hide(): Hides the 3D viewer component.
28
29
  * - load(config): Loads the provided configuration into the viewer.
29
- * - setOptions(options): Sets viewer options such as camera and materials.
30
+ * - setOptions(options): Sets viewer options such as camera, materials, and image-based lighting (IBL).
30
31
  * - showModel(): Shows the 3D model.
31
32
  * - hideModel(): Hides the 3D model.
32
33
  * - showEnvironment(): Shows the 3D environment/scene.
@@ -46,6 +47,14 @@ import { PrefViewer3DStyles } from "./styles.js";
46
47
  * - isLoaded: Indicates whether the GLTF/GLB content is loaded and ready.
47
48
  * - isVisible: Indicates whether the component is currently visible.
48
49
  *
50
+ * Internal Helpers:
51
+ * - #checkNeedToUpdateContainers(config): Flags model/environment/material containers that must reload.
52
+ * - #checkNeedToUpdateCamera(options): Detects camera option changes and marks them pending.
53
+ * - #checkNeedToUpdateMaterials(options): Resolves material overrides that require updates.
54
+ * - #checkNeedToUpdateIBL(options): Fetches HDR URLs/timestamps and enqueues IBL updates when needed.
55
+ * - #resetUpdateFlags(): Clears pending/success flags after loads or option-setting flows.
56
+ * - #onLoading/#onLoaded/#onSettingOptions/#onSetOptions(): Dispatch lifecycle events and synchronize attributes.
57
+ *
49
58
  * Events:
50
59
  * - "scene-loading": Dispatched when a loading operation starts.
51
60
  * - "scene-loaded": Dispatched when a loading operation completes.
@@ -202,6 +211,7 @@ export default class PrefViewer3D extends HTMLElement {
202
211
  innerFloor: new MaterialData("innerFloor", undefined, undefined, ["innerFloor"]),
203
212
  outerFloor: new MaterialData("outerFloor", undefined, undefined, ["outerFloor"]),
204
213
  },
214
+ ibl: new IBLData(),
205
215
  },
206
216
  };
207
217
  }
@@ -225,6 +235,7 @@ export default class PrefViewer3D extends HTMLElement {
225
235
  Object.values(this.#data.containers).forEach((container) => container.reset());
226
236
  Object.values(this.#data.options.materials).forEach((material) => material.reset());
227
237
  this.#data.options.camera.reset();
238
+ this.#data.options.ibl.reset();
228
239
  }
229
240
 
230
241
  /**
@@ -306,6 +317,53 @@ export default class PrefViewer3D extends HTMLElement {
306
317
  return someNeedUpdate;
307
318
  }
308
319
 
320
+ /**
321
+ * Resolves incoming IBL settings (HDR URL, timestamp, intensity, shadows) and marks the option as pending when changed.
322
+ * Fetches signed URLs/time stamps when storage keys are provided so the Babylon controller can reload the environment map.
323
+ * @private
324
+ * @param {object} options - Options payload that may contain an `ibl` block with url, intensity, or shadow flags.
325
+ * @returns {Promise<boolean>} Resolves to true when any IBL property differs from the cached state, otherwise false.
326
+ */
327
+ async #checkNeedToUpdateIBL(options) {
328
+ if (!options || options.ibl === undefined) {
329
+ return false;
330
+ }
331
+ const iblState = this.#data.options.ibl;
332
+
333
+ let url = undefined;
334
+ let timeStamp = undefined;
335
+ let shadows = undefined;
336
+ let intensity = undefined;
337
+
338
+ if (options.ibl.url) {
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
+ // }
347
+ }
348
+ if (options.ibl.shadows !== undefined) {
349
+ shadows = options.ibl.shadows;
350
+ }
351
+ if (options.ibl.intensity !== undefined) {
352
+ intensity = options.ibl.intensity;
353
+ }
354
+
355
+ const needUpdate = url !== undefined && url !== iblState.url ||
356
+ timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
357
+ shadows !== undefined && shadows !== iblState.shadows ||
358
+ intensity !== undefined && intensity !== iblState.intensity;
359
+ if (needUpdate) {
360
+ iblState.setValues(url, intensity, shadows, timeStamp);
361
+ iblState.setPending(true);
362
+ }
363
+
364
+ return needUpdate;
365
+ }
366
+
309
367
  /**
310
368
  * Dispatches a "prefviewer3d-loading" event and updates loading state attributes.
311
369
  * Used internally when a loading operation starts.
@@ -490,6 +548,7 @@ export default class PrefViewer3D extends HTMLElement {
490
548
  if (config.options) {
491
549
  this.#checkNeedToUpdateCamera(config.options);
492
550
  this.#checkNeedToUpdateMaterials(config.options);
551
+ this.#checkNeedToUpdateIBL(config.options);
493
552
  }
494
553
 
495
554
  const loadDetail = await this.#babylonJSController.load();
@@ -518,6 +577,9 @@ export default class PrefViewer3D extends HTMLElement {
518
577
  if (this.#checkNeedToUpdateMaterials(options)) {
519
578
  someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
520
579
  }
580
+ if (this.#checkNeedToUpdateIBL(options)) {
581
+ someSetted = someSetted || this.#babylonJSController.setIBLOptions();
582
+ }
521
583
  const detail = this.#onSetOptions();
522
584
  return { success: someSetted, detail: detail };
523
585
  }