How to stop framerate increases when swapping scenes?

Hello. I wanted to post a question regarding Babylon.js scene swapping that, based on my exploration of these forums, I have not seen mentioned before. Recently, I have been writing some code that allows for the swapping of scenes to occur within the Babylon coding environment. To do this, I have created 2 different scenes in the Babylon playground, downloaded their html files, combined the two scenes into one html file, and modified some of the code outside of the “Createscene” functions. These modifications were made using Visual studio. An example of the code that I ended up creating can be seen below: note that comments have been placed nearby the elements that I have modified. Essentially, this code uses a modified version of the “initFunction” function that can be found in most downloaded playground files: when the “8” key is pressed in any scene, it triggers this function to swap to another scene. In the example below, pressing “8” will let the user cycle between a scene with a sphere that’s oscillating in scale, and a scene with a box that is oscillating in scale. Note that the oscillating animations were done with the “registerBeforeRender” function.

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

        <title>Babylon.js sample code</title>

        <!-- Babylon.js -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>
        <script src="https://assets.babylonjs.com/generated/Assets.js"></script>
        <script src="https://preview.babylonjs.com/ammo.js"></script>
        <script src="https://preview.babylonjs.com/cannon.js"></script>
        <script src="https://preview.babylonjs.com/Oimo.js"></script>
        <script src="https://preview.babylonjs.com/earcut.min.js"></script>
        <script src="https://preview.babylonjs.com/babylon.js"></script>
        <script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
        <script src="https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
        <script src="https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
        <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.js"></script>
        <script src="https://preview.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
        <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
        <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

        <style>
            html, body {
                overflow: hidden;
                width: 100%;
                height: 100%;
                margin: 0;
                padding: 0;
            }

            #renderCanvas {
                width: 100%;
                height: 100%;
                touch-action: none;
            }
        </style>
    </head>
<body>
    <canvas id="renderCanvas"></canvas>
    <script>
        var canvas = document.getElementById("renderCanvas");

        var startRenderLoop = function (engine, canvas) {
            engine.runRenderLoop(function () {
                if (sceneToRender && sceneToRender.activeCamera) {
                    sceneToRender.render();
                }
            });
        }

        var engine = null;
        var scene = null;
        var sceneToRender = null;
        var default_engine_created=0;//marks if a engine has been assigned yet (0=no, 1=yes)
        var itterable=0;//this creates a term that changes with each frame render
        var scene=null;//the first scene 
        var scene2=null//the 2nd scene


        var createDefaultEngine = function() { 
            if (default_engine_created==0){//ie: if a default engine hasn't yet been created
                return new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true,  disableWebGL2Support: false,deterministicLockstep: true, lockstepMaxSteps: 4});
                //note that the engine is established with the "deterministicLockstep" property set to true: this is an attempt to any "registerBeforeRender" functionailty independent of framerate.
                default_engine_created=1;
            }
        };

        var createScene = function() {
    scene = new BABYLON.Scene(engine);//"scene" variable defined earlier, so this just modified variable,
    scene.MaxDeltaTime=18
    scene.MinDeltaTime=15
    //above 2 commands set the maximum and minimum framerates.

    //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);

    // This targets the camera to scene origin
    camera.setTarget(BABYLON.Vector3.Zero());

    // This attaches the camera to the canvas
    camera.attachControl(canvas, true);

    //this creates a light
    var hemiLight = new BABYLON.HemisphericLight("hemiLight", new BABYLON.Vector3(0, 1, 0), scene);

    //this creates a mesh
    var sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 1}, scene);

    //this creates a term that changes with each frame render
    itterable=0;

    //this creates an animation for the mesh
    scene.registerBeforeRender(function () {
        sphere.scaling.y=1*Math.sin(BABYLON.Tools.ToRadians(itterable))
        itterable+=2;
        if (itterable==360){
            //this resets the itterable value when it reaches a value of 360, so we don't have the itterable value increasing indefinitely
            itterable=0;
        }

        //engine.deltaTime=16;
        //scene.deltaTime=16;
        //above commands attempt to keep framerate fixed
    });

    //I have tried "newScene.onAfterStepObservable.add(function (theScene) {" in place of "registerBeforeRender", but this seems to have had no effect.

    //this allows the scene to be changed
    scene.onKeyboardObservable.add((kbInfo) => {
            switch (kbInfo.type) {
                case BABYLON.KeyboardEventTypes.KEYDOWN:
                    switch (kbInfo.event.key) { 
                        case "8":
                            initFunction("scene2").then(() => {sceneToRender = scene2});
                            //when the 8 key is pressed, the scene swaps to scene 2
                        break
                        case "1":
                            console.log("  current deltaTime: "+scene.deltaTime)
                        break
                    }
                break;
            }
        });

    return scene;
};


