Asset pipeline
How Meep loads, caches, and transforms assets — the AssetManager API, built-in loaders, the transformer chain, and CORS configuration.
Every image, model, texture, sound, and data file passes through a single
AssetManager. It deduplicates in-flight requests, queues work by priority,
caps network concurrency, and runs each loaded asset through a chain of
registered transformers before handing it to you. You register loaders for
the types you need; the engine does the rest.
Getting the asset manager
EngineHarness.bootstrap creates the engine and returns it. The manager is
exposed directly:
const engine = await EngineHarness.bootstrap({ configuration: (config) => {
// register loaders here (see below)
}});
const am = engine.assetManager; // AssetManager instance
Loaders are registered during the configuration callback — before bootstrap
starts the engine — using config.addLoader(type, loader). That call queues
a registerLoader call on the manager; by the time bootstrap resolves, all
loaders are linked and ready.
Requesting assets
promise — async/await
import { GameAssetType } from "@woosh/meep-engine/src/engine/asset/GameAssetType.js";
const asset = await am.promise("models/chair.gltf", GameAssetType.ModelGLTF);
const object3D = asset.create(); // produces a Three.js Object3D clone
promise(path, type) returns a Promise<Asset>. If the asset is already
loaded, the promise resolves on the next microtask. If it is in flight, the
new request is merged with the existing pending entry and resolved when that
load completes.
get — callback form
am.get({
path: "textures/ground.png",
type: GameAssetType.Texture,
callback: (asset) => applyTexture(asset.create()),
failure: (err) => console.error("load failed", err),
progress: (loaded, total) => updateBar(loaded / total),
});
get accepts an options object. callback, failure, and progress are all
optional (defaults: no-op, console.error, no-op). Passing skip_queue: true
bypasses the priority wait queue and dispatches the load immediately — useful
for assets that need to unblock rendering.
tryGet — synchronous, no side effects
const asset = am.tryGet("models/chair.gltf", GameAssetType.ModelGLTF);
if (asset !== null) {
scene.add(asset.create());
}
Returns the cached Asset if already loaded, null otherwise. Does not
trigger a load.
Status checks
am.isLoaded(path, type) // true if cached and ready
am.isPending(path, type) // true if in flight
am.isFailed(path, type) // true if the last attempt failed
The Asset object
A loaded asset wraps a factory function, not the resource itself:
const asset = await am.promise("models/chair.gltf", GameAssetType.ModelGLTF);
const clone1 = asset.create(); // fresh Three.js Object3D
const clone2 = asset.create(); // another independent clone
Calling asset.create() produces a new instance each time — for models this
is a deep clone of the parsed scene graph. The asset also carries byteSize
(RAM footprint in bytes) and a description ({ path, type }).
Priority and concurrency
load_concurrency caps how many loaders run in parallel. It defaults to
Infinity (no cap). Setting it — e.g. am.load_concurrency = 6 — keeps
network slots available for high-priority work:
am.load_concurrency = 6;
Requests queue in a binary-heap wait queue ordered by priority. Each
AssetRequest carries a priority number (default 1); higher numbers are
dispatched first. When multiple requests share the same pending asset, the
queue score is the maximum priority across those requests, so one high-priority
caller elevates an already-queued asset. Passing skip_queue: true in get
(or the skip_queue option in promise) force-dispatches the asset past the
queue immediately.
Loading multiple assets together — AssetPreloader
AssetPreloader batches requests into priority levels and fires progress and
completion signals:
import { AssetPreloader } from "@woosh/meep-engine/src/engine/asset/preloader/AssetPreloader.js";
import AssetLevel from "@woosh/meep-engine/src/engine/asset/preloader/AssetLevel.js";
const preloader = new AssetPreloader();
preloader.add("models/hero.gltf", GameAssetType.ModelGLTF, AssetLevel.CRITICAL);
preloader.add("audio/ambient.mp3", GameAssetType.Sound, AssetLevel.NORMAL);
preloader.add("textures/splash.png", GameAssetType.Texture, AssetLevel.OPTIONAL);
preloader.on.progress.add(({ global }) =>
updateLoadBar(global.progress) // 0–1
);
preloader.on.succeeded.add(() => startGame());
preloader.load(am);
AssetLevel values — CRITICAL (0), HIGH (1), NORMAL (2),
OPTIONAL (3) — control load order: lower numbers load first.
succeeded fires only when every asset loaded without error; resolved fires
regardless (receives a count of successes and failures). A preloader instance
is single-use — create a new one for each batch.
Built-in loaders
Register the loaders you need during configuration. The type strings come from
GameAssetType:
| Type constant | String value | Loader class | What it produces |
|---|---|---|---|
GameAssetType.ModelGLTF | "model/gltf" | GLTFAssetLoader | Three.js Object3D (with animations) |
GameAssetType.ModelGLTF_JSON | "model/gltf+json" | GLTFAssetLoader | same |
GameAssetType.Texture | "texture" | TextureAssetLoader | Three.js Texture (PNG, JPG, DDS) |
GameAssetType.Image | "image" | ImageRGBADataLoader | ImageRGBADataAsset (Sampler2D, supports 16-bit PNG) |
GameAssetType.ArrayBuffer | "arraybuffer" | ArrayBufferLoader | raw ArrayBuffer |
GameAssetType.JSON | "json" | JsonAssetLoader | parsed JS object |
GameAssetType.Text | "text" | TextAssetLoader | raw string |
GameAssetType.Sound | "audio" | SoundAssetLoader | decoded AudioBuffer |
GameAssetType.ImageSvg | "image/svg" | SVGAssetLoader | cloned SVGElement |
GameAssetType.JavaScript | "text/javascript" | JavascriptAssetLoader | compiled function |
JsonAssetLoader is registered automatically by EngineHarness if you don’t
add one yourself. ImageRGBADataLoader auto-registers an ArrayBufferLoader
if none is present when it links.
import { GLTFAssetLoader } from "@woosh/meep-engine/src/engine/asset/loaders/GLTFAssetLoader.js";
import { TextureAssetLoader } from "@woosh/meep-engine/src/engine/asset/loaders/texture/TextureAssetLoader.js";
import { ImageRGBADataLoader } from "@woosh/meep-engine/src/engine/asset/loaders/image/ImageRGBADataLoader.js";
import { GameAssetType } from "@woosh/meep-engine/src/engine/asset/GameAssetType.js";
await EngineHarness.bootstrap({
configuration: (config) => {
const gltfLoader = new GLTFAssetLoader();
config.addLoader(GameAssetType.ModelGLTF, gltfLoader);
config.addLoader(GameAssetType.ModelGLTF_JSON, gltfLoader);
config.addLoader(GameAssetType.Texture, new TextureAssetLoader());
config.addLoader(GameAssetType.Image, new ImageRGBADataLoader());
},
});
A single GLTFAssetLoader instance can be shared between ModelGLTF and
ModelGLTF_JSON — the GLTF loader handles both binary and JSON glTF files.
Registering a custom loader
Extend AssetLoader and override load. The link lifecycle method gives
you access to the manager and the engine context:
import { AssetLoader } from "@woosh/meep-engine/src/engine/asset/loaders/AssetLoader.js";
import { Asset } from "@woosh/meep-engine/src/engine/asset/Asset.js";
class YamlAssetLoader extends AssetLoader {
load(scope, path, success, failure, progress) {
fetch(path)
.then(r => r.text())
.then(text => {
const parsed = parseYaml(text);
success(new Asset(() => parsed));
})
.catch(failure);
}
}
// during configuration:
config.addLoader("application/yaml", new YamlAssetLoader());
load receives scope (AssetRequestScope), the resolved path (including
rootPath prefix), and three callbacks. It can return a Promise — the
manager catches rejections and routes them to failure.
The transformer chain
Transformers run after a loader resolves, before the asset is cached. They are applied in registration order and can be async:
import { AssetTransformer } from "@woosh/meep-engine/src/engine/asset/AssetTransformer.js";
class FlipNormalsTransformer extends AssetTransformer {
async transform(asset, description) {
flipNormals(asset.create());
return asset;
}
}
await am.registerTransformer(GameAssetType.ModelGLTF, new FlipNormalsTransformer());
Transformers do not apply retroactively — assets already in the cache are not
re-processed. Remove a transformer with am.unregisterTransformer(type, transformer).
Path prefix
am.rootPath is prepended to every path before it is handed to a loader. Set
it once to relocate all asset fetches to a CDN or a subdirectory:
am.rootPath = "https://cdn.example.com/assets/";
CORS and credentials
am.crossOriginConfig is a CrossOriginConfig object whose kind is one of
two CrossOriginKind values:
| Value | Meaning |
|---|---|
CrossOriginKind.Anonymous (default) | crossOrigin="anonymous", no credentials |
CrossOriginKind.UseCredentials | crossOrigin="use-credentials", cookies / auth headers included |
import { CrossOriginKind } from "@woosh/meep-engine/src/engine/asset/CORS/CrossOriginKind.js";
am.crossOriginConfig.kind = CrossOriginKind.UseCredentials;
Set this before loaders are linked (i.e., before bootstrap resolves).
GLTFAssetLoader reads the config in its link method and applies it to the
underlying Three.js loader. ArrayBufferLoader maps UseCredentials to
credentials: 'include' in its fetch call.
Manually inserting and aliasing assets
insert(path, type, asset) places a fully-resolved Asset into the cache
without going through a loader or transformer. Any pending requests for that
path/type are resolved immediately. insertAsync is the promise form for
assets you load yourself.
assignAlias(alias, path, type) names an asset description with a string
alias. Resolve the alias later with resolveAlias(alias) or load by alias
with promiseByAlias(alias). Useful for remapping asset paths at runtime.
Dumping the loaded asset list
console.log(am.dumpLoadedAssetList()); // JSON array of all cached assets