SDK2 → SDK3 migration
avatar migrate sdk3 takes an extracted SDK2 avatar project (as
avatar unitypackage extract produces) and turns it into an
SDK3 / Avatars 3.0 project the VRChat Creator Companion can open, the avatar prefab rewritten in place.
It exists because last-generation avatars — Cloth skirts, DynamicBones, gesture override
controllers, root motion — cannot be uploaded with today's SDK, and the manual conversion is a
long, error-prone Unity session. Everything except the final Unity/VCC open + SDK upload is done here.
Full text: docs/reference/migrate.md.
The command
avatar migrate sdk3 <extracted-project> -o <out-dir> --name MyAvatar \
--strip BhapticsVRC_Vest --drop-cloth --capsules-to-physbone-colliders \
--physbone "Hips|Spine,Left leg,Right leg|L cap,R cap|SkirtRoot" \
--eyes "Eye_L,Eye_R" --exclude Assets/Bhaptics \
--vpm-package com.poiyomi.toon-9.3.64.zip --relink-locked-shaders [--dry-run] [--json]What is migrated
Sdk2Project fixture pins the rewritten prefab and FX controller as goldens.
| SDK2 | SDK3 | How |
|---|---|---|
VRC_AvatarDescriptor | VRCAvatarDescriptor | Retyped at the same fileID so its slot in the root's m_Component list is untouched. View position, viseme setup, portrait offsets carried; playable layers on SDK defaults except FX (generated); expressions menu/parameters (generated); eye look derived (below). |
PipelineManager | PipelineManager (Standalone DLL) | Retyped in place; blueprintId cleared (an SDK3 upload is a new avatar). Added if missing. |
root Animator | same | m_ApplyRootMotion: 0, controller cleared — SDK3's playable layers drive it; root motion left on is the classic "avatar drifts / spins" bug. |
DynamicBone / DynamicBoneCollider | VRCPhysBone / VRCPhysBoneCollider | Retyped in place with the SDK's own conversion rules (next section). Because fileIDs are kept, converted PhysBones' colliders lists resolve without rewriting. |
Unity Cloth (--drop-cloth) | — | Removed; the mesh falls back to its skinning. Pair with --physbone to simulate the skirt's bones instead. |
Unity CapsuleCollider (--capsules-to-physbone-colliders) | VRCPhysBoneCollider | Retyped in place (class 136 → 114). |
— (--physbone ROOT|IGNORE…|COLLIDERS…|GROUP) | new VRCPhysBone | A new PhysBone over ROOT's bone-only children with skirt-ish defaults; with GROUP the chain roots are re-parented under a new empty child (identity local pose — nothing moves, skinning references bones by fileID) so colliders under ROOT don't trip VRChat's cyclic-dependency check. |
--strip NAME subtrees | — | The whole subtree (GameObjects, Transforms, components) removed and the parent's m_Children entry dropped — a haptics vest with cameras and its own Animator, say. |
CustomStandingAnims gesture slots | FX layer | See FX from gesture overrides, below. |
| Locomotion / emote / sitting overrides | — | Reported, not migrated — SDK3's Base/Action/Sitting layers are a different design, and an SDK2 idle/walk clip dropped into them is the source of the drift bugs the migration exists to remove. |
Anything else in the prefab (renderers, meshes, materials, non-VRChat scripts, transforms) is untouched
— byte for byte — because the rewrite is span-splicing over the raw text
(editing Unity YAML). The FBX and its .meta (humanoid
map, T-pose) are copied unchanged.
DynamicBone → PhysBone: the SDK's own rules
PhysBoneSpec::from_dynamic_bone reproduces
VRC.SDK3.Dynamics.PhysBone.PhysBoneMigration.Convert (SDK 3.10.4, disassembled) —
the conversion the SDK's own "Auto Fix" runs on import: pull from elasticity,
spring = 1 − damping, immobile from inert, radius and gravity scaled by
the transforms' world scale, and stiffness mapped through the SDK's StiffToMaxAngle table
to an Angle limit (a frozen axis becomes a Hinge). Colliders convert capsule-or-sphere by their
dimensions with the direction axis re-expressed as a rotation. Distribution curves are not carried
(rare on avatars); m_EndLength is reported.
11500000:
m_Script is {fileID: <MD4 class hash>, guid: <dll guid>, type: 3},
derived by avatar_unity_yaml::script_file_id and test-pinned against the SDK's own
serialized assets. Field layouts come from the same packages, so a body is what Unity would write itself.
FX from gesture overrides
SDK2's CustomStandingAnims override controller becomes a generated
FX.controller with one either-hand layer. For each gesture, only the
override clip's blendshape curves are lifted into a clean
Gesture_<Name>.anim (SDK2 clips also carried finger-muscle curves; in SDK3 hand
poses are the Gesture layer's job, and muscle curves in FX would fight it), plus a
Gesture_Neutral.anim zeroing every touched shape.
By default the layer is analog: each gesture gets a per-hand state whose motion is a
1D blend tree on that hand's GestureLeftWeight/GestureRightWeight, blending
Neutral → the gesture clip — SDK2's Vive "advanced controls", where trigger depth is
expression depth. Transitions are mutually exclusive and weight-gated (the squeezing hand owns the
face; a thumb resting at weight 0 can never mask the other hand), and both hands on the same gesture
route to a capped-sum 2D state with generated half-strength midpoint clips.
--no-analog-gestures emits discrete states instead. Empty
Parameters.asset/Menu.asset are generated alongside so toggles can be added
later (avatar toggle, anim-gen).
Eye look and blink
SDK3 eye look stores the local rotation of each eye bone per look state. It is derived geometrically
— local_state = (R_parent⁻¹ · R_delta · R_parent) · local_rest,
with the turn taken about the avatar-space axes — so it is right even when the eye
bones' own axes are wildly rolled (ripped/MMD rigs). Default angles 10° up/down, 12°
left/right (--eye-angles). The blink blendshape is found on the viseme mesh's FBX by name
(Blink, vrc.blink, まばたき, … or
--blink NAME) and its import-order index written into eyelidsBlendshapes.
Bundled packages and locked-shader relink
--vpm-package PATH (repeatable) bundles a VPM package — a directory with
package.json, or a .zip of one, e.g. a shader package's GitHub release —
into <out>/Packages/<name>/ and records it in vpm-manifest.json;
its legacyFolders and any source asset whose GUID the package already provides are
excluded from the copy, so the project opens without duplicate-GUID reassignments.
--relink-locked-shaders handles what shader lockers (Poiyomi/Thry's optimizer) leave
behind at export: each material points at a generated Hidden/… shader copy whose
#includes were never exported, remembering the real one in
stringTagMap.OriginalShader. The relink finds a matching shader among the source assets
and bundled packages, re-points m_Shader, disables the locker flag, and excludes the
generated copy. Materials whose original shader can't be found are reported and left alone.
Output layout
<out>/Assets/<copied source assets, minus --exclude and the SDK2 VRCSDK/, VRChat Examples/>
<out>/Assets/<Name>_SDK3/<Name>.prefab (+ .meta) # the migrated prefab
<out>/Assets/<Name>_SDK3/FX/FX.controller, Gesture_*.anim (+ .meta)
<out>/Assets/<Name>_SDK3/Parameters.asset, Menu.asset (+ .meta)
<out>/Packages/<bundled package>/… # each --vpm-package
<out>/Packages/vpm-manifest.json # com.vrchat.avatars/base + bundled
<out>/Packages/manifest.json # VCC-template Unity packages
<out>/ProjectSettings/ProjectVersion.txt
The output must not already contain Assets/ (refuse-before-write); --dry-run
plans and reports without touching the filesystem, --json makes the report
machine-readable. Generated GUIDs/fileIDs are deterministic (seeded from names). Then: open
<out> in the Creator Companion (Add Existing Project), drag the prefab into
a scene, check the descriptor, Build & Publish — and retune PhysBones offline with
avatar physbone.
Limits
- Locomotion/emote/sitting overrides are not migrated (by design); rebuild them on SDK3 layers if wanted.
- DynamicBone distribution curves and
m_EndLengthare not carried; endpoint set fromm_EndOffsetonly. - Prefabs only (one root Transform); nested prefab instances (
strippeddocuments) are not resolved. - The Unity import itself is not verified here (no editor in this toolchain);
avatar lint/avatar statson the output are the offline checks.