Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 07158574-5…

retarget

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

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

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. This is the runtime retarget path; the behaviour lives here, in Luau, so an agent can follow and tweak it. (__retarget.oracleBake is the Rust numerical oracle this is validated against, not the runtime path.)

How it works

Each .rig carries a profile — a canonical-role → bone map (the driver). 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 profiles without a per-pair mapping.
  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). Translation is re-expressed in the target parent's frame and size-scaled, then applied RELATIVE to the target's bind: every bone starts at the target's own offset and the clip adds its displacement from rest on top — a bone with no translation motion stays put (proportions preserved), while a bone that moves (the hips' vertical bob, the root's stride) carries that motion across, size-scaled to the target.

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)

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

module 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. require modules/retarget

parseRig(rig: ?) → void

Normalize a `.rig` (table or JSON string) into the enriched form the math below reads: bones, name->index, role->bone, bone->role, FK globals, lengths. Idempotent: an already-parsed rig is returned unchanged.

argtypedescription
rig?

vdot(a: ?, b: ?) → void

argtypedescription
a?
b?

fkOrder(bones: ?) → void

Parent-before-child traversal order over a bone array (0-based parents).

argtypedescription
bones?

symmetrizeBind(R: ?) → void

Absolute bind correction: force a perfectly symmetric canonical bind by mirroring each left/right bone's world position across x = 0 and rebuilding the local offsets from the symmetric positions. Orientation is already canonical (the caller ran the relative pass first), so only offsets change — this overrides the rig's authored left/right asymmetry and proportions, which is the whole point of absolute mode. `R` is a parsed, already-normalized rig.

argtypedescription
R?

computeBodyFrame(R: ?) → void

The rigid world rotation that aligns a rig's FACING to canonical: the horizontal hip-to-hip side onto +X, so the body faces +Z. Mesh-aware up: a humanoid mesh is modeled upright in model space, so +Y IS the body's up — a rig's bind SKELETON can sit at a tilt WITHIN that upright mesh (hips→neck leaning forward, say), and verticalizing to the skeleton's spine would rotate the upright mesh off vertical. So +Y up is left untouched and only the facing is aligned, a pure rotation about +Y that never tips the mesh. Read from joint POSITIONS, so it captures a baked facing an importer left in the bind regardless of bone rotations. `normalizeToTPose` applies it before aiming the limbs; the body's verticality stays as authored.

argtypedescription
R?

rolePos(roleName: ?) → void

argtypedescription
roleName?

computeUpAlign(R: ?) → void

Just the up-alignment of a rig's body frame: the rotation that stands the bind upright (hips→neck onto +Y), WITHOUT the yaw that would re-aim its facing. The retarget bake applies this to the source pose so a bind authored with a forward lean doesn't tilt the target, while leaving the source's heading intact — the character's world facing is the controller's job, and it drives source and target through the same convention, so re-aiming the clip's yaw would fight it (the character would face away from its travel — moonwalk).

argtypedescription
R?

rolePos(roleName: ?) → void

argtypedescription
roleName?

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. correction (force symmetry); default/false = relative (preserve proportions).

argtypedescription
rig?The rig to normalize (parsed rig / table / JSON).
opts?Optional `{ symmetrize: boolean }`. `symmetrize=true` = absolute

qnlerp(a: ?, b: ?, s: ?) → void

Linear-blend two quaternions along the shortest arc, normalized.

argtypedescription
a?
b?
s?

sampleRotAt(ch: ?, t: ?) → void

The value quaternion of a Rotation channel at time `t`, interpolated by the channel's own mode (Step holds; Linear / CubicSpline blend the value samples — the CubicSpline tangents are skipped, so the value is exact at keyframes and a stable nlerp between them, which is all the cold rebake needs).

argtypedescription
ch?
t?

bindLocalRot(R: ?, i: ?) → void

A bone's local bind rotation (relative to its parent), from the parsed rig's FK globals.

argtypedescription
R?
i?

validateScale(out: ?) → void

Scale is rig-independent; keep it, replacing non-finite / non-positive with 1.

argtypedescription
out?

copyArray(a: ?) → void

argtypedescription
a?

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.

argtypedescription
srcRig?Source rig — a parsed rig, a `.rig` table, or its JSON string.
tgtRig?Target rig — same forms.

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`). (e.g. `json.decode(skeleton.clipDecode(bytes))`).

argtypedescription
clip?A decoded clip table `{ name, duration, channels, bone_names }`
srcRig?Source rig the clip was authored on (parsed rig / table / JSON).
tgtRig?Target rig to retarget onto (parsed rig / table / JSON).
opts?Optional `{ symmetrize: boolean }` forwarded to `normalizeToTPose`.

addBone(name: ?) → void

argtypedescription
name?

yExtent(rig: ?) → void

argtypedescription
rig?

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`.

argtypedescription
ref?A `.rig` asset ref (identity / guid / path / handle).

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. (absolute vs relative bind correction). Each mode caches separately.

argtypedescription
clipBytes?The source clip's `zanim` payload bytes.
srcRig?Source rig (parsed rig / table / JSON).
tgtRig?Target rig (parsed rig / table / JSON).
cacheKey?Optional stable key; when given, the result is cached and reused.
opts?Optional `{ symmetrize: boolean }` forwarded to `normalizeToTPose`

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.