var createScene2 = function() {
    scene2 = new BABYLON.Scene(engine); //"scene2" variable defined earlier, so this just modified variable,
    scene2.MaxDeltaTime=18
    scene2.MinDeltaTime=15
    //above 2 commands set the maximum and minimum framerates. 

    //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
    var camera2 = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene2);

    // This targets the camera to scene origin
    camera2.setTarget(BABYLON.Vector3.Zero());

    // This attaches the camera to the canvas
    camera2.attachControl(canvas, true);

    //this creates a light
    var hemiLight2 = new BABYLON.HemisphericLight("hemiLight", new BABYLON.Vector3(0, 1, 0), scene2);

    //this creates a mesh
    var box = BABYLON.MeshBuilder.CreateBox("box", {size: 1}, scene2);

    //this creates a term that changes with each frame render
    itterable=0;

    //this creates an animation for the mesh
    scene2.registerBeforeRender(function () {
        box.scaling.y=1*Math.sin(BABYLON.Tools.ToRadians(itterable))+2
        itterable+=2;
        if (itterable==360){
            //this resets the itterable value when it reaches a value of 360, so we don't have the itterable value increasing indefinitely
            itterable=0;
        }

        //engine.deltaTime=16;
        //scene2.deltaTime=16;
        //above command tries to keep framerate fixed
    });

    //this allows the scene to be changed
    scene2.onKeyboardObservable.add((kbInfo) => {
            switch (kbInfo.type) {
                case BABYLON.KeyboardEventTypes.KEYDOWN:
                    switch (kbInfo.event.key) { 
                        case "8":
                            initFunction("scene").then(() => {sceneToRender = scene});
                            //when the 8 key is pressed, the scene swaps to scene 2
                        break
                        case "1":
                            console.log("  current deltaTime: "+scene2.deltaTime)
                        break
                    }
                break;
            }
        });

    return scene2;
};


                window.initFunction = async function(input) {
                    
                    
                    var asyncEngineCreation = async function() {
                        try {
                        return createDefaultEngine();
                        } catch(e) {
                        console.log("the available createEngine function failed. Creating the default engine instead");
                        return createDefaultEngine();
                        }
                    }

                    window.engine = await asyncEngineCreation();
                    engine=window.engine;
        if (!engine) throw 'engine should not be null.';
        startRenderLoop(engine, canvas);
        allscenedispose();//disposes of any scene currently existing (using custom function)
        if (window.scene){
            window.scene.dispose();//disposes previous scene that window held
            window.scene=null;
        }
        //the below if loops consider the input to this function, and load the corresponding environment.
        if (input=="scene"){
            window.scene = createScene();
        }
        if (input=="scene2"){
            window.scene = createScene2();
        }
    };

    var allscenedispose=()=>{
        //this function disposes everything loadeed currently into each scene. It's designed with try functions so that no errors come up if the scene hasn't got anything to dispose of.
        try {
            scene.dispose();
            scene=null;
        }
        catch(err) {
            console.log("scene dispose failed")
        }
        try {
            scene2.dispose();
            scene2=null;
            
        }
        catch(err) {
            console.log("scene2 dispose failed")
        }
    }


        initFunction("scene").then(() => {sceneToRender = scene                    
        });

        

        // Resize
        window.addEventListener("resize", function () {
            engine.resize();
        });
    </script>
</body>
</html>

The problem that I’ve encountered with this code is, if the scene is changed multiple times in a row, the frame rate seems to increase unexpectedly. You will be able to see this from how the rate of the sphere/box’s oscillation seems to speed up, as well as how the first-person camera’s movement speed seems to increase too. You can also press the “1” key in any scene, and the scene’s current “deltaTime” will be printed in the console.

