Installation

Install the engine, meet the WebGPU device floor, and configure your bundler for development and production.

From npm

npm install @woosh/meep-engine

This pulls the latest published release.

What comes with it

Two runtime dependencies, installed for you:

PackageVersionWhat it’s for
opentype.js1.3.4Font parsing
robust-predicates3.0.3Exact geometric predicates for triangulation and hull construction

PNG and compressed texture decoding use the platform’s asynchronous DecompressionStream API.

And one peer dependency:

PeerVersionRequired?Who imports it
dat.gui>=0.7.0YesDatGuiController, DatGuiUtils, the built-in OptionsView, one in-tree prototype script, and the editor’s own DatGuiController

dat.gui carries no optional flag, so npm installs it alongside the engine. It enters your bundle only if you import a module that imports it. The harness FPS counter - EngineHarness.addFpsCounter, or buildBasics({ showFps: true }) - is FrameRateView, part of the engine; it needs no package.

There is no renderer peer dependency. Shade, the renderer, ships inside the package. three.js is not a dependency of any kind - not required, not optional, not a peer.

The package declares "engines": { "node": ">=24" }. That is the Node version the toolchain expects when you install and build, not a constraint on the browsers you ship to.

There is no prebuilt bundle

package.json has no main and no module field. The exports map publishes ./src/*, ./build/*, ./editor/*, ./samples/*, the root *.md files and ./package.json, and build/ contains exactly two files:

FileWhat it is
build/bundle-worker-image-decoder.jsPrebuilt web worker for off-thread PNG decoding
build/bundle-worker-terrain.jsPrebuilt web worker for terrain processing

Everything else you import by source path, and your bundler builds it with the rest of your application.

Both workers are located relative to the engine’s own modules, so they need no configuration. If your page sets Cross-Origin-Embedder-Policy: require-corp - which WebGPU pushes you toward - serve them with Cross-Origin-Resource-Policy as well, or the worker’s importScripts is blocked with the same NetworkError a missing file gives. Without the terrain worker, terrain does not build; without the image-decoder worker, PNG decoding falls back to the main thread, which is correct and slower.

The package also ships samples/engine/first-scene/, the smallest complete application - boot, a lit scene, keyboard and pointer input in about eighty lines - and samples/engine/README.md beside it, with the bundler stanza below, why a scene is authored in metres, and photometric reference values for lights.

Importing

Meep is distributed as fine-grained ES modules - around 6,000 of them. There’s no monolithic root import; the exports map is a 1:1 passthrough of src/, so a module’s path in the package is its import specifier:

import { EntityManager } from "@woosh/meep-engine/src/engine/ecs/EntityManager.js";
import { Transform64 }   from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import Entity            from "@woosh/meep-engine/src/engine/ecs/Entity.js";

Export form varies by module - most are named, a handful are default (Entity and Vector3 are both, for instance). The docs show the form that actually works for each symbol; when in doubt, open the file in node_modules/@woosh/meep-engine/src/ and look for export class versus export default.

Tree-shaking eliminates anything you don’t reference - the bundle size of a “use the lerp function” demo is about four lines compiled.

Browser and device requirements

WebGPU only. There is no WebGL path, no software renderer, no RHI and no degradation tiers. Hardware below the floor is not accommodated; it is told clearly that it cannot run this.

Renderer.initialize() checks the following before it will hand back a device:

KindRequirementWhy
Adapter featureindirect-first-instanceGPU-driven indirect draws, used throughout
Adapter featurefloat32-blendableOrder-independent transparency
Adapter limitmaxStorageBuffersPerShaderStage >= 10The GPU-resident scene database
Device limitmaxColorAttachmentBytesPerSample: 32Large G-buffers

timestamp-query, subgroups and texture-formats-tier1 are requested when the adapter offers them and are never required - without timestamp-query the GPU timers simply go quiet instead of failing startup.

When it fails

  • A browser with no WebGPU at all - navigator.gpu missing - throws a plain Error. Every other failure throws a ShadeDeviceFailure carrying a ShadeDeviceFailureReason: WebGPUUnavailable, AdapterUnavailable, BelowFloor, DeviceRequestFailed or DeviceLost. Catch both types.
  • BelowFloor names the feature or limit that was missing, so the message is worth surfacing rather than swallowing.
  • A device lost while running is not recovered. Everything built on it is gone with it; the honest response is to say so and reload.

The WGSL extension nobody checks for you

Beyond the WebGPU floor, Shade’s shaders declare requires immediate_address_space;. That’s a WGSL language extension, not a GPUFeatureName, so it never appears in adapter.features and there is nothing to put in requiredFeatures. Detect it separately - synchronously, no adapter needed:

const ok = "gpu" in navigator
    && navigator.gpu.wgslLanguageFeatures.has("immediate_address_space");

Chrome shipped it in 149/150. On an older Chromium the engine acquires a device successfully and then fails when it tries to dispatch, which reads like a shader bug rather than a browser one - so check the pair (WebGPU present, extension listed) rather than a user-agent string. Verified against real browsers: Chromium 148 fails, Chrome 151 runs. In practice the floor today is Chromium 149 or newer.

Running without a GPU

Nothing outside src/shade/** and src/engine/graphics3/** touches navigator.gpu. A simulation-only build - ECS, physics, AI, navigation, generation, math, storage, networking - runs in a browser without WebGPU, and under Node. For testing the graphics half without a GPU, the package ships a validating software WebGPU device under src/shade/device/mock/.

Bundler configuration

Vite

// vite.config.js
import { defineConfig } from "vite";
import strip from "@rollup/plugin-strip";

export default defineConfig({
  optimizeDeps: {
    // ~6,000 source modules: far too large to pre-bundle, and pre-bundling
    // rewrites the engine's own asset imports. Excluding it also stops Vite's
    // scanner discovering the engine's CommonJS dependencies, so name them.
    exclude: ["@woosh/meep-engine"],
    include: ["dat.gui", "opentype.js"],
  },
  plugins: [
    { ...strip(), apply: "build" },
  ],
});

The optimizeDeps block is what a Vite dev server needs, and its absence fails in a way that does not mention meep: a SyntaxError about a package your application never referenced, and a blank page.

Rollup

// rollup.config.js
import strip from "@rollup/plugin-strip";

export default {
  // ...
  plugins: [
    strip(),
  ],
};

Webpack

Use babel-plugin-strip or a custom Terser pass. The pattern is the same - remove the assert.* calls before minification.

What strip actually does

In development, Meep runs 6,000+ assertions across the engine. They catch invalid component state, out-of-range indices, broken invariants and similar bugs at the moment they happen. In production you don’t want that overhead, so the strip plugin removes the calls entirely. The resulting bundle is meaningfully smaller and runs at full speed.

Two things about the defaults, since the configs above pass no options:

  • @rollup/plugin-strip’s default functions list is ['console.*', 'assert.*']. assert.* is what the engine needs; console.* comes along with it, so a bare strip() also removes your own console.log calls. Pass strip({ functions: ["assert.*"] }) to keep them.
  • The default include is **/*.js, which means the pass covers your application code as well as the engine’s.

Verifying your install

import { lerp } from "@woosh/meep-engine/src/core/math/lerp.js";
console.log(lerp(0, 10, 0.5)); // 5

If that runs without errors, you’re set up. @woosh/meep-engine/package.json is on the exports map, so npm ls @woosh/meep-engine and an import of it both report the installed version.

Next

Move on to the ECS overview. If you have a 2.x project, Migrating 2.x to 3 carries the import-path map and the constructor shapes.