Transitioning Scenes in Lite

Hi folks!

Long time babylon user and I’ve started messing with lite. I’m having a hard time understanding scene transitions though. This playground shows my basic approach, but after the first iteration (whether i starts at 0 or 1), things start to go wonky.

What mistake am I making? Thanks!

Shortly, make the scene functions build only. Do not register inside them.

Then dispose the old scene before registering the replacement.

Great, thanks. A couple follow-up questions:

registerScene is async. During a scene transition, what does the engine do between destroyScene and the completion of the upcoming registerScene? My preference is that the canvas would be unchanged during that period, which would be better than artifacts caused by the change.

Since there are periods where no scene is registered with the engine, is it reasonable to call startEngine immediately after creation, rather than waiting for the initial scene to be registered?

Hmm. I stumbled across the utility layer examples today, and I see that it creates and registers two scenes. Is the issue my initial playground that when there are multiple scenes registered to the same canvas, they need to be disposed of in LIFO order?

Your preferred behavior is what the current Babylon Lite implementation does.

disposeScene() unregisters the scene from its surface immediately. Conversely, registerScene() does not attach the new scene until its deferred builders, task preloads, renderable sorting, and frame-graph build have completed. Therefore, a partially built scene is never rendered.

During the period when no scene is registered, the engine’s RAF loop continues, but renderFrame() detects that there are zero rendering contexts and returns before creating a command encoder, acquiring a new swapchain texture, clearing, or submitting anything.

So, assuming the canvas is not resized or reconfigured externally, the last rendered image remains on the canvas during the transition:

disposeScene(currentScene);

// The previous frame remains visible while this is running.
await registerScene(nextScene);

currentScene = nextScene;

A canvas resize during that interval is the main caveat, since changing its backing-store size may invalidate the preserved contents.

startEngine() is independent of scene registration. With no scenes, its render loop simply performs no rendering work; scenes registered later are included in subsequent frames. The source explicitly allows registrations after the loop has started.

One small detail: the promise returned by startEngine() resolves after the first RAF callback, even when no scene was registered and no GPU frame was submitted. It should therefore be understood as “the engine loop has started,” not “the initial scene is ready.”

In practice:
Call startEngine() once.
Register and dispose scenes independently afterward.
During a zero-scene gap, Babylon Lite leaves the previous canvas image untouched.

Yes, that is likely the issue.

Babylon Lite currently treats scenes registered to the same surface as an ordered composition stack:

  1. The first scene is the base and clears the canvas.
  2. Each later scene is configured as an overlay that loads the pixels produced by the previously registered scene.
  3. Rendering follows registration order.

The utility layer relies on this behavior and explicitly requires the main scene to be registered first.

await registerScene(mainScene);
await registerUtilityLayer(utilityLayer);

However, removing a scene only splices it out of the registered-context array. Babylon Lite does not currently reconfigure the remaining scenes after an arbitrary removal.

That means this order is safe:

disposeUtilityLayer(utilityLayer);
disposeScene(mainScene);

But this order is problematic:

disposeScene(mainScene);
disposeUtilityLayer(utilityLayer);

After the main scene is disposed, the utility scene remains configured as an overlay:

overlayTask._config.clr = false;

So instead of becoming a new base scene and clearing the canvas, it continues loading the existing swapchain contents. That can produce stale pixels or other artifacts.

So the practical rule is:

// Registration: bottom to top
await registerScene(mainScene);
await registerUtilityLayer(utilityLayer);

// Disposal: top to bottom
disposeUtilityLayer(utilityLayer);
disposeScene(mainScene);

More generally, scenes sharing one canvas should currently be disposed in reverse registration order - LIFO. The current implementation behaves like a stack, not like a collection that is automatically recomposed after removing an arbitrary member.

Thank you so much for all of this clear detail!

@labris is ON FIRE :slight_smile: