retarget
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
clipRefany(optional) — Animation asset to retarget.targetMeshRefany(optional) — Target rig mesh whose skin defines the destination skeleton.sourceMeshRefany(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
meshBytesbuffer | 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
meshBytesbuffer | 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
meshBytesbuffer | 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
holderHumanoidHolder— A holder fromhumanoidProfile.
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
clipRefany(optional) — Animation asset to retarget.targetMeshRefany(optional) — Target rig mesh whose skin defines the destination skeleton.sourceMeshRefany?(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
bakeKeystring— A key frombakeKey().
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
clipany(optional) — A decoded clip table{ name, duration, channels, bone_names }(e.g.json.decode(skeleton.clipDecode(bytes))).srcRigany(optional) — Source rig the clip was authored on (parsed rig / table / JSON).tgtRigany(optional) — Target rig to retarget onto (parsed rig / table / JSON).optsany(optional) — Optional{ symmetrize: boolean }forwarded tonormalizeToTPose.
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
clipBytesany(optional) — The source clip'szanimpayload bytes.srcRigany(optional) — Source rig (parsed rig / table / JSON).tgtRigany(optional) — Target rig (parsed rig / table / JSON).cacheKeyany(optional) — Optional stable key; when given, the result is cached and reused.optsany(optional) — Optional{ symmetrize: boolean }forwarded tonormalizeToTPose(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
cacheKeystring— The stable key passed tobakeBytes.optsany(optional) — The same{ symmetrize }passed tobakeBytes.
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
cacheKeystring?(optional) — Optional single key to evict; omit to clear all. Accepts either the key passed tobakeBytesor an already-composedbakeKey().
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
meshBytesbuffer | 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
meshBytesbuffer | 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
meshBytesbuffer | 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
refany(optional) — A.rigasset 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
rigany(optional) — The rig to normalize (parsed rig / table / JSON).optsany(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
srcRigany(optional) — Source rig — a parsed rig, a.rigtable, or its JSON string.tgtRigany(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
holderHumanoidHolder— A holder fromhumanoidProfile.
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
bakeKeystring— A key frombakeKey().valueany(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
clipRefany(optional) — Animation asset to retarget.targetMeshRefany(optional) — Target rig mesh whose skin defines the destination skeleton.sourceMeshRefany(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
meshBytesbuffer | 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
meshBytesbuffer | 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
meshBytesbuffer | 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
holderHumanoidHolder— A holder fromhumanoidProfile.
Returns string — The YAML body to store as humanoid.profile.
files["humanoid.profile"] = retarget.serializeProfile(holder)