Log inGet started

retarget

Updated 5 September 2026

The retarget namespace — 25 functions.

globals/retarget/animation

retarget.animation(clipRef: any?, targetMeshRef: any?, sourceMeshRef: any?) -> (boolean, string)

Retarget an animation clip onto a target rig, returning the VFS path of a new .anim whose channels name the target skeleton's bones with bind-pose corrected rotations. The source rig is the clip's embedded rig.zmsh (else sourceMeshRef's skin, else the skinned mesh beside the clip in its bundle); the target rig is targetMeshRef's skin. Play the result with animGraph.addClip(entity, path). Pure asset transform — no entity/ECS state.

Parameters

  • clipRef any (optional) — Animation asset to retarget.
  • targetMeshRef any (optional) — Target rig mesh whose skin defines the destination skeleton.
  • sourceMeshRef any (optional) — Source rig mesh the clip was authored for; omit to use the clip's embedded rig.

Returns (boolean, string) — Success flag and the retargeted clip's VFS path (empty on failure).

local ok, path = retarget.animation(clipRef, targetMeshRef)

globals/retarget/extractRig

retarget.extractRig(meshBytes: buffer | string) -> string?

Strip a .mesh (ZMSH) payload to a lean skin-only rig: the skeleton with geometry removed, re-encoded as a ZMSH whose only content is the skin. Returns the rig bytes, or nil when the mesh carries no skin. A .animation composite embeds this as rig.zmsh so a clip travels with its own source rig.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns string? — Skin-only ZMSH rig bytes, or nil when the mesh has no skin.

local rig = retarget.extractRig(meshBytes)

globals/retarget/humanoidProfile

retarget.humanoidProfile(meshBytes: buffer | string) -> HumanoidHolder?

Derive the humanoid retarget holder for a rig from a .mesh (ZMSH) payload, when that skeleton has the essential humanoid structure (a hips root, a head or neck, at least one full arm chain and one full leg chain). Returns nil for a rig that is not a humanoid — a prop, a plant whose leaves animate, a quadruped — so a clip from it stays a plain clip rather than joining the shared humanoid-animation pool. A rig whose bone hierarchy loops answers nil and a message naming the bone edge that closes the loop, so a caller reading the second return value can tell malformed input from a plain non-humanoid.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns HumanoidHolder?{ base, boneCount, roles = { [role] = boneName } }, or nil when the rig is not a humanoid; nil and a message naming the closing bone edge when its hierarchy loops.

local holder = retarget.humanoidProfile(meshBytes)

globals/retarget/isHumanoid

retarget.isHumanoid(meshBytes: buffer | string) -> boolean

Whether a rig is a humanoid avatar — true when humanoidProfile resolves a holder for it. Use this to tell a humanoid character apart from a generic animated mesh (a prop, a plant, a quadruped) before treating its clips as shareable humanoid animations.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns boolean — True when the rig has the essential humanoid structure.

if retarget.isHumanoid(meshBytes) then ... end

globals/retarget/serializeProfile

retarget.serializeProfile(holder: HumanoidHolder) -> string

Serialize a humanoid holder to the humanoid.profile file body: an editable YAML role -> bone-name map. Roles list hips-first head-to-toe through the limbs, then any extras name-sorted, so the file reads top-down and diffs stably. Edit a value to correct an auto-derived mapping.

Parameters

  • holder HumanoidHolder — A holder from humanoidProfile.

Returns string — The YAML body to store as humanoid.profile.

files["humanoid.profile"] = retarget.serializeProfile(holder)

modules/retarget/README

require("@builtin/modules/retarget") -- retarget

Skeletal animation retargeting — map a clip authored on one rig onto another humanoid rig, preserving the target's shape. Pure, readable Luau: the behavior an agent follows and tweaks. bake is the cold, cached transform (clip + source rig + target rig -> a clip in the target's bone space); the hot path just samples the baked clip. plan reports which canonical roles map across the two rigs and which don't.

Usage: local retarget = require("@builtin/modules/retarget")

modules/retarget/animation

animation(clipRef: any, targetMeshRef: any, sourceMeshRef: any?): (boolean, string)

