skeleton
The skeleton pose-data pipeline: sample a clip into a pose buffer, and bind/apply a pose buffer onto a Skeleton + Model entity.
require("@builtin::modules.api.engine.skeleton") -- skeleton (also available as global 'skeleton')
The skeleton namespace: 42 functions.
globals/skeleton/applyPose
skeleton.applyPose(sinkHandle: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Snapshot the buffer's first layout.total_floats values and
queue a pending apply for the next ECS drain. Returns false on
unknown sink/buffer or buffer too small for the layout. The
Buffer is unchanged.
Parameters
sinkHandlenumber— Sink handle frombindPose.poseBufferSubstrate.TypedBuffer— The pose buffer to apply.
Returns boolean — True on success.
globals/skeleton/bindClip
skeleton.bindClip(zanimBytes: buffer | string, boneOrder: { string }) -> ClipBindInfo?
Decode a zanim payload and bind it to boneOrder,
precomputing which of the clip's channels feed each bone so
per-frame sampleClip is allocation-free. Returns
{ handle, matched, total, duration }, or nil on a malformed
payload / empty bone order. Check matched: 0 means the clip
drives none of these bones.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).boneOrder{ string }— Output bone names — one stride-10 record per bone.
Returns ClipBindInfo? — { handle, matched, total, duration }, or nil.
globals/skeleton/bindPose
skeleton.bindPose(entityId: (string | entityRef)?, opts: SkeletonLayout) -> number?
Register a pose sink targeting entityId. The opts table
carries the layout: boneOrder is the bone-name array
({"hip", "spine", ...}), stride defaults to 10
(translation.xyz + rotation.xyzw + scale.xyz). Pass entityId
as nil to use the current component's owning entity.
Parameters
entityId(string | entityRef)(optional) — Engine entity id or proxy, or nil for the current entity.optsSkeletonLayout—{ boneOrder, stride }.
Returns number? — Sink handle, or nil.
local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 })
globals/skeleton/clipBones
skeleton.clipBones(zanimBytes: buffer | string) -> { string }?
Decode a zanim payload and return its bone-name array. Pure:
build a bind order or a retarget map from a clip without binding a
sampler. Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { string }? — Bone names referenced by the clip, or nil.
local names = skeleton.clipBones(assert(vfs.read(path .. "/data.zanim")))
globals/skeleton/clipDecode
skeleton.clipDecode(zanimBytes: buffer | string) -> string?
Decode a zanim payload to its readable JSON form
({ name, duration, channels, bone_names }), the clip as text to
read, store or diff. The binary parse is the engine's. Code that
transforms a clip's channels in Luau (e.g. the retarget bake) reads
the same clip as a table with clipToTable, which skips the text.
Returns nil on bytes that aren't a valid zanim payload. Inverse of
clipEncode.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns string? — The clip as a JSON string, or nil.
local json = require("@builtin::modules.json")
local clipJson = skeleton.clipDecode(bytes)
local clip = if clipJson then json.decode(clipJson) else nil
globals/skeleton/clipEncode
skeleton.clipEncode(jsonString: string) -> string?
Encode a clip's JSON form (the shape clipDecode returns) back
to a zanim payload — the bytes a .animation stores and
bindClip/sampleClip consume. Inverse of clipDecode. A clip
held as a table encodes with clipFromTable, which skips the text.
Returns nil on invalid JSON.
Parameters
jsonStringstring— A clip JSON document.
Returns string? — The clip's zanim payload bytes, or nil.
local json = require("@builtin::modules.json")
local bytes = skeleton.clipEncode(json.encode(clip))
globals/skeleton/clipFromTable
skeleton.clipFromTable(clip: { [string]: any }) -> (string?, string?)
Encode a clip table (the shape clipToTable returns) to a zanim
payload, the bytes a .animation stores and bindClip/sampleClip
consume. Every field is required and every number finite. Inverse of
clipToTable. Returns nil and the reason, naming the field, when the
table is not a clip.
Parameters
clip{ [string]: any }— A clip table:{ name, duration, channels, bone_names }.
Returns (string?, string?) — The clip's zanim payload bytes, or nil and the reason.
local bytes = skeleton.clipFromTable(clip)
globals/skeleton/clipPack
skeleton.clipPack(packed: PackedClip) -> (string?, string?)
Encode a clip in packed form (the shape clipUnpack returns) into
zanim bytes. Channels may share one run of times by naming the same
keyOffset; interpolation defaults to Linear; with no boneNames the
clip names its bones in the order its channels first drive them.
times and values take a compute readback's resultBytes()
directly. Returns nil and the reason, naming the channel, when a
channel's keys fall outside the runs or its times run backwards.
Parameters
packedPackedClip— The packed clip.
Returns (string?, string?) — The clip's zanim payload bytes, or nil and the reason.
local bytes = skeleton.clipPack(packed)
globals/skeleton/clipToTable
skeleton.clipToTable(zanimBytes: buffer | string) -> { [string]: any }?
Decode a zanim payload to a table: { name, duration, channels = { { target_bone, path, keyframes_in, keyframes_out, interpolation } }, bone_names }, the shape clipDecode writes as JSON, built by the engine
in place. path is Translation, Rotation or Scale; interpolation
is Linear, Step or CubicSpline. Inverse of clipFromTable.
Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { [string]: any }? — The clip as a table, or nil.
local clip = skeleton.clipToTable(bytes)
globals/skeleton/clipUnpack
skeleton.clipUnpack(zanimBytes: buffer | string) -> PackedClip?
Decode a zanim payload into its packed form: a channel table
({ bone, path, interpolation, keyOffset, keyCount, valueOffset },
offsets 0-based element indices) and two buffers of little-endian
f32s, times and values, holding every channel's keys channel after
channel (3 floats a key for Translation and Scale, 4 for Rotation, three
times that for CubicSpline). The runs go straight into a typed buffer
(buf:writeBytes(packed.values)) for a compute pass. Returns nil on
bytes that aren't a valid zanim payload. Inverse of clipPack.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns PackedClip? — The packed clip, or nil.
local packed = skeleton.clipUnpack(bytes)
globals/skeleton/jointTransforms
skeleton.jointTransforms(entityId: string | entityRef) -> table
Read a skinned entity's per-joint world transforms for the current animated pose.
Parameters
entityIdstring | entityRef— Engine entity id or proxy of a skinned entity.
Returns table — Array of joint transforms: { position, matrix, parent, name }.
local joints = skeleton.jointTransforms(meshId)
globals/skeleton/sampleClip
skeleton.sampleClip(handle: number, time: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Sample the bound clip at time (clamped to [0, duration])
and write one stride-10 pose record per bound bone into the
Buffer, starting at index 0. Bones the clip does not drive are
written as identity. Returns false on unknown handle/buffer or a
buffer too small for the bone count.
Parameters
handlenumber— Sampler handle frombindClip.timenumber— Sample time in seconds.poseBufferSubstrate.TypedBuffer— The stride-10 pose buffer written into.
Returns boolean — True on success.
globals/skeleton/unbindClip
skeleton.unbindClip(handle: number) -> boolean
Drop the bound clip sampler from the registry.
Parameters
handlenumber— Sampler handle to remove.
Returns boolean — True if the sampler existed.
globals/skeleton/unbindPose
skeleton.unbindPose(sinkHandle: number) -> boolean
Remove the sink from the registry.
Parameters
sinkHandlenumber— Sink handle to remove.
Returns boolean — True if the sink was present.
modules/skeleton/applyPose
applyPose(sinkHandle: number, poseBuffer: Substrate.TypedBuffer): boolean
Snapshot the buffer's first layout.total_floats values and
queue a pending apply for the next ECS drain. Returns false on
unknown sink/buffer or buffer too small for the layout. The
Buffer is unchanged.
Parameters
sinkHandlenumber— Sink handle frombindPose.poseBufferSubstrate.TypedBuffer— The pose buffer to apply.
modules/skeleton/bindClip
bindClip(zanimBytes: buffer | string, boneOrder: { string }): ClipBindInfo?
Decode a zanim payload and bind it to boneOrder,
precomputing which of the clip's channels feed each bone so
per-frame sampleClip is allocation-free. Returns
{ handle, matched, total, duration }, or nil on a malformed
payload / empty bone order. Check matched: 0 means the clip
drives none of these bones.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).boneOrder{ string }— Output bone names — one stride-10 record per bone.
modules/skeleton/bindPose
bindPose(entityId: (string | entityRef)?, opts: SkeletonLayout): number?
Register a pose sink targeting entityId. The opts table
carries the layout: boneOrder is the bone-name array
({"hip", "spine", ...}), stride defaults to 10
(translation.xyz + rotation.xyzw + scale.xyz). Pass entityId
as nil to use the current component's owning entity.
Parameters
entityId(string | entityRef)?(optional) — Engine entity id or proxy, or nil for the current entity.optsSkeletonLayout—{ boneOrder, stride }.
local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 })
modules/skeleton/clipBones
clipBones(zanimBytes: buffer | string): { string }?
Decode a zanim payload and return its bone-name array. Pure:
build a bind order or a retarget map from a clip without binding a
sampler. Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
local names = skeleton.clipBones(assert(vfs.read(path .. "/data.zanim")))
modules/skeleton/clipDecode
clipDecode(zanimBytes: buffer | string): string?
Decode a zanim payload to its readable JSON form
({ name, duration, channels, bone_names }), the clip as text to
read, store or diff. The binary parse is the engine's. Code that
transforms a clip's channels in Luau (e.g. the retarget bake) reads
the same clip as a table with clipToTable, which skips the text.
Returns nil on bytes that aren't a valid zanim payload. Inverse of
clipEncode.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
local json = require("@builtin::modules.json")
local clipJson = skeleton.clipDecode(bytes)
local clip = if clipJson then json.decode(clipJson) else nil
modules/skeleton/clipEncode
clipEncode(jsonString: string): string?
Encode a clip's JSON form (the shape clipDecode returns) back
to a zanim payload — the bytes a .animation stores and
bindClip/sampleClip consume. Inverse of clipDecode. A clip
held as a table encodes with clipFromTable, which skips the text.
Returns nil on invalid JSON.
Parameters
jsonStringstring— A clip JSON document.
local json = require("@builtin::modules.json")
local bytes = skeleton.clipEncode(json.encode(clip))
modules/skeleton/clipFromTable
clipFromTable(clip: { [string]: any }): (string?, string?)
Encode a clip table (the shape clipToTable returns) to a zanim
payload, the bytes a .animation stores and bindClip/sampleClip
consume. Every field is required and every number finite. Inverse of
clipToTable. Returns nil and the reason, naming the field, when the
table is not a clip.
Parameters
clip{ [string]: any }— A clip table:{ name, duration, channels, bone_names }.
local bytes = skeleton.clipFromTable(clip)
modules/skeleton/clipPack
clipPack(packed: PackedClip): (string?, string?)
Encode a clip in packed form (the shape clipUnpack returns) into
zanim bytes. Channels may share one run of times by naming the same
keyOffset; interpolation defaults to Linear; with no boneNames the
clip names its bones in the order its channels first drive them.
times and values take a compute readback's resultBytes()
directly. Returns nil and the reason, naming the channel, when a
channel's keys fall outside the runs or its times run backwards.
Parameters
packedPackedClip— The packed clip.
local bytes = skeleton.clipPack(packed)
modules/skeleton/clipToTable
clipToTable(zanimBytes: buffer | string): { [string]: any }?
Decode a zanim payload to a table: { name, duration, channels = { { target_bone, path, keyframes_in, keyframes_out, interpolation } }, bone_names }, the shape clipDecode writes as JSON, built by the engine
in place. path is Translation, Rotation or Scale; interpolation
is Linear, Step or CubicSpline. Inverse of clipFromTable.
Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
local clip = skeleton.clipToTable(bytes)
modules/skeleton/clipUnpack
clipUnpack(zanimBytes: buffer | string): PackedClip?
Decode a zanim payload into its packed form: a channel table
({ bone, path, interpolation, keyOffset, keyCount, valueOffset },
offsets 0-based element indices) and two buffers of little-endian
f32s, times and values, holding every channel's keys channel after
channel (3 floats a key for Translation and Scale, 4 for Rotation, three
times that for CubicSpline). The runs go straight into a typed buffer
(buf:writeBytes(packed.values)) for a compute pass. Returns nil on
bytes that aren't a valid zanim payload. Inverse of clipPack.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
local packed = skeleton.clipUnpack(bytes)
modules/skeleton/jointTransforms
jointTransforms(entityId: string | entityRef): table
Read a skinned entity's per-joint world transforms for the current animated pose.
Parameters
entityIdstring | entityRef— Engine entity id or proxy of a skinned entity.
local joints = skeleton.jointTransforms(meshId)
modules/skeleton/sampleClip
sampleClip(handle: number, time: number, poseBuffer: Substrate.TypedBuffer): boolean
Sample the bound clip at time (clamped to [0, duration])
and write one stride-10 pose record per bound bone into the
Buffer, starting at index 0. Bones the clip does not drive are
written as identity. Returns false on unknown handle/buffer or a
buffer too small for the bone count.
Parameters
handlenumber— Sampler handle frombindClip.timenumber— Sample time in seconds.poseBufferSubstrate.TypedBuffer— The stride-10 pose buffer written into.
modules/skeleton/unbindClip
unbindClip(handle: number): boolean
Drop the bound clip sampler from the registry.
Parameters
handlenumber— Sampler handle to remove.
modules/skeleton/unbindPose
unbindPose(sinkHandle: number): boolean
Remove the sink from the registry.
Parameters
sinkHandlenumber— Sink handle to remove.
typed/builtin//modules/api/engine/skeleton/skeleton/applyPose
skeleton.applyPose(sinkHandle: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Snapshot the buffer's first layout.total_floats values and
queue a pending apply for the next ECS drain. Returns false on
unknown sink/buffer or buffer too small for the layout. The
Buffer is unchanged.
Parameters
sinkHandlenumber— Sink handle frombindPose.poseBufferSubstrate.TypedBuffer— The pose buffer to apply.
Returns boolean — True on success.
typed/builtin//modules/api/engine/skeleton/skeleton/bindClip
skeleton.bindClip(zanimBytes: buffer | string, boneOrder: { string }) -> ClipBindInfo?
Decode a zanim payload and bind it to boneOrder,
precomputing which of the clip's channels feed each bone so
per-frame sampleClip is allocation-free. Returns
{ handle, matched, total, duration }, or nil on a malformed
payload / empty bone order. Check matched: 0 means the clip
drives none of these bones.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).boneOrder{ string }— Output bone names — one stride-10 record per bone.
Returns ClipBindInfo? — { handle, matched, total, duration }, or nil.
typed/builtin//modules/api/engine/skeleton/skeleton/bindPose
skeleton.bindPose(entityId: (string | entityRef)?, opts: SkeletonLayout) -> number?
Register a pose sink targeting entityId. The opts table
carries the layout: boneOrder is the bone-name array
({"hip", "spine", ...}), stride defaults to 10
(translation.xyz + rotation.xyzw + scale.xyz). Pass entityId
as nil to use the current component's owning entity.
Parameters
entityId(string | entityRef)(optional) — Engine entity id or proxy, or nil for the current entity.optsSkeletonLayout—{ boneOrder, stride }.
Returns number? — Sink handle, or nil.
local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 })
typed/builtin//modules/api/engine/skeleton/skeleton/clipBones
skeleton.clipBones(zanimBytes: buffer | string) -> { string }?
Decode a zanim payload and return its bone-name array. Pure:
build a bind order or a retarget map from a clip without binding a
sampler. Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { string }? — Bone names referenced by the clip, or nil.
local names = skeleton.clipBones(assert(vfs.read(path .. "/data.zanim")))
typed/builtin//modules/api/engine/skeleton/skeleton/clipDecode
skeleton.clipDecode(zanimBytes: buffer | string) -> string?
Decode a zanim payload to its readable JSON form
({ name, duration, channels, bone_names }), the clip as text to
read, store or diff. The binary parse is the engine's. Code that
transforms a clip's channels in Luau (e.g. the retarget bake) reads
the same clip as a table with clipToTable, which skips the text.
Returns nil on bytes that aren't a valid zanim payload. Inverse of
clipEncode.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns string? — The clip as a JSON string, or nil.
local json = require("@builtin::modules.json")
local clipJson = skeleton.clipDecode(bytes)
local clip = if clipJson then json.decode(clipJson) else nil
typed/builtin//modules/api/engine/skeleton/skeleton/clipEncode
skeleton.clipEncode(jsonString: string) -> string?
Encode a clip's JSON form (the shape clipDecode returns) back
to a zanim payload — the bytes a .animation stores and
bindClip/sampleClip consume. Inverse of clipDecode. A clip
held as a table encodes with clipFromTable, which skips the text.
Returns nil on invalid JSON.
Parameters
jsonStringstring— A clip JSON document.
Returns string? — The clip's zanim payload bytes, or nil.
local json = require("@builtin::modules.json")
local bytes = skeleton.clipEncode(json.encode(clip))
typed/builtin//modules/api/engine/skeleton/skeleton/clipFromTable
skeleton.clipFromTable(clip: { [string]: any }) -> (string?, string?)
Encode a clip table (the shape clipToTable returns) to a zanim
payload, the bytes a .animation stores and bindClip/sampleClip
consume. Every field is required and every number finite. Inverse of
clipToTable. Returns nil and the reason, naming the field, when the
table is not a clip.
Parameters
clip{ [string]: any }— A clip table:{ name, duration, channels, bone_names }.
Returns (string?, string?) — The clip's zanim payload bytes, or nil and the reason.
local bytes = skeleton.clipFromTable(clip)
typed/builtin//modules/api/engine/skeleton/skeleton/clipPack
skeleton.clipPack(packed: PackedClip) -> (string?, string?)
Encode a clip in packed form (the shape clipUnpack returns) into
zanim bytes. Channels may share one run of times by naming the same
keyOffset; interpolation defaults to Linear; with no boneNames the
clip names its bones in the order its channels first drive them.
times and values take a compute readback's resultBytes()
directly. Returns nil and the reason, naming the channel, when a
channel's keys fall outside the runs or its times run backwards.
Parameters
packedPackedClip— The packed clip.
Returns (string?, string?) — The clip's zanim payload bytes, or nil and the reason.
local bytes = skeleton.clipPack(packed)
typed/builtin//modules/api/engine/skeleton/skeleton/clipToTable
skeleton.clipToTable(zanimBytes: buffer | string) -> { [string]: any }?
Decode a zanim payload to a table: { name, duration, channels = { { target_bone, path, keyframes_in, keyframes_out, interpolation } }, bone_names }, the shape clipDecode writes as JSON, built by the engine
in place. path is Translation, Rotation or Scale; interpolation
is Linear, Step or CubicSpline. Inverse of clipFromTable.
Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { [string]: any }? — The clip as a table, or nil.
local clip = skeleton.clipToTable(bytes)
typed/builtin//modules/api/engine/skeleton/skeleton/clipUnpack
skeleton.clipUnpack(zanimBytes: buffer | string) -> PackedClip?
Decode a zanim payload into its packed form: a channel table
({ bone, path, interpolation, keyOffset, keyCount, valueOffset },
offsets 0-based element indices) and two buffers of little-endian
f32s, times and values, holding every channel's keys channel after
channel (3 floats a key for Translation and Scale, 4 for Rotation, three
times that for CubicSpline). The runs go straight into a typed buffer
(buf:writeBytes(packed.values)) for a compute pass. Returns nil on
bytes that aren't a valid zanim payload. Inverse of clipPack.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns PackedClip? — The packed clip, or nil.
local packed = skeleton.clipUnpack(bytes)
typed/builtin//modules/api/engine/skeleton/skeleton/jointTransforms
skeleton.jointTransforms(entityId: string | entityRef) -> table
Read a skinned entity's per-joint world transforms for the current animated pose.
Parameters
entityIdstring | entityRef— Engine entity id or proxy of a skinned entity.
Returns table — Array of joint transforms: { position, matrix, parent, name }.
local joints = skeleton.jointTransforms(meshId)
typed/builtin//modules/api/engine/skeleton/skeleton/sampleClip
skeleton.sampleClip(handle: number, time: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Sample the bound clip at time (clamped to [0, duration])
and write one stride-10 pose record per bound bone into the
Buffer, starting at index 0. Bones the clip does not drive are
written as identity. Returns false on unknown handle/buffer or a
buffer too small for the bone count.
Parameters
handlenumber— Sampler handle frombindClip.timenumber— Sample time in seconds.poseBufferSubstrate.TypedBuffer— The stride-10 pose buffer written into.
Returns boolean — True on success.
typed/builtin//modules/api/engine/skeleton/skeleton/unbindClip
skeleton.unbindClip(handle: number) -> boolean
Drop the bound clip sampler from the registry.
Parameters
handlenumber— Sampler handle to remove.
Returns boolean — True if the sampler existed.
typed/builtin//modules/api/engine/skeleton/skeleton/unbindPose
skeleton.unbindPose(sinkHandle: number) -> boolean
Remove the sink from the registry.
Parameters
sinkHandlenumber— Sink handle to remove.
Returns boolean — True if the sink was present.