Skip to content

Scenes

The scene system provides the visual foundation for a game. Powered by BabylonJS, SceneEngine wraps common scene creation tasks while leaving the underlying BabylonJS API reachable.

SceneEngine

typescript
const sceneEngine = engine.engines.sceneEngine;

Scene methods

MethodSignatureDescription
createScene() => SceneCreate a new BabylonJS scene
enablePhysics(scene, gravity?, floatingOriginWorldRadius?) => voidEnable Havok physics on a scene

Camera methods

MethodSignatureDescription
createFreeCamera(name, position, scene, canvas?) => FreeCameraA free camera aimed at the origin, with controls attached when a canvas is given

Light methods

MethodSignatureDescription
createHemisphericLight(name, direction, scene, intensity?) => HemisphericLightAmbient light from a hemisphere
createDirectionalLight(name, direction, scene, intensity?) => DirectionalLightDirectional light
createPointLight(name, position, scene, intensity?) => PointLightOmnidirectional point light
createSpotLight(name, position, direction, angle, exponent, scene, intensity?) => SpotLightSpot light
createRectAreaLight(name, position, width, height, scene, intensity?) => RectAreaLightRectangular area light

Mesh methods

MethodSignatureDescription
createSphere(name, options, scene) => MeshSphere
createBox(name, options, scene) => MeshBox
createGround(name, options, scene) => MeshGround plane
createCylinder(name, options, scene) => MeshCylinder

Physics methods

MethodSignatureDescription
addPhysics(mesh, shapeType, options, scene) => PhysicsAggregateAdd a physics body to a mesh

Model loading

MethodSignatureDescription
loadModel(path, scene) => Promise<AssetContainer>Load a GLB/glTF into an asset container
importMeshes(meshNames, path, scene) => Promise<ImportMeshResult>Import meshes from a file straight into the scene. An empty meshNames imports all of them.

Creating a scene

In most cases, scenes are created inside a Level's buildScene() method:

typescript
const scene = sceneEngine.createScene();

sceneEngine.enablePhysics(scene);

const camera = sceneEngine.createFreeCamera('mainCamera', new Vector3(0, 5, -10), scene, canvas);

sceneEngine.createHemisphericLight('ambient', new Vector3(0, 1, 0), scene, 0.4);
sceneEngine.createDirectionalLight('sun', new Vector3(0.5, -0.6, 0.5), scene);

const ground = sceneEngine.createGround('ground', { width: 50, height: 50 }, scene);
sceneEngine.addPhysics(ground, PhysicsShapeType.BOX, { mass: 0 }, scene);

A level that spawns entities also binds a simulation to its scene with this.gameEngine.rebuildSimulation(scene, meshSource). GameLevel does this itself; a custom Level does it in buildScene. See Levels.

Cameras

CameraUse
FreeCameraFirst-person, 6 degrees of freedom
ArcRotateCameraThird-person, orbits around a point
FollowCameraFollows a mesh
typescript
import { ArcRotateCamera, FreeCamera, Vector3 } from '@babylonjs/core';

const fps = new FreeCamera('fps', new Vector3(0, 5, -10), scene);
fps.setTarget(Vector3.Zero());
fps.attachControl(canvas, true);

const orbit = new ArcRotateCamera('orbit', Math.PI / 4, Math.PI / 3, 10, Vector3.Zero(), scene);
orbit.attachControl(canvas, true);

A camera whose keyboard controls would compete with SAGE's own bindings can drop them:

typescript
orbit.inputs.removeByType('ArcRotateCameraKeyboardMoveInput');

Lighting

typescript
import { DirectionalLight, HemisphericLight, PointLight, Vector3 } from '@babylonjs/core';

const ambient = new HemisphericLight('ambient', new Vector3(0, 1, 0), scene);
ambient.intensity = 0.4;

const sun = new DirectionalLight('sun', new Vector3(0.5, -0.6, 0.5), scene);
sun.intensity = 0.7;

const torch = new PointLight('torch', new Vector3(5, 3, 0), scene);
torch.intensity = 0.8;

A scene that contains Blender-exported content uses PBR materials and physical light units. See the Blender Workflow page for the intensities that work there.

Meshes and materials

