Porting from Babylon.js to Babylon Lite
July 28, 2026 · View on GitHub
This guide shows how to translate a Babylon.js (BJS) scene to Babylon Lite, side by side. Babylon Lite uses factory functions instead of constructors, plain data instead of class instances, and explicit addToScene() instead of auto-registration.
Not ready for a full rewrite? Start with
@babylonjs/lite-compat. The@babylonjs/lite-compatpackage is an opt-in, Babylon.js-shaped compatibility layer built on top of the native Lite API described below. It keeps the familiar class-based surface (new WebGPUEngine,new Scene,new ArcRotateCamera,MeshBuilder,StandardMaterial, …), so an existing BJS scene runs on Lite's WebGPU renderer with little or no code change — and its bundler plugins (Vite / Rollup / Webpack / esbuild) can even rewrite your existing@babylonjs/core,@babylonjs/loaders,@babylonjs/addons, and@babylonjs/materialsimports at build time, so you don't touch a single import. Unsupported APIs throwLiteCompatErrorrather than mis-rendering. The intended path is:@babylonjs/core → @babylonjs/lite-compat → @babylonjs/lite (native)Use lite-compat to get running fast, then port to the native factory-function API in this guide (smaller bundles, full tree-shaking) at your own pace.
Quick Reference
| Babylon.js | Babylon Lite |
|---|---|
new WebGPUEngine(canvas); await engine.initAsync() | const engine = await createEngine(canvas) |
new Scene(engine) | createSceneContext(engine) |
engine.runRenderLoop(() => scene.render()) | await startEngine(engine) |
new ArcRotateCamera("cam", α, β, r, target, scene) | createArcRotateCamera(α, β, r, target) |
new FreeCamera("cam", position, scene) | createFreeCamera(position, target) |
scene.createDefaultCamera(true, true, true) | createDefaultCamera(scene) |
camera.attachControl(canvas, true) | attachControl(camera, canvas, scene) (arc-rotate) / attachFreeControl(camera, canvas, scene) (free) |
camera.mode = Camera.ORTHOGRAPHIC_CAMERA | enableOrthographicCamera(camera, { halfHeight }) |
new HemisphericLight("h", new Vector3(0,1,0), scene) | createHemisphericLight([0,1,0], 1.0) |
new DirectionalLight("d", new Vector3(0,-1,0), scene) | createDirectionalLight([0,-1,0]) |
new SpotLight("s", pos, dir, angle, exp, scene) | createSpotLight(pos, dir, angle, exp) |
MeshBuilder.CreateSphere("s", {}, scene) | createSphere(engine) |
MeshBuilder.CreateBox("b", {}, scene) | createBox(engine) |
MeshBuilder.CreateGround("g", {}, scene) | createGround(engine, opts) |
new StandardMaterial("mat", scene) | createStandardMaterial() |
new PBRMaterial("pbr", scene) | createPbrMaterial() |
new GridMaterial("grid", scene) (@babylonjs/materials) | createGridMaterial(opts) |
SceneLoader.ImportMeshAsync("", url, file, scene) | addToScene(scene, await loadGltf(engine, url)) |
new CubeTexture(url, scene) + createDefaultEnvironment() | await loadEnvironment(scene, url, opts) |
new Texture(url, scene) | await loadTexture2D(engine, url) |
| KTX1 compressed 2D texture | await loadKtxTexture2D(engine, baseUrl, suffixes) |
glTF KTX2 / KHR_texture_basisu texture source | addToScene(scene, await loadGltf(engine, ktx2GltfUrl)) (auto-detected) |
| Basis Universal (.basis) 2D texture | await loadBasisTexture2D(engine, url) |
new ShadowGenerator(size, light) with a directional light and ESM | createEsmDirectionalShadowGenerator(engine, light, opts) |
sg.usePercentageCloserFiltering = true with a spotlight | createPcfSpotlightShadowGenerator(engine, light, opts) |
sg.usePercentageCloserFiltering = true with a directional light | createPcfDirectionalShadowGenerator(engine, light, opts) |
mesh.thinInstanceSetBuffer("matrix", data, 16) | setThinInstances(mesh, data, count) |
mesh.thinInstanceSetBuffer("color", data, 4) | setThinInstanceColors(mesh, data) |
new Vector3(x, y, z) | { x, y, z } or [x, y, z] |
new Color3(r, g, b) | [r, g, b] |
Matrix.Identity() | mat4Identity() |
mesh.dispose() | removeFromScene(scene, mesh) |
scene.onBeforeRenderObservable.add(fn) | onBeforeRender(scene, fn) |
Key Differences
1. No Scene in Constructors
BJS objects take scene in their constructor and auto-register. Lite objects are plain data — you create them, then addToScene() them explicitly.
// ❌ Babylon.js
const light = new HemisphericLight("light", new Vector3(0, 1, 0), scene);
// ✅ Babylon Lite
const light = createHemisphericLight([0, 1, 0], 1.0);
addToScene(scene, light);
2. Engine & Render Loop
BJS uses runRenderLoop with a callback. Lite uses a single startEngine(engine) that returns a promise resolving after the first frame.
// ❌ Babylon.js
const engine = new WebGPUEngine(canvas);
await engine.initAsync();
const scene = new Scene(engine);
// ... setup ...
engine.runRenderLoop(() => scene.render());
// ✅ Babylon Lite
const engine = await createEngine(canvas);
const scene = createSceneContext(engine);
// ... setup ...
await startEngine(engine);
3. Plain Data, Not Classes
Lite uses plain objects, arrays, and Float32Array instead of BJS classes like Vector3, Color3, Matrix.
// ❌ Babylon.js
light.direction = new Vector3(0, -1, 0);
light.diffuse = new Color3(1, 0, 0);
// ✅ Babylon Lite
const light = createDirectionalLight([0, -1, 0]);
light.diffuse = [1, 0, 0];
4. Camera Controls Are Separate
BJS cameras have attachControl as a method. Lite separates camera data from input handling.
// ❌ Babylon.js
const camera = new ArcRotateCamera("cam", -Math.PI / 2, Math.PI / 2, 5, Vector3.Zero(), scene);
camera.attachControl(canvas, true);
// ✅ Babylon Lite
const camera = createArcRotateCamera(-Math.PI / 2, Math.PI / 2, 5, { x: 0, y: 0, z: 0 });
scene.camera = camera;
attachControl(camera, canvas, scene);
5. Loaders and Scene Registration
loadEnvironment() adds its environment data/renderables to the scene internally. loadGltf() returns an asset container; pass it to addToScene() so transform-node hierarchies, meshes, and animation groups are registered explicitly.
// ❌ Babylon.js
await SceneLoader.ImportMeshAsync("", baseUrl, "model.glb", scene);
scene.environmentTexture = new CubeTexture(envUrl, scene);
scene.createDefaultEnvironment({ createSkybox: true, skyboxSize: 1000 });
// ✅ Babylon Lite
addToScene(scene, await loadGltf(engine, "model.glb"));
await loadEnvironment(scene, envUrl, {
skyboxUrl: "skybox.dds",
skyboxSize: 1000,
groundTextureUrl: "ground.png",
brdfUrl: "/brdf-lut.png",
});
6. Shadows Attach to Lights
BJS creates a ShadowGenerator separately. Lite assigns it directly to the light.
// ❌ Babylon.js
const sg = new ShadowGenerator(1024, light);
sg.addShadowCaster(mesh);
sg.useBlurExponentialShadowMap = true;
ground.receiveShadows = true;
// ✅ Babylon Lite
light.shadowGenerator = createEsmDirectionalShadowGenerator(engine, light, {
mapSize: 1024,
depthScale: 50,
blurScale: 2,
});
setShadowTaskCasterMeshes(light.shadowGenerator, [mesh]);
ground.receiveShadows = true;
await registerSceneWithShadowSupport(scene);
For PCF shadows:
// ❌ Babylon.js
const sg = new ShadowGenerator(1024, spotLight);
sg.usePercentageCloserFiltering = true;
// ✅ Babylon Lite
spotLight.shadowGenerator = createPcfSpotlightShadowGenerator(engine, spotLight, {
mapSize: 1024,
});
setShadowTaskCasterMeshes(spotLight.shadowGenerator, [mesh]);
await registerSceneWithShadowSupport(scene);
7. Thin Instances Use Raw Arrays
No Matrix class needed. Pass raw Float32Array with 16 floats per instance.
// ❌ Babylon.js
const matrices = new Float32Array(count * 16);
// ... fill with Matrix values ...
mesh.thinInstanceSetBuffer("matrix", matrices, 16);
mesh.thinInstanceSetBuffer("color", colors, 4);
// ✅ Babylon Lite
const matrices = new Float32Array(count * 16);
// ... fill directly (column-major 4x4) ...
setThinInstances(mesh, matrices, count);
setThinInstanceColors(mesh, colors);
addToScene(scene, mesh);
8. Mesh Factories Take Engine, Not Scene
BJS mesh builders take scene. Lite mesh factories take engine (for GPU buffer creation) and return plain mesh data.
// ❌ Babylon.js
const sphere = MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
sphere.material = new StandardMaterial("mat", scene);
// ✅ Babylon Lite
const sphere = createSphere(engine);
sphere.material = createStandardMaterial();
addToScene(scene, sphere);
9. StandardMaterial Vertex Colors Are Opt-In
PBR materials consume mesh vertex colors automatically. Standard materials use an explicit opt-in so scenes without vertex colors retain no feature code. Supply a tightly packed RGBA buffer (four floats per vertex), call enableStandardVertexColors() once, then register the scene.
import { enableStandardVertexColors } from "babylon-lite";
const vertexCount = positions.length / 3;
const colors = new Float32Array(vertexCount * 4);
for (let i = 0; i < vertexCount; i++) {
const offset = i * 4;
colors[offset] = 1;
colors[offset + 3] = 1;
}
const mesh = createMeshFromData(engine, "colored", positions, normals, indices, undefined, undefined, undefined, colors);
mesh.material = createStandardMaterial();
addToScene(scene, mesh);
enableStandardVertexColors();
await registerScene(scene);
10. Mirrored (Negatively Scaled) Meshes Are Opt-In
Babylon flips sideOrientation automatically whenever a mesh's world-matrix determinant turns
negative. Lite's glTF loader already reverses winding for negative-scale nodes it finds at load
time, but the remaining cases go through an explicit opt-in so scenes that never mirror anything
carry no winding code. Call await enableMirroredMeshes(scene) once — after your assets are added
and before registerScene() — when you mirror a Standard-material mesh, a procedural mesh, or
change a mesh's mirroring after load.
import { enableMirroredMeshes } from "babylon-lite";
const box = createBox(engine, 2);
box.scaling.set(-1, 1, 1); // mirrored — winding is reversed for you
addToScene(scene, box);
await enableMirroredMeshes(scene);
await registerScene(scene);
It also keeps working when the mirroring changes at runtime: each frame the watcher only looks at meshes whose world matrix actually changed (an integer version compare), computes a determinant for those alone, and rebuilds a pipeline only when the sign really flipped.
Only that runtime watcher is scoped to the scene you pass — the pipeline-side winding resolution is installed process-wide on the first call, so in a multi-scene app the other scenes also stop rendering mirrored meshes inside-out, but they will not track a mirroring that changes after their renderables are built unless you call it for them too.
11. Removing & Disposing Entities
BJS uses mesh.dispose() on individual objects. Lite uses removeFromScene() which removes the mesh from the scene and destroys all its GPU resources (buffers, textures, skeleton data).
// ❌ Babylon.js
sphere.dispose();
// ✅ Babylon Lite
removeFromScene(scene, sphere);
For full teardown:
// ✅ Babylon Lite — tear down everything
disposeScene(scene); // releases all meshes, renderables, disposables
disposeEngine(engine); // destroys GPU device, render targets, swapchain
Full Example: Porting a PBR Scene
Babylon.js
const engine = new WebGPUEngine(canvas);
await engine.initAsync();
const scene = new Scene(engine);
scene.clearColor = new Color4(0.2, 0.2, 0.3, 1);
const light = new HemisphericLight("h", new Vector3(0, 1, 0), scene);
light.intensity = 1.0;
await SceneLoader.ImportMeshAsync("", baseUrl, "BoomBox.glb", scene);
const envTex = new CubeTexture(envUrl, scene);
scene.environmentTexture = envTex;
scene.createDefaultCamera(true, true, true);
scene.createDefaultEnvironment({ skyboxSize: 1000 });
engine.runRenderLoop(() => scene.render());
Babylon Lite
const engine = await createEngine(canvas);
const scene = createSceneContext(engine);
addToScene(scene, await loadGltf(engine, "BoomBox.glb"));
await loadEnvironment(scene, envUrl, {
skyboxUrl: "skybox.dds",
skyboxSize: 1000,
groundTextureUrl: "ground.png",
brdfUrl: "/brdf-lut.png",
});
const cam = createDefaultCamera(scene);
attachControl(cam, canvas, scene);
addToScene(scene, createHemisphericLight([0, 1, 0], 1.0));
await startEngine(engine);
Gotchas
| Gotcha | Details |
|---|---|
| No auto-add | Meshes, lights, transform nodes, and loadGltf() asset containers must be explicitly added with addToScene(). loadEnvironment() adds its environment data/renderables internally. |
No new keyword | Everything is created via factory functions, not constructors. |
| Assign camera explicitly | Either use createDefaultCamera(scene) (auto-assigns) or set scene.camera = myCamera manually. |
| Materials are optional | createStandardMaterial() / createPbrMaterial() return props objects. Assign to mesh.material. |
| Standard vertex colors | Supply four floats (RGBA) per vertex and call enableStandardVertexColors() before registerScene(). PBR vertex colors remain automatic. |
| Mirrored meshes | Call await enableMirroredMeshes(scene) before registerScene() when you give a mesh (or an ancestor) a negative scale, so its triangle winding is reversed. glTF negative-scale nodes are already handled at load time. |
| WebGPU only | No WebGL fallback. createEngine() throws if WebGPU is unavailable. |
No dispose() on meshes | Use removeFromScene(scene, mesh) to remove a single mesh and destroy its GPU resources. Use disposeScene(scene) + disposeEngine(engine) to tear down everything. |
| Tree-shakable imports | Import only what you use. Unused features are stripped from the bundle. |
| KTX2 is glTF-scoped | KTX1 has a direct loadKtxTexture2D() helper. KTX2/BasisU texture sources are handled through glTF KHR_texture_basisu during loadGltf() so non-KTX2 scenes pay zero runtime bundle cost. |
| Material property animation | Mutating material props at runtime requires marking the material dirty. See Material Animation section below. |
Material Animation
Babylon Lite supports animating material properties at runtime (e.g. changing colors, alpha, anisotropy intensity per frame). Two approaches are available:
Manual (default — zero overhead)
Mutate the property, then call markMaterialUboDirty():
import { markMaterialUboDirty } from "@babylonjs/lite";
onBeforeRender(scene, () => {
material.alpha = Math.sin(time) * 0.5 + 0.5;
markMaterialUboDirty(material);
});
This works for both PBR and Standard materials. Zero runtime cost when nothing changes.
Automatic tracking (opt-in)
Call enableMaterialTracking() once on a material to install property setters that auto-detect changes — including in-place array mutations like material.diffuseColor[0] = 0.5:
import { enableMaterialTracking } from "@babylonjs/lite";
const mat = createPbrMaterial({ anisotropy: { isEnabled: true, intensity: 1.0 } });
enableMaterialTracking(mat);
// Now mutations auto-mark the material UBO dirty — no manual call needed:
onBeforeRender(scene, () => {
mat.anisotropy!.intensity = Math.cos(a) * 0.5 + 0.5; // auto-dirty
mat.emissiveColor![0] = 0.5; // auto-dirty (index write)
});
enableMaterialTracking is fully tree-shakable — scenes that don't import it pay zero bundle cost.
| Feature | markMaterialUboDirty | enableMaterialTracking |
|---|---|---|
| Bundle cost | ~50 bytes | ~1.5 KB (only if imported) |
| Per-frame cost | Zero (manual call) | Zero (setter fires only on change) |
Catches color[0] = x | ❌ (must call manually) | ✅ |
Catches mat.alpha = x | ❌ (must call manually) | ✅ |
Material Stencil (opt-in)
Babylon Lite supports a per-material stencil test baked into the main color pass — for masking effects like portals and decals (one material writes the stencil buffer where it draws, another discards fragments where the stencil was written). It is an explicit opt-in so stencil-free scenes stay byte-near-identical:
import { createStandardMaterial, enableMaterialStencil, registerScene } from "@babylonjs/lite";
// Writer: stamp the stencil buffer (0 → 1) everywhere it draws.
const mask = createStandardMaterial();
mask.stencil = { passOp: "increment-clamp" };
// Tester: draw only where the stencil is still the pass's default reference of 0
// (i.e. where the writer did NOT draw). No dynamic stencil reference needed.
const masked = createStandardMaterial();
masked.stencil = { compare: "equal" };
enableMaterialStencil(); // ← opt-in, BEFORE registerScene
await registerScene(scene);
StencilState accepts compare, passOp, failOp, depthFailOp, readMask, and writeMask (all
optional; defaults "always" / "keep" / 0xff). Stencil is applied only on a stencil-capable target (the
main color pass) and ignored on depth-only/shadow passes. Without calling enableMaterialStencil, a
material's stencil field is inert and the pipeline builders carry no stencil code — enableMaterialStencil
is fully tree-shakable, so scenes that don't import it pay no bundle cost.
glTF / PBR Extensions
Babylon Lite's glTF loader + PBR material understand the following extensions. Each feature is tree-shakable: scenes that don't use it pay no bundle cost.
| Extension / Feature | Support | Notes |
|---|---|---|
KHR_materials_pbrSpecularGlossiness | ✅ | Auto-detected by loadGltf() |
KHR_materials_clearcoat | ✅ | Auto-detected; or createPbrMaterial({ clearCoat: { ... } }) |
KHR_materials_sheen | ✅ | Auto-detected (BJS-spec albedo scaling for glTF); or createPbrMaterial({ sheen: { ... } }) |
KHR_materials_anisotropy | ✅ | Auto-detected; or createPbrMaterial({ anisotropy: { ... } }) |
KHR_materials_variants | ✅ | selectVariant(scene, name), getVariantNames(scene), resetVariant(scene) |
KHR_materials_ior | ✅ | Auto-detected; index of refraction for dielectrics (Scene 30) |
KHR_materials_specular | ✅ | Auto-detected; dielectric specular intensity + color (Scene 30) |
KHR_materials_volume | ✅ | Auto-detected; attenuation color/distance + thickness (Scene 30) |
KHR_materials_transmission | ✅ | Frame-graph scene-texture transmission for transmissive glTF materials (Scenes 30/33/112). Screen-space scene-texture refraction; parity is within-5 = 100% of pixels. |
KHR_texture_transform | ✅ | Auto-resolved at load (material-wide UV transform) |
KHR_texture_basisu | ✅ | Auto-detected; dynamically loads KTX2 decoder/upload path only for glTF assets that declare the extension (Scene 112) |
EXT_texture_webp | ✅ | Auto-detected through texture source selection; image decode is browser-native (Scene 37) |
KHR_draco_mesh_compression | ✅ | Auto-detected; loads draco_decoder.js + .wasm on demand from site root (override via setDracoBaseUrl()) |
KHR_materials_emissive_strength | ✅ | Auto-detected; multiplies emissive output (Scene 31) |
KHR_materials_unlit | ✅ | Auto-detected; emits base color directly with no lighting (Scene 32) |
KHR_lights_punctual | ✅ | Auto-detected; point / spot / directional lights baked from glTF nodes (Scene 33) |
KHR_node_visibility | ✅ | Auto-detected; per-node visibility flag honoured at render time (Scene 34) |
KHR_animation_pointer | ✅ | Auto-detected; animates arbitrary JSON pointers (e.g. node visibility, material UBO fields) (Scene 34) |
EXT_mesh_gpu_instancing | ✅ | Auto-detected; per-node TRS accessors expanded into thin instances (Scene 35) |
EXT_meshopt_compression | ✅ | Auto-detected; meshopt-decodes vertex/index buffers via a dynamically-imported decoder (Scene 211) |
KHR_mesh_quantization | ✅ | Auto-detected; normalized/quantized vertex attributes uploaded with native typed formats (Scene 211) |
KHR_xmp_json_ld | ✅ | Auto-detected; JSON-LD metadata packets surfaced on AssetContainer.xmpMetadata with zero render impact (Scene 210) |
ExtrasAsMetadata | ✅ | Promotes glTF node, mesh, primitive, and material extras to metadata.gltf.extras |
| Interleaved vertex buffers | ✅ | Genuine GPU-level interleave: a strided bufferView is uploaded once and bound to each attribute slot via arrayStride/offset — no CPU de-interleave or asset rewrite (Scene 210) |
| Subsurface translucency + thickness | ✅ | createPbrMaterial({ subsurface: { translucency, thickness } }) |
| Specular anti-aliasing | ✅ | Auto-on for glTF; manual: createPbrMaterial({ enableSpecularAA: true }) |
| Morph targets | ✅ | PBR meshes only (not StandardMaterial) |
| Skeletal animation (4 or 8 bones) | ✅ | Driven by createAnimationController(scene) |
| Animation blending / weights / additive clips | ✅ | AnimationManager with setAnimationWeight(), crossFadeAnimationGroups(), and setAnimationAdditive() (Scenes 155-158) |
| ShaderMaterial | ✅ | WGSL-only createShaderMaterial() with typed uniforms, samplers, defines, alpha blend/test (Scenes 159-163) |
| GridMaterial | ✅ | Procedural unlit object-space grid via createGridMaterial(): mainColor/lineColor, gridRatio, gridOffset, major/minor units, opacity, antialias, useMaxLine, preMultiplyAlpha, opacityTexture, visibility (Scene 213) |
| Node Material | ✅ | NME snippet parser covering core, PBR, math, texture, procedural, normal, screen/depth, matrix, loop, and storage blocks (Scenes 60-89) |
| Sprites / billboards | ⚡ | 2D layers, depth-hosted sprites, facing/axis-locked/cutout billboards; not the full BJS SpriteManager API (Scenes 50-57) |
| Gaussian splatting | ✅ | .ply, .splat, .sog, .spz, bake transforms, material plugin fragments (Scenes 120-126) |
| CSG / CSG2 | ✅ | Mesh boolean subtract/intersect/union/add APIs (Scenes 90-91) |
| Physics | ⚡ | Havok Physics V2 subset (Scene 40) |
| Navigation / Recast | ⚡ | Recast V2 navmesh, crowd pathing, tile-cache obstacles, off-mesh links, raycast (Scenes 170-175) |
| Device-lost recovery | ✅ | Opt-in SceneContext, SpriteRenderer, and TextRenderer recovery via the corresponding enableDeviceLost*Recovery API (Scene 164 covers SceneContext) |
| Screen-space SSS (PrePass) | ❌ | Not implemented — only BRDF-layer translucency |
See lab/lite/src/lite/scene*.ts for end-to-end examples of each extension in action.