Retarget an animation clip onto a target rig, returning the VFS path of a new .anim whose channels name the target skeleton's bones with bind-pose corrected rotations. The source rig is the clip's embedded rig.zmsh (else sourceMeshRef's skin, else the skinned mesh beside the clip in its bundle); the target rig is targetMeshRef's skin. Play the result with animGraph.addClip(entity, path). Pure asset transform — no entity/ECS state.

Parameters

  • clipRef any (optional) — Animation asset to retarget.
  • targetMeshRef any (optional) — Target rig mesh whose skin defines the destination skeleton.
  • sourceMeshRef any? (optional) — Source rig mesh the clip was authored for; omit to use the clip's embedded rig.
local ok, path = retarget.animation(clipRef, targetMeshRef)

modules/retarget/aux

aux(bakeKey: string)

Read what was cached beside the bake at bakeKey.

Parameters

  • bakeKey string — A key from bakeKey().

modules/retarget/bake

bake(clip, srcRig, tgtRig, opts)

Retarget a decoded clip from its source rig onto a target rig, producing a clip in the TARGET's bone space (channels named for target bones). Per frame it forward-kinematics the source pose on the rig AS AUTHORED (the bind the clip's channel rotations are local to), measures each mapped role's world rotation as a deviation from the source's CANONICAL bind, re-applies that deviation to the target's CANONICAL bind, and converts back to a target-local rotation through the target's ANIMATED parent. The target chain is rebuilt top-down, so error never accumulates down a limb. Both rigs are re-posed to the geometry-derived canonical T-pose (normalizeToTPose) for that deviation, so the result depends only on the T-pose the two rigs share — never on whatever arbitrary pose either was authored in (an A-pose source idle lands the target's arms down, not splayed out at the A-pose offset). The apply path seeds that same canonical rest. Translation routes through the role and is size-scaled by the hip-height ratio; the apply path makes it relative to the target's bind. This is the cold step — bake once per (clip, target rig) and cache (see bakeBytes).

Parameters

  • clip any (optional) — A decoded clip table { name, duration, channels, bone_names } (e.g. json.decode(skeleton.clipDecode(bytes))).
  • srcRig any (optional) — Source rig the clip was authored on (parsed rig / table / JSON).
  • tgtRig any (optional) — Target rig to retarget onto (parsed rig / table / JSON).
  • opts any (optional) — Optional { symmetrize: boolean } forwarded to normalizeToTPose.

modules/retarget/bakeBytes

bakeBytes(clipBytes, srcRig, tgtRig, cacheKey, opts)

Bake from clip BYTES to retargeted clip BYTES — clipDecode -> bake -> clipEncode — with an in-memory cache. Retarget is cold: pass a stable cacheKey (e.g. clip identity + target rig identity) and the second call for the same pair returns the cached bytes. The hot path then just samples the result like any native clip.

Parameters

  • clipBytes any (optional) — The source clip's zanim payload bytes.
  • srcRig any (optional) — Source rig (parsed rig / table / JSON).
  • tgtRig any (optional) — Target rig (parsed rig / table / JSON).
  • cacheKey any (optional) — Optional stable key; when given, the result is cached and reused.
  • opts any (optional) — Optional { symmetrize: boolean } forwarded to normalizeToTPose (absolute vs relative bind correction). Each mode caches separately.

modules/retarget/bakeKey

bakeKey(cacheKey: string, opts): string

The key a bake is stored under, so a caller that derives something FROM a bake can hold it under the same key and have it dropped at the same time. Each correction mode bakes separately, which is what the suffix carries.

Parameters

  • cacheKey string — The stable key passed to bakeBytes.
  • opts any (optional) — The same { symmetrize } passed to bakeBytes.

modules/retarget/clearCache

clearCache(cacheKey: string?)

Drop every cached bake and everything derived from it (or just cacheKey when given). Call after editing a rig's profile so clips re-bake against the corrected mapping.

Parameters

  • cacheKey string? (optional) — Optional single key to evict; omit to clear all. Accepts either the key passed to bakeBytes or an already-composed bakeKey().

modules/retarget/extractRig

extractRig(meshBytes: buffer | string): string?

Strip a .mesh (ZMSH) payload to a lean skin-only rig: the skeleton with geometry removed, re-encoded as a ZMSH whose only content is the skin. Returns the rig bytes, or nil when the mesh carries no skin. A .animation composite embeds this as rig.zmsh so a clip travels with its own source rig.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.
local rig = retarget.extractRig(meshBytes)

modules/retarget/humanoidProfile

humanoidProfile(meshBytes: buffer | string): HumanoidHolder?

Derive the humanoid retarget holder for a rig from a .mesh (ZMSH) payload, when that skeleton has the essential humanoid structure (a hips root, a head or neck, at least one full arm chain and one full leg chain). Returns nil for a rig that is not a humanoid — a prop, a plant whose leaves animate, a quadruped — so a clip from it stays a plain clip rather than joining the shared humanoid-animation pool. A rig whose bone hierarchy loops answers nil and a message naming the bone edge that closes the loop, so a caller reading the second return value can tell malformed input from a plain non-humanoid.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.
local holder = retarget.humanoidProfile(meshBytes)

modules/retarget/isHumanoid

isHumanoid(meshBytes: buffer | string): boolean

Whether a rig is a humanoid avatar — true when humanoidProfile resolves a holder for it. Use this to tell a humanoid character apart from a generic animated mesh (a prop, a plant, a quadruped) before treating its clips as shareable humanoid animations.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.
if retarget.isHumanoid(meshBytes) then ... end

modules/retarget/loadRig

loadRig(ref)

Resolve a .rig asset and return its parsed, FK-enriched rig (ready for plan / bake). The rig payload is the asset's rig.json.

Parameters

  • ref any (optional) — A .rig asset ref (identity / guid / path / handle).

modules/retarget/normalizeToTPose

normalizeToTPose(rig, opts)

Re-pose a rig's bind to the EXACT canonical T-pose, so a clip's source rig and the avatar it drives share one reference pose. Retarget transfers RELATIVE motion, so any residual bind mismatch (hands rolled the wrong way, feet pointing askew, an A-pose vs a T-pose) shows up as broken hands/feet in the result. Each bone is posed to a canonical world frame for its role — direction AND roll: arms horizontal palms-down, legs straight down, spine up. Only the limb bones whose bind direction differs between an A-pose and a T-pose are aimed; limb ENDPOINTS (hands, feet, toes) and bones with no role keep their authored orientation relative to the re-posed parent (their pose is mesh-defined, so aiming them twists the hand / tips the foot). The whole body is also rigidly de-rotated into an upright, forward-facing frame (handling a baked root rotation) and centered on its sagittal plane. Bind correction is RELATIVE by default — bone offsets/lengths/inverse-bind are untouched, so the rig keeps its own proportions and any authored left/right asymmetry. Pass opts.symmetrize = true for ABSOLUTE correction: left/right bones are mirrored across the sagittal plane for a perfectly symmetric bind, overriding the rig's authored asymmetry/proportions.

Parameters

  • rig any (optional) — The rig to normalize (parsed rig / table / JSON).
  • opts any (optional) — Optional { symmetrize: boolean }. symmetrize=true = absolute correction (force symmetry); default/false = relative (preserve proportions).

modules/retarget/plan

plan(srcRig, tgtRig)

Report how a source rig's clips map onto a target rig: which canonical roles both rigs fill (mapped), which the source has but the target lacks (unmappedSource — those channels are dropped), and which the target has spare (unmappedTarget). Use it to see why a retarget is partial and which bone to hand-map in a rig's profile.

Parameters

  • srcRig any (optional) — Source rig — a parsed rig, a .rig table, or its JSON string.
  • tgtRig any (optional) — Target rig — same forms.

modules/retarget/serializeProfile

serializeProfile(holder: HumanoidHolder): string

Serialize a humanoid holder to the humanoid.profile file body: an editable YAML role -> bone-name map. Roles list hips-first head-to-toe through the limbs, then any extras name-sorted, so the file reads top-down and diffs stably. Edit a value to correct an auto-derived mapping.

Parameters

  • holder HumanoidHolder — A holder from humanoidProfile.
files["humanoid.profile"] = retarget.serializeProfile(holder)

modules/retarget/setAux

setAux(bakeKey: string, value)

Cache a value beside the bake at bakeKey. It is dropped whenever that bake is, so it cannot outlive the bytes it was derived from.

Parameters

  • bakeKey string — A key from bakeKey().
  • value any (optional) — The value to hold.

typed/builtin//modules/api/engine/retarget/retarget/animation

retarget.animation(clipRef: any?, targetMeshRef: any?, sourceMeshRef: any?) -> (boolean, string)

Retarget an animation clip onto a target rig, returning the VFS path of a new .anim whose channels name the target skeleton's bones with bind-pose corrected rotations. The source rig is the clip's embedded rig.zmsh (else sourceMeshRef's skin, else the skinned mesh beside the clip in its bundle); the target rig is targetMeshRef's skin. Play the result with animGraph.addClip(entity, path). Pure asset transform — no entity/ECS state.

Parameters

  • clipRef any (optional) — Animation asset to retarget.
  • targetMeshRef any (optional) — Target rig mesh whose skin defines the destination skeleton.
  • sourceMeshRef any (optional) — Source rig mesh the clip was authored for; omit to use the clip's embedded rig.

Returns (boolean, string) — Success flag and the retargeted clip's VFS path (empty on failure).

local ok, path = retarget.animation(clipRef, targetMeshRef)

typed/builtin//modules/api/engine/retarget/retarget/extractRig

retarget.extractRig(meshBytes: buffer | string) -> string?

Strip a .mesh (ZMSH) payload to a lean skin-only rig: the skeleton with geometry removed, re-encoded as a ZMSH whose only content is the skin. Returns the rig bytes, or nil when the mesh carries no skin. A .animation composite embeds this as rig.zmsh so a clip travels with its own source rig.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns string? — Skin-only ZMSH rig bytes, or nil when the mesh has no skin.

local rig = retarget.extractRig(meshBytes)

typed/builtin//modules/api/engine/retarget/retarget/humanoidProfile

retarget.humanoidProfile(meshBytes: buffer | string) -> HumanoidHolder?

Derive the humanoid retarget holder for a rig from a .mesh (ZMSH) payload, when that skeleton has the essential humanoid structure (a hips root, a head or neck, at least one full arm chain and one full leg chain). Returns nil for a rig that is not a humanoid — a prop, a plant whose leaves animate, a quadruped — so a clip from it stays a plain clip rather than joining the shared humanoid-animation pool. A rig whose bone hierarchy loops answers nil and a message naming the bone edge that closes the loop, so a caller reading the second return value can tell malformed input from a plain non-humanoid.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns HumanoidHolder?{ base, boneCount, roles = { [role] = boneName } }, or nil when the rig is not a humanoid; nil and a message naming the closing bone edge when its hierarchy loops.

local holder = retarget.humanoidProfile(meshBytes)

typed/builtin//modules/api/engine/retarget/retarget/isHumanoid

retarget.isHumanoid(meshBytes: buffer | string) -> boolean

Whether a rig is a humanoid avatar — true when humanoidProfile resolves a holder for it. Use this to tell a humanoid character apart from a generic animated mesh (a prop, a plant, a quadruped) before treating its clips as shareable humanoid animations.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns boolean — True when the rig has the essential humanoid structure.

if retarget.isHumanoid(meshBytes) then ... end

typed/builtin//modules/api/engine/retarget/retarget/serializeProfile

retarget.serializeProfile(holder: HumanoidHolder) -> string

Serialize a humanoid holder to the humanoid.profile file body: an editable YAML role -> bone-name map. Roles list hips-first head-to-toe through the limbs, then any extras name-sorted, so the file reads top-down and diffs stably. Edit a value to correct an auto-derived mapping.

Parameters

  • holder HumanoidHolder — A holder from humanoidProfile.

Returns string — The YAML body to store as humanoid.profile.

files["humanoid.profile"] = retarget.serializeProfile(holder)
  • api
  • reference