Multi-source character assembly
animsmith assemble combines one authoritative skinned base with animation
takes from separate FBX or glTF inputs. Use it when a delivery spreads clips
across individual files or long master timelines but the runtime needs one GLB.
animsmith assemble character-assembly.toml \
-o character.glb \
--evidence character.assembly.json
The command is available in default builds with the fbx feature. It always
writes a GLB and versioned JSON evidence as one publication pair. It prepares
both files before replacing either destination and restores prior outputs if
the second replacement fails.
This rollback contract covers every error returned by the command. No portable filesystem primitive can make two independently named destinations power-loss-atomic, so after abrupt process or machine termination the consumer must verify the evidence artifact digest before publication.
Boundary
Assembly owns format-generic scene, skin, animation, material, and GLB work. The consuming project still owns source-package extraction, gameplay naming and acceptance policy, cache/generation policy, provenance beyond assembly evidence, and publication. DCC automation is outside this command.
Recipe
Start from examples/character-assembly.toml:
schema_version = 7
schema = "urn:animsmith:schema:character-assembly-recipe:7"
input_root = "inputs"
base_input = "base-character.fbx"
mesh_instances = ["character-mesh"]
remove_nodes = ["sword-placeholder"]
complete_tracks = true
canonicalize_skin = true
ground_and_center = true
prune_constant_tracks = false
fps = 30.0
# Optional for glTF/GLB and the inventory-complete FBX subset; the exact root
# name and factor are required together.
# [rest_bind_scale]
# root_node_name = "root"
# expected_factor = 0.01
[[clips]]
name = "idle"
input = "idle.fbx"
take = "Take 001"
frame_window = [1, 61]
drop_closing_endpoint = true
strip_bones = ["motion-root"]
All input paths are relative to input_root, which is itself relative to the
recipe. Absolute paths, parent traversal, symlink components, and paths that
escape the canonical root are rejected. Output paths are command arguments so
the recipe remains relocatable.
The input root is an operator-controlled snapshot: do not mutate or replace its entries concurrently with assembly. Symlink checks and reads are separate portable filesystem operations, so the command does not claim to defend against an actor racing the input tree while it runs.
Discover exact recipe names
Inspect the base before writing the recipe:
animsmith inspect inputs/base-character.fbx
Copy the quoted name after node in the mesh instances section exactly into
mesh_instances; quoted names use TOML-compatible escapes. Each entry also
shows its source node, referenced mesh,
skinned status, and the material used by every primitive. The top-level
materials section lists the exact, case-sensitive names required by a
material texture recipe. Duplicate skeleton node
or material names are marked ambiguous; assembly and material recipe
validation reject ambiguous authored names instead of guessing. Multiple mesh
instances attached to one uniquely named node are not ambiguous and are
selected together. inspect only reports the authored scene—it does not rename
nodes or materials, merge duplicates, or repair the asset.
The base supplies the authoritative skeleton, rest pose, meshes, skins,
materials, and textures. mesh_instances names the exact base nodes whose
mesh instances survive; omitting it retains every instance. An explicit node
name must be unique; every mesh instance attached to that selected node
survives. Unreferenced mesh and material definitions are pruned.
For a recipe-v7 FBX base, assembly resolves this selection once against the
captured FBX node names and carries the selected scene through its private GLB
scale stage. Any holder names generated by that private serialization are not
a second recipe namespace. When rest_bind_scale is enabled, an explicit
selection that retains no skinned base instance refuses as an
asset-recipe-mismatch at the selection boundary; its diagnostic names the
recipe selection rather than the private staging artifact.
Each [[clips]] entry selects one exact source take, renames it, and remaps
its tracks to the base skeleton by exact unique bone name. Missing referenced
bones and ambiguous names fail rather than retarget motion. An entry may then
apply:
frame_window = [START, END]: one-based, inclusive source frame endpoints atfps, ortime_window = [START, END]in seconds;drop_closing_endpoint = true: remove one final key from every channel;hold_frames = N: duplicate the final pose afterN / fpsseconds;gait_anchor = true: explicitly declare an in-place cyclic gait and run the measured gait-anchor transform using the selectedanimsmith.tomlrig configuration. Assembly refuses before publication when the Root role (or Hips fallback) has missing/non-finite trajectory evidence, more than 1 cm of horizontal endpoint displacement, or more than 1° of yaw accumulation; no interior step is subtracted as an allowance. Every nonconstant channel the operation would rotate must contain exactly one key at each declaredfpswhole-frame sample over the clip duration, at the exact representable f32key / fpstime and period endpoint. Sparse, differently framed, duplicate-time, or off-grid evidence refuses, as do duplicate(bone, property)channels (including constant channels). Exact admitted f32 key times drive verification, and an integer-index permutation performs the rewrite; exempt constant tracks cannot influence its period or shift. The complete skeleton, resolved roles, and track shapes are validated first; declared frames × skeleton bones, declared frames × tracks, and maximum authored keys × skeleton bones are independently bounded at an inclusive 1,000,000 samples. Yaw uses f64 first/final headings plus counted full-turn crossings. At sample zero it chooses the local+Z,+Y, or+Xbasis axis with the greatest finite horizontal projection, in that deterministic tie order, and retains it for the entire proof. This supports different source-axis conventions without allowing a later axis switch to hide yaw; loss of the selected horizontal projection refuses. Error does not grow with segment count; four f32 successors at the inclusive 1 cm/1° caps cover only authored endpoint translation/quaternion quantization;strip_bones = [...]: remove every TRS track for named base bones.
Optional rest/bind scale canonicalization
Recipe v7 can opt into the accepted rest/bind scale operation with one top-level block:
[rest_bind_scale]
root_node_name = "root"
expected_factor = 0.01
The block has no defaults: the exact normalized root-node name and finite
positive expected factor are both required. In the captured base, the name
must resolve to exactly one source node and exactly one non-empty source skin
whose every joint is that node or its descendant. Every distinct FBX
clip-only input uses the track-only case described below. A glTF/GLB clip
retains its existing successful full rest/bind or meshless track-only path;
role-specific admission additionally selects the track-only path when only
geometry, material, deformation, or bind obligations that assembly never
consumes would fail. Framing, parser, topology, dependency, raw-coverage,
named-skeleton, and animation accessor/layout checks remain strict.
Missing or repeated names and zero or multiple applicable base skins fail
closed. Leading or trailing
whitespace is invalid rather than
silently trimmed; internal whitespace remains exact. No
source-array index is reused across files. Recipe v7 accepts glTF/GLB and the
narrow FBX subset admitted by the existing normalized/baked rest-bind
capability boundary. That boundary
requires a complete ufbx inventory for the normalized metre/Y-up hierarchy,
derived bind evidence, rebuilt geometry, and baked animation. Its same-load
gate may admit enumerated scale-invariant conversion fidelity, including
omitted authored vertex/face/edge metadata, influence projection with complete
effective coverage, triangulation, and exact-bit welding, while retaining every
inventory fact. Incomplete source-skeleton projection, unclassified payload,
missing effective influences or geometry, incomplete bind evidence, residual
unmodeled source elements or extensions,
incomplete resource/construct coverage, or an incompatible basis fails closed.
The source-aware gate may admit user-defined properties and bounded external
texture/video declarations because neither supplies rest/bind state to the
normalized GLB bridge. It also admits exactly ufbx's marker, LOD-group,
stereo-camera, camera-switcher, and display-layer typed lists: they cannot
supply hierarchy transforms, skin binds, tracks, or geometry to that bridge.
Display layers contribute only node membership and editor
visibility/freeze/color state; they do not alter projected nodes. The gate
admits shader/binding-table metadata on the same basis. A BindPose is admitted
only when it covers every joint of each skin it touches and its converted rows are
finite, unambiguous, and agree with the converted cluster bind or node
rest-world matrix already consumed by the bridge; no Pose remains required.
Those elements remain counted in the raw aggregate, while every residual
unmodeled typed list fails closed and is named by kind and count in the
refusal. Rooted capture
retains supported linked texture bytes through the normalized artifact, and
neither publication destination may name one of the safe source-relative
dependency keys retained by that same-load closure, including a key whose
capture was unavailable or refused. The command
stops before publication when a retained key was refused as
symlink-mediated rather than resolving through the link, or when the closure
resource budget leaves a declaration tail unchecked. The frozen v7 evidence
retains declaration counts in its capability inventory; same-load raw
facts and closure remain loader-side rather than being added to that wire
record. Admission does not claim raw FBX object-property, marker, camera,
material, or texture-assignment preservation. A
refusal identifies the exact failed source fact, semantic domain, or inventory
counter. Nothing is inferred from filenames, bounds,
character height, inverse-bind magnitudes, or source units. Recipe v6 remains
immutable with its explicit source indices;
recipes v4/v5 remain immutable and continue to reject FBX when
rest_bind_scale is active.
Recipe v7 retains recipe v6's composition with canonicalize_skin,
ground_and_center, and
remove_nodes. It captures and validates the raw inputs first, applies the
same mesh selection and canonical assembly transforms to the staged candidate
and an independently source-rebased reference, then maps selectors by names and
ordered selected joint topology after any index changes. It serializes the
staged candidate, performs the raw rest-bind rewrite last, reloads those exact
bytes once for proof, and atomically publishes only after the final clips match
the rebased reference. Recipe v4 remains immutable and continues to reject
that combination.
Before any clip keys are remapped or copied, assembly captures and validates
the exact bytes of the base and every separately supplied clip. Each input
must agree on the selected domain and factor, named parent topology, target
local rest matrices and orientations, helper-node layout, and coordinate
convention. Each input's own target paths and plan-owned effective factors are
validated and fingerprinted; distinct clips need not contain identical target
paths. A matching digest alone is not compatibility evidence. Topology,
orientation, helper-layout, basis, selector, or factor disagreement rejects
the complete operation before publication.
Topology, resolved names, and the declared factor compare exactly. V7 compares
the exact resolved root and ordered selected-skin joint names plus the named
source/helper parent paths; format-local skin/node numbers remain per-input
evidence. Each input's selector identity is derived from its accepted plan and
source document; a declared/resolved disagreement rejects the operation.
V6 retains exact raw-selector comparison.
Rest translations, scales, and matrices use the named Appendix D tolerance
policy, while quaternion orientation uses shortest-path angular distance, so
equivalent q/-q spellings do not become false incompatibilities.
For a compatible clip, translation values and both translation tangents of a
CUBICSPLINE track are rebased in the source basis before exact-name remapping.
Assembly reuses the same validated scale request, compiled write-set, raw glTF
rewrite, tolerance policy, and paired proof residuals as animsmith scale.
The assembled artifact is serialized, reloaded from its exact staged bytes,
and proved once before the artifact/evidence pair can be published.
complete_tracks = true fills absent TRS channels from the base rest pose for
the union of selected skin joints and nodes targeted by any emitted clip.
Per-clip strip_bones entries stay excluded. Rotation key values are
unit-normalized and made hemisphere-consistent without changing cubic tangent
sign relationships. When FBX scale staging produces an independent reference
skeleton, completion targets are carried by exact unique bone name and resolved
separately in each skeleton; numeric bone IDs are document-local. Each
skeleton's own node-removal closure is excluded before completion.
canonicalize_skin = true bakes each skin's common geometry-to-world transform
into positions and normals and rewrites inverse binds consistently.
ground_and_center = true additionally places the bind-pose geometry on Y=0
and centers it in X/Z while translating skeleton roots and root-translation
animation tracks by the same offset. Inconsistent skin transforms, singular
normal transforms, or shared mesh definitions that require incompatible bakes
are rejected.
prune_constant_tracks = true removes tracks that the existing core constant-
track predicate proves constant, after completion, quaternion cleanup, and all
other assembly transforms. This is all-property pruning: it can remove
completion-generated (bone, property) coverage when the completed value is
constant, so it may undo part of complete_tracks = true. The carve-out uses
the effective output clip's animates_bones exact names; it never uses
required_bones, so declared motion evidence is preserved without retaining
unrelated tracks. For an independent scale reference, authoritative prune
decisions and the final clip proof are projected by exact unique bone name and
channel property rather than by document-local bone or track indices. The
canonical example leaves pruning disabled; opt in only after reviewing the
completed clip and the consumer's transition behavior. A consumer that does
not explicitly reset an omitted property during a transition can retain the
outgoing clip's value, so leave pruning disabled where dense transition
coverage matters until property-scoped selection is available (see
#401). Set it to false or
omit it to retain every otherwise eligible track.
remove_nodes = [...] selects exact, case-sensitive names from the
post-canonicalization base skeleton and removes each selected node together
with its complete descendant subtree. Every name must resolve exactly once;
an absent or ambiguous name fails rather than being skipped. Overlapping
ancestor and descendant selections are allowed and their union is removed
once, in original parent-before-child node order. Selecting the entire
skeleton is refused. Surviving descendants are never reparented because every
descendant of a removed node belongs to the removal closure.
Removal is planned before track completion, and closure nodes are excluded
from completion targets. The structural projection is applied only after
clip processing and constant-track pruning. It is
refused if a final track still targets the closure, if a skinned mesh instance
is attached to it, or if a selected node remains referenced by a skin joint or
complete source-skin identity. For an FBX input with rest_bind_scale, an
unskinned mesh instance attached inside the declared closure is excluded from
the private normalized scale stage and removed with that closure. The same
instance still blocks scale when its node is not declared for removal. This is
the only attachment reference consumed implicitly; per-clip strip_bones and
constant-track pruning remain necessary to make an animated decorative
subtree removable. The effective clip's
animates_bones configuration still protects named tracks from pruning, so a
protected surviving track causes removal to fail. [rig] required_bones is a
lint presence policy, not a node-removal selector or protection list.
Errors are deterministic and no output is published on any refusal. A valid
recipe that does not fit loaded asset facts is a typed asset refusal (exit
1); JSON mode emits
producer-refusal:1, while text
mode reports the same stable kind on stderr. Recipe syntax/schema/value,
input/path, and publication failures are operator errors (exit 2) and emit
no stdout. Recipe schema and value validation precede input loading. After the base's selected
mesh and canonicalization steps, selectors and their descendant closure are
resolved deterministically and the whole-skeleton rule is checked. After all
clip transforms, every surviving track, mesh attachment, skin reference, and
complete source-skin fact is checked before the hierarchy is changed. The
reported refusal identifies the offending stored record; traversal order is
an implementation detail rather than a wire contract.
Accepted projection preserves the original order and parent links of every
surviving node, remaps all surviving node references through one stable map,
and clears optional source-native skeleton projection because it can no longer
describe the authored source completely. Node removal performs no mesh,
material, or texture garbage collection. Outside the bounded FBX scale
composition above, a mesh instance inside the closure is refused. The earlier
explicit mesh_instances selection and its existing resource pruning remain
the only assembly resource-garbage-collection pass. Evidence counts an
unskinned instance consumed by declared node removal in
transforms.removed_mesh_instances and omits its node name from
retained_mesh_instances.
An optional material_texture_recipe reuses the exact material-name recipe
boundary described in Material texture recipes,
including BaseColor, normal, metallic-roughness, and occlusion slots.
The normative current recipe schema is
character-assembly-recipe-v7.schema.json.
Recipe v1 through v6 remain immutable historical contracts. Migrate a v6
rest/bind recipe by changing its identity to v7 and replacing both source-index
fields with root_node_name. Omitting the block remains ordinary assembly
behavior; v3 continues to reject the block as unknown.
Evidence and determinism
The current evidence identity is
urn:animsmith:schema:character-assembly-evidence:7. It records the effective
recipe, recipe and input SHA-256 digests, the selected configuration file's
declared path and digest (or an explicit built-in-defaults marker), selected takes and windows, exact
source/base bone remap names and indices, and track
operation counts, start/end/delta facts for named translation tracks removed by
strip_bones, mesh selection, canonicalization flags, tool identity, and the
final artifact digest and counts. When pruning is enabled, each clip records
the exact removed tracks in pruned_constant_tracks: the index in the
completed, normalized output clip immediately before pruning (retaining its
pre-prune authored order), exact bone name and bone_index in the
post-canonicalization/pre-node-removal skeleton, TRS property, interpolation,
and key count. bone_remaps.base_index uses that same pre-removal coordinate
space. Consumers derive a surviving final index by subtracting earlier
transforms.removed_nodes[].original_node_index entries; a pruned track whose
node is later removed intentionally has no final-artifact node index.
The array is empty when pruning is disabled or no track is removed. The
top-level transforms.removed_nodes array records every projected node exactly
once in original pre-removal node order: its exact name, original node index,
nullable original parent index, and whether the recipe selected it directly
rather than through an ancestor. It is empty when remove_nodes is omitted or
empty. See
character-assembly-evidence-v7.schema.json.
When rest_bind_scale is active, v7 pins each exact base/clip
input digest and its versioned basis fingerprint, the declared root name,
each input's resolved root name/source-node index, the source-skin index for
full rest/bind inputs, and the
effective staged selectors after canonicalization/removal,
the factor, every compatibility result, and the shared final-artifact scale proof.
The staged-source digest and exact emitted-byte read-back digest make the two
serialization boundaries explicit; the read-back digest equals the published
artifact identity on every accepted run.
Each input records application = rest-bind with fingerprint contract
urn:animsmith:character-assembly-scale-basis:1, or
application = skinless-clip-tracks with
urn:animsmith:character-assembly-skinless-clip-scale-basis:1, so consumers
never infer the applied domain or version from the digest. The operation record also carries
the sibling residual_comparison_counts record pairing every shared
maximum residual to its comparison count by the same stable field name. These
records let a consumer verify that assembly validated and transformed every
participating input before remapping and that proof consumed the exact bytes
subsequently published.
V7 constructs an explicit animation-only projection for each distinct FBX
clip-only input and for each glTF/GLB clip admitted to the track-only path. The
projection retains complete source-skeleton node coverage
and every normalized take, while excluding source meshes, mesh instances,
materials, skins, and inverse-bind state that cannot contribute to assembly.
The captured input digest and source-projection evidence still describe the
unmodified source, including unsupported FBX deformation or glTF bind payload.
For glTF/GLB, the role-specific raw gate excludes only obligations wholly owned
by those projected domains; malformed animation accessors, matrix-target
animation, unsafe dependencies, and incomplete raw construct coverage still
refuse. The clip projection's
exact named root, every joint in the selected base skin, and the named
ancestors connecting that rig domain must match the accepted skinned base
basis in normalized topology. Rest translation, rotation, and scale remain
strict independently for every node/property that any take omits; a component
may differ only when every take explicitly authors that named channel, so
assembly never falls back to the differing clip rest. Each actual clip track
target and its named ancestors join that compatibility domain, and the target
must belong to the base plan's affected closure. Base-only geometry and
attachment descendants do not become clip requirements merely because the
base plan rewrites them. The
base plan supplies each named translation/scale target factor; cubic-spline
in/value/out triplets are rebased together. A skinless base still refuses, as
does any clip projection with incomplete source coverage, an incompatible
named basis, or a track outside the base scale domain. Bind matrices are
neither synthesized nor claimed for the track-only row. The retained
skinless-clip-tracks label names the projected operation domain, not a
requirement that the captured source was skinless. The exact final
assembled artifact remains covered by the shared skinned-base rewrite and
proof. Independently serialized animation-only FBX clip projections are
compared under that proof's Appendix-D numeric tolerance; full rest/bind inputs
retain exact pre-remap oracle comparison.
Each v7 scale-input row also identifies the captured container and its source
projection. raw-gltf records authored-curve and raw-span preservation.
normalized-baked-fbx explicitly records both as false, embeds the complete
FBX capability inventory, and binds the private normalized GLB stage by digest
and byte count. When declared node removal consumes an unskinned instance, that
private stage excludes the instance while the raw input digest and capability
inventory remain unchanged. This stage is not a second public artifact or evidence chain:
assembly still serializes, rewrites, reloads, proves, digest-checks, and
atomically publishes one final GLB/evidence pair.
Evidence v1 through v6 remain immutable historical contracts; v1 does not
describe pruning, v1 and v2 do not describe structural node removal, and none
of them describe rest/bind scale compatibility. Consumers migrating from v2 or
v3 must verify removed_nodes against the effective
recipe and output hierarchy rather than inferring removal from track evidence
or lint configuration.
Given identical recipe bytes, input and config bytes, tool build, paths, and platform, repeated runs emit byte-identical GLB and evidence. Evidence keeps operator-declared paths rather than leaking canonical host paths.