Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 at fps, or time_window = [START, END] in seconds;
  • drop_closing_endpoint = true: remove one final key from every channel;
  • hold_frames = N: duplicate the final pose after N / fps seconds;
  • gait_anchor = true: explicitly declare an in-place cyclic gait and run the measured gait-anchor transform using the selected animsmith.toml rig 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 declared fps whole-frame sample over the clip duration, at the exact representable f32 key / fps time 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 +X basis 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.