Fixes the two root causes behind four of the seven reported breakages, and
rejects the source that cannot be fixed.
STAND UP FIRST. flatten_and_scale() now derives the up axis from the
skeleton and rotates the model upright before measuring anything. Aria and
Momo are correct: 1.75 m tall, 1.43 x 0.41 and 1.52 x 0.39 across, verified
by render.
The up vector is measured from the FEET to the HIPS, not from the hips to
the head. The head is not a reliable landmark — the spine walk ends on the
last non-cosmetic bone in the chain, which on a rig with a facial skeleton
can sit BELOW the hips. Momo's did, so the first cut of this fix stood her
neatly on her head: right size, right proportions, upside down. Feet cannot
be mistaken.
REACH BONES BY ROLE. SkinnedPlayerModel gained _role_bone(), and set_weapon
uses it. Four characters could not hold a gun because one hardcoded lookup
knew three spellings and their hands are called "Right wrist" and
"J_Bip_R_Hand" — both resolved perfectly in the sidecar the whole time.
HIKARI IS REJECTED. She now fails the gate: her feet and spine disagree
about which way is up, so the stand-up correction cannot resolve her
either, on top of zero-length cosmetic bones and a second armature that was
smuggling its own clips into the export. That is not a tuning problem, it
is a file that has been through two toolchains. De-registered and removed
rather than shipped broken — which is what the gate is for.
Six GLB skins remain, all passing. Smoke 0 failures, 11/11 movement tests.
Still open, recorded in the skill: kiyoko faces backwards, miku's grown
hair stretches under animation, the mannequin's rifle hold does not
convince, and taila's front skirt clipping.
Co-Authored-By: Claude Opus 5 <[email protected]>
Seven characters shipped "All checks passed" and four were visibly broken
in game — lying on their backs at seven times scale, facing backwards, or
holding a gun that floated near their chest. Nothing in the suite was
wrong; it just never asked the questions that mattered. That distinction
is the whole lesson, and it is now written down in the skill as
references/failure-modes.md, generalised per failure.
Two root causes are measured and certain:
- flatten_and_scale() normalises the bounding box along Blender Z because
Z is up. For a model that arrives lying along Y that measures the
character's THICKNESS, so it scales by ~7 and leaves them on their back.
One assumption, both symptoms. The trap is that the normalised number
always comes out right — the exporter maps Blender Z to glTF Y, so "is
the height 1.75" passes on a character who is 7.5 m tall lying down.
All three casualties are VRM files that went through a Blender
round-trip and came back with a baked axis rotation.
- SkinnedPlayerModel.set_weapon() finds the hand with three hardcoded
spellings, which match none of the four non-Rigify rigs — their hands
resolve perfectly in the sidecar as "Right wrist" and "J_Bip_R_Hand".
When it misses, the weapon is parented to the model root at a fixed
chest offset, so it is not attached to the character at all. Same class
of bug as the leg check that name-matched thigh/shin. rig_map.py exists
so nothing downstream has to guess a bone name; only some consumers read
the roles it publishes.
Three new hard checks, none needing more than the vertices and the
sidecar:
character stands up in world space — against WORLD up, not against the
model's own proportions. "Is the spine the longest axis?" catches
nothing: a model rotated as a whole is internally consistent and
passes it comfortably.
character is a plausible size / height
every role the runtime needs is resolved
They separate the four good characters from the three broken ones on the
first run. Also fixed the posture measurement to read vertices rather than
object.bound_box, which is cached and still stale right after an import —
it reported a 1.75 m character as 1.18 m tall.
Recorded but not yet fixed: kiyoko faces backwards (facing is inferred by
two independent mechanisms and verified by neither), miku's grown hair
chains stretch under animation (generated chains are never validated
against the geometry they drive), and the mannequin's rifle hold does not
convince despite resolving correctly.
Co-Authored-By: Claude Opus 5 <[email protected]>
Names lie and anatomy does not — the four VRoid imports each broke role
resolution differently, and the lesson generalises: anything guessing
anatomy from a bone name needs a structural fallback. Also lists what is
shipping and which characters have chains that do not simulate.
Co-Authored-By: Claude Opus 5 <[email protected]>
A third playable character, built with the pipeline skill from a source
that was already in the repo: the animation library ships a rigged
Mannequin mesh on the exact 53-joint reference skeleton, CC0, so it needed
no download and retargets perfectly. 18 clips, 0.3% cross-leg bleed, 7% of
verts at four influences — a clean authored-weight import. Licence
recorded in mannequin.license.json as the other skins do.
It has no cloth chains, correctly: it is a mannequin and has neither hair
nor clothes.
Importing it turned up two real bugs, both of which would have hit any
flat-coloured or single-piece model:
- LevelMaterials.apply_character_look treated ANY untextured surface on a
character as the model's own outline shell and hid it, so the mannequin
rendered as a solid black silhouette — its body and joint materials are
untextured flat colours, not ink. _is_line_work() now asks whether the
surface is named eyes*, is drawn front-face-culled (the inverted-hull
setup), or is near-black. Taila and Miku are unaffected: their materials
are textured and never reach that branch. Verified by render.
- verify_character.py failed the build for having one mesh. That check
cannot tell "the pipeline joined them" from "the artist authored one
mesh" — Quaternius' mannequin is one piece on purpose. It is advisory
now; the join path's two unambiguous signatures, cross-leg bleed and the
4-influences-everywhere spread, are still hard checks.
Also restored Miku's description, which the re-import had blanked.
3 GLB skins selectable (6 with the built-in colour skins). Smoke 0
failures, 11/11 movement tests, cloth idle 0.024-0.078 deg/frame.
Co-Authored-By: Claude Opus 5 <[email protected]>
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]>
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]>
Everything about getting an anime-styled character into the game — import,
stylization, the body/garment/hair separation, rigging, retargeting, cloth
and hair physics, and how to measure any of it — collected into
.claude/skills/character-pipeline/.
Organised around the principle the Hoyoverse-class pipelines are built on
and that every failure in this project traced back to: a character is not
one object. It is a body, a set of garments and hair, authored and rigged
separately and moved by different systems. The body is skinned and
animated; the garments and hair are bone chains the animation never
touches and physics moves. The skill's non-negotiables are the four ways
that separation has been destroyed here before — joining meshes, keying
cosmetic bones, skinning cloth to a leg, and running the auto-rig repair
on authored weights.
references/verification.md leads with the trap that invalidated every
cloth measurement ever taken in this repo: Godot restores bone poses after
the modifier pass, so a tool that reads them afterwards measures the
animation and never sees what any modifier did.
Also records what is known-unsolved, with numbers: peak cloth clipping in
a run/slide/dash, no foot IK, no strafe clips.
Co-Authored-By: Claude Opus 5 <[email protected]>