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

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.2",
3
+ "version": "2.12.0-beta.4",
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 { 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, 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,7 +115,7 @@ 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
 
@@ -123,6 +131,8 @@ export default class BabylonJSController {
123
131
  renderLoop: null,
124
132
  };
125
133
 
134
+ #shadowsEnabled = false;
135
+
126
136
  /**
127
137
  * Constructs a new BabylonJSController instance.
128
138
  * Initializes the canvas, asset containers, and options for the Babylon.js scene.
@@ -288,33 +298,151 @@ export default class BabylonJSController {
288
298
  * @returns {void}
289
299
  */
290
300
  #createLights() {
291
- this.#initializeEnvironmentTexture();
301
+ if (this.#options.ibl && this.#options.ibl.url) {
302
+ this.#hemiLight = this.#dirLight = this.#cameraLight = null;
303
+ this.#initializeEnvironmentTexture();
304
+ }
292
305
 
293
306
  if (this.#scene.environmentTexture) {
294
307
  return true;
295
308
  }
296
309
 
297
310
  // 1) Stronger ambient fill
298
- this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
311
+ this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
299
312
  this.#hemiLight.intensity = 0.6;
300
313
 
301
314
  // 2) Directional light from the front-right, angled slightly down
302
- this.#dirLight = new DirectionalLight("dirLight", new Vector3(-10, 10, -10), this.#scene);
315
+ this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
303
316
  this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
304
317
  this.#dirLight.intensity = 0.6;
305
318
 
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);
319
+ // 3) Camera‐attached headlight
320
+ this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
314
321
  this.#cameraLight.parent = this.#camera;
315
322
  this.#cameraLight.intensity = 0.3;
316
323
  }
317
324
 
