Asset generation (M4)
avatar-anim-gen emits Unity-YAML assets Unity will accept: .anim clips,
analog-gesture blend trees, complete FX AnimatorControllers, and VRC expression
parameters/menu assets — plus two composites, the avatar toggle bundle and
avatar anim-gen puppet grafting. The hard part is correct fileIDs and field names —
solved with a faithful hand-written emitter and deterministic, diffable IDs. Full text:
docs/reference/anim-gen.md.
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 anm_IsActivefloat curve. - Analog-gesture blend trees (
BlendTree, class 206) — a 1D tree blending onGestureLeft/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_treeassembles the class-91 document, auto-declares the blend parameter as aFloat, 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 onGestureLeft/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&11400000convention, with the SDK's real DLL class-hashm_Scriptreferences (-1506855854/-340790334intoVRCSDK3A.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 (Off ⇆ On, 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/| Command | Emits |
|---|---|
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 DIR | the 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).
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.