How to render a new scene by clicking a button in current scene? (With React.js)

Playcode example
Preview of the example

What I need is to create a new scene under the same engine, and also dispose the current scene by clicking a button. And it’s in a React page (this point might be not very important).

In my example, the page seems halted after I enter to the new scene. And the page may turn into black and then white if I stay for long. Console outputs warned something like:

WebGL: CONTEXT_LOST_WEBGL: loseContext: context lost
BJS - [19:53:39]: WebGL context lost.
BJS - [19:54:00]: WebGL context successfully restored.

I think it’s because I had made some mistakes on the orders on rendering new scene, but I have no idea how to correct it.

Appreciate for any answers, thanks.

Hey @gunpowderfans,

Sorry but I don’t know how to save the result in the playcode.
But here you have it:

import React, { useEffect, useRef } from 'react';
import {
  Engine,
  Scene,
  HemisphericLight,
  Vector3,
  ArcRotateCamera,
  MeshBuilder,
} from '@babylonjs/core';
import {
  AdvancedDynamicTexture,
  TextBlock,
  Control,
  Rectangle,
  Button,
} from '@babylonjs/gui';

export default function App() {
  const reactCanvas = useRef();

  const startEngineLoop = ({canvas, engine, scene}) => {
    // Light
    const light = new HemisphericLight('light', Vector3.Up(), scene);
    light.intensity = 0.5;

    // Camera
    const camera: ArcRotateCamera = new ArcRotateCamera(
      'camera',
      Math.PI,
      Math.PI / 4,
      10,
      new Vector3(0, -5, 0),
      scene
    );
    scene.activeCamera = camera;
    scene.activeCamera.attachControl(canvas, true);

    camera.attachControl(canvas, true);

    engine.runRenderLoop(() => {
      scene.render();
    });

    return { camera };
  };

  useEffect(() => {
    const { current: canvas } = reactCanvas;
    if (!canvas) return;

    const engine = new Engine(canvas);
    const scene = new Scene(engine);

    const {camera} = startEngineLoop({canvas, engine, scene});

    // Some objects
    const ground = MeshBuilder.CreateGround(
      'ground',
      { height: 20, width: 20, subdivisions: 4 },
      scene
    );
    const box = MeshBuilder.CreateBox('box', { size: 2 }, scene);
    const ball = MeshBuilder.CreateSphere(
      'ball',
      { segments: 8, diameter: 2 },
      scene
    );
    ball.position.set(0, 1, 0);
    camera.setTarget(ball);

    // UI Text
    const advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI(
      'textUI',
      undefined,
      scene
    );
    const uiText = new TextBlock('instructions');
    uiText.text = 'Test Multiple Scenes';
    uiText.color = '#f0ff00';
    uiText.fontFamily = 'Roboto';
    uiText.fontSize = 48;
    uiText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
    uiText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;
    uiText.paddingBottom = '10px';
    advancedTexture.addControl(uiText);


    // A button to click
    const clickBox = MeshBuilder.CreateBox('clickBox', { size: 1 }, scene);
    clickBox.isPickable = true;
    clickBox.position.set(5, 1, -5);
    const rect = new Rectangle();
    rect.width = '125px';
    rect.height = '20px';
    rect.color = 'black';
    rect.background = 'white';
    rect.thickness = 0;
    advancedTexture.addControl(rect);
    rect.linkWithMesh(clickBox);
    const button = Button.CreateSimpleButton('button', 'Goto another scene');
    button.width = 2;
    button.height = 2;
    button.color = 'white';
    button.background = 'black';
    button.fontSize = 12;
    rect.addControl(button);
    button.onPointerClickObservable.add(() => {
      console.log('Button Clicked');
      scene.dispose();
      engine.stopRenderLoop();

      const sceneAlt = new Scene(engine);
      startEngineLoop({canvas, engine, scene: sceneAlt});
      

        // UI
        const advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI(
          'textUI',
          undefined,
          sceneAlt
        );
        const uiText = new TextBlock('instructions');
        uiText.text = 'Welcome to another scene';
        uiText.color = '#f0ff00';
        uiText.fontFamily = 'Roboto';
        uiText.fontSize = 48;
        uiText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
        uiText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;
        uiText.paddingBottom = '10px';
        advancedTexture.addControl(uiText);

        // Some items
        const box = MeshBuilder.CreateBox('box', { size: 2 }, sceneAlt);
        const ball = MeshBuilder.CreateSphere(
          'player',
          { segments: 8, diameter: 2 },
          sceneAlt
        );
        ball.position.set(0, 1, 0);
    });

    // Actually I have no idea how the latter lines work, it's not important now
    // https://doc.babylonjs.com/communityExtensions/Babylon.js+ExternalLibraries/BabylonJS_and_ReactJS
    const resize = () => {
      scene.getEngine().resize();
    };
    if (window) {
      window.addEventListener('resize', resize);
    }
    return () => {
      scene.getEngine().dispose();
      if (window) {
        window.removeEventListener('resize', resize);
      }
    };
  }, []);

  return (
    <canvas
      style={{ width: '100%', height: '100%' }}
      ref={reactCanvas}
    ></canvas>
  );
}

// Log to console
console.log('Hello console');

I don’t know if it’s the best approach but in general what I do is:

  1. I have function startEngineLoop which I call when I want to start the engine loop. I call it twice here: at the beginning and after clicking the button
  2. second thing is to call scene.dispose(); when you want to switch to new scene

Let me know if that helped!

4 Likes