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.

  • Game Development
  • $babylonjs
  • 162 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- babylonjs

Installs to ~/.agents/skills/babylonjs/ for every project on this machine.

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

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.

~/.agents/skills/babylonjs/SKILL.md

--- name: babylonjs description: Builds 3D browser games and scenes with Babylon.js 8 - Engine and WebGPUEngine setup, ES module imports that tree-shake, glTF loading with asset… license: MIT ---

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

  1. Read AGENTS.md and package.json. Check for @babylonjs/core (ES modules, preferred) versus the UMD babylonjs package; do not mix the two.
  2. Note the major version (8.x is current; 7.x code is mostly compatible, older loader calls via SceneLoader still work but newer module-level functions exist).
  3. Check the bundler config (Vite typical) for how .glb, .env and .wasm files are served.
  4. Installing @babylonjs/loaders, @babylonjs/havok or @babylonjs/inspector needs 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/core root pulls in far more. Some features need side-effect imports to register themselves (loaders, @babylonjs/core/Physics/v2/physicsEngineComponent for physics, @babylonjs/core/Culling/ray for picking). A missing one shows as a runtime error naming the component.
  • adaptToDeviceRatio: true renders at full device pixel ratio; on phones that can mean 3x the pixels. Leave it off and use engine.setHardwareScalingLevel(1 / Math.min(devicePixelRatio, 1.5)).
  • WebGPU: const engine = new WebGPUEngine(canvas); await engine.initAsync(); Check await WebGPUEngine.IsSupportedAsync and fall back to Engine.

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;
}
NeedUse
Load one level, add everythingLoadAssetContainerAsync + addAllToScene() (or AppendSceneAsync)
Many copies of a character with own animationsAsset container + instantiateModelsToScene
Thousands of identical static meshes (rocks, grass)Thin instances (mesh.thinInstanceAdd(matrix))
Hundreds of identical meshes needing individual pickingmesh.createInstance(name)
Unload a levelcontainer.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); MESH shapes are for static level geometry only, and CONVEX_HULL for 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 .wasm file is served correctly; otherwise HavokPhysics() 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.

SymptomFix
Hundreds of draw calls from repeated propsInstances or thin instances; merge static meshes with Mesh.MergeMeshes
High “active meshes evaluation” timescene.freezeActiveMeshes() for static scenes; set mesh.alwaysSelectAsActiveMesh where culling is pointless
Static objects still computing matricesmesh.freezeWorldMatrix(); material.freeze() for unchanging materials
Pointer move picking every framescene.skipPointerMovePicking = true; set isPickable = false on decor
Many objects, little interactionscene.performancePriority = ScenePerformancePriority.Intermediate (or Aggressive, read the trade-offs)
Shadows expensiveOne shadow generator, small mapSize (1024), restrict renderList to casters that matter
Mobile GPU boundHardware 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; pass dispose(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 build succeeds; check bundle size (e.g. vite build report) 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.length and scene.textures.length return 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem