Repository: BabylonJS/Babylon.js
Pull request #16527: IBL Shadows - Init passes only after voxelization
Author: login: MiiBond
Created: 2025-04-24T22:04:25Z | Merged: 2025-04-30T16:28:11Z

Okay, I've been investigating the issue where the shadow accumulation isn't working initially after loading the [IBL Shadow Demo playground](https://playground.babylonjs.com/#8R5SSE#618). On some loads, the shadows will appear noisy and not accumulate over multiple frames. Note that this issue never happens if you have Spector.js enabled, which is why I had such a hard time reproducing it originally.
The actual issue is that the uniform, `accumulationParameters` fails to be set with 
`WebGL: INVALID_OPERATION: uniform4f: location is not from the associated program`

Even though this fails, the uniform value is still cached and Babylon doesn't set the uniform again until the value changes. That is why accumulation appears to be disabled until you move the camera (which causes the uniform to change and update).
I have not been able to figure out _why_ the uniform set fails in the first place. It seems to be consistently the 3rd render that fails. Before that, the uniform is set successfully and, as far as I can tell, the shader program isn't updated after that point. And after the failure, the uniform can be set again.

The one thing that I was able to change to make the issue seem to go away was to put enough of a delay on the rendering of the accumulation pass. I've added logic to only start rendering the accumulation pass once the first voxelization is complete. This is fine since we can't render shadows until this is done anyway but I don't know why it should fix the problem.


Diff:
diff --git a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
index bb9d975c242c..77c10ecb154c 100644
--- a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
+++ b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
@@ -10,6 +10,8 @@ import { ProceduralTexture } from "core/Materials/Textures/Procedurals/procedura
 import type { IProceduralTextureCreationOptions } from "core/Materials/Textures/Procedurals/proceduralTexture";
 import type { IblShadowsRenderPipeline } from "./iblShadowsRenderPipeline";
 import { Observable } from "../../Misc/observable";
+import type { EventState } from "../../Misc/observable";
+import type { Nullable } from "../../types";
 
 /**
  * This should not be instanciated directly, as it is part of a scene component
@@ -207,17 +209,10 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
-                }
-            }
+        this._renderWhenGBufferReady = this._render.bind(this);
+        // Don't start rendering until the first vozelization is done.
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(this._renderWhenGBufferReady);
         });
 
         // Create the accumulation texture for the previous frame.
@@ -284,7 +279,7 @@ export class _IblShadowsAccumulationPass {
         this._oldPositionCopy.onBeforeGenerationObservable.add(this._updatePositionCopy.bind(this));
     }
 
-    private _setOutputTextureBindings() {
+    private _setOutputTextureBindings(): boolean {
         const remanence = this._isMoving ? this.remanence : 0.99;
         this._accumulationParams.set(remanence, this.reset ? 1.0 : 0.0, this._renderPipeline.voxelGridSize, 0.0);
         this._outputTexture.setTexture("spatialBlurSampler", this._renderPipeline._getSpatialBlurTexture());
@@ -294,7 +289,7 @@ export class _IblShadowsAccumulationPass {
 
         const geometryBufferRenderer = this._scene.geometryBufferRenderer;
         if (!geometryBufferRenderer) {
-            return;
+            return false;
         }
         const velocityIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.VELOCITY_LINEAR_TEXTURE_TYPE);
         this._outputTexture.setTexture("motionSampler", geometryBufferRenderer.getGBuffer().textures[velocityIndex]);
@@ -303,6 +298,7 @@ export class _IblShadowsAccumulationPass {
 
         this.reset = false;
         this._isMoving = false;
+        return true;
     }
 
     private _updatePositionCopy() {
@@ -315,6 +311,16 @@ export class _IblShadowsAccumulationPass {
         this._oldAccumulationCopy.setTexture("textureSampler", this._outputTexture);
     }
 
+    private _render() {
+        if (this.enabled && this._outputTexture.isReady() && this._outputTexture.getEffect()?.isReady()) {
+            if (this._setOutputTextureBindings()) {
+                this._outputTexture.render();
+            }
+        }
+    }
+
+    private _renderWhenGBufferReady: Nullable<(eventData: number, eventState: EventState) => void> = null;
+
     /**
      * Called by render pipeline when canvas resized.
      * @param scaleFactor The factor by which to scale the canvas size.
@@ -324,6 +330,10 @@ export class _IblShadowsAccumulationPass {
             width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
             height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
         };
+        // Don't resize if the size is the same as the current size.
+        if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
+            return;
+        }
         this._outputTexture.resize(newSize, false);
         this._oldAccumulationCopy.resize(newSize, false);
         this._oldPositionCopy.resize({ width: this._engine.getRenderWidth(), height: this._engine.getRenderHeight() }, false);
@@ -355,6 +365,10 @@ export class _IblShadowsAccumulationPass {
      * Disposes the associated resources
      */
     public dispose() {
+        if (this._scene.geometryBufferRenderer && this._renderWhenGBufferReady) {
+            const gBuffer = this._scene.geometryBufferRenderer.getGBuffer();
+            gBuffer.onAfterRenderObservable.removeCallback(this._renderWhenGBufferReady);
+        }
         this._disposeTextures();
         if (this._debugPassPP) {
             this._debugPassPP.dispose();
diff --git a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsRenderPipeline.ts b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsRenderPipeline.ts
index 4027e36fc392..23e16d9f94e5 100644
--- a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsRenderPipeline.ts
+++ b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsRenderPipeline.ts
@@ -1125,6 +1125,7 @@ export class IblShadowsRenderPipeline extends PostProcessRenderPipeline {
         this._dummyTexture3d.dispose();
         this.onNewIblReadyObservable.clear();
         this.onShadowTextureReadyObservable.clear();
+        this.onVoxelizationCompleteObservable.clear();
         super.dispose();
     }
 }
diff --git a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
index b0ac0b360ca3..7c9054832905 100644
--- a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
+++ b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
@@ -9,6 +9,8 @@ import { GeometryBufferRenderer } from "../../Rendering/geometryBufferRenderer";
 import { ProceduralTexture } from "core/Materials/Textures/Procedurals/proceduralTexture";
 import type { IProceduralTextureCreationOptions } from "core/Materials/Textures/Procedurals/proceduralTexture";
 import type { IblShadowsRenderPipeline } from "./iblShadowsRenderPipeline";
+import type { EventState } from "../../Misc/observable";
+import type { Nullable } from "../../types";
 
 /**
  * This should not be instanciated directly, as it is part of a scene component
@@ -161,35 +163,39 @@ export class _IblShadowsSpatialBlurPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setBindings();
-                    this._outputTexture.render();
-                }
-            }
+        this._renderWhenGBufferReady = this._render.bind(this);
+        // Don't start rendering until the first vozelization is done.
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(this._renderWhenGBufferReady);
         });
     }
 
-    private _setBindings() {
+    private _setBindings(): boolean {
         this._outputTexture.setTexture("voxelTracingSampler", this._renderPipeline._getVoxelTracingTexture());
         const iterationCount = 1;
         this._blurParameters.set(iterationCount, this._worldScale, 0.0, 0.0);
         this._outputTexture.setVector4("blurParameters", this._blurParameters);
         const geometryBufferRenderer = this._scene.geometryBufferRenderer;
         if (!geometryBufferRenderer) {
-            return;
+            return false;
         }
         const depthIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.SCREENSPACE_DEPTH_TEXTURE_TYPE);
         this._outputTexture.setTexture("depthSampler", geometryBufferRenderer.getGBuffer().textures[depthIndex]);
         const wnormalIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.NORMAL_TEXTURE_TYPE);
         this._outputTexture.setTexture("worldNormalSampler", geometryBufferRenderer.getGBuffer().textures[wnormalIndex]);
+        return true;
+    }
+
+    private _render() {
+        if (this.enabled && this._outputTexture.isReady() && this._outputTexture.getEffect()?.isReady()) {
+            if (this._setBindings()) {
+                this._outputTexture.render();
+            }
+        }
     }
 
+    private _renderWhenGBufferReady: Nullable<(eventData: number, eventState: EventState) => void> = null;
+
     /**
      * Called by render pipeline when canvas resized.
      * @param scaleFactor The factor by which to scale the canvas size.
@@ -199,6 +205,10 @@ export class _IblShadowsSpatialBlurPass {
             width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
             height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
         };
+        // Don't resize if the size is the same as the current size.
+        if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
+            return;
+        }
         this._outputTexture.resize(newSize, false);
     }
 
@@ -214,6 +224,10 @@ export class _IblShadowsSpatialBlurPass {
      * Disposes the associated resources
      */
     public dispose() {
+        if (this._scene.geometryBufferRenderer && this._renderWhenGBufferReady) {
+            const gBuffer = this._scene.geometryBufferRenderer.getGBuffer();
+            gBuffer.onAfterRenderObservable.removeCallback(this._renderWhenGBufferReady);
+        }
         this._outputTexture.dispose();
         if (this._debugPassPP) {
             this._debugPassPP.dispose();
diff --git a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsVoxelTracingPass.ts b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsVoxelTracingPass.ts
index 028c152955bc..fca811c34d5e 100644
--- a/packages/dev/core/src/Rendering/IBLShadows/iblShadowsVoxelTracingPass.ts
+++ b/packages/dev/core/src/Rendering/IBLShadows/iblShadowsVoxelTracingPass.ts
@@ -11,6 +11,9 @@ import { GeometryBufferRenderer } from "../../Rendering/geometryBufferRenderer";
 import { ProceduralTexture } from "core/Materials/Textures/Procedurals/proceduralTexture";
 import type { IProceduralTextureCreationOptions } from "core/Materials/Textures/Procedurals/proceduralTexture";
 import type { CubeTexture } from "../../Materials/Textures/cubeTexture";
+import { Logger } from "../../Misc/logger";
+import type { EventState } from "../../Misc/observable";
+import type { Nullable } from "../../types";
 
 /**
  * Build cdf maps for IBL importance sampling during IBL shadow computation.
@@ -322,18 +325,10 @@ export class _IblShadowsVoxelTracingPass {
         this._outputTexture.defines = defines;
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setBindings(this._scene.activeCamera!);
-
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setBindings(this._scene.activeCamera!);
-                    this._outputTexture.render();
-                }
-            }
+        this._renderWhenGBufferReady = this._render.bind(this);
+        // Don't start rendering until the first vozelization is done.
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(this._renderWhenGBufferReady);
         });
     }
 
@@ -351,7 +346,7 @@ export class _IblShadowsVoxelTracingPass {
         return defines;
     }
 
-    private _setBindings(camera: Camera) {
+    private _setBindings(camera: Camera): boolean {
         this._outputTexture.defines = this._createDefines();
         this._outputTexture.setMatrix("viewMtx", camera.getViewMatrix());
         this._outputTexture.setMatrix("projMtx", camera.getProjectionMatrix());
@@ -384,23 +379,37 @@ export class _IblShadowsVoxelTracingPass {
         this._outputTexture.setTexture("voxelGridSampler", voxelGrid);
         this._outputTexture.setTexture("blueNoiseSampler", this._renderPipeline!._getNoiseTexture());
         const cdfGenerator = this._scene.iblCdfGenerator;
-        if (cdfGenerator) {
-            this._outputTexture.setTexture("icdfSampler", cdfGenerator.getIcdfTexture());
+        if (!cdfGenerator) {
+            Logger.Warn("IBLShadowsVoxelTracingPass: Can't bind for render because iblCdfGenerator is not enabled.");
+            return false;
         }
+        this._outputTexture.setTexture("icdfSampler", cdfGenerator.getIcdfTexture());
         if (this._coloredShadows && this._scene.environmentTexture) {
             this._outputTexture.setTexture("iblSampler", this._scene.environmentTexture);
         }
 
         const geometryBufferRenderer = this._scene.geometryBufferRenderer;
         if (!geometryBufferRenderer) {
-            return;
+            Logger.Warn("IBLShadowsVoxelTracingPass: Can't bind for render because GeometryBufferRenderer is not enabled.");
+            return false;
         }
         const depthIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.SCREENSPACE_DEPTH_TEXTURE_TYPE);
         this._outputTexture.setTexture("depthSampler", geometryBufferRenderer.getGBuffer().textures[depthIndex]);
         const wnormalIndex = geometryBufferRenderer.getTextureIndex(GeometryBufferRenderer.NORMAL_TEXTURE_TYPE);
         this._outputTexture.setTexture("worldNormalSampler", geometryBufferRenderer.getGBuffer().textures[wnormalIndex]);
+        return true;
     }
 
+    private _render() {
+        if (this.enabled && this._outputTexture.isReady() && this._outputTexture.getEffect()?.isReady()) {
+            if (this._setBindings(this._scene.activeCamera!)) {
+                this._outputTexture.render();
+            }
+        }
+    }
+
+    private _renderWhenGBufferReady: Nullable<(eventData: number, eventState: EventState) => void> = null;
+
     /**
      * Called by render pipeline when canvas resized.
      * @param scaleFactor The factor by which to scale the canvas size.
@@ -410,6 +419,10 @@ export class _IblShadowsVoxelTracingPass {
             width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
             height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
         };
+        // Don't resize if the size is the same as the current size.
+        if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
+            return;
+        }
         this._outputTexture.resize(newSize, false);
     }
 
@@ -431,6 +444,10 @@ export class _IblShadowsVoxelTracingPass {
      * Disposes the associated resources
      */
     public dispose() {
+        if (this._scene.geometryBufferRenderer && this._renderWhenGBufferReady) {
+            const gBuffer = this._scene.geometryBufferRenderer.getGBuffer();
+            gBuffer.onAfterRenderObservable.removeCallback(this._renderWhenGBufferReady);
+        }
         this._outputTexture.dispose();
         if (this._debugPassPP) {
             this._debugPassPP.dispose();


Reviews:
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T22:06:04Z
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T22:12:04Z
user: login: Popov72
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T09:31:59Z
user: login: sebavan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T19:25:45Z
user: login: Popov72
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:41:35Z
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T15:59:23Z
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T16:14:56Z
user: login: sebavan
body: Just a tiny fix and good to go
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T19:19:48Z
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T22:59:36Z
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T22:59:41Z
user: login: MiiBond
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T22:59:45Z

Review comments:
diff_hunk: @@ -199,6 +203,10 @@ export class _IblShadowsSpatialBlurPass {
             width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
             height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
         };
+        // Don't resize if the size is the same as the current size.
+        if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
user: login: MiiBond
body: I also added logic to prevent resizing if the size hasn't actually changed. I assume that resizing the procedural texture may result in the shader program needing to be recreated so I was trying to prevent this from happening.
created_at: 2025-04-24T22:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,22 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            let counter = 0;
+            this._scene.onBeforeRenderObservable.add(() => {
+                counter = 0;
+            });
+            // onAfterRenderTargetsRenderObservable is called twice during a frame and we only want to render
+            // on the second call, after the scene has been rendered to the GBuffer.
+            this._scene.onAfterRenderTargetsRenderObservable.add(() => {
+                if (++counter == 2) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: MiiBond
body: I still don't like this counter logic and I feel like it might be related to the problem. The `onAfterRenderTargetsRenderObservable` fires twice during a frame and I only want to run these passes once, after the geometry buffer is rendered and before the main render. Is there a better way to do this?
created_at: 2025-04-24T22:12:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,22 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            let counter = 0;
+            this._scene.onBeforeRenderObservable.add(() => {
+                counter = 0;
+            });
+            // onAfterRenderTargetsRenderObservable is called twice during a frame and we only want to render
+            // on the second call, after the scene has been rendered to the GBuffer.
+            this._scene.onAfterRenderTargetsRenderObservable.add(() => {
+                if (++counter == 2) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: Popov72
body: I don't think this is related to the problem, although I do agree it's a bit ugly...

The second `onAfterRenderTargetsRenderObservable` may not be notified if there is no camera in the scene, but I don't think this is a problem(?)

At some point, we will need to port the IBL shadows to the frame graph, which will allow us to get rid of this workaround.
created_at: 2025-04-28T09:27:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -199,6 +203,10 @@ export class _IblShadowsSpatialBlurPass {
             width: Math.max(1.0, Math.floor(this._engine.getRenderWidth() * scaleFactor)),
             height: Math.max(1.0, Math.floor(this._engine.getRenderHeight() * scaleFactor)),
         };
+        // Don't resize if the size is the same as the current size.
+        if (this._outputTexture.getSize().width === newSize.width && this._outputTexture.getSize().height === newSize.height) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
user: login: Popov72
body: No, resizing the procedural texture does not trigger a recompilation of the shader, but it is better not to resize the texture if it is not necessary.
created_at: 2025-04-28T09:31:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,22 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            let counter = 0;
+            this._scene.onBeforeRenderObservable.add(() => {
+                counter = 0;
+            });
+            // onAfterRenderTargetsRenderObservable is called twice during a frame and we only want to render
+            // on the second call, after the scene has been rendered to the GBuffer.
+            this._scene.onAfterRenderTargetsRenderObservable.add(() => {
+                if (++counter == 2) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: sebavan
body: About this is there no way to actually hook on the GBuffer onAfterRender observable ?
created_at: 2025-04-28T19:25:45Z
author_association: MEMBER
diff_hunk: @@ -207,17 +207,22 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            let counter = 0;
+            this._scene.onBeforeRenderObservable.add(() => {
+                counter = 0;
+            });
+            // onAfterRenderTargetsRenderObservable is called twice during a frame and we only want to render
+            // on the second call, after the scene has been rendered to the GBuffer.
+            this._scene.onAfterRenderTargetsRenderObservable.add(() => {
+                if (++counter == 2) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: Popov72
body: Ah, I didn't think about it, I was looking at hooks at the scene level!

Yes, @MiiBond you could hook on `geometryBufferRenderer.getGBuffer().onAfterRenderObservable`
created_at: 2025-04-28T19:41:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,22 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            let counter = 0;
+            this._scene.onBeforeRenderObservable.add(() => {
+                counter = 0;
+            });
+            // onAfterRenderTargetsRenderObservable is called twice during a frame and we only want to render
+            // on the second call, after the scene has been rendered to the GBuffer.
+            this._scene.onAfterRenderTargetsRenderObservable.add(() => {
+                if (++counter == 2) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: MiiBond
body: Oh, this is excellent! Thank you.
created_at: 2025-04-29T15:59:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,22 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            let counter = 0;
+            this._scene.onBeforeRenderObservable.add(() => {
+                counter = 0;
+            });
+            // onAfterRenderTargetsRenderObservable is called twice during a frame and we only want to render
+            // on the second call, after the scene has been rendered to the GBuffer.
+            this._scene.onAfterRenderTargetsRenderObservable.add(() => {
+                if (++counter == 2) {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: MiiBond
body: I've made this change now.
created_at: 2025-04-29T16:14:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,14 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(() => {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: sebavan
body: should we not clear the observer on the dispose of the accumulation pass ?
created_at: 2025-04-29T19:18:30Z
author_association: MEMBER
diff_hunk: @@ -161,33 +161,31 @@ export class _IblShadowsSpatialBlurPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(() => {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
user: login: sebavan
body: Same comment than before with a remove observer in the dipose ?
created_at: 2025-04-29T19:19:08Z
author_association: MEMBER
diff_hunk: @@ -323,17 +324,15 @@ export class _IblShadowsVoxelTracingPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setBindings(this._scene.activeCamera!);
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setBindings(this._scene.activeCamera!);
-                    this._outputTexture.render();
+        // Don't start rendering until the first vozelization is done.
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(() => {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsVoxelTracingPass.ts
user: login: sebavan
body: Same and sorry again I did not notice in the past.
created_at: 2025-04-29T19:19:31Z
author_association: MEMBER
diff_hunk: @@ -323,17 +324,15 @@ export class _IblShadowsVoxelTracingPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setBindings(this._scene.activeCamera!);
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setBindings(this._scene.activeCamera!);
-                    this._outputTexture.render();
+        // Don't start rendering until the first vozelization is done.
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(() => {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsVoxelTracingPass.ts
user: login: MiiBond
body: Done
created_at: 2025-04-29T22:59:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -161,33 +161,31 @@ export class _IblShadowsSpatialBlurPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(() => {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsSpatialBlurPass.ts
user: login: MiiBond
body: Done
created_at: 2025-04-29T22:59:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -207,17 +207,14 @@ export class _IblShadowsAccumulationPass {
         // Need to set all the textures first so that the effect gets created with the proper uniforms.
         this._setOutputTextureBindings();
 
-        let counter = 0;
-        this._scene.onBeforeRenderObservable.add(() => {
-            counter = 0;
-        });
-        this._scene.onAfterRenderTargetsRenderObservable.add(() => {
-            if (++counter == 2) {
-                if (this.enabled && this._outputTexture.isReady()) {
-                    this._setOutputTextureBindings();
-                    this._outputTexture.render();
+        this._renderPipeline.onVoxelizationCompleteObservable.addOnce(() => {
+            this._scene.geometryBufferRenderer!.getGBuffer().onAfterRenderObservable.add(() => {
path: packages/dev/core/src/Rendering/IBLShadows/iblShadowsAccumulationPass.ts
user: login: MiiBond
body: Done
created_at: 2025-04-29T22:59:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: bjsplat
created_at: 2025-04-24T22:05:58Z
body: Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.
author_association: COLLABORATOR
user: login: MiiBond
created_at: 2025-04-24T22:12:56Z
body: I think I've verified that the uniform does exist by doing a `gl.getUniform` before the render that results in the error so I'm really at a loss.
author_association: CONTRIBUTOR
user: login: MiiBond
created_at: 2025-04-24T22:20:41Z
body: Another thing to note is that I can't reproduce this issue in Firefox or Safari. It only seems to happen in Chrome and Edge.
author_association: CONTRIBUTOR
user: login: bjsplat
created_at: 2025-04-24T22:21:46Z
body: Snapshot stored with reference name:
refs/pull/16527/merge

Test environment: 
https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/index.html

To test a playground add it to the URL, for example:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/index.html#WGZLGJ#4600

Links to test babylon tools with this snapshot:

https://playground.babylonjs.com/?snapshot=refs/pull/16527/merge
https://sandbox.babylonjs.com/?snapshot=refs/pull/16527/merge
https://gui.babylonjs.com/?snapshot=refs/pull/16527/merge
https://nme.babylonjs.com/?snapshot=refs/pull/16527/merge

To test the snapshot in the playground with a playground ID add it after the snapshot query string:

https://playground.babylonjs.com/?snapshot=refs/pull/16527/merge#BCU1XR#0
author_association: COLLABORATOR
user: login: bjsplat
created_at: 2025-04-24T22:33:56Z
body: WebGL2 visualization test reporter:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/testResults/webgl2playwright/index.html
author_association: COLLABORATOR
user: login: bjsplat
created_at: 2025-04-24T22:34:12Z
body: Visualization tests for WebGPU

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/testResults/webgpuplaywright/index.html
author_association: COLLABORATOR
user: login: Popov72
created_at: 2025-04-28T09:18:43Z
body: It looks like it could be that an effect is used before being ready, when that effect is not ready before at least the next frame, because setting `engine.getCaps().parallelShaderCompile = null;` makes it work (at least I could not reproduce the bug with this line). So, this change could be the fix:

![image](https://github.com/user-attachments/assets/fae707a0-855a-4182-a110-fdcfc3234717)

author_association: CONTRIBUTOR
user: login: MiiBond
created_at: 2025-04-29T16:13:40Z
body: > So, this change could be the fix:
> 
> ![image](https://private-user-images.githubusercontent.com/4152247/438130483-fae707a0-855a-4182-a110-fdcfc3234717.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDU5NDA0NzAsIm5iZiI6MTc0NTk0MDE3MCwicGF0aCI6Ii80MTUyMjQ3LzQzODEzMDQ4My1mYWU3MDdhMC04NTVhLTQxODItYTExMC1mZGNmYzMyMzQ3MTcucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQyOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MjlUMTUyMjUwWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MmI0MGVhMmJkMmRiYjcwMzU1ZDAzZWVhZDQxYjEyYTcyNmZiMDEzNTY0N2I5OWQ5ODEyNWZmMThhMjMxMjNkOSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.BSfNqvSZxK7r8K781hTA8PNJcStyj5SARiH0d61VOgM)

Yes, that was my thought in adding this line. However, it doesn't, by itself, prevent the issue from happening.
author_association: CONTRIBUTOR
user: login: bjsplat
created_at: 2025-04-29T16:48:08Z
body: WebGL2 visualization test reporter:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/testResults/webgl2playwright/index.html
author_association: COLLABORATOR
user: login: bjsplat
created_at: 2025-04-29T16:58:59Z
body: Visualization tests for WebGPU

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/testResults/webgpuplaywright/index.html
author_association: COLLABORATOR
user: login: bjsplat
created_at: 2025-04-29T23:31:16Z
body: WebGL2 visualization test reporter:

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/testResults/webgl2playwright/index.html
author_association: COLLABORATOR
user: login: bjsplat
created_at: 2025-04-29T23:31:36Z
body: Visualization tests for WebGPU

https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/16527/merge/testResults/webgpuplaywright/index.html
author_association: COLLABORATOR
user: login: RaananW
created_at: 2025-04-30T14:22:36Z
body: @sebavan , are we good to merge this?
author_association: MEMBER