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
const sceneEngine = engine.engines.sceneEngine;Scene methods
| Method | Signature | Description |
|---|---|---|
createScene | () => Scene | Create 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
| Method | Signature | Description |
|---|---|---|
createHemisphericLight | (name, direction, scene, intensity?) => HemisphericLight | Ambient light from a hemisphere |
createDirectionalLight | (name, direction, scene, intensity?) => DirectionalLight | Directional light |
createPointLight | (name, position, scene, intensity?) => PointLight | Omnidirectional point light |
createSpotLight | (name, position, direction, angle, exponent, scene, intensity?) => SpotLight | Spot light |
createRectAreaLight | (name, position, width, height, scene, intensity?) => RectAreaLight | Rectangular area light |
Mesh methods
| Method | Signature | Description |
|---|---|---|
createSphere | (name, options, scene) => Mesh | Sphere |
createBox | (name, options, scene) => Mesh | Box |
createGround | (name, options, scene) => Mesh | Ground plane |
createCylinder | (name, options, scene) => Mesh | Cylinder |
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.
| Method | Signature | Description |
|---|---|---|
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.
| Builder | Field | Default |
|---|---|---|
createHemisphericLight | intensity | 0.7 |
createDirectionalLight | intensity | 1 |
createPointLight | intensity | 1 |
createSpotLight | intensity | 1 |
createRectAreaLight | intensity | 1 |
createSphere | diameter | 1 |
createSphere | segments | 32 |
createBox | size | 1 |
createGround | width | 6 |
createGround | height | 6 |
createGround | subdivisions | 2 |
createCylinder | height | 2 |
createCylinder | diameterTop | 1 |
createCylinder | diameterBottom | 1 |
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:
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
| Camera | Use |
|---|---|
FreeCamera | First-person, 6 degrees of freedom |
ArcRotateCamera | Third-person, orbits around a point |
FollowCamera | Follows a mesh |
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:
orbit.inputs.removeByType('ArcRotateCameraKeyboardMoveInput');Lighting
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
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
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:
const assetManager = engine.managers.assetManager;Loading
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:
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.
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
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:
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
import { Color3 } from '@babylonjs/core';
scene.fogEnabled = true;
scene.fogColor = new Color3(0.8, 0.9, 0.8);
scene.fogDensity = 0.01;Particle systems
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.
import '@babylonjs/inspector';
window.addEventListener('keydown', (event) =>
{
if(event.key === 'F12')
{
if(scene.debugLayer.isVisible())
{
scene.debugLayer.hide();
}
else
{
scene.debugLayer.show();
}
}
});