Getting started
A 10-minute tour of the Meep engine, from install to a lit cube rendering on WebGPU.
Meep is a pure-ECS, zero-allocation JavaScript game engine. It’s designed for engineers who want to own the architecture of their game and aren’t afraid of writing code instead of clicking through an editor.
Meep draws through Shade, its own renderer, and Shade runs on WebGPU only - there is no WebGL path, no software fallback and no degradation tier. In practice that means Chromium 149 or newer today: the shaders declare the WGSL immediate_address_space extension, which Chrome shipped in 149/150, so an older browser acquires a GPU device and then fails the moment it tries to dispatch. The rest of the engine - ECS, physics, AI, generation, math, networking - never touches navigator.gpu and runs anywhere, including Node.
What you’ll need
- A WebGPU browser. Chromium 149+ in practice; see Installation for the exact device floor and how to feature-detect it.
- Node.js 24 or newer. That’s the toolchain requirement, not a constraint on what you ship to.
- A bundler that handles ES modules and
@rollup/plugin-strip. Vite, Rollup, esbuild and webpack 5 all work. - A valid Meep license. The Free tier covers evaluation and most hobby projects.
Install
npm install @woosh/meep-engine
The package ships the full engine source under src/ - around 6,000 fine-grained ES modules, unobfuscated, no binary blobs. There is no prebuilt bundle to import: you import module paths directly and let your bundler tree-shake. samples/engine/first-scene/ inside the package is the smallest complete application - boot, a lit scene, keyboard and pointer input - and the README beside it covers the Vite configuration, why a scene is authored in metres, and reference values for photometric lights.
The two core pieces
Meep splits the ECS into two cooperating objects:
EntityComponentDatasetowns the entities and components - the data.EntityManagerowns the systems and orchestrates the simulation loop - the logic.
You usually create one of each, register your systems on the EntityManager, attach the dataset, and then call update(dt) once per frame.
import { EntityComponentDataset } from "@woosh/meep-engine/src/engine/ecs/EntityComponentDataset.js";
import { EntityManager } from "@woosh/meep-engine/src/engine/ecs/EntityManager.js";
import { System } from "@woosh/meep-engine/src/engine/ecs/System.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { t64_announce_change } from "@woosh/meep-engine/src/engine/ecs/transform/t64_announce_change.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
Spawning entities (high-level API)
The fluent way to build an entity is through the Entity class. It’s a thin wrapper that lets you compose an entity in one expression and then commit it to a dataset.
First, a component to attach. Components are just classes - tiny, focused data containers. Transform64 is built in; here’s a Velocity of your own:
class Velocity {
velocity = new Vector3(0, 0, 0);
}
Now compose an entity from a Transform64 and a Velocity:
const ecd = new EntityComponentDataset();
const t = new Transform64();
t.setTranslation(0, 0, 0);
const v = new Velocity();
v.velocity.set(1, 0, 0); // moving along +X at 1 unit/sec
const id = new Entity()
.add(t)
.add(v)
.build(ecd);
Entity is the high-level builder. It hides the verbosity of the low-level dataset.createEntity() + addComponentToEntity() calls and gives you a chainable API.
You can also construct a Transform64 from JSON:
const t = Transform64.fromJSON({ translation: { x: 0, y: 0, z: 0 } });
Transform64 is a Float64Array of 23 elements: a matrix at 0..15 whose translation column is the translation, a rotation quaternion at 16..19 and a scale at 20..22. Read a component with translation_x … scale_z, write one with setTranslation / setRotation / setScale, and call updateMatrix() after a rotation or a scale so the matrix catches up. The Velocity above carries a single velocity vector. Components hold no logic of their own - that lives in systems.
Iterating entities
To do something with every entity matching a component shape, use traverseEntities:
ecd.traverseEntities(
[Transform64, Velocity],
(transform, vel, entity) => {
transform.setTranslation(
transform.translation_x + vel.velocity.x * dt,
transform.translation_y + vel.velocity.y * dt,
transform.translation_z + vel.velocity.z * dt,
);
// a Transform64 has no signals: nothing downstream sees the move until
// the writer says so
t64_announce_change(ecd, entity);
}
);
The callback receives one argument per requested component, plus the entity id. Internally the dataset walks a tight typed-array index - querying for a component shape with a million matches has the same per-iteration cost as querying for a hundred.
Wiring up a system
Most of the time you don’t call traverseEntities directly from your game loop - you put the logic in a System and let the EntityManager schedule it for you.
class MovementSystem extends System {
dependencies = [Transform64, Velocity];
update(timeDelta) {
const ecd = this.entityManager.dataset;
ecd.traverseEntities(
[Transform64, Velocity],
(transform, vel, entity) => {
transform.setTranslation(
transform.translation_x + vel.velocity.x * timeDelta,
transform.translation_y + vel.velocity.y * timeDelta,
transform.translation_z + vel.velocity.z * timeDelta,
);
t64_announce_change(ecd, entity);
}
);
}
}
const em = new EntityManager();
em.addSystem(new MovementSystem());
em.attachDataset(ecd);
em.startup();
// Per-frame:
em.update(0.016); // advance the simulation by 16ms
The dependencies array tells the engine which components the system cares about, which lets it compute an efficient execution order. The base System class also provides a fixedUpdate(dt) hook for physics-stable stepping, and link(...)/unlink(...) hooks for per-entity setup and teardown.
em.simulate(dt)is a deprecated alias ofupdate; callupdate.
MovementSystem is deliberately the smallest system that does something - a hand-rolled integrator to show the shape. You won’t write your own for real physics: Meep ships a built-in PhysicsSystem that simulates RigidBody and Collider components (gravity, contacts, joints, raycasts) for you. See the physics docs for that path.
A live example
Everything above, running right here on the page - a single cube spawned with
the high-level Entity builder and drawn by ShadedGeometrySystem. Drag to
orbit, scroll to zoom. This is a real meep build embedded in the docs, not a
video, so it needs a WebGPU browser like everything else Shade draws.
Two pieces are new here. EngineHarness spins up the canvas, camera and lights
so there’s something to look at. And ShadedGeometrySystem - the system that
puts an entity’s geometry in front of the renderer - is constructed with the
graphics facade and the Shade Scene to draw into, which
EngineHarness.shadeScene(engine) hands out (one per engine, made on first
ask).
The engine’s view stack uses pointer-events: none so DOM UI can sit over the
game. The render viewport opts back in with auto, and GraphicsEngine preserves
that setting when it mounts or replaces the canvas. Pointer input needs no extra
canvas styling in this example.
Source
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { StandardShadeMaterial } from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
import { Color } from "@woosh/meep-engine/src/core/color/Color.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
// EngineHarness.bootstrap creates the EntityManager + dataset, attaches a
// canvas, and gives us a hook to register systems. ShadedGeometrySystem is what
// actually draws any entity carrying a ShadedGeometry component.
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
config.addSystem(new ShadedGeometrySystem(engine.graphics, EngineHarness.shadeScene(engine)));
},
});
// buildBasics frames the camera, adds a directional sun with a default
// environment behind it, and wires the orbital drag-to-look / WASD-to-pan /
// wheel-to-zoom controls.
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: false,
focus: new Vector3(0, 0, 0),
distance: 4,
pitch: 0.6,
yaw: 0.4,
showFps: false,
});
const ecd = engine.entityManager.dataset;
// The shape and the material are Shade's. `make_box_geometry` builds an
// authoring-form Geometry; `meshlet_geometry_build_from_geometry` clusters it
// into the GPU form the renderer draws - a precompute you do once per shape,
// not per frame.
const cubeGeometry = meshlet_geometry_build_from_geometry(make_box_geometry(1, 1, 1));
// Material colours are linear. `Color.parse` speaks encoded sRGB - the space a
// hex code from a design tool is in - so decode it before assigning.
const cubeMaterial = new StandardShadeMaterial();
cubeMaterial.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse("#4ef0a8")));
const cubeMesh = ShadedGeometry.from(cubeGeometry, cubeMaterial);
const t = new Transform64();
t.setTranslation(0, 0, 0);
new Entity()
.add(t)
.add(cubeMesh)
.build(ecd);Three things in there are worth naming, because they’re the ones people trip on:
- The component and the system live in different trees.
ShadedGeometryis atgraphics/ecs/mesh-v2/; every rendering system is undergraphics3/and takes explicit collaborators rather than the engine. - Meshletisation is a one-time cost per shape, not per entity. Build the
MeshletGeometryonce and hand the same one to everyShadedGeometrythat needs it. - Something has to give the scene to the graphics engine.
ShadedGeometrySystemdeliberately doesn’t;LightSystemdoes, andbuildBasics({ enableLights: true })registers it for you. Drop the lights and you get a black frame with the cube in it. See the rendering overview for the wiring rule.
Adding particle effects
Use ParticleEffect with Transform64 for GPU particles. Register
GPUParticleEmitterSystem(engine.graphics, EngineHarness.shadeScene(engine), engine.assetManager)
in the bootstrap configuration, alongside any other rendering systems.
A particle simulation has three authored parts: a layout declaring each
particle’s state, an INIT graph that seeds newborn particles, and an
UPDATE graph that moves, colours and eventually kills them. Compile those
graphs with create_particle_effect, then pass the result to
ParticleEffect.from with an emission rate, texture and render bindings.
The system handles GPU simulation and the shared sprite atlas.
Start with the complete small graph,
or open the five-effect particle example and choose
View source. Its src/particleGraphs.js keeps the simulation graphs in the
example, and src/specs/ holds the editable layer settings and curves.
Production builds
In development the engine runs 6,000+ assertions that catch invalid component state, out-of-range indices and broken invariants at the moment they happen. For production, strip them with @rollup/plugin-strip:
// vite.config.js
import { defineConfig } from "vite";
import strip from "@rollup/plugin-strip";
export default defineConfig({
plugins: [
{ ...strip(), apply: "build" },
],
});
The plugin’s defaults already cover assert.*. They also cover console.* - pass strip({ functions: ["assert.*"] }) if you want to keep your own logging. Stripped builds run at native speed and tree-shake down to only the modules you actually import.
Next up
- Installation guide - device floor, peer dependencies, bundler configuration.
- ECS overview - the architecture and design philosophy.
- Rendering overview - how Shade puts a frame together.
- Migrating 2.x to 3 - if you have an existing project.
- FAQ - answers to the most common questions.
For licensing - tiers, pricing, contract language - see /pricing and /license.
Heads up - this documentation is being built out as we go. Subsystems with thinner coverage are marked
draftin the sidebar. Email us if there’s a specific area you need expanded sooner.