The first thing anyone does after wearing a migrated (or freshly authored) avatar is retune its
PhysBones: the hair is too floppy, the skirt flaps, the tips of a long chain go wild. That is a
read-modify-write of one VRCPhysBone component — a job for the surgical editor, not a
regeneration. avatar physbone does it on the prefab file, so a change is a one-line command
that lands on the exact component and leaves every other byte of the prefab (fileIDs, references,
the blueprint id, formatting) alone. It lives in avatar-migrate (avatar_migrate::physbone)
because it is the post-migration step and reuses the migration's prefab graph + rewriter; it works
on any SDK3 prefab, migrated or not.
avatar physbone list Avatar.prefab [--json]
avatar physbone set Avatar.prefab <TARGET> [tuning…] [--ignore N]… [--collider N]… -o Avatar.prefab --force
avatar physbone split Avatar.prefab <TARGET> --chain Hair_1 --chain Hair_2 [tuning…] -o … --force
avatar physbone stretch Avatar.prefab <TARGET> --factor 1.5 | --by 0.077 [--from-depth 2] -o … --force
avatar physbone flare Avatar.prefab <TARGET> --angle 10 | --scale 0.5 [--hinge-depth 1] -o … --force
avatar physbone nudge Avatar.prefab <TARGET> --out 0.008 [--up M] [--chain N]… [--hinge-depth 1] -o … --forceTARGET is the PhysBone's root transform (a unique bone name or an A/B/C path from the
avatar root), the GameObject carrying it, or its &fileID (the list output prints all three;
an ambiguous name errors with the candidates). Edits follow the shared write policy: -o FILE
writes (the input path with --force edits in place), no -o prints the edited prefab to stdout,
--dry-run reports without writing, --json prints a machine-readable before/after report
(the schema is avatar schema physbone — one PhysBoneInfo). The file is written before the
report is printed, so a closed pipe can never lose the edit.
For each VRCPhysBone (recognised by its DLL class reference, {fileID: 1661641543, guid: 2a2c05204084d904aa4945ccff20d8e5}): the object it sits on, the root it simulates from
(rootTransform, else its own), the chains it drives — for each leaf reachable from the root
without crossing an ignoreTransforms entry, the bones from the first simulated bone to the
leaf and their length in avatar space (root scale included) — the total transforms simulated
(the number VRChat's perf rank counts), colliders (by the objects that carry them), and the
tuning: version, integration, multi-child, pull/spring/stiffness, gravity + falloff, immobile
(+ type), radius, limit + max angles, grab/pose/collision flags, stretch/squish, isAnimated,
parameter — each with its curve if one is set. Each chain also reports its flare: the
angle between its first-bone→leaf direction and straight down (0° = hangs vertically).
Which bone is the first bone? PhysBone simulates from the root transform. When the root has
several live children and multiChildType is Ignore (the default), the root itself stays put
and each child starts a chain; with a single child the root is the first bone. list shows
exactly that (a 14-bone pigtail rooted on Hair_1 lists 14 bones; a hair root with two chains
lists 2×N and doesn't count itself) — and it is why splitting all-but-one chain off a root turns
the root into a simulated bone.
Every tuning value is optional; unset = keep. Values: --pull --spring --stiffness --gravity --gravity-falloff --immobile --immobile-type all|world --radius --limit-type none|angle|hinge|polar --max-angle --max-angle-z --integration simplified|advanced --version 1.0|1.1 --multi-child ignore|first|average --allow-collision 0|1 --allow-grabbing 0|1 --allow-posing 0|1 --max-stretch --max-squish --is-animated 0|1. Lists: --ignore NAME / --unignore NAME (a bare child name of
the root, or a name/path), --collider OBJ / --uncollider OBJ (GameObjects carrying a
VRCPhysBoneCollider).
Curves. --pull-curve, --spring-curve, --stiffness-curve, --gravity-curve,
--immobile-curve, --radius-curve, --max-angle-curve take T:V,T:V,… keys — T is the
position along the chain (0 = first bone, 1 = tip), V a 0..1 multiplier of the base value,
which is how the SDK applies them (pull 0.3 with 0:0.5,1:1 pulls 0.15 at the root and 0.3 at
the tip). Keys are written with free tangents equal to the secants to their neighbours, so the
Hermite curve Unity evaluates is exactly piecewise-linear between your keys — no editor
smoothing surprises; an empty string clears a curve. "More weight at the ends" is a pull curve
rising toward the tip plus a spring curve falling toward it.
The component body is re-rendered from a typed PhysBoneSpec read back from the prefab
(PhysBoneSpec::from_yaml → tune → to_body, the same emitter the migration uses; round trip
is byte-stable and test-pinned), so a set rewrites exactly one document and touches nothing
else.
Long pigtails and short bangs usually share one component rooted on Head/Hair, and one set of
numbers can't suit both. split moves each named chain (a child of the root, by name or path)
onto its own VRCPhysBone on that chain's first bone (root = itself), tuned like the parent
plus whatever tuning flags you pass, with the parent's colliders and any of its ignores that lie
inside the chain — and adds the chain to the parent's ignoreTransforms. New fileIDs are derived
from the bone path (stable across runs). Component count goes up by one per chain (PhysBone
components are a perf metric: PC 4/8/16/32); simulated transforms don't change.
--factor F multiplies the local position (the offset from the parent bone) of every chain
transform at depth ≥ --from-depth below the PhysBone root (1 = the root's children, 2 =
grandchildren, …; the root itself never moves). --by METERS instead adds the same length
(avatar space) to every chain — each chain gets its own factor 1 + by / length — which is what
you want when chains have unequal bone counts: a uniform factor grows a 5-bone chain more than
its 4-bone neighbours and the hem grows peaks there (the mikunpc skirt: three front chains have
five bones), equal added length keeps an even hem even. Negative shortens; going past zero is
refused. --chain NAME=METERS (repeatable; NAME = any transform of the chain, e.g. its
hinge) overrides the added length for one chain — the knob for levelling a hem whose sectors
respond unequally to the same added length (skin weights decide which bones a hem vertex
follows, and a sector's height is set by its chain: measure with avatar render --pose, or
in-game, then shorten/lengthen that chain alone). The skinned mesh follows its bones, so what hangs
off the chain gets longer: each ring of vertices bound to a deeper bone moves further down the
chain and the faces between rings stretch. Only translations change — no rotation, no
non-uniform scale, so no shear, and PhysBone sees ordinary (longer) bones with the same
radius. A non-zero endpointPosition is scaled too. Default depth 2 keeps the root's children —
the hinges of a many-chain skirt — in place; use --from-depth 1 on a component rooted on a
chain's own first bone (a split-off pigtail).
Caveats: it's a stretch, so a texture stretches with it (×F along the chain; up to ~1.5–1.7 reads
fine on a plain skirt, beyond that a hem's frill ring visibly distorts); a mesh's m_AABB /
bounds are not enlarged (Unity's skinned bounds are usually generous — turn on Update When
Offscreen if the hem gets culled at frame edges); and colliders placed for the old length may
now sit above the hem.
Preview before Unity: avatar render --avatar model.fbx --pose Avatar.prefab -o out.png
draws the FBX with every bone's local transform taken from the prefab (matched by name, Unity's
mirrored import undone), so what you see is what Unity will show for that prefab — a stretched,
re-angled skirt, anything. --stretch 'Skirt_0_*:1.5' is the lighter one-knob form for a
stretch alone (render.md).
A skirt whose chains tilt outward gets a funnel silhouette, and stretching it along that tilt
makes the funnel wider. flare re-angles each chain in avatar space: the transform at
--hinge-depth below the root (1 = the root's children, a skirt's hinge ring; 0 = the root itself,
for a component rooted on the chain's first bone — careful, that swings every child of the root,
ignored ones too) is rotated about the axis perpendicular to its chain direction and −Y so the
hinge→leaf direction makes the target angle with straight down: --angle 10 for every chain, or
--scale 0.5 to halve each chain's current angle. Only that one local rotation changes per chain
(local' = R_parent⁻¹·Δ·R_parent·local, the eye-look construction), so the chain swings rigidly
like a panel about its hinge and the skinned mesh follows. It is a rest-pose edit — PhysBone
takes the new pose as rest, and colliders still push the chain out where the new rest sits inside
them (thighs), which is exactly the "drape over the legs" you want. list shows the resulting
angles; note it measures from the chain's first simulated bone, which is the hinge for a
many-chain root but the root itself for a single-chain component.
--out METERS moves each chain's hinge radially away from the root's vertical axis (negative =
inward), --up METERS along +Y; --chain NAME restricts it to those chains. The panel hanging
off the hinge shifts rigidly — drape, angle and hem shape untouched — which is the fix when
flare has swung a skirt's top ring inside a waistband (mikunpc: 8 mm out on every hinge).
The pigtails go on their own components with weight and damped tips, the bangs/antenna/sideburns
that stay on Hair get calm settings, the skirt gets firmer and 50 % longer:
P=Assets/MikuNPC_SDK3/MikuNPC.prefab
avatar physbone split $P Hair --chain Hair_1 --chain Hair_2 \
--pull 0.3 --pull-curve "0:0.7,1:1" --spring 0.3 --spring-curve "0:1,1:0.5" --stiffness 0.2 \
--gravity 0.15 --gravity-falloff 0 --immobile 0.7 --immobile-type world \
--limit-type angle --max-angle 60 -o $P --force
avatar physbone set $P Hair --pull 0.35 --spring 0.3 --stiffness 0.3 --immobile 0.7 \
--immobile-type world --limit-type angle --max-angle 30 -o $P --force
avatar physbone set $P SkirtRoot --pull 0.4 --spring 0.2 --stiffness 0.35 --gravity 0.05 \
--gravity-falloff 0.5 --immobile 0.6 --immobile-type world --limit-type angle --max-angle 35 \
-o $P --force
avatar physbone stretch $P SkirtRoot --factor 1.5 -o $P --force
avatar physbone list $P
# round two, after wearing it: longer still, and the funnel pulled in to hug the legs
avatar physbone stretch $P SkirtRoot --factor 1.3 -o $P --force # x1.95 in total
avatar physbone flare $P SkirtRoot --angle 10 -o $P --force # chains were 13–33° out
avatar render --avatar final.fbx --pose $P -o preview.png # what Unity will show
# round three: 10° was a touch too vertical, and the uniform factor had grown the three
# 5-bone front chains more than the rest (two peaks in the hem) — undo, add equal length, re-aim
avatar physbone stretch $P SkirtRoot --factor 0.512820513 -o $P --force # back to the original offsets
avatar physbone stretch $P SkirtRoot --by 0.077 -o $P --force # +7.7 cm on every chain
avatar physbone flare $P SkirtRoot --angle 16 -o $P --force
# round four: two hem points still low in-game — they sit on the two front-diagonal chains
avatar physbone stretch $P SkirtRoot --by -0.077 -o $P --force # undo (exact per chain)
avatar physbone stretch $P SkirtRoot --by 0.077 --chain Skirt_0_1=0.055 --chain Skirt_0_9=0.055 -o $P --force
# round five: the top ring clipped through the underwear band at the sides — 8 mm out, all hinges
avatar physbone nudge $P SkirtRoot --out 0.008 -o $P --force
# round six: fine in the editor, a cone again in-game — the *runtime* pushes chains off the thigh
# colliders: 3 cm bone radius + 0.08 leg capsules (x largest axis) held chain centres ~10 cm off
# the thigh axis. Thin fabric radius, smaller capsules, real weight:
avatar physbone set $P SkirtRoot --radius 0.012 --gravity 0.15 --gravity-falloff 0.3 --immobile 0.7 -o $P --force
avatar asset set $P --doc <L cap collider fileID> --path radius --value 0.06 -o $P --force # and R capLesson from that last round: the rest pose you see in the editor is not what PhysBone shows
in-game — collision radius (bone and collider, the collider scaled by the transform's largest
axis) sets how far a chain can hang from a limb, and gravity/immobile set how it moves. list
prints both radii; check them against the limb before blaming the pose.
avatar lint stays clean and avatar stats reports the change (5 PhysBone components: Good;
transforms/collision checks unchanged: Medium).
crates/migrate/src/physbone.rs unit-tests list/find/set/split/stretch on a synthetic prefab;
crates/migrate/tests/golden.rs runs the migration on fixtures/projects/Sdk2Project, then
lists (Sdk2Project.physbones.json), splits the fixture's Bang chain off its hair, retunes the
pigtail with curves, re-angles it, stretches the skirt, and pins the result (Sdk2Project.physbones.tuned.json)
while asserting untouched documents are byte-identical. sdk3.rs pins the
PhysBoneSpec YAML round trip and the linear-tangent curve text.