Log inGet started

skeleton

Updated 5 September 2026

The skeleton namespace — 31 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

  • sinkHandle number — Sink handle from bindPose.
  • poseBuffer Substrate.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

  • zanimBytes buffer | string — The clip's data.zanim payload 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.
  • opts SkeletonLayout{ 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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).

Returns { string }? — Bone names referenced by the clip, or nil.

local names = skeleton.clipBones(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 binary parse is the engine's; json.decode the result to inspect or transform a clip's channels (e.g. the retarget bake) in Luau. Returns nil on bytes that aren't a valid zanim payload. Inverse of clipEncode.

Parameters

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).

Returns string? — The clip as a JSON string, or nil.

local clip = json.decode(skeleton.clipDecode(bytes))

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. Returns nil on invalid JSON.

Parameters

  • jsonString string — A clip JSON document.

Returns string? — The clip's zanim payload bytes, or nil.

local bytes = skeleton.clipEncode(json.encode(clip))

globals/skeleton/jointTransforms

skeleton.jointTransforms(entityId: string | entityRef) -> table

Read a skinned entity's per-joint world transforms for the current animated pose.

Parameters

  • entityId string | 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

  • handle number — Sampler handle from bindClip.
  • time number — Sample time in seconds.
  • poseBuffer Substrate.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

  • handle number — 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

  • sinkHandle number — Sink handle to remove.

Returns boolean — True if the sink was present.

modules/skeleton/README

require("@builtin/modules/api/engine/skeleton") -- skeleton (also available as global 'skeleton')

The skeleton pose-data pipeline: sample a clip into a pose buffer, and bind/apply a pose buffer onto a Skeleton + Model entity. Public Luau surface over the __skeleton and __clip Internal FFI namespaces.

Usage: local skeleton = require("@builtin/modules/api/engine/skeleton") Also available as global: skeleton

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

  • sinkHandle number — Sink handle from bindPose.
  • poseBuffer Substrate.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

  • zanimBytes buffer | string — The clip's data.zanim payload 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.
  • opts SkeletonLayout{ 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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).
local names = skeleton.clipBones(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 binary parse is the engine's; json.decode the result to inspect or transform a clip's channels (e.g. the retarget bake) in Luau. Returns nil on bytes that aren't a valid zanim payload. Inverse of clipEncode.

Parameters

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).
local clip = json.decode(skeleton.clipDecode(bytes))

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. Returns nil on invalid JSON.

Parameters

  • jsonString string — A clip JSON document.
local bytes = skeleton.clipEncode(json.encode(clip))

modules/skeleton/jointTransforms

jointTransforms(entityId: string | entityRef): table

Read a skinned entity's per-joint world transforms for the current animated pose.

Parameters

  • entityId string | 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

  • handle number — Sampler handle from bindClip.
  • time number — Sample time in seconds.
  • poseBuffer Substrate.TypedBuffer — The stride-10 pose buffer written into.

modules/skeleton/unbindClip

unbindClip(handle: number): boolean

Drop the bound clip sampler from the registry.

Parameters

  • handle number — Sampler handle to remove.

modules/skeleton/unbindPose

unbindPose(sinkHandle: number): boolean

Remove the sink from the registry.

Parameters

  • sinkHandle number — 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

  • sinkHandle number — Sink handle from bindPose.
  • poseBuffer Substrate.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

  • zanimBytes buffer | string — The clip's data.zanim payload 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.
  • opts SkeletonLayout{ 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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).

Returns { string }? — Bone names referenced by the clip, or nil.

local names = skeleton.clipBones(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 binary parse is the engine's; json.decode the result to inspect or transform a clip's channels (e.g. the retarget bake) in Luau. Returns nil on bytes that aren't a valid zanim payload. Inverse of clipEncode.

Parameters

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).

Returns string? — The clip as a JSON string, or nil.

local clip = json.decode(skeleton.clipDecode(bytes))

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. Returns nil on invalid JSON.

Parameters

  • jsonString string — A clip JSON document.

Returns string? — The clip's zanim payload bytes, or nil.

local bytes = skeleton.clipEncode(json.encode(clip))

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

  • entityId string | 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

  • handle number — Sampler handle from bindClip.
  • time number — Sample time in seconds.
  • poseBuffer Substrate.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

  • handle number — 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

  • sinkHandle number — Sink handle to remove.

Returns boolean — True if the sink was present.

  • api
  • reference