docs(skill): record why Miku has no cloth — the source had no skeleton

The two shipped characters are a controlled comparison: Taila's source
arrived rigged (35 cloth chains, 8 twist bones, 18 meshes, artist
weights), Miku's did not (5 meshes, 0 joints), so Miku was auto-rigged
into one mesh with nearest-bone weights and no cloth chains at all. Her
twin tails and skirt are dead geometry and the destructive load-time
weight repair runs on her every spawn.

None of that is recoverable downstream, which makes picking a source that
already has skirt and hair bones the highest-leverage decision in the
pipeline. Added the no-Blender check for vetting a candidate.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-26 12:47:42 -04:00
co-authored by Claude Opus 5
parent daf9627ece
commit 4559a1adc3
@@ -97,6 +97,42 @@ guesses.
tracks onto them (`keep_anim_armature`), the AnimationPlayer overwrites the
spring solver every frame.
## Worked example: why the two shipped characters differ so much
Both are in `assets/characters/skins/`. Compare their sidecars:
| | Taila | Miku |
|---|---|---|
| source had a skeleton | yes | **no — 5 meshes, 0 joints** |
| `weights_authored` | true | **false** |
| cloth chains | 35 (127 bones) | **0** |
| twist bones | 8 | **0** |
| meshes shipped | 18 | **1** |
Miku's source (`assets/characters/incoming/miku_test.glb`) is an unrigged mesh,
so she went through `autorig.py`: joined to one mesh, rebound by nearest-bone
weighting, no cloth chains. Her twin tails and skirt are dead geometry that
cannot move, and `SkinLegRepair` runs destructively on her every spawn.
Nothing downstream can recover this. **The single highest-leverage decision in
this whole pipeline is choosing a source model that already has a skeleton with
skirt and hair bones.** Everything else is recoverable; this is not.
A quick check on any candidate, without Blender:
```python
import json, struct
with open(path,'rb') as f:
f.read(12); clen,_=struct.unpack('<II',f.read(8))
j=json.loads(f.read(clen))
nodes=[n.get('name','') for n in j['nodes']]
joints=[nodes[i] for s in j.get('skins',[]) for i in s['joints']]
print(len(j['meshes']), 'meshes', len(joints), 'joints')
print([n for n in joints if any(t in n.lower() for t in ('hair','skirt','tail','ribbon'))])
```
Several meshes, 50+ joints, and a non-empty cosmetic list means a good source.
## Checking a source model before importing
```bash