@preference-sl/pref-viewer 2.13.0-beta.1 → 2.13.0-beta.2

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.13.0-beta.1",
3
+ "version": "2.13.0-beta.2",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -96,7 +96,13 @@ export default class BabylonJSController {
96
96
 
97
97
  #gltfResolver = null; // GLTFResolver instance
98
98
  #babylonJSAnimationController = null; // AnimationController instance
99
-
99
+
100
+ #renderPipelines = {
101
+ default: null,
102
+ ssao: null,
103
+ iblShadows: null,
104
+ };
105
+
100
106
  #handlers = {
101
107
  onKeyUp: null,
102
108
  onPointerObservable: null,
@@ -412,6 +418,43 @@ export default class BabylonJSController {
412
418
  }
413
419
  }
414
420
 
421
+ /**
422
+ * Detaches and disposes the SSAO render pipeline from the active camera when it exists.
423
+ * Guards against missing scene resources or absent pipelines, returning false when no cleanup is needed.
424
+ * @private
425
+ * @returns {boolean} Returns true when the SSAO pipeline was disabled, false otherwise.
426
+ */
427
+ #disableAmbientOcclusion() {
428
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
429
+
430
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
431
+ return false;
432
+ }
433
+
434
+ const supportedPipelines = pipelineManager.supportedPipelines;
435
+
436
+ if (supportedPipelines === undefined) {
437
+ return false;
438
+ }
439
+
440
+ if (!this.#renderPipelines.ssao) {
441
+ return false;
442
+ }
443
+
444
+ const pipelineName = this.#renderPipelines.ssao.name;
445
+ const ssaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
446
+
447
+ if (ssaoPipeline) {
448
+ pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
449
+ ssaoPipeline.dispose();
450
+ pipelineManager.removePipeline(pipelineName);
451
+ pipelineManager.update();
452
+ this.#renderPipelines.ssao = null;
453
+ }
454
+
455
+ return true;
456
+ }
457
+
415
458
  /**
416
459
  * Rebuilds the SSAO post-process pipeline to inject screenspace ambient occlusion on the active camera.
417
460
  * Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
@@ -420,138 +463,166 @@ export default class BabylonJSController {
420
463
  * @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
421
464
  */
422
465
  #initializeAmbientOcclussion() {
423
- if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
466
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
467
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
424
468
  return false;
425
469
  }
426
470
 
427
471
  if (!this.#settings.ambientOcclusionEnabled) {
428
472
  return false;
429
473
  }
