I’m trying to change between scenes, which, in my code, are structured as follows:
- A base
BasicScene
TypeScript class is responsible for the functionality shared by every scene, such as creating anEngine
, a BabylonScene
, aUniversalCamera
, a skybox, a ground for collisions, etc. - Derived scenes import their own models and make various specific changes, such as repositioning the camera or spawning a video screen.
-
index.ts
, the entry point of the program, holds the functionSetScene()
, receiving aSceneType
enum as the argument (e.g.SceneType.Mall
) and switching to it. Upon doing so, it saves a reference to the new scene. That way, the next time the function is called, it first disposes the existing scene and then creates the new one.
Here’s what the code looks like.
The Base BasicScene
export class BasicScene {
engine: Engine;
scene: Scene;
camera: UniversalCamera;
constructor(canvas: HTMLCanvasElement) {
const { engine, scene, camera } = this.CreateBasicScene(canvas);
this.engine = engine;
this.scene = scene;
this.camera = camera;
engine.runRenderLoop(() => scene.render());
}
protected async ImportEnvironmentModel(
scene: Scene,
model_directory: string,
model_name: string
): Promise<void> {
const root = new TransformNode("environment_model", scene);
const container = await SceneLoader.LoadAssetContainerAsync(
model_directory,
model_name,
scene
);
for (const mesh of container.meshes) {
if (mesh.parent == null) mesh.setParent(root);
}
root.scaling = new Vector3(2, 2, 2);
container.addAllToScene();
}
private CreateBasicScene(canvas: HTMLCanvasElement) {
const engine = new Engine(canvas, true);
const scene = new Scene(engine);
const camera = this.CreateCamera(scene, canvas);
const ground = this.CreateGround(scene);
this.CreateHemisphericLight(scene);
this.CreateSkybox(scene);
this.HandleDevices(engine, camera);
this.EnableXRSupport(ground, scene);
this.RegisterInspectorOnInput(scene);
scene.collisionsEnabled = true;
scene.gravity = new Vector3(0, -0.15, 0);
return { engine, scene, camera };
}
private CreateGround(scene: Scene): Mesh {
const ground = MeshBuilder.CreateGround(
"collision_ground",
{
width: 200,
height: 200,
},
scene
);
ground.checkCollisions = true;
ground.position = new Vector3(0, 0.074, 0);
ground.isVisible = false;
return ground;
}
private CreateCamera(
scene: Scene,
canvas: HTMLCanvasElement
): UniversalCamera {
const camera = new UniversalCamera(
"universal_camera",
new Vector3(0, 0.74 * 2, 0.74 * 2),
scene
);
camera.attachControl(canvas, true);
const yRotation = Angle.FromDegrees(90);
camera.rotation = new Vector3(0, yRotation.radians(), 0);
camera.speed = 0.15;
camera.minZ = 0;
camera.ellipsoid = new Vector3(0.3, 0.7, 0.3);
camera.checkCollisions = true;
camera.applyGravity = true;
// Add WASD movement.
const ascii = (char: String) => char.charCodeAt(0);
camera.keysUp.push(ascii("W"));
camera.keysLeft.push(ascii("A"));
camera.keysDown.push(ascii("S"));
camera.keysRight.push(ascii("D"));
return camera;
}
private EnableXRSupport(ground: Mesh, scene: Scene) {
scene.createDefaultXRExperienceAsync({
floorMeshes: [ground],
});
}
private CreateHemisphericLight(scene: Scene): void {
const light = new HemisphericLight(
"hemispheric_light",
Vector3.Up(),
scene
);
light.intensity = 0.5;
}
private CreateSkybox(scene: Scene) {
const skyboxTex = new CubeTexture(
"./assets/environments/environmentSpecular.env",
scene
);
const skybox = scene.createDefaultSkybox(skyboxTex, true);
const skyboxMat = skybox?.material as PBRMaterial;
if (skyboxMat) skyboxMat.microSurface = 0.7;
}
private HandleDevices(engine: Engine, camera: UniversalCamera) {
const deviceSrcManager = new DeviceSourceManager(engine);
deviceSrcManager.onDeviceConnectedObservable.add(device => {
if (device.deviceType == DeviceType.Touch) {
camera.inputs.removeByType("FreeCameraTouchInput");
camera.inputs.addVirtualJoystick();
camera.speed = 0.1;
}
});
}
private RegisterInspectorOnInput(scene: Scene) {
const toggleInspector = () => {
if (scene.debugLayer.isVisible()) scene.debugLayer.hide();
else scene.debugLayer.show();
};
scene.onKeyboardObservable.add(kbInfo => {
if (kbInfo.type == KeyboardEventTypes.KEYDOWN && kbInfo.event.key == "i")
toggleInspector();
});
}
}
Derived Scenes, e.g. ShoppingMall
export class ShoppingMall extends BasicScene {
changeScene: Function;
constructor(canvas: HTMLCanvasElement, changeScene: Function) {
super(canvas);
this.changeScene = changeScene;
super
.ImportEnvironmentModel(
this.scene,
"./assets/models/mall/",
"scene.babylon"
)
.then(() => {
const door = this.scene.getMeshByName("door store");
if (door) door.checkCollisions = true;
// ...And other changes.
});
this.camera.position = new Vector3(0.35, 1.48, 55.8);
this.camera.rotation = new Vector3(0, Tools.ToRadians(180), 0);
this.camera.onCollide = otherMesh => {
if (otherMesh.name == "door store") this.changeScene(SceneType.Main);
};
}
}
The Entry Point, index.ts
const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
const streamUrl = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
let currentScene: BasicScene;
export enum SceneType {
Main,
Mall,
}
function SetScene(newScene: SceneType): void {
if (currentScene) {
currentScene.engine.stopRenderLoop();
currentScene.scene.dispose();
///currentScene.engine.dispose();
// As you can see, I tried various methods & combinations
// of disposing the existing scene. None worked.
}
switch (newScene) {
case SceneType.Main:
currentScene = new Main(canvas, streamUrl, SetScene);
break;
case SceneType.Mall:
currentScene = new ShoppingMall(canvas, SetScene);
break;
}
}
// Set up the first scene.
SetScene(SceneType.Main);
The Problem
When I’m switching from one scene to another (i.e. when currentScene
isn’t null), immediately after any of the dispose()
functions are called, I get the following error:
Uncaught TypeError: Cannot read properties of null (reading 'cameraRigMode')
at Scene.render (scene.js:3430:1)
at basic-scene.ts:58:44
at Engine._renderFrame (engine.js:807:1)
at Engine._renderLoop (engine.js:822:1)
The 58th line in basic-scene.ts
is the following:
engine.runRenderLoop(() => scene.render());
I put a time-out between disposing the current scene and creating a new one, and the error message pops up immediately after the first, so the problem is that, despite calling engine.stopRenderLoop()
, it’s still going.
The Question
What’s the proper way of disposing of a scene, then?
I have only one (Babylon) scene at a time, but just in case, I also tried to get rid of all scenes in engine.scenes
, and nothing changed.