What it generates

  • Animation clips (AnimationClip, class 74) — blendshape-weight and GameObject-active curves; a curve binds to (path, attribute, classID), and a toggle is just an m_IsActive float curve.
  • Analog-gesture blend trees (BlendTree, class 206) — a 1D tree blending on GestureLeft/RightWeight, emitted either as a bare 206 document with a wiring note or as a self-contained state-machine/state/tree fragment (to_state_fragment, Write Defaults off).
  • Full FX controllers (AnimatorController, class 91) — fx_blend_tree assembles the class-91 document, auto-declares the blend parameter as a Float, and wires one layer to the fragment's state machine; field set/order matched against a real Unity-authored controller.
  • Gesture-driven FX layers (GestureLayer) — the idiomatic SDK3 face-expression layer: Neutral + one state per gesture, Any-State transitions on GestureLeft/Right Equals n. Either-hand layers get mutually-exclusive conditions (later parameter wins, weight-gated with hysteresis in analog mode); a both-hands-same-gesture state plays a 2D freeform tree encoding the capped sum of the two weights. Used by migrate to rebuild SDK2 gesture overrides.
  • Expression Parameters & Expressions Menu (.asset, class 114) — single-document MonoBehaviour ScriptableObjects at Unity's &11400000 convention, with the SDK's real DLL class-hash m_Script references (-1506855854 / -340790334 into VRCSDK3A.dll). synced_bits() reports the 256-bit-budget cost up front.

The toggle bundle (avatar toggle)

A working in-game toggle needs five cooperating assets; avatar toggle generates all of them as one internally-consistent ten-file bundle: <N>_On/Off.anim (targets held on / written back to 0 — authoritative both ways under Write Defaults off), a two-state <N>_FX.controller (OffOn, instant transitions on a Bool), the params + menu assets, and .meta sidecars with deterministic GUIDs so the controller's clip references resolve on first import. Targets are GameObject-active paths and/or blendshape weights; the bundle's wiring note walks through descriptor hookup. The overwrite check runs across the whole bundle first, so a partial bundle is never left behind.

Radial-puppet grafting (avatar anim-gen puppet)

Where everything above generates fresh assets, puppet grafts an analog dial into an existing avatar by span-splice (via EditableUnityFile — fileIDs and formatting preserved, see unity-yaml edit): the float parameter plus a gated blend-tree layer into the FX controller (a default Off state that plays nothing, an On state entered/left with hysteresis), the float into the VRCExpressionParameters asset, and a RadialPuppet control into the menu — dial depth = expression depth. avatar lint cross-checks the three assets after the splice.

CLI surface

avatar anim-gen clip --name Smile --blendshape Body:Smile:100 -o Smile.anim avatar anim-gen controller --name FX --clip <guid>@0.0 --clip <guid>@1.0 -o FX.controller avatar anim-gen params --param Hat:bool -o Params.asset avatar anim-gen menu --toggle Hat:Hat -o Menu.asset avatar toggle --name Hat --toggle Armature/Head/Hat -o HatBundle/
CommandEmits
avatar anim-gen clip --name N [--blendshape PATH:SHAPE:VALUE]… [--toggle PATH]…a .anim AnimationClip
avatar anim-gen blendtree --name N [--parameter P] [--clip GUID@THRESHOLD]… [--tree-only]the blend-tree fragment (or the bare 206 doc)
avatar anim-gen controller --name N [--layer L] [--clip GUID@THRESHOLD]…a complete FX .controller
avatar anim-gen params --param NAME:TYPE[:DEFAULT][:unsaved][:local]…a VRCExpressionParameters asset
avatar anim-gen menu [--toggle L:P[:V]]… [--button…] [--radial…] [--submenu…]a VRCExpressionsMenu asset (≤ 8 controls enforced)
avatar anim-gen puppet --controller … --parameters … --menu … --param P --clip G@T…the radial-dial graft above, spliced in place
avatar toggle --name N [--toggle PATH]… [--blendshape…] -o DIRthe ten-file toggle bundle

Shared flags: -o writes to a file (stdout otherwise), --dry-run previews, --force is required to overwrite, and --json switches stdout to a machine-readable report (allocated fileIDs, wiring note, the YAML embedded under yaml) so an agent can wire the asset without parsing YAML.

The emitter & the fileID strategy

A faithful hand-written YAML emitter (yaml_emit) handles Unity's exact field names, block indentation, flow maps, and Unity-style float printing. IdGen hands out deterministic FNV-1a-seeded fileIDs — same input, byte-identical output — so generated assets are diffable and reproducible; the name-derived base keeps independently-generated assets collision-free if later combined. Canonical fixed ids are reproduced where load-bearing: a .anim's clip is fileID 7400000, a loose .cs script 11500000 (DLL classes use Unity's per-class hash instead).

Field-name gotcha verified against Unity serialization: m_BlendParameter, m_BlendParameterY, and a child's m_DirectBlendParameter are string fields (parameter names), not numeric.

What's proven

Every generated asset round-trips through the repo's own readers: clips and fragments through avatar-unity-yaml, controllers through avatar-unity-asset's typed AnimatorController reader (cross-references resolve, Write Defaults off, layer → state machine ids match), expression assets through avatar-vrc-descriptor's structural classifier — the same one avatar lint trusts. Determinism is pinned by same-seed-byte-identical tests. A headless Unity-acceptance gate (opt-in CI, self-skips without a UNITY_LICENSE) imports CLI-generated .anim/.controller assets in a real editor and asserts they parse with no import errors; only the in-game behaviour check remains manual.