I have tried a few things to remedy this issue. Initially, I tried to set each scene’s “deltaTime” value as a constant, but to no avail. I’ve also tried to use the “deterministicLockstep” property when creating the Babylon.js engine (combined with replacing all “registerBeforeRender” commands with “onAfterStepObservable” commands) in order to make elements like object animation and camera movement independent of the increasing framerate. However, this hasn’t had an effect either. Other than these approaches, I have neither been able to find a way to remove the issue of this increasing framerate, nor have I found the source of the issue.
Therefore, I was wondering, in my construction of this code, whether I have missed any obvious mistakes that are leading to this increasing framerate? Thank you for your help with this.

Hey @101635306 , how are you doing?

I believe the issue might be on the setup you do using the window.initFunction. You are creating the engine multiple times and subscribing the scene, I believe that might be causing the unexpected behavior.

I think you can safely create the engine and scene only once and just swap you scene that is been rendered during the key input events.

1 Like

Hello :slight_smile: Sergio beat me to it but I’ll post here what I was writing:
When you check the console in your example scene and press “8” a few times, you can see that the engine and scenes are being disposed and created multiple times, which makes the “registerBeforeRender” function be called multiple times. That’s because you’re calling the scene’s initFunction every time the “8” key is pressed.

Here’s a version that creates each scene and the engine only once:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>Babylon.js sample code</title>

    <!-- Babylon.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>
    <script src="https://assets.babylonjs.com/generated/Assets.js"></script>
    <script src="https://preview.babylonjs.com/ammo.js"></script>
    <script src="https://preview.babylonjs.com/cannon.js"></script>
    <script src="https://preview.babylonjs.com/Oimo.js"></script>
    <script src="https://preview.babylonjs.com/earcut.min.js"></script>
    <script src="https://preview.babylonjs.com/babylon.js"></script>
    <script src="https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
    <script src="https://preview.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
    <script src="https://preview.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
    <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.js"></script>
    <script src="https://preview.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
    <script src="https://preview.babylonjs.com/gui/babylon.gui.min.js"></script>
    <script src="https://preview.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

    <style>
      html,
      body {
        overflow: hidden;
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
      }

      #renderCanvas {
        width: 100%;
        height: 100%;
        touch-action: none;
      }
    </style>
  </head>
  <body>
    <canvas id="renderCanvas"></canvas>
    <script>
      var canvas = document.getElementById("renderCanvas");

      var engine = null;
      var scene = null;
      var sceneToRender = null;
      var default_engine_created = 0; //marks if a engine has been assigned yet (0=no, 1=yes)
      var itterable = 0; //this creates a term that changes with each frame render
      var scene = null; //the first scene
      var scene2 = null; //the 2nd scene

      var createDefaultEngine = function () {
        if (default_engine_created == 0) {
          //ie: if a default engine hasn't yet been created
          return new BABYLON.Engine(canvas, true, {
            preserveDrawingBuffer: true,
            stencil: true,
            disableWebGL2Support: false,
            deterministicLockstep: true,
            lockstepMaxSteps: 4,
          });
          //note that the engine is established with the "deterministicLockstep" property set to true: this is an attempt to any "registerBeforeRender" functionailty independent of framerate.
          default_engine_created = 1;
        }
      };

      var createScene = function () {
        scene = new BABYLON.Scene(engine); //"scene" variable defined earlier, so this just modified variable,
        scene.name = "scene1";
        scene.MaxDeltaTime = 18;
        scene.MinDeltaTime = 15;
        //above 2 commands set the maximum and minimum framerates.

        //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
        var camera = new BABYLON.FreeCamera(
          "camera1",
          new BABYLON.Vector3(0, 5, -10),
          scene
        );

        // This targets the camera to scene origin
        camera.setTarget(BABYLON.Vector3.Zero());

        // This attaches the camera to the canvas
        camera.attachControl(canvas, true);

        //this creates a light
        var hemiLight = new BABYLON.HemisphericLight(
          "hemiLight",
          new BABYLON.Vector3(0, 1, 0),
          scene
        );

        //this creates a mesh
        var sphere = BABYLON.MeshBuilder.CreateSphere(
          "sphere",
          { diameter: 1 },
          scene
        );

        //this creates a term that changes with each frame render
        itterable = 0;

        //this creates an animation for the mesh
        scene.registerBeforeRender(function () {
          sphere.scaling.y = 1 * Math.sin(BABYLON.Tools.ToRadians(itterable));
          itterable += 2;
          if (itterable == 360) {
            //this resets the itterable value when it reaches a value of 360, so we don't have the itterable value increasing indefinitely
            itterable = 0;
          }
        });

        return scene;
      };

      var createScene2 = function () {
        scene2 = new BABYLON.Scene(engine); //"scene2" variable defined earlier, so this just modified variable,
        scene2.name = "scene2";
        scene2.MaxDeltaTime = 18;
        scene2.MinDeltaTime = 15;
        //above 2 commands set the maximum and minimum framerates.

        //var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 12, BABYLON.Vector3.Zero(), scene);
        var camera2 = new BABYLON.FreeCamera(
          "camera1",
          new BABYLON.Vector3(0, 5, -10),
          scene2
        );

        // This targets the camera to scene origin
        camera2.setTarget(BABYLON.Vector3.Zero());

        // This attaches the camera to the canvas
        camera2.attachControl(canvas, true);

        //this creates a light
        var hemiLight2 = new BABYLON.HemisphericLight(
          "hemiLight",
          new BABYLON.Vector3(0, 1, 0),
          scene2
        );

        //this creates a mesh
        var box = BABYLON.MeshBuilder.CreateBox("box", { size: 1 }, scene2);

        //this creates a term that changes with each frame render
        itterable = 0;

        //this creates an animation for the mesh
        scene2.registerBeforeRender(function () {
          box.scaling.y = 1 * Math.sin(BABYLON.Tools.ToRadians(itterable)) + 2;
          itterable += 2;
          if (itterable == 360) {
            //this resets the itterable value when it reaches a value of 360, so we don't have the itterable value increasing indefinitely
            itterable = 0;
          }

          //engine.deltaTime=16;
          //scene2.deltaTime=16;
          //above command tries to keep framerate fixed
        });

        return scene2;
      };

      engine = createDefaultEngine();
      createScene();
      createScene2();

      sceneToRender = scene;

      window.addEventListener("keydown", (evt) => {
        if (evt.key == "8") {
          if (sceneToRender === scene) {
            sceneToRender = scene2;
          } else {
            sceneToRender = scene;
          }
        }
      })

      engine.runRenderLoop(() => {
        if (sceneToRender) {
          sceneToRender.render();
        }
      });

      // Resize
      window.addEventListener("resize", function () {
        engine.resize();
      });
    </script>
  </body>
