Babylon.js 3D Games skill for Codex
Web 3D that loads lean and holds its frame rate. Codex sets up Babylon.js with deep ES module imports, loads glTF levels and characters through asset containers, adds Havok physics with primitive shapes and correct Vite wasm handling, and uses instances or thin instances for repeated props. It reads Inspector statistics on the target device and applies freezing, picking and scaling fixes with before-and-after numbers.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- babylonjs Installs to ~/.agents/skills/babylonjs/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install babylonjs The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project babylonjs Installs to .agents/skills/babylonjs/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/babylonjs.zipunzip -o babylonjs.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) babylonjs PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $babylonjs, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds 3D browser games and scenes with Babylon.js 8 - Engine and WebGPUEngine setup, ES module imports that tree-shake, glTF loading with asset containers, PBR materials, cameras and input, Havok physics with PhysicsAggregate, thin instances, the Inspector, and frame-time optimisation (freezing, picking, active mesh evaluation). Use when creating or debugging a Babylon.js scene, loading GLB models, adding physics, building a web 3D game in TypeScript, or when a Babylon scene has too many draw calls, a huge bundle or low FPS on mobile.
Use it when
- Creating a 3D browser game or interactive scene with Babylon.js
- Loading GLB or glTF models and managing level loading and unloading
- Adding Havok physics to a Babylon scene
- A Babylon scene has high draw calls, low FPS or a heavy bundle
Use something else when
- The project uses Three.js: use threejs-scenes
- You only need 2D sprites and tilemaps: use phaser-games
How to check the result
Inspector statistics on the target device show draw calls and frame time within budget, and loading and unloading a level three times leaves mesh, material and texture counts unchanged.
The mistake it prevents
Importing from the @babylonjs/core root or forgetting side-effect imports, which either bloats the bundle or fails at runtime with a missing component error.
Done well means: the bundle imports only what it uses, assets load through asset containers with a loading screen, physics runs on Havok with simple collision shapes, repeated meshes are instanced, and the Inspector shows draw calls and active meshes within budget on the slowest target device.
Before writing code
- Read
AGENTS.mdandpackage.json. Check for@babylonjs/core(ES modules, preferred) versus the UMDbabylonjspackage; do not mix the two. - Note the major version (8.x is current; 7.x code is mostly compatible, older loader
calls via
SceneLoaderstill work but newer module-level functions exist). - Check the bundler config (Vite typical) for how
.glb,.envand.wasmfiles are served. - Installing
@babylonjs/loaders,@babylonjs/havokor@babylonjs/inspectorneeds network; ask if the sandbox blocks it.
Engine and scene
import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera";
import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight";
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import "@babylonjs/loaders/glTF"; // side-effect import registers the loader
const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
const engine = new Engine(canvas, true, { stencil: true, adaptToDeviceRatio: false });
const scene = new Scene(engine);
const camera = new ArcRotateCamera("cam", -Math.PI / 2, Math.PI / 3, 12, Vector3.Zero(), scene);
camera.attachControl(canvas, true);
camera.lowerRadiusLimit = 4;
camera.upperRadiusLimit = 30;
new HemisphericLight("sky", new Vector3(0, 1, 0), scene).intensity = 0.8;
engine.runRenderLoop(() => scene.render());
window.addEventListener("resize", () => engine.resize());
- Deep imports (
@babylonjs/core/Meshes/meshBuilder) keep bundles small; importing from the@babylonjs/coreroot pulls in far more. Some features need side-effect imports to register themselves (loaders,@babylonjs/core/Physics/v2/physicsEngineComponentfor physics,@babylonjs/core/Culling/rayfor picking). A missing one shows as a runtime error naming the component. adaptToDeviceRatio: truerenders at full device pixel ratio; on phones that can mean 3x the pixels. Leave it off and useengine.setHardwareScalingLevel(1 / Math.min(devicePixelRatio, 1.5)).- WebGPU:
const engine = new WebGPUEngine(canvas); await engine.initAsync();Checkawait WebGPUEngine.IsSupportedAsyncand fall back toEngine.
Loading assets
import { LoadAssetContainerAsync } from "@babylonjs/core/Loading/sceneLoader";
const container = await LoadAssetContainerAsync("/models/level1.glb", scene);
container.addAllToScene();
const enemyTemplate = await LoadAssetContainerAsync("/models/enemy.glb", scene);
function spawnEnemy(pos: Vector3) {
const inst = enemyTemplate.instantiateModelsToScene((n) => `enemy_${n}`, false, { doNotInstantiate: false });
const root = inst.rootNodes[0];
root.position.copyFrom(pos);
inst.animationGroups[0]?.play(true);
return inst;
}
| Need | Use |
|---|---|
| Load one level, add everything | LoadAssetContainerAsync + addAllToScene() (or AppendSceneAsync) |
| Many copies of a character with own animations | Asset container + instantiateModelsToScene |
| Thousands of identical static meshes (rocks, grass) | Thin instances (mesh.thinInstanceAdd(matrix)) |
| Hundreds of identical meshes needing individual picking | mesh.createInstance(name) |
| Unload a level | container.removeAllFromScene() then container.dispose() |
- Export glTF/GLB from Blender with Draco or meshopt compression and KTX2 textures for large scenes; the matching decoders load from Babylon’s CDN by default, which needs network at runtime. Self-host decoders for offline or strict CSP deployments.
- Image-based lighting:
scene.environmentTexture = CubeTexture.CreateFromPrefilteredData("/env/studio.env", scene). PBR materials look flat without an environment texture.
Physics with Havok
import HavokPhysics from "@babylonjs/havok";
import { HavokPlugin } from "@babylonjs/core/Physics/v2/Plugins/havokPlugin";
import { PhysicsAggregate } from "@babylonjs/core/Physics/v2/physicsAggregate";
import { PhysicsShapeType } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugin";
import "@babylonjs/core/Physics/v2/physicsEngineComponent";
const havok = await HavokPhysics();
scene.enablePhysics(new Vector3(0, -9.81, 0), new HavokPlugin(true, havok));
new PhysicsAggregate(ground, PhysicsShapeType.BOX, { mass: 0 }, scene); // static
const crate = new PhysicsAggregate(box, PhysicsShapeType.BOX, { mass: 2, restitution: 0.1 }, scene);
crate.body.applyImpulse(new Vector3(0, 6, 0), box.getAbsolutePosition());
- Mass 0 means static. Use primitive shapes (BOX, SPHERE, CAPSULE);
MESHshapes are for static level geometry only, andCONVEX_HULLfor dynamic irregular objects. - For a player, use a capsule body with locked rotation (
body.setMassProperties({ inertia: Vector3.Zero() })) or a kinematic character controller rather than pushing a free rigid body around. - Vite: add
optimizeDeps: { exclude: ["@babylonjs/havok"] }so the.wasmfile is served correctly; otherwiseHavokPhysics()fails to fetch it.
Performance
Open the Inspector during development (import "@babylonjs/inspector"; scene.debugLayer.show();,
dynamic import so it never ships) and read Statistics: draw calls, active meshes, frame time.
| Symptom | Fix |
|---|---|
| Hundreds of draw calls from repeated props | Instances or thin instances; merge static meshes with Mesh.MergeMeshes |
| High “active meshes evaluation” time | scene.freezeActiveMeshes() for static scenes; set mesh.alwaysSelectAsActiveMesh where culling is pointless |
| Static objects still computing matrices | mesh.freezeWorldMatrix(); material.freeze() for unchanging materials |
| Pointer move picking every frame | scene.skipPointerMovePicking = true; set isPickable = false on decor |
| Many objects, little interaction | scene.performancePriority = ScenePerformancePriority.Intermediate (or Aggressive, read the trade-offs) |
| Shadows expensive | One shadow generator, small mapSize (1024), restrict renderList to casters that matter |
| Mobile GPU bound | Hardware scaling level, fewer post-processes, no real-time reflections |
Measure with engine.getFps() and the Inspector, on the real device. SceneOptimizer can
degrade settings automatically to hit a target FPS; use it as a safety net, not the plan.
Gotchas
- Creating materials or meshes per frame leaks GPU memory. Create once, reuse,
dispose()when done. mesh.dispose()does not dispose shared materials or textures by default; passdispose(false, true)only when nothing else uses them.- Frozen materials ignore property changes until
unfreeze(). scene.freezeActiveMeshes()stops picking up newly added meshes; unfreeze or call it again after spawning.- glTF uses right-handed coordinates; the loader adds a
__root__node with a negative scale to convert. Parent to that node or transform its children, not the root, when moving models. - Units: physics assumes metres. A character modelled at 180 units tall falls in slow motion.
Verification
npm run buildsucceeds; check bundle size (e.g.vite buildreport) after adding imports.- Inspector Statistics on the target device: frame time within budget (16.6 ms for 60 FPS), draw calls and active meshes recorded before and after.
- Load and unload a level three times:
scene.meshes.length,scene.materials.lengthandscene.textures.lengthreturn to the same numbers. - Physics: objects at rest go to sleep, nothing jitters through the floor at 30 and 144 Hz displays.
Hand-off summary
**Babylon.js:** 8.x ES modules, WebGL engine (WebGPU fallback checked), Havok physics
**Scene:** level1.glb via asset container; 400 rocks as thin instances; enemy template instantiated per spawn
**Perf (Pixel 7):** draw calls 512 -> 74, frame 24 ms -> 13 ms; hardware scaling 1/1.5
**Cleanup:** level unload disposes container; counts stable over 3 reloads
**Not done:** shadow cascade tuning for large outdoor map What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Babylon.js 3D Games"
short_description: "Web 3D that loads lean and holds its frame rate"
default_prompt: "$babylonjs Build a small Babylon.js scene where the player pushes physics crates around" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.