325
+ /**
326
+ * Rebuilds the SSAO post-process pipeline to inject screenspace ambient occlusion on the active camera.
327
+ * Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
328
+ * current camera so contact shadows enhance depth perception once assets reload or the camera changes.
329
+ * @private
330
+ * @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
331
+ */
332
+ #initializeAmbientOcclussion() {
333
+ if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
334
+ return false;
335
+ }
336
+
337
+ const pipelineName = "PrefViewerSSAORenderingPipeline";
338
+
339
+ const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
340
+
341
+ if (!supportedPipelines) {
342
+ return false;
343
+ }
344
+
345
+ const oldSsaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
346
+
347
+ if (oldSsaoPipeline) {
348
+ oldSsaoPipeline.dispose();
349
+ this.#scene.postProcessRenderPipelineManager.update();
350
+ }
351
+
352
+ const ssaoRatio = {
353
+ ssaoRatio: 0.5,
354
+ combineRatio: 1.0
355
+ };
356
+
357
+ const ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
358
+
359
+ if (!ssaoPipeline){
360
+ return false;
361
+ }
362
+
363
+ if (ssaoPipeline.isSupported) {
364
+ ssaoPipeline.fallOff = 0.000001;
365
+ ssaoPipeline.area = 1;
366
+ ssaoPipeline.radius = 0.0001;
367
+ ssaoPipeline.totalStrength = 1;
368
+ ssaoPipeline.base = 0.6;
369
+ this.#scene.postProcessRenderPipelineManager.update();
370
+ return true;
371
+ } else {
372
+ ssaoPipeline.dispose();
373
+ this.#scene.postProcessRenderPipelineManager.update();
374
+ return false;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
380
+ * Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
381
+ * `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
382
+ * @private
383
+ * @returns {boolean} True when the pipeline is supported and active, otherwise false.
384
+ * @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
385
+ */
386
+ #initializeVisualImprovements() {
387
+ if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
388
+ return false;
389
+ }
390
+
391
+ const pipelineName = "PrefViewerDefaultRenderingPipeline";
392
+ const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
393
+
394
+ if (!supportedPipelines) {
395
+ return false;
396
+ }
397
+
398
+ const oldDefaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
399
+
400
+ if (oldDefaultPipeline) {
401
+ oldDefaultPipeline.dispose();
402
+ this.#scene.postProcessRenderPipelineManager.update();
403
+ }
404
+
405
+ const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
406
+
407
+ if (!defaultPipeline){
408
+ return false;
409
+ }
410
+
411
+ if (defaultPipeline.isSupported) {
412
+ // MSAA - Multisample Anti-Aliasing
413
+ const caps = this.#scene.getEngine()?.getCaps?.() || {};
414
+ const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
415
+ defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
416
+
417
+ // FXAA - Fast Approximate Anti-Aliasing
418
+ defaultPipeline.fxaaEnabled = true;
419
+ defaultPipeline.fxaa.samples = 8;
420
+ defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
421
+ if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
422
+ defaultPipeline.fxaa.edgeThreshold = 0.125;
423
+ }
424
+ if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
425
+ defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
426
+ }
427
+ if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
428
+ defaultPipeline.fxaa.subPixelQuality = 0.75;
429
+ }
430
+
431
+ // Grain
432
+ defaultPipeline.grainEnabled = true;
433
+ defaultPipeline.grain.adaptScaleToCurrentViewport = true;
434
+ defaultPipeline.grain.animated = false;
435
+ defaultPipeline.grain.intensity = 3;
436
+
437
+ this.#scene.postProcessRenderPipelineManager.update();
438
+ return true;
439
+ } else {
440
+ defaultPipeline.dispose();
441
+ this.#scene.postProcessRenderPipelineManager.update();
442
+ return false;
443
+ }
444
+ }
445
+
318
446
  /**
319
447
  * Initializes the environment texture for the Babylon.js scene.
320
448
  * Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
@@ -323,15 +451,9 @@ export default class BabylonJSController {
323
451
  * @returns {boolean}
324
452
  */
325
453
  #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;
454
+ const hdrTextureURI = this.#options.ibl.url;
455
+ const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128, false, false, false, true);
456
+ hdrTexture.level = this.#options.ibl.intensity;
335
457
  this.#scene.environmentTexture = hdrTexture;
336
458
  return true;
337
459
  }
@@ -345,61 +467,141 @@ export default class BabylonJSController {
345
467
  * @returns {void|false} Returns false if no environment texture is set; otherwise void.
346
468
  */
347
469
  #initializeIBLShadows() {
348
- if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
470
+ if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
349
471
  return false;
350
472
  }
351
473
 
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
- );
474
+ // if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
475
+ if (!this.#scene.environmentTexture) {
476
+ return false;
477
+ }
478
+ const pipelineName = "PrefViewerIblShadowsRenderPipeline";
479
+ const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
480
+
481
+ if (!supportedPipelines) {
482
+ return false;
483
+ }
484
+
485
+ const oldIblShadowsRenderPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
486
+
487
+ if (oldIblShadowsRenderPipeline) {
488
+ oldIblShadowsRenderPipeline.dispose();
489
+ this.#scene.postProcessRenderPipelineManager.update();
490
+ }
491
+
492
+ const pipelineOptions = {
493
+ resolutionExp: 8, // Higher resolution for better shadow quality
494
+ sampleDirections: 4, // More sample directions for smoother shadows
495
+ ssShadowsEnabled: true,
496
+ shadowRemanence: 0.85,
497
+ triPlanarVoxelization: true,
498
+ shadowOpacity: 0.85,
499
+ };
500
+
501
+ const iblShadowsRenderPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
502
+
503
+ if (!iblShadowsRenderPipeline) {
504
+ return false;
505
+ }
506
+
507
+ if (iblShadowsRenderPipeline.isSupported) {
375
508
  // Disable all debug passes for performance
376
509
  const pipelineProps = {
377
510
  allowDebugPasses: false,
378
511
  gbufferDebugEnabled: false,
379
512
  importanceSamplingDebugEnabled: false,
380
513
  voxelDebugEnabled: false,
381
- voxelDebugDisplayMip: 0,
514
+ voxelDebugDisplayMip: 1,
382
515
  voxelDebugAxis: 0,
383
516
  voxelTracingDebugEnabled: false,
384
517
  spatialBlurPassDebugEnabled: false,
385
518
  accumulationPassDebugEnabled: false,
386
519
  };
387
- Object.assign(pipeline, pipelineProps);
388
- return pipeline;
389
- };
390
520
 
391
- let iblShadowsPipeline = createIBLShadowPipeline(this.#scene);
521
+ Object.assign(iblShadowsRenderPipeline, pipelineProps);
522
+
523
+ this.#scene.meshes.forEach((mesh) => {
524
+ if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
525
+ return false;
526
+ }
527
+ iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
528
+ iblShadowsRenderPipeline.updateSceneBounds();
529
+ mesh.receiveShadows = true; // Not necessary for IBL shadows, but yes for standard shadows
530
+ });
531
+
532
+ this.#scene.materials.forEach((material) => {
533
+ if (material instanceof PBRMaterial) {
534
+ material.enableSpecularAntiAliasing = false;
535
+ }
536
+ iblShadowsRenderPipeline.addShadowReceivingMaterial(material);
537
+ });
392
538
 
539
+ iblShadowsRenderPipeline.updateVoxelization();
540
+ this.#scene.postProcessRenderPipelineManager.update();
541
+ return true;
542
+ } else {
543
+ iblShadowsRenderPipeline.dispose();
544
+ this.#scene.postProcessRenderPipelineManager.update();
545
+ return false;
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Configures soft shadows for the built-in directional light used when no HDR environment is present.
551
+ * @private
552
+ * @returns {void}
553
+ */
554
+ #initializeDefaultLightShadows() {
555
+ this.#shadowGen = [];
556
+ const shadowGenerator = new ShadowGenerator(1024, this.#dirLight);
557
+ shadowGenerator.useBlurExponentialShadowMap = true;
558
+ shadowGenerator.blurKernel = 16;
559
+ shadowGenerator.darkness = 0.5;
393
560
  this.#scene.meshes.forEach((mesh) => {
394
- if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
561
+ if (mesh.id.startsWith("__root__")) {
395
562
  return false;
396
563
  }
397
- iblShadowsPipeline.addShadowCastingMesh(mesh);
398
- iblShadowsPipeline.updateSceneBounds();
564
+ if (mesh.name !== "hdri") {
565
+ shadowGenerator.addShadowCaster(mesh, true);
566
+ }
567
+ mesh.receiveShadows = true;
568
+ });
569
+ this.#shadowGen.push(shadowGenerator);
570
+ }
571
+
572
+ /**
573
+ * Rebuilds the shadow generators contributed by the environment container.
574
+ * Keeps only the generator bound to the built-in `PrefViewerDirLight` so its shadows persist,
575
+ * then adds generators for every directional or spot light coming from the environment asset container.
576
+ * @private
577
+ * @returns {void}
578
+ */
579
+ #initializeEnvironmentShadows() {
580
+ this.#shadowGen = this.#shadowGen.filter((generator) => {
581
+ if (!generator || typeof generator.getLight !== "function") {
582
+ return false;
583
+ }
584
+ return generator.getLight()?.name === "PrefViewerDirLight";
399
585
  });
400
586
 
401
- this.#scene.materials.forEach((material) => {
402
- iblShadowsPipeline.addShadowReceivingMaterial(material);
587
+ this.#containers.environment?.AssetContainer?.lights.forEach((light) => {
588
+ // Only shadows for DirectionalLight and SpotLight types
589
+ if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
590
+ return;
591
+ }
592
+ const shadowGenerator = new ShadowGenerator(1024, light);
593
+ shadowGenerator.useBlurExponentialShadowMap = true;
594
+ shadowGenerator.blurKernel = 16;
595
+ shadowGenerator.darkness = 0.5;
596
+ this.#scene.meshes.forEach((mesh) => {
597
+ if (mesh.id.startsWith("__root__")) {
598
+ return false;
599
+ }
600
+ if (mesh.name !== "hdri") {
601
+ shadowGenerator.addShadowCaster(mesh, true);
602
+ }
603
+ });
604
+ this.#shadowGen.push(shadowGenerator);
403
605
  });
404
606
  }
405
607
 
@@ -412,20 +614,24 @@ export default class BabylonJSController {
412
614
  * Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
413
615
  */
414
616
  #initializeShadows() {
415
- if (this.#scene.environmentTexture) {
416
- this.#initializeIBLShadows();
417
- return true;
617
+ if (!this.#shadowsEnabled) {
618
+ return false;
418
619
  }
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);
620
+ if (this.#scene.environmentTexture) {
621
+ if (this.#options.ibl.shadows) {
622
+ if (this.#scene.environmentTexture.isReady()) {
623
+ this.#initializeIBLShadows();
624
+ } else {
625
+ const self = this;
626
+ this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
627
+ self.#initializeIBLShadows();
628
+ });
629
+ }
427
630
  }
428
- });
631
+ } else {
632
+ this.#initializeDefaultLightShadows();
633
+ }
634
+ this.#initializeEnvironmentShadows();
429
635
  }
430
636
 
431
637
  /**
@@ -435,7 +641,7 @@ export default class BabylonJSController {
435
641
  * @returns {void}
436
642
  */
437
643
  #setMaxSimultaneousLights() {
438
- let lightsNumber = 1; // Como mínimo una luz correspondiente a la textura de environmentTexture
644
+ let lightsNumber = 1; // At least one light coming from the environment texture contribution
439
645
  this.#scene.lights.forEach((light) => {
440
646
  if (light.isEnabled()) {
441
647
  ++lightsNumber;
@@ -543,7 +749,7 @@ export default class BabylonJSController {
543
749
  this.#engine.dispose();
544
750
  this.#engine = this.#scene = this.#camera = null;
545
751
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
546
- this.#shadowGen = null;
752
+ this.#shadowGen = [];
547
753
  }
548
754
 
549
755
  /**
@@ -689,7 +895,7 @@ export default class BabylonJSController {
689
895
  const modelContainer = this.#containers.model;
690
896
  const environmentContainer = this.#containers.environment;
691
897
 
692
- if (!cameraState.isPending && !modelContainer.state.isPending && !environmentContainer.state.isPending) {
898
+ if (!cameraState.isPending && !modelContainer.state.isSuccess && !environmentContainer.state.isSuccess) {
693
899
  return false;
694
900
  }
695
901
 
@@ -733,6 +939,7 @@ export default class BabylonJSController {
733
939
  cameraState.setSuccess(true);
734
940
  }
735
941
  }
942
+ this.#scene.activeCamera?.detachControl();
736
943
  if (!cameraState.locked && cameraState.value !== null) {
737
944
  camera.attachControl(this.#canvas, true);
738
945
  }
@@ -740,6 +947,21 @@ export default class BabylonJSController {
740
947
  return true;
741
948
  }
742
949
 
950
+ /**
951
+ * Applies pending image-based lighting (IBL) option updates.
952
+ * Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
953
+ * @private
954
+ * @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
955
+ */
956
+ #setOptions_IBL() {
957
+ if (this.#options.ibl.isPending) {
958
+ this.#options.ibl.setSuccess(true);
959
+ this.#createLights();
960
+ return true;
961
+ }
962
+ return false;
963
+ }
964
+
743
965
  /**
744
966
  * Finds and returns the asset container object by its name.
745
967
  * @private
@@ -915,8 +1137,9 @@ export default class BabylonJSController {
915
1137
  if (!this.#gltfResolver) {
916
1138
  this.#gltfResolver = new GLTFResolver();
917
1139
  }
918
-
919
- let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, container.state.size, container.state.timeStamp);
1140
+ //let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, container.state.size, container.state.timeStamp);
1141
+ // TEMPORARY: We never pass 'size' or 'timeStamp' to always force a reload and avoid issues with the active camera in reloading assetContainers
1142
+ let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, 0, null);
920
1143
  if (!sourceData) {
921
1144
  return [container, false];
922
1145
  }
@@ -949,6 +1172,7 @@ export default class BabylonJSController {
949
1172
  */
950
1173
  async #loadContainers() {
951
1174
  this.#stopRender();
1175
+ this.#scene.postProcessRenderPipelineManager?.dispose();
952
1176
 
953
1177
  let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
954
1178
  let newModelMetadata = {};
@@ -986,6 +1210,7 @@ export default class BabylonJSController {
986
1210
 
987
1211
  this.#setOptions_Materials();
988
1212
  this.#setOptions_Camera();
1213
+ this.#setOptions_IBL();
989
1214
  this.#setVisibilityOfWallAndFloorInModel();
990
1215
  detail.success = true;
991
1216
  })
@@ -998,13 +1223,25 @@ export default class BabylonJSController {
998
1223
  .finally(async () => {
999
1224
  this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
1000
1225
  this.#setMaxSimultaneousLights();
1001
- this.#initializeShadows();
1226
+ this.#loadCameraDepentEffects();
1002
1227
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
1003
1228
  this.#startRender();
1004
1229
  });
1005
1230
  return detail;
1006
1231
  }
1007
1232
 
1233
+ /**
1234
+ * Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
1235
+ * Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
1236
+ * @private
1237
+ * @returns {void}
1238
+ */
1239
+ #loadCameraDepentEffects() {
1240
+ this.#initializeVisualImprovements();
1241
+ this.#initializeAmbientOcclussion();
1242
+ this.#initializeShadows();
1243
+ }
1244
+
1008
1245
  /**
1009
1246
  * Checks and applies model metadata changes after asset loading.
1010
1247
  * @private
@@ -1233,6 +1470,17 @@ export default class BabylonJSController {
1233
1470
  this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
1234
1471
  this.#engine.disableUniformBuffers = true;
1235
1472
  this.#scene = new Scene(this.#engine);
1473
+
1474
+ // Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
1475
+ // SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
1476
+ // different buffers (depth, normals, velocity) for later use in shaders.
1477
+ const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
1478
+ if (geometryBufferRenderer) {
1479
+ geometryBufferRenderer.enableScreenspaceDepth = true;
1480
+ geometryBufferRenderer.enableDepth = false;
1481
+ geometryBufferRenderer.generateNormalsInWorldSpace = true;
1482
+ }
1483
+
1236
1484
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
1237
1485
  this.#createCamera();
1238
1486
  this.#createLights();
@@ -1276,6 +1524,7 @@ export default class BabylonJSController {
1276
1524
  setCameraOptions() {
1277
1525
  this.#stopRender();
1278
1526
  const cameraOptionsSetted = this.#setOptions_Camera();
1527
+ this.#loadCameraDepentEffects();
1279
1528
  this.#startRender();
1280
1529
  return cameraOptionsSetted;
1281
1530
  }
@@ -1293,6 +1542,20 @@ export default class BabylonJSController {
1293
1542
  return materialsOptionsSetted;
1294
1543
  }
1295
1544
 
1545
+ /**
1546
+ * Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
1547
+ * Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
1548
+ * @public
1549
+ * @returns {void}
1550
+ */
1551
+ setIBLOptions() {
1552
+ this.#stopRender();
1553
+ const IBLOptionsSetted = this.#setOptions_IBL();
1554
+ this.#loadCameraDepentEffects();
1555
+ this.#startRender();
1556
+ return IBLOptionsSetted;
1557
+ }
1558
+
1296
1559
  /**
1297
1560
  * Sets the visibility of a container (model, environment, etc.) by name.
1298
1561
  * 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,51 @@ 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
+ 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
+ }
345
+ }
346
+ if (options.ibl.shadows !== undefined) {
347
+ shadows = options.ibl.shadows;
348
+ }
349
+ if (options.ibl.intensity !== undefined) {
350
+ intensity = options.ibl.intensity;
351
+ }
352
+
353
+ const needUpdate = url !== undefined && url !== iblState.url ||
354
+ timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
355
+ shadows !== undefined && shadows !== iblState.shadows ||
356
+ intensity !== undefined && intensity !== iblState.intensity;
357
+ if (needUpdate) {
358
+ iblState.setValues(url, intensity, shadows, timeStamp);
359
+ iblState.setPending(true);
360
+ }
361
+
362
+ return needUpdate;
363
+ }
364
+
309
365
  /**
310
366
  * Dispatches a "prefviewer3d-loading" event and updates loading state attributes.
311
367
  * Used internally when a loading operation starts.
@@ -490,6 +546,7 @@ export default class PrefViewer3D extends HTMLElement {
490
546
  if (config.options) {
491
547
  this.#checkNeedToUpdateCamera(config.options);
492
548
  this.#checkNeedToUpdateMaterials(config.options);
549
+ this.#checkNeedToUpdateIBL(config.options);
493
550
  }
494
551
 
495
552
  const loadDetail = await this.#babylonJSController.load();
@@ -518,6 +575,9 @@ export default class PrefViewer3D extends HTMLElement {
518
575
  if (this.#checkNeedToUpdateMaterials(options)) {
519
576
  someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
520
577
  }
578
+ if (this.#checkNeedToUpdateIBL(options)) {
579
+ someSetted = someSetted || this.#babylonJSController.setIBLOptions();
580
+ }
521
581
  const detail = this.#onSetOptions();
522
582
  return { success: someSetted, detail: detail };
523
583
  }