Model & ModelAnimation
A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
distinct from Mesh (raw primitives, no animation). Extends Node. Every
Model carries the ModelAnimation aspect pre-attached at model.anim — you never attach it
yourself.
At a glance
const hero = await Model.load(asset('./hero.glb'))
hero.position = [0, 0, -2]
scene.add(hero)
hero.anim.play('Run', { loop: true })
hero.addEventListener('loopReached', clip => console.log('lap', clip))Loading
Model.load(source: string | FetchResponse, options?: {
culling?: boolean // default false — the whole model always draws
onProgress?: (p: { loaded: number, total?: number }) => void
}): Promise<Model>source is an asset('./file.glb') path, a remote https://… URL, or an already-fetched
FetchResponse. Rejects on HTTP ≥ 400 or a failed decode.
frustum culling is off by default for models (skinned meshes move outside their
import-time bounds). Pass culling: true for large static props to let off-screen parts skip
drawing.
Cloning
const enemy = template.clone(): Modelclone() makes a deep copy of the GLB — meshes, skeleton and baked animation clips — reusing the
already-decoded asset (no re-fetch, no re-parse), so it is far cheaper than a second Model.load.
The clone is attached to the source's parent and joins its scene, and starts at the source's current
transform. It has its own independent animation state, reached via clone.anim. Culling is off
by default, matching Model.load.
const template = await Model.load(asset('./enemy.glb')) // decode once
const wave = Array.from({ length: 8 }, () => template.clone()) // cheap copies, no await
wave.forEach((e, i) => { e.x = i * 2; scene.add(e); e.anim.play('Walk', { loop: true }) })Prefer clone() over re-loading the same URL for spawn pools — one decode, then N instances.
The node hierarchy
A Model is the GLB's root node; the file's internal nodes hang under it as plain Nodes:
model.traverse(node => { // this node, then every descendant
if (node.name === 'Sword') node.visible = false
})
model.children // direct childrenTransform, visible, events, aspects (Shape, Physics, …) all work on the root and on the
internal nodes.
Animation — model.anim
model.anim.clips // { name: string, duration: number }[] — baked into the glb (seconds)
model.anim.play(clip?: string | number, options?: { loop?: boolean }): this
model.anim.stop(): this
model.anim.playing = true // get/set — resume/pause the current clip
model.anim.loop = true // get/set — also settable via play() options
model.anim.speed = 1.5 // playback rate multiplier (1 = authored speed)
model.anim.time = 0.5 // get/set — seek within the current clip (seconds)play() accepts a clip name or index; with no argument it (re)plays the current clip.
Name-based play only works when the GLB's clips are usefully named — check model.anim.clips (or
name them in the DCC tool/exporter).
an unknown clip name is silently ignored — play('Runn') keeps the previously selected
clip and plays that. Validate against clips if the name comes from data.
Clip-end events
Fired on the node (not on anim), with the clip index as the argument:
model.addEventListener('completed', clip => { /* non-looping clip finished */ })
model.addEventListener('loopReached', clip => { /* looping clip wrapped around */ })After completed the aspect flips to playing = false; a looping clip keeps going until stop().
Limitations
- No cross-fade or blending —
play()hard-switches to the new clip on the next frame. One clip plays at a time; there is no layered/partial-body mixing.
Pitfalls
// ✗ awaiting a fresh load per spawn — re-fetches + re-decodes, hitches at spawn time
const enemy = await Model.load(url) // per spawn
// ✓ load once, then clone() — no await, no re-decode
const template = await Model.load(url)
const enemy2 = template.clone()
// ✗ expecting a smooth transition
hero.anim.play('Idle') // snaps — no cross-fade existsSee also
- Node — transform, hierarchy,
traverse, events (inherited) - Mesh — primitives / custom geometry (the other renderable kind)
- Physics & shapes — give a model a collision shape
- Scene — adding nodes, the camera