430
-
431
- const pipelineName = "PrefViewerSSAORenderingPipeline";
432
-
433
- const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
434
-
435
- if (!supportedPipelines) {
474
+ const supportedPipelines = pipelineManager.supportedPipelines;
475
+
476
+ if (supportedPipelines === undefined) {
436
477
  return false;
437
478
  }
438
-
439
- const oldSsaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
440
-
441
- if (oldSsaoPipeline) {
442
- oldSsaoPipeline.dispose();
443
- this.#scene.postProcessRenderPipelineManager.update();
444
- }
479
+
480
+ const pipelineName = "PrefViewerSSAORenderingPipeline";
445
481
 
446
482
  const ssaoRatio = {
447
483
  ssaoRatio: 0.5,
448
484
  combineRatio: 1.0
449
485
  };
450
486
 
451
- const ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
487
+ this.#renderPipelines.ssao = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
452
488
 
453
- if (!ssaoPipeline){
489
+ if (!this.#renderPipelines.ssao){
454
490
  return false;
455
491
  }
456
492
 
457
- if (ssaoPipeline.isSupported) {
458
- ssaoPipeline.fallOff = 0.000001;
459
- ssaoPipeline.area = 1;
460
- ssaoPipeline.radius = 0.0001;
461
- ssaoPipeline.totalStrength = 1;
462
- ssaoPipeline.base = 0.6;
493
+ if (this.#renderPipelines.ssao.isSupported) {
494
+ this.#renderPipelines.ssao.fallOff = 0.000001;
495
+ this.#renderPipelines.ssao.area = 1;
496
+ this.#renderPipelines.ssao.radius = 0.0001;
497
+ this.#renderPipelines.ssao.totalStrength = 1;
498
+ this.#renderPipelines.ssao.base = 0.6;
463
499
 
464
500
  // Configure SSAO to calculate only once instead of every frame for better performance
465
- if (ssaoPipeline._ssaoPostProcess) {
466
- ssaoPipeline._ssaoPostProcess.autoClear = false;
467
- ssaoPipeline._ssaoPostProcess.samples = 1;
501
+ if (this.#renderPipelines.ssao._ssaoPostProcess) {
502
+ this.#renderPipelines.ssao._ssaoPostProcess.autoClear = false;
503
+ this.#renderPipelines.ssao._ssaoPostProcess.samples = 1;
504
+ }
505
+ if (this.#renderPipelines.ssao._combinePostProcess) {
506
+ this.#renderPipelines.ssao._combinePostProcess.autoClear = false;
507
+ this.#renderPipelines.ssao._combinePostProcess.samples = 1;
468
508
  }
469
509
 
470
- this.#scene.postProcessRenderPipelineManager.update();
510
+ pipelineManager.update();
471
511
  return true;
472
512
  } else {
473
- ssaoPipeline.dispose();
474
- this.#scene.postProcessRenderPipelineManager.update();
513
+ this.#renderPipelines.ssao.dispose();
514
+ this.#renderPipelines.ssao = null;
515
+ pipelineManager.update();
475
516
  return false;
476
517
  }
477
518
  }
478
519
 
479
520
  /**
480
- * Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
481
- * Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
482
- * `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
521
+ * Tears down the default rendering pipeline (MSAA/FXAA/grain) for the active camera when present.
522
+ * Ensures stale pipelines detach cleanly so a fresh one can be installed on the next load.
483
523
  * @private
484
- * @returns {boolean} True when the pipeline is supported and active, otherwise false.
485
- * @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
524
+ * @returns {boolean} Returns true when the pipeline was removed, false otherwise.
486
525
  */
487
- #initializeVisualImprovements() {
488
- if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
526
+ #disableVisualImprovements() {
527
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
528
+
529
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
489
530
  return false;
490
531
  }
491
532
 
492
- const pipelineName = "PrefViewerDefaultRenderingPipeline";
493
- const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
533
+ const supportedPipelines = pipelineManager.supportedPipelines;
534
+
535
+ if (supportedPipelines === undefined) {
536
+ return false;
537
+ }
494
538
 
495
- if (!supportedPipelines) {
539
+ if (!this.#renderPipelines.default) {
496
540
  return false;
497
541
  }
498
542
 
499
- const oldDefaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
543
+ const pipelineName = this.#renderPipelines.default.name;
544
+ const defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
545
+
546
+ if (defaultPipeline) {
547
+ pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
548
+ defaultPipeline.dispose();
549
+ pipelineManager.removePipeline(pipelineName);
550
+ pipelineManager.update();
551
+ this.#renderPipelines.default = null;
552
+ }
500
553
 
501
- if (oldDefaultPipeline) {
502
- oldDefaultPipeline.dispose();
503
- this.#scene.postProcessRenderPipelineManager.update();
554
+ return true;
555
+ }
556
+
557
+ /**
558
+ * Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
559
+ * Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
560
+ * `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
561
+ * @private
562
+ * @returns {boolean} True when the pipeline is supported and active, otherwise false.
563
+ * @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
564
+ */
565
+ #initializeVisualImprovements() {
566
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
567
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
568
+ return false;
504
569
  }
505
570
 
506
571
  if (!this.#settings.antiAliasingEnabled) {
507
572
  return false;
508
573
  }
574
+ const supportedPipelines = pipelineManager.supportedPipelines;
575
+
576
+ if (supportedPipelines === undefined) {
577
+ return false;
578
+ }
579
+
580
+ const pipelineName = "PrefViewerDefaultRenderingPipeline";
509
581
 
510
- const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
582
+ this.#renderPipelines.default = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
511
583
 
512
- if (!defaultPipeline){
584
+ if (!this.#renderPipelines.default){
513
585
  return false;
514
586
  }
515
587
 
516
- if (defaultPipeline.isSupported) {
588
+ if (this.#renderPipelines.default.isSupported) {
517
589
  // MSAA - Multisample Anti-Aliasing
518
590
  const caps = this.#scene.getEngine()?.getCaps?.() || {};
519
591
  const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
520
- defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
521
-
592
+ this.#renderPipelines.default.samples = Math.max(1, Math.min(8, maxSamples));
522
593
  // FXAA - Fast Approximate Anti-Aliasing
523
- defaultPipeline.fxaaEnabled = true;
524
- defaultPipeline.fxaa.samples = 8;
525
- defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
526
- if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
527
- defaultPipeline.fxaa.edgeThreshold = 0.125;
594
+ this.#renderPipelines.default.fxaaEnabled = true;
595
+ this.#renderPipelines.default.fxaa.samples = 8;
596
+ this.#renderPipelines.default.fxaa.adaptScaleToCurrentViewport = true;
597
+ if (this.#renderPipelines.default.fxaa.edgeThreshold !== undefined) {
598
+ this.#renderPipelines.default.fxaa.edgeThreshold = 0.125;
528
599
  }
529
- if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
530
- defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
600
+ if (this.#renderPipelines.default.fxaa.edgeThresholdMin !== undefined) {
601
+ this.#renderPipelines.default.fxaa.edgeThresholdMin = 0.0625;
531
602
  }
532
- if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
533
- defaultPipeline.fxaa.subPixelQuality = 0.75;
603
+ if (this.#renderPipelines.default.fxaa.subPixelQuality !== undefined) {
604
+ this.#renderPipelines.default.fxaa.subPixelQuality = 0.75;
534
605
  }
535
606
 
536
607
  // Grain
537
- defaultPipeline.grainEnabled = true;
538
- defaultPipeline.grain.adaptScaleToCurrentViewport = true;
539
- defaultPipeline.grain.animated = false;
540
- defaultPipeline.grain.intensity = 3;
608
+ this.#renderPipelines.default.grainEnabled = true;
609
+ this.#renderPipelines.default.grain.adaptScaleToCurrentViewport = true;
610
+ this.#renderPipelines.default.grain.animated = false;
611
+ this.#renderPipelines.default.grain.intensity = 3;
541
612
 
542
613
  // Configure post-processes to calculate only once instead of every frame for better performance
543
- if (defaultPipeline.fxaa?._postProcess) {
544
- defaultPipeline.fxaa._postProcess.autoClear = false;
614
+ if (this.#renderPipelines.default.fxaa?._postProcess) {
615
+ this.#renderPipelines.default.fxaa._postProcess.autoClear = false;
545
616
  }
546
- if (defaultPipeline.grain?._postProcess) {
547
- defaultPipeline.grain._postProcess.autoClear = false;
617
+ if (this.#renderPipelines.default.grain?._postProcess) {
618
+ this.#renderPipelines.default.grain._postProcess.autoClear = false;
548
619
  }
549
620
 
550
- this.#scene.postProcessRenderPipelineManager.update();
621
+ pipelineManager.update();
551
622
  return true;
552
623
  } else {
553
- defaultPipeline.dispose();
554
- this.#scene.postProcessRenderPipelineManager.update();
624
+ this.#renderPipelines.default.dispose();
625
+ pipelineManager.update();
555
626
  return false;
556
627
  }
557
628
  }
@@ -571,6 +642,43 @@ export default class BabylonJSController {
571
642
  return true;
572
643
  }
573
644
 
645
+ /**
646
+ * Removes the IBL shadow render pipeline from the active camera when present.
647
+ * Ensures voxelized shadow data is disposed so reloading environments installs a clean pipeline.
648
+ * @private
649
+ * @returns {boolean} Returns true when the pipeline was removed, false otherwise.
650
+ */
651
+ #disableIBLShadows() {
652
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
653
+
654
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
655
+ return false;
656
+ }
657
+
658
+ const supportedPipelines = pipelineManager.supportedPipelines;
659
+
660
+ if (supportedPipelines === undefined) {
661
+ return false;
662
+ }
663
+
664
+ if (!this.#renderPipelines.iblShadows) {
665
+ return false;
666
+ }
667
+
668
+ const pipelineName = this.#renderPipelines.iblShadows.name;
669
+ const defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
670
+
671
+ if (defaultPipeline) {
672
+ pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
673
+ defaultPipeline.dispose();
674
+ pipelineManager.removePipeline(pipelineName);
675
+ pipelineManager.update();
676
+ this.#renderPipelines.iblShadows = null;
677
+ }
678
+
679
+ return true;
680
+ }
681
+
574
682
  /**
575
683
  * Initializes the Image-Based Lighting (IBL) shadows for the Babylon.js scene.
576
684
  * Creates an IBL shadow render pipeline and adds all relevant meshes and materials for shadow casting and receiving.
@@ -580,27 +688,22 @@ export default class BabylonJSController {
580
688
  * @returns {void|false} Returns false if no environment texture is set; otherwise void.
581
689
  */
582
690
  #initializeIBLShadows() {
583
- if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
691
+ const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
692
+
693
+ if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
584
694
  return false;
585
695
  }
586
696
 
587
- // if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
588
697
  if (!this.#scene.environmentTexture) {
589
698
  return false;
590
699
  }
591
- const pipelineName = "PrefViewerIblShadowsRenderPipeline";
592
- const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
593
-
700
+ const supportedPipelines = pipelineManager.supportedPipelines;
701
+
594
702
  if (!supportedPipelines) {
595
703
  return false;
596
704
  }
597
-
598
- const oldIblShadowsRenderPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
599
-
600
- if (oldIblShadowsRenderPipeline) {
601
- oldIblShadowsRenderPipeline.dispose();
602
- this.#scene.postProcessRenderPipelineManager.update();
603
- }
705
+
706
+ const pipelineName = "PrefViewerIblShadowsRenderPipeline";
604
707
 
605
708
  const pipelineOptions = {
606
709
  resolutionExp: 1, // Higher resolution for better shadow quality (recomended 8)
@@ -611,13 +714,13 @@ export default class BabylonJSController {
611
714
  shadowOpacity: 0.85,
612
715
  };
613
716
 
614
- const iblShadowsRenderPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
717
+ this.#renderPipelines.iblShadows = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
615
718
 
616
- if (!iblShadowsRenderPipeline) {
719
+ if (!this.#renderPipelines.iblShadows) {
617
720
  return false;
618
721
  }
619
722
 
620
- if (iblShadowsRenderPipeline.isSupported) {
723
+ if (this.#renderPipelines.iblShadows.isSupported) {
621
724
  // Disable all debug passes for performance
622
725
  const pipelineProps = {
623
726
  allowDebugPasses: false,
@@ -631,11 +734,11 @@ export default class BabylonJSController {
631
734
  accumulationPassDebugEnabled: false,
632
735
  };
633
736
 
634
- Object.assign(iblShadowsRenderPipeline, pipelineProps);
737
+ Object.assign(this.#renderPipelines.iblShadows, pipelineProps);
635
738
 
636
- if (iblShadowsRenderPipeline._ssaoPostProcess) {
637
- iblShadowsRenderPipeline._ssaoPostProcess.autoClear = false;
638
- iblShadowsRenderPipeline._ssaoPostProcess.samples = 1;
739
+ if (this.#renderPipelines.iblShadows._ssaoPostProcess) {
740
+ this.#renderPipelines.iblShadows._ssaoPostProcess.autoClear = false;
741
+ this.#renderPipelines.iblShadows._ssaoPostProcess.samples = 1;
639
742
  }
640
743
 
641
744
  this.#scene.meshes.forEach((mesh) => {
@@ -649,8 +752,8 @@ export default class BabylonJSController {
649
752
  const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
650
753
 
651
754
  if (meshGenerateShadows) {
652
- iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
653
- iblShadowsRenderPipeline.updateSceneBounds();
755
+ this.#renderPipelines.iblShadows.addShadowCastingMesh(mesh);
756
+ this.#renderPipelines.iblShadows.updateSceneBounds();
654
757
  }
655
758
  });
656
759
 
@@ -658,15 +761,15 @@ export default class BabylonJSController {
658
761
  if (material instanceof PBRMaterial) {
659
762
  material.enableSpecularAntiAliasing = false;
660
763
  }
661
- iblShadowsRenderPipeline.addShadowReceivingMaterial(material);
764
+ this.#renderPipelines.iblShadows.addShadowReceivingMaterial(material);
662
765
  });
663
766
 
664
- iblShadowsRenderPipeline.updateVoxelization();
665
- this.#scene.postProcessRenderPipelineManager.update();
767
+ this.#renderPipelines.iblShadows.updateVoxelization();
768
+ pipelineManager.update();
666
769
  return true;
667
770
  } else {
668
- iblShadowsRenderPipeline.dispose();
669
- this.#scene.postProcessRenderPipelineManager.update();
771
+ this.#renderPipelines.iblShadows.dispose();
772
+ pipelineManager.update();
670
773
  return false;
671
774
  }
672
775
  }
@@ -700,7 +803,6 @@ export default class BabylonJSController {
700
803
  * @returns {void}
701
804
  */
702
805
  #initializeDefaultLightShadows() {
703
- this.#shadowGen = [];
704
806
  if (!this.#dirLight) {
705
807
  return;
706
808
  }
@@ -766,6 +868,20 @@ export default class BabylonJSController {
766
868
  });
767
869
  }
768
870
 
871
+ /**
872
+ * Disposes every active shadow generator plus the IBL shadow pipeline to avoid stale casters across reloads.
873
+ * Clears the cached `#shadowGen` array so subsequent loads can rebuild fresh generators.
874
+ * @private
875
+ * @returns {void}
876
+ */
877
+ #disableShadows() {
878
+ this.#shadowGen.forEach((shadowGenerator) => {
879
+ shadowGenerator.dispose();
880
+ });
881
+ this.#shadowGen = [];
882
+ this.#disableIBLShadows();
883
+ }
884
+
769
885
  /**
770
886
  * Initializes shadows for the Babylon.js scene.
771
887
  * @private
@@ -913,7 +1029,6 @@ export default class BabylonJSController {
913
1029
  this.#engine.dispose();
914
1030
  this.#engine = this.#scene = this.#camera = null;
915
1031
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
916
- this.#shadowGen = [];
917
1032
  }
918
1033
 
919
1034
  /**
@@ -1281,6 +1396,7 @@ export default class BabylonJSController {
1281
1396
  */
1282
1397
  #stopRender() {
1283
1398
  this.#engine.stopRenderLoop(this.#handlers.renderLoop);
1399
+ this.#unloadCameraDependentEffects();
1284
1400
  }
