Is it possible to run two independent BabylonJS engines on two canvases?

I have been trying to run two BabylonJS instances on two separate canvases, but I’m running into various problems. I’m wondering if this scenario is supported at all or not.

Unfortunately I don’t know how to sensibly reproduce this problem on the playground which uses just a single canvas to render a scene. However, here’s an example of what I’ve tried to do:

I downloaded the code of the official GizmoManager example: https://playground.babylonjs.com/#4TBMBR#33

I cleaned it up from global variables (ugh) and wrapped inside the following function:

function runBabylonJs(canvasId) {
  var canvas = document.getElementById(canvasId);

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

  var engine = null;
  var scene = null;
  var sceneToRender = null;
  var createDefaultEngine = function () {
    return new BABYLON.Engine(canvas, true, {
      preserveDrawingBuffer: true,
      stencil: true,
      disableWebGL2Support: false,
    });
  };
  var createScene = function () {
    // Create scene
    var scene = new BABYLON.Scene(engine);
    var camera = new BABYLON.FreeCamera(
      "camera1",
      new BABYLON.Vector3(0, 0, 0),
      scene,
    );
    var light = new BABYLON.DirectionalLight(
      "light",
      new BABYLON.Vector3(0, -0.5, 1.0),
      scene,
    );
    light.position = new BABYLON.Vector3(0, 5, -2);

    // Create simple meshes
    var spheres = [];
    for (var i = 0; i < 5; i++) {
      var sphere = BABYLON.Mesh.CreateIcoSphere(
        "sphere",
        { radius: 0.2, flat: true, subdivisions: 1 },
        scene,
      );
      sphere.scaling.x = 2;
      sphere.position.y = 1;
      sphere.material = new BABYLON.StandardMaterial("sphere material", scene);
      sphere.position.z = i + 5;
      spheres.push(sphere);
    }

    // Initialize GizmoManager
    var gizmoManager = new BABYLON.GizmoManager(scene);
    gizmoManager.boundingBoxGizmoEnabled = true;
    // Restrict gizmos to only spheres
    gizmoManager.attachableMeshes = spheres;
    // Toggle gizmos with keyboard buttons
    document.onkeydown = (e) => {
      if (e.key == "w") {
        gizmoManager.positionGizmoEnabled = !gizmoManager.positionGizmoEnabled;
      }
      if (e.key == "e") {
        gizmoManager.rotationGizmoEnabled = !gizmoManager.rotationGizmoEnabled;
      }
      if (e.key == "r") {
        gizmoManager.scaleGizmoEnabled = !gizmoManager.scaleGizmoEnabled;
      }
      if (e.key == "q") {
        gizmoManager.boundingBoxGizmoEnabled =
          !gizmoManager.boundingBoxGizmoEnabled;
      }
    };
    return scene;
  };
  var initFunction = async function () {
    var asyncEngineCreation = async function () {
      try {
        return createDefaultEngine();
      } catch (e) {
        console.log(
          "the available createEngine function failed. Creating the default engine instead",
        );
        return createDefaultEngine();
      }
    };

    engine = await asyncEngineCreation();
    if (!engine) throw "engine should not be null.";
    startRenderLoop(engine, canvas);
    scene = createScene();
  };
  initFunction().then(() => {
    sceneToRender = scene;
  });
}

Then inside my page I have the two canvas elements:

<canvas id="renderCanvas1" style="width: 400px; height: 400px"></canvas>
<canvas id="renderCanvas2" style="width: 400px; height: 400px"></canvas>

And then I call the runBabylonJs() function twice to render on both canvases:

runBabylonJs("renderCanvas1");
runBabylonJs("renderCanvas2");

The result of running this is that I do get two scenes, but the gizmomanagers are not independent. The gizmo gets rendered on one canvas, but the movement happens on another canvas:

I also tried adding a 10 second sleep between the two runBabylonJs() calls to ensure I don’t run into this bug. Didn’t help.

FYI, I’m able to achieve this functionality just fine with THREE.js, so it doesn’t look like some inherent WebGL limitation.

1 Like

cc @Cedric

for more context an issue was also reported in react-bablyonjs, which had set usePointerToAttachGizmos to true. I looked at underlying code and it uses scene.onPointerObservable, but I didn’t dig deeper where those events are attached (windows/canvas/etc.). I didn’t have time for searching for root cause. Hopefully Cedric can help there. Cheers.

I don’t see a particular reason why this scenario should not be supported. I’ll take a look ASAP.

1 Like

Hi @renku
Sorry for the delay, it’s possible to make it work with a simple change. Create an Utility Layer then use it for the GizmoManager:

