Platform

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 constantString valueLoader classWhat it produces
GameAssetType.ModelGLTF"model/gltf"GLTFAssetLoaderThree.js Object3D (with animations)
GameAssetType.ModelGLTF_JSON"model/gltf+json"GLTFAssetLoadersame
GameAssetType.Texture"texture"TextureAssetLoaderThree.js Texture (PNG, JPG, DDS)
GameAssetType.Image"image"ImageRGBADataLoaderImageRGBADataAsset (Sampler2D, supports 16-bit PNG)
GameAssetType.ArrayBuffer"arraybuffer"ArrayBufferLoaderraw ArrayBuffer
GameAssetType.JSON"json"JsonAssetLoaderparsed JS object
GameAssetType.Text"text"TextAssetLoaderraw string
GameAssetType.Sound"audio"SoundAssetLoaderdecoded AudioBuffer
GameAssetType.ImageSvg"image/svg"SVGAssetLoadercloned SVGElement
GameAssetType.JavaScript"text/javascript"JavascriptAssetLoadercompiled 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:

ValueMeaning
CrossOriginKind.Anonymous (default)crossOrigin="anonymous", no credentials
CrossOriginKind.UseCredentialscrossOrigin="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