web-3d-threejs-babylon
Use when building a 3D web game with three.js, Babylon.js or PlayCanvas - render loop and delta per library, fixed-step physics with Rapier or Havok, explicit dispose of GPU resources, instancing and draw-call budgets, glTF with Draco/meshopt/KTX2, pixel ratio and resize, WebGPU opt-in, React Three Fiber notes, and headless testing.
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-15
- Stars
- ★ 122
- Forks
- 18
01 前言 Front Matter
- name
- web-3d-threejs-babylon
- source
- informed by the three.js, Babylon.js, PlayCanvas and Rapier documentation; own wording
- category
- engine
- tech_stack
- Web 3D
- description
- Use when building a 3D web game with three.js, Babylon.js or PlayCanvas - render loop and delta per library, fixed-step physics with Rapier or Havok, explicit dispose of GPU resources, instancing and draw-call budgets, glTF with Draco/meshopt/KTX2, pixel ratio and resize, WebGPU opt-in, React Three Fiber notes, and headless testing.
03 SKILL.md 正文
Web 3D: three.js, Babylon.js, PlayCanvas
Overview
three.js is a rendering library with a huge ecosystem; Babylon.js is a full engine (physics plugin, GUI, inspector, a NullEngine for headless tests); PlayCanvas is an engine built around its hosted editor, also usable engine-only. All three hand you the GPU directly — and with it the job of freeing what you allocate, keeping draw calls low, and staying inside a mobile browser's memory.
Core principle: Simulate on a fixed step, render every frame, dispose every GPU resource you create, and count draw calls.
The loop and delta
// three.js — Timer replaces the deprecated Clock (r183+); setAnimationLoop also drives WebXR
const timer = new THREE.Timer();
timer.connect(document); // ignores the huge delta after a hidden tab
renderer.setAnimationLoop((t) => {
timer.update(t);
const dt = Math.min(timer.getDelta(), 0.25); // seconds
stepFixed(dt); // accumulator: physics + rules at 1/60 (game-loop-and-time)
renderer.render(scene, camera);
});
// Babylon.js — getDeltaTime() is milliseconds
scene.onBeforeRenderObservable.add(() => stepFixed(Math.min(engine.getDeltaTime() / 1000, 0.25)));
engine.runRenderLoop(() => scene.render());
window.addEventListener("resize", () => engine.resize());
// PlayCanvas (engine API / ESM scripts) — dt in seconds
export class Spin extends Script {
static scriptName = "spin";
speed = 90;
update(dt: number) { this.entity.rotate(0, this.speed * dt, 0); }
}
Physics on a fixed step
- Rapier (WASM, three.js or Babylon):
await RAPIER.init(),world.timestep = 1 / 60, callworld.step()from the accumulator, copy body transforms to meshes with interpolation. The deterministic build exists for lockstep/rollback. - Babylon: Havok via the physics V2 plugin (
new HavokPlugin(true, await HavokPhysics())); keep its fixed timestep consistent across devices. - PlayCanvas: its rigidbody components (ammo.js) step with the app.
- Never move a dynamic body by writing
mesh.position; set velocities/impulses or use kinematic bodies.
Dispose what you create
// ❌ removing from the scene does not free GPU memory
scene.remove(level);
// ✅ free geometry, materials and textures explicitly (unless shared with something still alive)
level.traverse((o) => {
if (o instanceof THREE.Mesh) {
o.geometry.dispose();
for (const m of Array.isArray(o.material) ? o.material : [o.material]) {
for (const v of Object.values(m)) if (v instanceof THREE.Texture) v.dispose();
m.dispose();
}
}
});
scene.remove(level);
console.assert(renderer.info.memory.geometries <= baseline.geometries);
- three.js: also dispose render targets,
PMREMGeneratoroutputs, and the renderer itself on teardown.renderer.info.memorybefore and after a level change is the leak test. - Babylon:
mesh.dispose(false, true)frees materials and textures too; load levels into anAssetContaineranddispose()it on exit;scene.dispose()/engine.dispose()on teardown. - PlayCanvas:
entity.destroy(), andasset.unload()for assets no longer used.
Draw calls and instancing
- Many copies of one mesh:
THREE.InstancedMesh(setMatrixAt+instanceMatrix.needsUpdate = true,countfor the live number) or Babylon thin instances (mesh.thinInstanceAdd(matrix)). - Merge static scenery (
BufferGeometryUtils.mergeGeometries, BabylonMesh.MergeMeshes) and share materials. - Budget draw calls per frame — a few hundred for mobile browsers, more on desktop — and read the actual number (
renderer.info.render.calls, Babylon Inspector's statistics) before and after. - Shadows are expensive: one shadow-casting light, tight shadow camera bounds,
castShadowonly where it shows; bake lighting for static scenes. - Cap the pixel ratio:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); resize from the canvas size, not the window, with aResizeObserver.
Assets
- glTF/GLB with Draco or meshopt geometry compression and KTX2 textures: three.js
GLTFLoader+DRACOLoader/MeshoptDecoder+KTX2Loader(decoder files served from the app's own path); Babylon's glTF loader with its Draco/KTX2 support;LoadAssetContainerAsyncto keep a level's assets disposable together. - Optimise models offline (
npx @gltf-transform/cli optimize) and keep originals out of the shipped bundle (game-asset-pipeline).
WebGPU
three.js WebGPURenderer (from three/webgpu, falling back to WebGL2) and Babylon WebGPUEngine are usable; adopt them only when the repository or task opts in — shader code, post-processing and some materials differ between the paths.
React Three Fiber (if the repo uses it)
useFrame((state, delta) => ...) mutates refs; never setState per frame. Dispose is automatic for objects R3F created declaratively, not for ones you created imperatively. <Instances> / <instancedMesh> for repetition.
Testing
- Rules and simulation: plain TS with Vitest, no renderer import.
- Babylon:
new NullEngine()+new Scene(engine)runs scene logic headless in Vitest (no pixels). - three.js: scene-graph logic can be tested without a renderer; rendering is checked with the seeded Playwright smoke run and screenshots (game-visual-self-review). Headless Chromium uses a software GL — correct images, meaningless frame rates.
Common Mistakes
scene.removewithoutdispose— memory grows every level.- Allocating
new THREE.Vector3()in the frame loop instead of reusing module-level temporaries. - Full device pixel ratio on 3× phones.
- One mesh per bullet or tree instead of instancing.
THREE.Clockin new code on a revision that deprecated it.
Red Flags
- No
dispose()call anywhere in a project that loads more than one level. renderer.info.render.callsin the thousands on a mobile target.- Physics stepped with the raw render delta.
04 同仓库更多 Skills
visual-craft
暂无简介
ux-copy
Use when you write any words a user reads in a mockup or a hand-off - content language and voice, buttons, labels, errors, empty states, confirmations, numbers and dates, and the longest-string check
screen-mockup-html
Use when you draw screens as the design task's HTML mockup - ONE canvas document holding every screen and every variant side by side, the sanitizer-safe recipe, phone and wide frames, the states grid, static control states, and the loopback self-check with screenshots at 375, 768 and 1440 attached to the task
performance-awareness
How the performance score moves for your role. Use when the score context shows a declining trend, or before handing off work you have not self-checked against every criterion.
design-variants
Use when a screen design needs variants - how many, variants as side-by-side rows on the one canvas, what makes two variants genuinely different, recommending one, how the human's choice is read, and narrowing to the chosen one in revision
design-system-authoring
Use when you derive, change or extend a design system - where tokens live per stack, the DTCG token tree, role-based names for both themes, base vs layer vs drift, the DESIGN.md section order, the inventory format, the contrast check and the lint every proposal returns