const utilLayer = new BABYLON.UtilityLayerRenderer(scene);
var gizmoManager = new BABYLON.GizmoManager(scene,1,utilLayer, utilLayer);

It’s important to use the same utility layer for parameters 3 and 4.

Copy/Pasting my test HTML for preservation.

<!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://cdn.babylonjs.com/recast.js"></script>
        <script src="https://cdn.babylonjs.com/ammo.js"></script>
        <script src="https://cdn.babylonjs.com/havok/HavokPhysics_umd.js"></script>
        <script src="https://cdn.babylonjs.com/cannon.js"></script>
        <script src="https://cdn.babylonjs.com/Oimo.js"></script>
        <script src="https://cdn.babylonjs.com/earcut.min.js"></script>
        <script src="https://cdn.babylonjs.com/babylon.js"></script>
        <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
        <script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
        <script src="https://cdn.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
        <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.js"></script>
        <script src="https://cdn.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
        <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
        <script src="https://cdn.babylonjs.com/addons/babylonjs.addons.min.js"></script>
        <script src="https://cdn.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;
            }

            #canvasZone {
                width: 100%;
                height: 100%;
            }
        </style>
    </head>
    <body>
        <canvas id="renderCanvas1" style="width: 600px; height: 300px"></canvas>
<canvas id="renderCanvas2" style="width: 300px; height: 600px"></canvas>
        <script>
                    function runBabylonJs(canvasId, index) {
  var canvas = document.getElementById(canvasId);

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

  var engine = null;
  var scene = null;
  var sceneToRender = null;
  var createDefaultEngine = function () {
    return new BABYLON.Engine(canvas, true, {
      preserveDrawingBuffer: true,
      stencil: true,
      disableWebGL2Support: false,
    });
  };
  var createScene = function (index) {
  console.log(index);
    // Create scene
    var scene = new BABYLON.Scene(engine);
    var camera = new BABYLON.FreeCamera(
      "camera1"+index,
      new BABYLON.Vector3(index, 0, 0),
      scene,
    );
	engine.name = "engine"+index;
    var light = new BABYLON.DirectionalLight(
      "light",
      new BABYLON.Vector3(0, -0.5, 1.0),
      scene,
    );
    light.position = new BABYLON.Vector3(0, 5, -2);

    // Create simple meshes
    var spheres = [];
    for (var i = 0; i < 5; i++) {
      var sphere = BABYLON.Mesh.CreateIcoSphere(
        "sphere",
        { radius: 0.2, flat: true, subdivisions: 1 },
        scene,
      );
      sphere.scaling.x = 2;
      sphere.position.y = 1;
      sphere.material = new BABYLON.StandardMaterial("sphere material", scene);
      sphere.position.z = i + 5;
      spheres.push(sphere);
    }

    // Initialize GizmoManager
    //var gizmoManager = new BABYLON.GizmoManager(scene);
	const utilLayer = new BABYLON.UtilityLayerRenderer(scene);
	var gizmoManager = new BABYLON.GizmoManager(scene,1,utilLayer, utilLayer);

    gizmoManager.boundingBoxGizmoEnabled = true;
    // Restrict gizmos to only spheres
    gizmoManager.attachableMeshes = spheres;
    // Toggle gizmos with keyboard buttons
    document.onkeydown = (e) => {
      if (e.key == "w") {
        gizmoManager.positionGizmoEnabled = !gizmoManager.positionGizmoEnabled;
      }
      if (e.key == "e") {
        gizmoManager.rotationGizmoEnabled = !gizmoManager.rotationGizmoEnabled;
      }
      if (e.key == "r") {
        gizmoManager.scaleGizmoEnabled = !gizmoManager.scaleGizmoEnabled;
      }
      if (e.key == "q") {
        gizmoManager.boundingBoxGizmoEnabled =
          !gizmoManager.boundingBoxGizmoEnabled;
      }
    };
    return scene;
  };
  var initFunction = async function () {
    var asyncEngineCreation = async function () {
      try {
        return createDefaultEngine();
      } catch (e) {
        console.log(
          "the available createEngine function failed. Creating the default engine instead",
        );
        return createDefaultEngine();
      }
    };

    engine = await asyncEngineCreation();
    if (!engine) throw "engine should not be null.";
    startRenderLoop(engine, canvas);
    scene = createScene(index);
  };
  initFunction().then(() => {
    sceneToRender = scene;
  });
}
  runBabylonJs("renderCanvas1",0);
runBabylonJs("renderCanvas2",1);

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

Thanks a lot @Cedric. I tested it, and it does work.

1 Like