feat(pipeline): grow skirt and hair bone chains for a costume that has none
Miku shipped with 0 cloth chains against Taila's 35, so her twin tails hung off her skull like a helmet. Nothing downstream could fix it: the spring solver simulates cloth BONES, and a garment with none is welded to whatever body bone it was weighted to. Every auto-rigged model is in that state, and her source was an unrigged mesh. tools/cloth_bones.py builds them, which is the job a technical artist does by hand on a model like this. It finds the geometry by MATERIAL SLOT — the artist already answered which surface is hair, and on a joined mesh (what the auto-rig leaves behind) the slot is the only separation left. Hair is split into connected islands, because a strand is a connected piece of surface and clustering by position would merge two ponytails passing near each other. A skirt is split into radial wedges instead, because a skirt is ONE connected surface and islands would return the whole thing as a single piece — the bell-shaped failure. Each clump gets a polyline fitted down its middle by binning vertices by distance and taking centroids, so the chain follows the piece's own curve rather than cutting the corner on a bend, and vertices are re-weighted onto it while the first 22% keeps its original body weight so the scalp stays on the skull. On Miku: 19 chains, 57 bones from one `hair` slot. Sidecar 0 -> 19 chains. Idle stability 0.007-0.018 deg/frame. Mesh intact, verified by render. Opt-in, via `pipeline.py --grow-cloth`, and run before the retarget so describe_rig() finds the chains by name exactly as it would an artist's. Known limits, recorded in the skill: it cannot find a garment sharing a material with the body (Miku's skirt is on her `body` slot, so she got hair and no skirt), and grown chains are a fallback — an artist's chains carry intent that no geometric fit recovers. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4559a1adc3
commit
7892669319
@@ -14,6 +14,9 @@ Examples:
|
||||
# From an ALREADY-RIGGED model (Mixamo/AccuRig/Tripo output) — skips autorig:
|
||||
python tools/pipeline.py --input rigged/knight.fbx --name knight --rigged
|
||||
|
||||
# Rigged, but with no skirt/hair bones — grow them so the costume can move:
|
||||
python tools/pipeline.py --input rigged/knight.fbx --name knight --rigged --grow-cloth
|
||||
|
||||
The result is assets/characters/skins/<name>.glb with the full canonical
|
||||
animation set, plus a registry entry in assets/characters/skins/skins.json
|
||||
that SkinManager picks up automatically — no code changes needed.
|
||||
@@ -133,6 +136,10 @@ def main() -> None:
|
||||
p.add_argument("--height", type=float, default=1.75, help="target character height in meters")
|
||||
p.add_argument("--keep-root-motion", action="store_true", help="don't strip hips motion from clips")
|
||||
p.add_argument("--anim-dir", default=ANIM_DIR, help="animation library directory")
|
||||
p.add_argument("--grow-cloth", action="store_true",
|
||||
help="grow skirt/hair bone chains for a model that has none "
|
||||
"(see tools/cloth_bones.py) — needed for anything that "
|
||||
"went through the auto-rig, whose costume is welded solid")
|
||||
args = p.parse_args()
|
||||
|
||||
name = args.name
|
||||
@@ -178,6 +185,22 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
final_path = os.path.join(SKINS_DIR, f"{name}.glb")
|
||||
|
||||
# Grow cloth chains BEFORE the retarget, so describe_rig() finds them by name
|
||||
# exactly as it would an artist's and writes them to the sidecar.
|
||||
#
|
||||
# A model with no cosmetic bones has a costume that cannot move: the spring
|
||||
# solver simulates cloth BONES, so a skirt with none is welded to the hips
|
||||
# whatever the runtime does. That is every auto-rigged model, and it is why
|
||||
# Miku shipped with 0 cloth chains against Taila's 35.
|
||||
if args.grow_cloth:
|
||||
grown = os.path.join(STAGING, f"{name}_cloth.glb")
|
||||
run([blender, "--background", "--python", os.path.join(TOOLS, "cloth_bones.py"),
|
||||
"--", input_path, grown],
|
||||
"Grow skirt/hair bone chains (Blender)")
|
||||
input_path = grown
|
||||
rigged = True
|
||||
|
||||
if rigged:
|
||||
print("Model is rigged — keeping its skeleton, weights and cloth chains")
|
||||
cmd = [blender, "--background", "--python", os.path.join(TOOLS, "retarget.py"),
|
||||
|
||||
Reference in New Issue
Block a user