1285
1401
  /**
1286
1402
  * Starts the Babylon.js render loop for the current scene.
@@ -1289,6 +1405,7 @@ export default class BabylonJSController {
1289
1405
  * @returns {Promise<void>}
1290
1406
  */
1291
1407
  async #startRender() {
1408
+ this.#loadCameraDependentEffects();
1292
1409
  await this.#scene.whenReadyAsync();
1293
1410
  this.#engine.runRenderLoop(this.#handlers.renderLoop);
1294
1411
  }
@@ -1359,7 +1476,6 @@ export default class BabylonJSController {
1359
1476
  */
1360
1477
  async #loadContainers() {
1361
1478
  this.#stopRender();
1362
- this.#scene.postProcessRenderPipelineManager?.dispose();
1363
1479
 
1364
1480
  let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
1365
1481
  let newModelMetadata = {};
@@ -1385,6 +1501,13 @@ export default class BabylonJSController {
1385
1501
  assetContainer.lights = [];
1386
1502
  newModelMetadata = { ...(container.state.update.metadata ?? {}) };
1387
1503
  }
1504
+ if (container.state.name === "model" || container.state.name === "environment") {
1505
+ assetContainer.cameras.forEach((camera) => {
1506
+ // To avoid conflicts when reloading the model we rename the id because Babylon.js caches the camera's SSAO effect by id.
1507
+ const sufix = "_" + Date.now();
1508
+ camera.id = `${camera.id || camera.name || "camera"}${sufix}`;
1509
+ });
1510
+ }
1388
1511
  this.#replaceContainer(container, assetContainer);
1389
1512
  container.state.setSuccess(true);
1390
1513
  } else {
@@ -1410,7 +1533,6 @@ export default class BabylonJSController {
1410
1533
  .finally(async () => {
1411
1534
  this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
1412
1535
  this.#setMaxSimultaneousLights();
1413
- this.#loadCameraDepentEffects();
1414
1536
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
1415
1537
  this.#startRender();
1416
1538
  });
@@ -1423,12 +1545,24 @@ export default class BabylonJSController {
1423
1545
  * @private
1424
1546
  * @returns {void}
1425
1547
  */
1426
- #loadCameraDepentEffects() {
1548
+ #loadCameraDependentEffects() {
1427
1549
  this.#initializeVisualImprovements();
1428
1550
  this.#initializeAmbientOcclussion();
1429
1551
  this.#initializeShadows();
1430
1552
  }
1431
1553
 
1554
+ /**
1555
+ * Shuts down every post-process tied to the active camera before stopping the render loop.
1556
+ * Ensures AA, SSAO, and shadow resources detach cleanly so reloads rebuild from scratch.
1557
+ * @private
1558
+ * @returns {void}
1559
+ */
1560
+ #unloadCameraDependentEffects() {
1561
+ this.#disableVisualImprovements();
1562
+ this.#disableAmbientOcclusion();
1563
+ this.#disableShadows();
1564
+ }
1565
+
1432
1566
  /**
1433
1567
  * Checks and applies model metadata changes after asset loading.
1434
1568
  * @private
@@ -1690,6 +1824,7 @@ export default class BabylonJSController {
1690
1824
  this.#disableInteraction();
1691
1825
  this.#disposeAnimationController();
1692
1826
  this.#disposeXRExperience();
1827
+ this.#unloadCameraDependentEffects();
1693
1828
  this.#disposeEngine();
1694
1829
  }
1695
1830
 
@@ -1844,7 +1979,6 @@ export default class BabylonJSController {
1844
1979
  setCameraOptions() {
1845
1980
  this.#stopRender();
1846
1981
  const cameraOptionsSetted = this.#setOptions_Camera();
1847
- this.#loadCameraDepentEffects();
1848
1982
  this.#startRender();
1849
1983
  return cameraOptionsSetted;
1850
1984
  }
@@ -1871,7 +2005,6 @@ export default class BabylonJSController {
1871
2005
  setIBLOptions() {
1872
2006
  this.#stopRender();
1873
2007
  const IBLOptionsSetted = this.#setOptions_IBL();
1874
- this.#loadCameraDepentEffects();
1875
2008
  this.#startRender();
1876
2009
  return IBLOptionsSetted;
1877
2010
  }