</html>

3 Likes

Hello. Thank you very much for your example code. I tried out your modifications, and the code worked just as expected, so thank you for that. I attempted to place the scene disposing/creation functionality I had in my original example back into the code that you wrote (example seen below), and the resulting code seems to still work the same. I added this functionality in due to the advantage it gives in re-setting a scene each time it is revisited.

var scenedisposer=()=>{
    try{
        scene.dispose()
    } catch(e){

    }
    try{
        scene2.dispose()
    } catch(e){

    }
}

    window.addEventListener("keydown", (evt) => {
        if (evt.key == "8") {
          if (sceneToRender === scene) {
            scenedisposer();
            createScene2();
            sceneToRender = scene2;
          } else {
            scenedisposer();
            createScene();
            sceneToRender = scene;
          }
        }
      })

There’s one question I had about the addEventListener function. As part of a larger project that I’m working on, I’m trying to trigger scene changes from within the scenes themselves (hence why I was using keyboard observables within the scene functions themselves for scene swapping). For example, if the camera were to intersect a mesh in a certain scene, it would trigger functionality that causes the scene to change. Based on my searching of the documentation, I haven’t yet found a way that addEventListener could be used to do something like this. Is there any way I may be able to get the addEventListener function to track the value of a variable, which, upon changing, would trigger the swapping of the scene? Once again, thanks for your help.

This is part of what @carolhmj shared:

engine.runRenderLoop(() => {
        if (sceneToRender) {
          sceneToRender.render();
        }
      });

The sceneToRender is the variable you want to update to switch the active scene

Hi Srzerbetto. Thanks for your help. I now concur that having multiple instances of the engine being created was the problem. Carolhmj has helped me sort out a separate function that ensures multiple engines are not created: for the time being, it has resolved the framerate problem.

I see: I didn’t realize that this was a looping function. Thank you for pointing this out: now I am able to change scenes from within a scene by changing “sceneToRender” variable by itself.

2 Likes