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?) => Promise<void>Enable Havok physics on a scene, loading the Havok module on the first call in a session. Two simultaneous asks join the same load, and a load that fails is not remembered.
dispose() => Promise<void>Release the Havok module and forget a load still in flight. The scenes it created belong to their levels and are left alone.

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

Model loading ​

The scene engine registers every loader BabylonJS ships -- glTF and GLB, OBJ, STL, and the rest -- so both methods read whichever of those formats a path names. Each loader's own code is fetched the first time a file of its kind is read.

MethodSignatureDescription
loadModel(path, scene) => Promise<AssetContainer>Read a file into an asset container without adding anything to the scene
importMeshes(meshNames, path, scene) => Promise<{ meshes, particleSystems, skeletons, animationGroups, transformNodes, geometries, lights }>Import meshes from a file straight into the scene. An empty meshNames imports all of them.

Builder defaults ​

Each light and mesh builder fills in one of SAGE's own defaults for the field a caller left out; a caller's own option replaces a default, field by field.

BuilderFieldDefault
createHemisphericLightintensity0.7
createDirectionalLightintensity1
createPointLightintensity1
createSpotLightintensity1
createRectAreaLightintensity1
createSpherediameter1
createSpheresegments32
createBoxsize1
createGroundwidth6
createGroundheight6
createGroundsubdivisions2
createCylinderheight2
createCylinderdiameterTop1
createCylinderdiameterBottom1

Creating a scene ​

In most cases, scenes are created inside a Level's buildScene() method, through this.context.sceneEngine. The scene engine builds no camera and attaches nothing to a canvas: the game constructs its own camera, aims it, and decides which camera takes control by calling attachControl on it:

typescript
import { FreeCamera, PhysicsAggregate, PhysicsShapeType, Vector3 } from '@babylonjs/core';

const scene = sceneEngine.createScene();

await sceneEngine.enablePhysics(scene);

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

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);
new PhysicsAggregate(ground, PhysicsShapeType.BOX, { mass: 0 }, scene);

A level that spawns entities also binds a simulation to its scene with the base class's own this.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(true);

const orbit = new ArcRotateCamera('orbit', Math.PI / 4, Math.PI / 3, 10, Vector3.Zero(), scene);
orbit.attachControl(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
import { ImportMeshAsync } from '@babylonjs/core';

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

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
import { AssetContainer } from '@babylonjs/core';

const result = await assetManager.load('models/environment.glb');

if(result instanceof AssetContainer)
{
    result.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 ​

instance and clone both read the source mesh out of the cache, so the path has to have been loaded first. Either one throws Asset '<path>' is not loaded. Call load() first. for a path the manager is not holding.

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

// 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, built into the scene it is for, for a mesh that needs its own geometry or material
const unique = assetManager.clone('models/props.glb#barrel', scene);
unique.scaling.y = 1.5;

Preloading ​

typescript
engine.bus.on('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. release(path) 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.release('models/props.glb#barrel');
assetManager.release('models/props.glb#barrel');

// Empties the whole cache, whatever its reference counts
await assetManager.dispose();

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
import { Color3 } from '@babylonjs/core';

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 ​

@babylonjs/inspector is a separate package -- add it as its own dependency to use it.

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.