typescript
import { Color3, MeshBuilder, PBRMaterial, StandardMaterial } from '@babylonjs/core';

const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 2, segments: 32 }, scene);
sphere.position.y = 1;

const mat = new StandardMaterial('mat', scene);
mat.diffuseColor = new Color3(0.2, 0.4, 0.8);
sphere.material = mat;

const pbr = new PBRMaterial('pbr', scene);
pbr.albedoColor = new Color3(0.8, 0.2, 0.1);
pbr.metallic = 0.7;
pbr.roughness = 0.3;

An entity's mesh is built by its mesh source, keyed by the entity's name, or declared through meshConfig and the standard mesh source. See Authoring.

Model loading

typescript
const container = await sceneEngine.loadModel('assets/models/character.glb', scene);
container.addAllToScene();

import { ImportMeshAsync } from '@babylonjs/core';

const result = await ImportMeshAsync('assets/models/character.glb', scene);

AssetManager

For production games, use the AssetManager for centralized loading with caching, reference counting, and GLB fragment extraction:

typescript
const assetManager = engine.managers.assetManager;

Loading

typescript
const container = await assetManager.load('models/environment.glb');
container.addAllToScene();

Fragment syntax

Extract individual meshes from a multi-object GLB with path#meshName:

typescript
const chestLid = await assetManager.load('models/props.glb#chest_lid');
const torch = await assetManager.load('models/props.glb#wall_torch');

The underlying container is loaded once, however many fragments are extracted. The standard mesh source uses the same syntax for a meshConfig whose source names a glb.

Instancing and cloning

typescript
// GPU instances share geometry and material
const barrel1 = assetManager.instance('models/props.glb#barrel');
const barrel2 = assetManager.instance('models/props.glb#barrel');

// An independent clone, for a mesh that needs its own geometry or material
const unique = assetManager.clone('models/props.glb#barrel');
unique.scaling.y = 1.5;

Preloading

typescript
engine.eventBus.subscribe('asset:progress', (event) =>
{
    const { path, loaded, total } = event.payload;
    console.log(`Loaded ${ loaded }/${ total }: ${ path }`);
});

await assetManager.preload([
    'models/environment.glb',
    'models/props.glb#barrel',
    'models/props.glb#crate',
]);

A level config's preload list does the same before buildScene runs.

Reference counting and disposal

Each load() call increments a reference count. dispose() decrements it, and the container is freed when the count reaches zero:

typescript
await assetManager.load('models/props.glb#barrel');
await assetManager.load('models/props.glb#barrel');

assetManager.dispose('models/props.glb#barrel');
assetManager.dispose('models/props.glb#barrel');

assetManager.disposeAll();

Picking

Resolving what a ray or a screen position hits to the entity that owns it is NodeSimulation's. See Picking.

Environment effects

Fog

typescript
scene.fogEnabled = true;
scene.fogColor = new Color3(0.8, 0.9, 0.8);
scene.fogDensity = 0.01;

Particle systems

typescript
import { Color4, ParticleSystem, Texture, Vector3 } from '@babylonjs/core';

const fire = new ParticleSystem('fire', 2000, scene);
fire.particleTexture = new Texture('assets/textures/flame.png', scene);
fire.emitter = new Vector3(5, 0, 10);
fire.minEmitBox = new Vector3(-0.2, 0, -0.2);
fire.maxEmitBox = new Vector3(0.2, 0, 0.2);
fire.color1 = new Color4(1, 0.9, 0.3, 1);
fire.color2 = new Color4(1, 0.5, 0.2, 1);
fire.minSize = 0.3;
fire.maxSize = 1.5;
fire.minLifeTime = 0.2;
fire.maxLifeTime = 0.8;
fire.emitRate = 500;
fire.direction1 = new Vector3(-0.5, 4, -0.5);
fire.direction2 = new Vector3(0.5, 4, 0.5);
fire.minEmitPower = 1;
fire.maxEmitPower = 3;
fire.start();

The inspector

typescript
import '@babylonjs/inspector';

window.addEventListener('keydown', (event) =>
{
    if(event.key === 'F12')
    {
        if(scene.debugLayer.isVisible())
        {
            scene.debugLayer.hide();
        }
        else
        {
            scene.debugLayer.show();
        }
    }
});

Released under the MIT License.