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 overfadeseconds (0 = hard cut). Unknown names are ignored.seek(t: number): void— Jump the current clip totseconds, wrapped into its duration when it loops.timeis 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. Honoursplaying: 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). Whenbaked, 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; afadecrossfades from whatever is playing, andatstarts it that many seconds in.atis how a CROWD stops marching in lockstep. Spawn eight of the same model, play the same clip on each at a differentat, 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.


