Phaser AE

3D model files (.glb, .obj)

Free packv3.0.0-13DLogic

Loads models an artist made, glTF 2.0 binary (`.glb`) and Wavefront OBJ with its MTL, and turns them into meshes the 3D world can spawn, animated or not. Load once, spawn as often as you like.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 19, 2026 · updated October 20, 2026

3D model files (.glb, .obj): a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

modelFiles3d

function modelFiles3d(): Pack<{ modelFiles: ModelFiles3dApi }>

The model-files-3d pack: .glb files as meshes. Needs a 3D world to draw them and the kernel loader to fetch them. Headless (no image decoding) a model still loads and spawns — it simply wears no texture.

Functions

composeWorlds

function composeWorlds( parents: Int32Array | number[], locals: Float32Array, worlds: Float32Array, ): void

Compose world matrices for every node from local matrices. parents[i] is the parent node index or -1 for roots — parents may appear ANYWHERE in the array (glTF imposes no parent-before-child order), so nodes are resolved iteratively until every one has a resolved parent. locals and worlds are 16-floats-per-node arrays; worlds must not alias locals.

decomposeTRS

function decomposeTRS(m: Float32Array): { t: [number, number, number]; e: [number, number, number]; // [yaw, pitch, roll] s: [number, number, number]; }

Decompose an affine TRS matrix (glTF forbids skew) into translation, scale and Euler angles in the ENGINE's rotation order: the mesh shader applies roll (Z), then pitch (X), then yaw (Y) — R = Ry(yaw)·Rx(pitch)·Rz(roll) — so writing these onto a mesh's yaw/pitch/roll reproduces the matrix's orientation exactly. Same convention (gimbal branch included) as physics3d's quatToEuler: at pitch = ±90° roll is folded into yaw. Scale is per-column length; a negative determinant folds into the sign of s.z.

jointPalette

function jointPalette(skin: GlbSkinOut, worlds: Float32Array, out: Float32Array): void

palette[j] = worlds[skin.joints[j]] × inverseBind[j] — 16 floats per joint into out. Allocation-free hot path, called per frame per skinned model.

mat4MulTo

function mat4MulTo(a: Float32Array, b: Float32Array, out: Float32Array): void

out = a × b, allocation-free (out must not alias a or b). Column-major.

parseGlb

function parseGlb(buffer: ArrayBuffer): GlbData

Parses a GLB (glTF 2.0 Binary) file into renderer-ready data. Pure: no DOM, no GPU, no fetch. Static primitives come out world-baked; animated or skinned content stays unbaked, with the rig on rig. Throws on a malformed file, on one that is Draco- or meshopt-compressed (this reader decodes neither), on one that requires another extension it does not implement, and on sparse accessors or data outside the GLB's own BIN chunk.

parseMtl

function parseMtl(text: string): Record<string, ObjMaterial>

Parse a .mtl file into materials by name.

parseObj

function parseObj(text: string): ObjData

Parse a .obj file into per-material triangle soups.

quatSlerp

function quatSlerp( ax: number, ay: number, az: number, aw: number, bx: number, by: number, bz: number, bw: number, t: number, ): [number, number, number, number]

Shortest-path spherical lerp between quats a and b (components, not objects — the hot path never allocates inputs). When the quats are near-parallel the sin() denominator degenerates, so we fall back to normalized lerp, which is indistinguishable there.

sampleChannel

function sampleChannel(ch: GlbChannel, tt: number, out: Float32Array): void

Sample one animation channel at time tt (caller pre-wraps for looping) into out (3 floats for translation/scale, 4 for rotation). Binary-searches the keyframe span; STEP holds the left key, LINEAR lerps (slerp for rotation), CUBICSPLINE reads the middle element of each in/value/out triplet and lerps linearly between keys. Clamps outside the keyframe range.

skinInto

function skinInto( rest: Float32Array, joints: Uint16Array, weights: Float32Array, palette: Float32Array, out: Float32Array, ): void

CPU-skin a stride-8 vertex soup: each position by the weighted palette matrices, each normal by their rotation only. This is what puts a skinned pose on screen without a second render pipeline; a vertex-shader skin is the same maths moved onto the GPU, and is the optimisation this leaves on the table.

trsToMat4

function trsToMat4( t: readonly number[], r: readonly number[], s: readonly number[], out?: Float32Array, ): Float32Array

Bake translation + quaternion + scale into a column-major mat4 (T·R·S — scale first, then rotate, then translate; same convention as math3d).

Classes

AnimMixer

class AnimMixer

The pure clip mixer: tracks the active clip plus an optional crossfade from the previous one. Every update(dt) advances time, samples each animated channel, blends poses during a fade (lerp t/s, slerp r), bakes local matrices and composes world matrices. Nodes no clip ever animates keep their rest matrices, computed once. All per-frame work is allocation-free.

  • readonly anims: readonly string[] — Clip names, in rig order — play() accepts a name or this index.
  • readonly locals: Float32Array — Local (16/node) and world (16/node) matrices, rebuilt by update().
  • readonly worlds: Float32Array — The composed world matrices, 16 floats per node, rebuilt by update().
  • time: number — Seconds into the current clip.
  • playing: boolean — True while a clip is active (one-shots flip to false when they finish).
  • done: boolean — One-shot finished — sticky until the next play().
  • constructor(rig: GlbRig)
  • play(clip: string | number, opts: { loop?: boolean; fade?: number } = {}): void — Start a clip by name or index. Crossfades from the current clip over fade seconds (0 = hard cut). Unknown names are ignored.
  • seek(t: number): void — Jump the current clip to t seconds, wrapped into its duration when it loops. time is only a MIRROR of the playing clip's own clock, rebuilt by every update — assigning it looks like it works and is overwritten on the next frame. This is the real seek, and it is what phasing a crowd off each other needs.
  • update(dt: number): void — Advance by dt seconds and rebuild locals + worlds. Honours playing: set it false to FREEZE the pose (walk → idle), true to resume.

Types

GlbAnimOut

interface GlbAnimOut

One animation: a named set of channels and its total length.

  • name: string — The animation name — 'anim0', 'anim1'… when the file gives none.
  • duration: number — The animation length, in seconds — the latest end time across the channels.
  • channels: GlbChannel[] — The channels this animation drives.

GlbChannel

interface GlbChannel

One animation channel: keyframes that drive one property of one node.

  • node: number — The target node index into rig.nodes.
  • path: 'translation' | 'rotation' | 'scale' — Which node property the channel drives.
  • interpolation: 'LINEAR' | 'STEP' | 'CUBICSPLINE' — The glTF sampler interpolation mode.
  • times: Float32Array — The key times, in seconds, ascending.
  • values: Float32Array — Packed values: 3 floats per key (t/s) or 4 (rotation). For CUBICSPLINE the raw inTangent/value/outTangent triplets are packed as authored (3× the element count per key) — the runtime samples the middle element.

GlbData

interface GlbData

The full result of parseGlb — everything the renderer needs from one GLB file.

  • primitives: GlbPrimitiveOut[] — The expanded mesh primitives, one per glTF primitive.
  • materials: GlbMaterial[] — The materials the primitives index into.
  • images: GlbImage[] — The embedded texture images the materials index into.
  • bounds: { min: [number, number, number]; max: [number, number, number] } — The world-space bounding box over all primitives.
  • rig: GlbRig | null — Node hierarchy + animations + skins — null when the file has neither animations nor skins (everything baked, nothing to drive).

GlbImage

interface GlbImage

One embedded texture image — the raw encoded bytes, not decoded pixels.

  • mimeType: string — The image format: 'image/png' or 'image/jpeg'.
  • bytes: Uint8Array — The encoded image bytes — a view into the file's BIN chunk, not a copy.

GlbMaterial

interface GlbMaterial

One material record — the metallic-roughness factors and texture indices the renderer consumes.

  • name: string — The material name from the file — 'material0', 'material1'… when the file gives none.
  • color: [number, number, number, number] — baseColorFactor rgba (default [1,1,1,1]).
  • metallic: number — metallicFactor (default 1 per spec).
  • rough: number — roughnessFactor (default 1).
  • colorImage?: number — images[] index for baseColorTexture, if any.
  • mrImage?: number — metallicRoughness texture image index (glTF: g = rough, b = metallic — MULTIPLIES the factors). DamagedHelmet-class models keep all their material variation here.
  • normalImage?: number — images[] index for normalTexture, if any.
  • normalScale?: number — normalTexture.scale (default 1), only when normalImage present.
  • emissive?: [number, number, number] — emissiveFactor if non-zero.

GlbNodeOut

interface GlbNodeOut

One node of the retained hierarchy, with its local TRS transform.

  • name: string — The node name from the file — 'node0', 'node1'… when the file gives none.
  • parent: number — Parent node index, -1 for roots.
  • t: [number, number, number] — Local TRS (matrix nodes are decomposed — glTF forbids skew so the decomposition is exact).
  • r: [number, number, number, number] — The local rotation, as a quaternion x, y, z, w.
  • s: [number, number, number] — The local scale, per axis.

GlbPrimitiveOut

interface GlbPrimitiveOut

One mesh primitive, expanded to a renderer-ready triangle soup.

  • verts: Float32Array — NON-INDEXED triangle soup, stride 8: pos(3) normal(3) uv(2). When baked, the owning node's WORLD transform is applied to positions and (inverse-transpose-rotated, renormalised) normals; otherwise verts are as authored — node-local, or skin-space for skinned primitives.
  • material: number | null — materials[] index or null.
  • node: number — Owning node index into rig.nodes (always set, even when rig is null).
  • skin: number | null — skins[] index, or null when the primitive is not skinned.
  • baked: boolean — true = verts carry the world transform (the static fast path).
  • joints?: Uint16Array — 4 joint indices per vertex (into skins[skin].joints), expanded non-indexed like verts. Only present when skinned.
  • weights?: Float32Array — 4 weights per vertex, normalized to sum 1 (degenerate all-zero quads become [1,0,0,0]). Only present when skinned.

GlbRig

interface GlbRig

The retained animation data: node hierarchy, animations and skins, for the runtime to sample.

  • nodes: GlbNodeOut[] — The full node hierarchy, in glTF node order.
  • anims: GlbAnimOut[] — The animations, each with its sampled channels.
  • skins: GlbSkinOut[] — The skins the skinned primitives index into.

GlbSkinOut

interface GlbSkinOut

One skin: the joint nodes and their inverse bind matrices.

  • joints: number[] — The joint list, as indices into rig.nodes.
  • inverseBind: Float32Array — The inverse bind matrices — 16 floats per joint, column-major.

Model

interface Model

A loaded model: its parts, and the size it was before being fitted to the unit box.

  • readonly key: string — The key it was loaded under.
  • readonly parts: ReadonlyArray<ModelPart> — One part per material.
  • readonly size: { x: number; y: number; z: number } — The model's size in its own file's units, before unit-fitting — multiply by it to restore real scale.
  • readonly triangles: number — Triangles across every part.
  • readonly rig: GlbRig | null — The node hierarchy, animations and skins, or null when the file has none.
  • readonly fit: { scale: number; cx: number; cy: number; cz: number } — The unit-fit that was baked into the vertices, so an animated pose can be put through the same one.

ModelFiles3dApi

interface ModelFiles3dApi

game.modelFiles.

  • load(key: string, url: string): Promise<Model> — Load a .glb or .obj and register its meshes — the extension decides. Resolves to the model; loading the same key twice returns the first.
  • add(key: string, buffer: ArrayBuffer): Model — Turn an already-fetched .glb buffer into a model — for a file that arrived some other way.
  • addObj(key: string, objText: string, mtlText?: string): Model — Turn already-fetched OBJ text (and optionally its MTL) into a model.
  • get(key: string): Model | undefined — A loaded model by key, or undefined.
  • readonly keys: ReadonlyArray<string> — Every loaded key.
  • spawn(key: string, spec?: ModelSpawnSpec): ModelHandle — Spawn a loaded model. Throws if the key is not loaded.

ModelHandle

interface ModelHandle

A spawned model. Its parts are ordinary roster objects that all share one origin (the file's transforms are baked into the vertices), so setting a pose here sets the same pose on each — which is why a model needs no parenting to move as one thing. A NODE-ANIMATED part is the one exception: its node's own moving transform rides on the handle's pose (the model's yaw turns a door's offset, not just the door), and the handle keeps it composed whenever x, y, z, yaw, pitch, roll or size is set.

  • readonly parts: ReadonlyArray<Handle> — The roster objects, one per part.
  • x: number — World x.
  • y: number — World y.
  • z: number — World z.
  • yaw: number — Yaw, radians.
  • pitch: number — Pitch, radians.
  • roll: number — Roll, radians.
  • size: number — Size in world units.
  • readonly alive: boolean — Whether every part is still alive.
  • visible: boolean — Show or hide the whole model.
  • kill(): void — Remove every part.
  • readonly clips: ReadonlyArray<string> — The animation names this model carries, in file order.
  • play(clip: string | number, opts?: { loop?: boolean; speed?: number; fade?: number; at?: number }): void — Play a clip by name or index; a fade crossfades from whatever is playing, and at starts it that many seconds in. at is how a CROWD stops marching in lockstep. Spawn eight of the same model, play the same clip on each at a different at, and they read as eight people rather than one person copied — it is the cheapest thing that buys that, and without it the only alternative is eight models.
  • stop(): void — Stop animating and freeze the pose where it is (the model does not return to its rest pose).
  • readonly playing: string | null — The clip playing now, or null.
  • time: number — Seconds into the current clip. Settable, so a clip can be scrubbed or re-phased while it runs.
  • speed: number — Playback speed. Default 1.

ModelPart

interface ModelPart

One drawable piece of a loaded model: a mesh, and the material it wears.

  • readonly mesh: number — The mesh id instances carry.
  • readonly frame: number — Atlas frame for the base-colour texture, or 0 for none.
  • readonly color: string — The material's base colour, css.
  • readonly alpha: number — Below 1 the part draws in the transparent pass.
  • readonly material: number — The registered material this part wears — its metallic, roughness, emissive and maps.
  • readonly node: number — The rig node this part belongs to, or −1 when it was merged into the static soup.
  • readonly skin: number | null — The skin index this part is bound to, or null when it is not skinned.
  • readonly rest?: Float32Array — A skinned part's rest vertices, kept so it can be re-skinned every frame.
  • readonly verts: Float32Array — This part's INTERLEAVED vertices as they were registered — position, normal and uv a vertex. Kept so a loaded model can be handed to something that needs the geometry rather than the mesh id: vector3d.wireframe(part.verts) turns any model into outline edges without a second loader.
  • readonly joints?: Uint16Array — Four joint indices a vertex.
  • readonly weights?: Float32Array — Their weights.

ModelSpawnSpec

interface ModelSpawnSpec

What modelFiles.spawn takes.

  • x?: number — World position. Default 0.
  • y?: number — Height; y is UP. Default 0.
  • z?: number — Depth. Default 0.
  • size?: number — Size in world units — the model is fitted to a unit box, so this is its largest dimension. Default 1.
  • yaw?: number — Yaw about y, radians.
  • pitch?: number — Pitch about x, radians.
  • roll?: number — Roll about z, radians.
  • tint?: string — Multiply this over every part's own material colour. Default white.
  • id?: string — Object id prefix. Default the model key.
  • layer?: LayerName — Layer. Default the world's first.
  • tags?: string[] — Tags put on every part.

ObjData

interface ObjData

A parsed .obj file: triangle groups, the mtllib reference and the bounds.

  • groups: ObjGroup[] — The per-material triangle groups, in file order.
  • mtllib: string | null — The mtllib filename as written (may not match reality), or null.
  • bounds: { min: [number, number, number]; max: [number, number, number] } — The axis-aligned bounds of every vertex, as min and max [x, y, z].

ObjGroup

interface ObjGroup

One per-material triangle group in a parsed .obj file.

  • material: string | null — usemtl name, or null before any usemtl.
  • verts: Float32Array — Triangulated soup, stride 8: pos(3) normal(3) uv(2), CCW as authored.

ObjMaterial

interface ObjMaterial

One material record parsed from a .mtl file, as parseMtl returns it.

  • name: string — The newmtl name — the key usemtl lines refer to.
  • color: [number, number, number] — Kd diffuse 0..1 (default [0.7, 0.7, 0.7]).
  • map?: string — map_Kd path EXACTLY as written (may be broken/absolute — the loader layer decides what to do with it).
  • ns?: number — Ns shininess if present.
  • opacity?: number — d / (1 - Tr) opacity if present (default 1).
  • emissive?: [number, number, number] — Ke emissive if present.