argtypedescription
cacheKeystringThe stable key passed to `bakeBytes`.
opts?The same `{ symmetrize }` passed to `bakeBytes`.

aux(bakeKey: string) →

Read what was cached beside the bake at `bakeKey`.

argtypedescription
bakeKeystringA key from `bakeKey()`.

setAux(bakeKey: string, value: ?) → void

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.

argtypedescription
bakeKeystringA key from `bakeKey()`.
value?The value to hold.

clearCache(cacheKey: string?) → void

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. the key passed to `bakeBytes` or an already-composed `bakeKey()`.

argtypedescription
cacheKeystring?Optional single key to evict; omit to clear all. Accepts either

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

9items
▣
module · born here
❒asset
# json JSON encode/decode library for Luau. Encodes Lua values to JSON strings and decodes JSON strings back to Lua values. Used for communication with the Rust side of the engine, the VFS read/write bridge, and any wire-format that needs JSON. Pure Luau, no engine dependencies. Compact and pretty-printed encoders, plus a hand-rolled decoder that streams the input by position so it works under WASM as well as native. ## Exports - `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`. - `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly). - `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array. - `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure. ## Usage ```luau local Json = require("@builtin::modules.json") local widget = { type = "button", text = "Click Me" } local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}' local pretty = Json.encodePretty(widget, " ") local decoded = Json.decode(compact) local v, err = Json.decode("oops") -- v = nil, err = error message ``` ## Notes - Object keys are sorted alphabetically in both encoders for consistent output across runs. - Numeric keys on objects are stringified at encode time (JSON has no numeric keys). Pure-integer key sets get detected as arrays via `isArray` and encoded with brackets. - NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round trips through `decode` recover `null` (Lua `nil`), so they don't preserve. - Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe. Only the BMP is covered; supplementary planes via surrogate pairs are not. - Functions encode as `null`. - Decode is character-streamed — no regex, no `string.match` patterns on the whole input — so the line-and-column information needs to be reconstructed from the position offset.
▲ 0↑ born
▣
module · born here
❒asset
# rigmath Quaternion algebra, vector helpers, and forward kinematics over a bone hierarchy. One definition of the math, shared by retargeting and IK. Quaternions are `{ x, y, z, w }` arrays and vectors are `{ x, y, z }` arrays, matching the conventions the engine's rig data already uses, so values read straight out of `ecs.Skeleton.bones` or a parsed `.rig` need no conversion. ## Exports Vectors: - `vdot(a, b) -> number` — dot product. - `vcross(a, b) -> { number }` — cross product. - `vlen(v) -> number` — Euclidean length. - `vsub(a, b)`, `vadd(a, b)`, `vscale(v, s) -> { number }` — component-wise arithmetic. - `vnormalize(v) -> { number }` — unit vector; a zero-length input returns zero. - `vperpendicular(v) -> { number }` — a deterministic unit vector at right angles to `v`. Quaternions: - `IDENTITY` — `{ 0, 0, 0, 1 }`. - `qmul(a, b) -> { number }` — Hamilton product; applies `b`, then `a`. - `qnormalize(q)`, `qinverse(q) -> { number }`. - `qrotvec(q, v) -> { number }` — rotate a vector. - `shortestArc(a, b) -> { number }` — the rotation carrying unit vector `a` onto `b`. - `axisAngle(axis, angle) -> { number }` — from an axis and radians. - `qslerp(a, b, t) -> { number }` — shortest-arc interpolation. - `qangle(q) -> number` — rotation magnitude in radians, `[0, pi]`. - `signedAngle(a, b, axis) -> number` — roll from `a` to `b` about `axis`, in radians. - `swingTwist(q, axis) -> ({ number }, { number })` — twist about `axis`, then the remaining swing. Scalars: - `isFinite(n) -> boolean`, `clamp(v, lo, hi) -> number`. Forward kinematics: - `computeGlobals(bones) -> (gRot, gPos)` — global rest transforms from local ones. - `computeBoneLengths(bones, gPos) -> { number }` — each bone's distance to its farthest child. ## Usage ```luau local rigmath = require("modules.rigmath") -- Point a bone's forward axis at a target. local dir = rigmath.vnormalize(rigmath.vsub(targetPos, bonePos)) local swing = rigmath.shortestArc(rigmath.qrotvec(boneRot, { 0, 0, 1 }), dir) local aimed = rigmath.qmul(swing, boneRot) -- Blend the result in at a weight. local final = rigmath.qslerp(boneRot, aimed, 0.5) ``` ## Notes - Degenerate input never produces NaN. A zero-length vector normalizes to zero, a degenerate quaternion normalizes to identity, and `shortestArc` on antiparallel vectors resolves to a half turn about a perpendicular axis. - `computeGlobals` tolerates any bone ordering, including a parent listed after its child, and falls back to the local transform for a bone left unresolved by a cyclic parent. - Bone `parent` indices are 0-based with -1 for a root, matching the rig format; the returned arrays are 1-based and parallel to the input.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.