Enabling shadow generator (ShadowGenerator) unexpectedly fixes intermittent ghosting/flickering on WebGPU

  • OS: Win11
  • Browser Chrome
  • Version 147.0.7727.57

When rendering a scene on WebGPU, I am experiencing intermittent ghosting/flickering artifacts on moving objects or during camera movement.

Interestingly, simply creating a ShadowGenerator (even without adding any shadows to meshes) completely eliminates the ghosting. If I comment out the ShadowGenerator creation code, the ghosting reappears.

The ghosting is not consistently reproducible – it happens intermittently under certain conditions (possibly related to browser state, camera angle, or scene complexity).

Reproduction steps
Run the scene on WebGPU backend

Observe moving objects or move the camera – ghosting/flickering appears intermittently

Add the following code (without any additional shadow setup):

const light = LightManager.getDirectionalLight()
const shadowGenerator = new ShadowGenerator(1024, light)
// No shadow casting meshes added, no shadow-related material settings

Ghosting disappears immediately

Comment out the ShadowGenerator creation – ghosting reappears

Expected result
Scene renders correctly without ghosting, regardless of whether ShadowGenerator exists.

Current result
Without ShadowGenerator: Intermittent ghosting/flickering on WebGPU

With ShadowGenerator: Ghosting completely gone

**As a new user, I cannot upload attachments. Could you please provide an email address so I can send you a video of this bug?
**

cc @Evgeni_Popov

Welcome aboard!

Can you reproduce the problem in the Playground? Without a reproduction (even intermittent), it will be difficult to help.

Regarding the video, I think you can provide a link(?)

Thank you for taking the time out of your busy schedule to help me solve this issue. I have uploaded the code to GitHub at: https://github.com/Yasinli29/babylon-shadowGenerator-bug. After running the project, use WASD to control the character’s movement, and you will clearly see a ghosting/trailing effect on the character’s arms — preferably on a 120Hz screen. The bug is located in src/core/WorldScene.ts, line 51. Uncomment the code there, and you will see the bug disappear.:laughing:

Root cause: deterministic lockstep accumulator drift, not shadows / WebGPU.

The bug had nothing to do with ShadowGenerator directly — that was just a red herring that masked the real issue.

What’s actually happening

The engine is created with deterministicLockstep: true and a fixed physics timestep (~16.67 ms):

 new Engine(canvas, true, {
   powerPreference: PowerPreference.HighPerformance,
   deterministicLockstep: true,
 })

Player movement was being applied inside scene.onBeforePhysicsObservable:

 scene.onBeforePhysicsObservable.add(({ deltaTime }) => {
   this.movePlayer(scene, deltaTime / 1000) // <-- mutates playerController position
   this.openFire(scene)
 })

With deterministic lockstep, onBeforePhysics does not fire exactly once per rendered frame. The internal accumulator drifts and periodically fires 0 times in one frame and 2 times in
the next to stay aligned with the fixed timestep.

So instead of seeing the camera advance by +0.1, +0.1, +0.1, … every frame, you get …, +0.1, +0.0, +0.2, +0.1, …. That single-frame “stutter” is the wobble. Since the FP arm is parented to the camera and the camera follows the player, the whole view jerks for one frame.

I confirmed this with a probe on onAfterRenderObservable measuring per-frame camera deltas: every ~15-20 s a frame had delta = 0 followed by a frame with delta = 0.2.

Why ShadowGenerator “fixed” it

Adding new ShadowGenerator(1024, light) adds an extra render-target pass each frame, which makes frame time consistently a bit longer and keeps the physics accumulator aligned with
the render frames. No more 0/2-step frames → no wobble. It was never about shadow casters (none were registered).

The fix

Move position-driving logic out of the physics observable and into the render observable so it runs exactly once per rendered frame, and use the engine’s frame delta time:

 scene.onBeforePhysicsObservable.add(() => {
   this.openFire(scene)
 })
 
 scene.onBeforeRenderObservable.add(() => {
   const frameDt = scene.getEngine().getDeltaTime() / 1000
   this.movePlayer(scene, frameDt)
   const { x, y, z } = this.playerController.getPosition()
   this.playerNode.position.copyFromFloats(x, y - CAPSULE_HEIGHT / 2, z)
 })

After the fix, 3000+ consecutive frames showed a perfectly uniform 0.1 unit step (min = max = mean), no anomalies.

General rule of thumb

When using deterministicLockstep, anything that drives visual state (camera position, mesh transforms parented to physics-driven nodes, etc.) should be updated in onBeforeRenderObservable, not in onBeforePhysicsObservable. The physics callback is for physics inputs/forces; mixing visual-state updates into it creates exactly this kind of intermittent jitter. If you need true smoothness, the proper solution is interpolation between the previous and current physics state based on the lockstep alpha.