Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit44fd9e59bd

README.md

retarget module

require("modules.retarget") — skeletal animation retargeting in readable Luau. Map a clip authored on one humanoid rig onto another, preserving the target's shape. The behaviour lives here, in Luau, so an agent can follow and tweak it.

How it works

Each rig maps onto the humanoid avatar type — a role → bone map derived from the skeleton's structure, with the author's overrides laid over it (rig:qualify(), see the rig asset type). Retarget is two steps:

  1. Bone map — source bone → its role → the target bone filling that role. Roles are the shared vocabulary, so any two rigs interoperate through their mappings without a per-pair table.
  2. Shape-preserving transfer — the target keeps its own bone lengths and rest orientations; the animation contributes only delta-from-rest motion. Rotations are bind-pose-corrected (the target reproduces the source's world-space motion relative to its own rest). Only the pelvis and the root carry translation: the pelvis's offset is taken through its animated source parent, stood on the axis the source stands along, sized by the leg-length ratio (thigh plus shin, the same in every pose) and re-expressed in the target pelvis's animated parent frame, then applied relative to the target's bind. A source root's travel rides the target root, or folds onto the target pelvis when the target has no root. Every other bone keeps the target's own offset, so no limb or spine stretches.

Retarget is cold: bake once per (clip, target rig) and cache; the hot path just samples the baked clip, exactly like a native one.

Surface

  • parseRig(rig) → enriched rig. Accepts a parsed .rig table, a .rig JSON string, or an already-parsed rig (idempotent).
  • plan(srcRig, tgtRig) → { mapped, unmappedSource, unmappedTarget, … } — which roles map across the rigs, and which don't (the diagnostic).
  • bake(clip, srcRig, tgtRig) → a decoded clip table in the target's bone space. clip is a decoded clip ({ name, duration, channels, bone_names }).
  • bakeBytes(clipBytes, srcRig, tgtRig, cacheKey?) → retargeted clip zanim bytes, with an in-memory cache keyed by cacheKey.
  • loadRig(ref) → parsed rig from a .rig asset.
  • clearCache(cacheKey?) → drop cached bakes (call after editing a rig profile).

Example

local retarget = require("modules.retarget")

local src = retarget.loadRig(asset.ref("synty_character", "rig"))
local tgt = retarget.loadRig(asset.ref("hero", "rig"))

-- inspect the mapping
local plan = retarget.plan(src, tgt)
print(plan.mapped.leftarm.source, "->", plan.mapped.leftarm.target)

-- bake a walk clip onto the hero rig (cold, cached), then sample it like any clip
local walkBytes = vfs.read(asset.source("walk", "animation") .. "/data.zanim")
local walkOnHero = retarget.bakeBytes(walkBytes, src, tgt, "walk|hero")
local bind = skeleton.bindClip(walkOnHero, heroBoneOrder)