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]>
8.9 KiB
name, description
| name | description |
|---|---|
| character-pipeline | Import, rig, stylize and animate an anime-styled character into Papaya-Shooter as a selectable skin — keeping the model's own skeleton, artist weights and separate body/cloth/hair meshes, with cloth and hair driven by the spring solver. Use when adding a new playable character, re-importing an existing one, debugging skinning/cloth/hair/animation problems on a character, or changing the cel-shaded look. Triggers on "add a character", "import a skin", "new playable model", "skirt clipping", "hair flailing", "T-posing", "character looks squashed". |
Character pipeline
Turns a source model into a playable, cel-shaded, cloth-simulated character skin.
The whole design follows one principle, which is also what the Hoyoverse-class anime pipelines (Genshin / Star Rail / Zenless Zone Zero) are built on:
The character is not one object. It is a body, a set of garments, and hair — authored separately, rigged separately, and moved by different systems. The body is skinned and animated. The garments and hair are bone chains that the animation never touches; physics moves them. Keeping those separate is what makes the result read as an anime character instead of a mannequin in a painted-on costume.
Everything below exists to protect that separation.
The one rule
If a model arrives with a skeleton, that skeleton ships. Its bones, its artist-painted weights, its per-part meshes and its skirt/hair chains all survive. Only the ANIMATION is moved onto it.
The old route (strip_rig.py → autorig.py → merge_animations.py) solved a
bone-naming problem by destroying the asset — 18 meshes became 1, 21 skirt
bones and ~50 hair bones became 0, and 16% of vertices ended up pulled by both
legs. tools/rig_map.py solves naming properly now. Never reach for
strip_rig.py or --rebind unless the model genuinely has no skeleton at all.
Doing it
# From an already-rigged local model (the normal case)
python tools/pipeline.py --input assets/characters/incoming/<name>.glb --name <name> --rigged
# From a Sketchfab UID (needs SKETCHFAB_API_TOKEN)
python tools/pipeline.py --uid <uid> --name <name>
# From an unrigged mesh — auto-rigs, and accepts the quality loss
python tools/pipeline.py --input <mesh.glb> --name <name>
# Rigged (or auto-rigged) but with NO skirt/hair bones — grow them, or the
# costume is welded solid and the spring solver has nothing to simulate
python tools/pipeline.py --input <model.glb> --name <name> --rigged --grow-cloth
Then, once, so Godot sees the new files:
godot --headless --path . --import
The result is assets/characters/skins/<name>.glb + <name>.rig.json, and a
registry entry in skins.json that SkinManager picks up with no code change.
Blender is required (BLENDER_PATH, or auto-found under
C:\Program Files\Blender Foundation). Godot lives at
C:\Program Files\Godot\Godot_v4.7-stable_win64_console.exe.
The stages, and what each one protects
| Stage | Where | Protects |
|---|---|---|
| Fix unlit/emissive materials | tools/gltf_fix.py |
Textures surviving import at all |
| Resolve bone ROLES, not names | tools/rig_map.py |
The model's own skeleton |
| Rebuild parenting | retarget.py::rebuild_hierarchy |
Limbs/cloth following the hips |
| Grow cloth chains (opt-in) | tools/cloth_bones.py |
A costume that has no bones being able to move at all |
| Subdivide cloth panels | retarget.py::subdivide_cloth_panels |
A skirt being able to bend at all |
| Retarget clips as rest-relative deltas | retarget.py::retarget_clip |
Limbs not being twisted by foreign bone roll |
| Leave cosmetic bones unkeyed | export_optimize_animation_keep_anim_armature=False |
Physics owning the cloth |
| Write the rig sidecar | retarget.py::describe_rig |
The runtime never re-guessing anatomy |
| Cel look | LevelMaterials.apply_character_look |
The model's own line-work not being re-lit |
| Cloth + hair | characters/spring_bones.gd |
Clothes reading as clothes |
Read before you touch anything
Load the reference that matches what you are doing. They are short and each one is a list of things that cost a debugging cycle to learn.
references/failure-modes.md— read this first. Seven characters shipped "All checks passed" and four were visibly broken. What each failure was, why the suite missed it, and the rule that generalises it to any model.references/separation.md— body vs. garments vs. hair: what must stay separate, how cloth chains are detected and classed, why cloth is never skinned to a leg, and the ZZZ-convention mapping.references/growing-cloth-bones.md— what to do when the source has no skirt or hair bones: how the chains are fitted to the geometry and re-weighted.references/rigging.md— role resolution, hierarchy rebuild, cloth panel subdivision, twist bones, joint helpers, the retarget maths.references/cloth-and-hair.md— the position-based spring solver, its colliders, per-class tuning, collision hulls, LOD and cost.references/stylization.md— cel shading, outlines, the imported line-work trap, eyes, materials.references/verification.md— every measuring tool, what each one actually measures, and the pose-reading trap that invalidated all of them once. Read this before trusting any measurement.
Non-negotiables
- Never join meshes. Per-part meshes are how body, cloth and hair stay separable — for materials, for the outline pass, and for the cloth solver's hull extraction.
- Never key cosmetic bones. If a clip has tracks on skirt/hair bones, the AnimationPlayer overwrites the solver every frame and the cloth goes rigid.
- Never skin cloth to a leg. A vertex weighted 0.9 to a thigh cannot be
moved by its own cloth bone, so the solver loses the authority to push it out
of that thigh — and the leg still overtakes it. There was a
bind_cloth_to_legs(); it is deleted, and the note above its grave inretarget.pysays why. - Never run
SkinLegRepairon authored weights. It snaps weights and deletes triangles. It exists only to undo auto-rigging. It is gated onweights_authored, which is MEASURED, not assumed. - Measure from inside the modifier pass. See
references/verification.md. - Never assume an axis. Up, forward and scale are all measurable from the
skeleton. Assuming +Z is up scaled three characters 7x and left them on their
backs — and the height check passed on every one of them, because the number
being normalised always comes out right whether or not it was the right
number. See
references/failure-modes.md. - Never look a bone up by name.
tools/rig_map.pyresolves roles and writes them to the sidecar so nothing downstream has to guess. Any hardcoded spelling —_find_bone(["RightHand", ...]), athigh/shinsubstring test — is a rig this project has not met yet. Four characters could not hold a gun because of exactly one such lookup.
After importing
godot --headless --path . -s res://debug/spawn_smoke_test.gd # 29 checks
godot --headless --path . -s res://debug/cloth_clip_check.gd # leg-through-cloth
godot --headless --path . -s res://debug/cloth_settle_check.gd # idle stability
godot --headless --path . -s res://debug/cloth_perf_check.gd # ms per character
godot --headless --path . -s res://debug/travel_dir_check.gd # legs face travel
What "good" looks like on Taila, for calibration:
| Measure | Good | Bad |
|---|---|---|
| Idle skirt movement | < 0.1 deg/frame | 0.5+, or never decaying |
| Leg inside cloth, idle/walk | < 25 mm | 100 mm |
| Leg inside cloth, run/slide/dash | ~95 mm (current, unsolved) | — |
| Solver cost | ~2.6 ms/character | 10 ms |
| Stride vs. travel direction | < 10° (except a capped sidestep) | 90° |
| Bind-pose AABB | tall on the hips→head axis, others < 2.5 m | tallest axis is depth |
Characters currently shipping
| Skin | Source | Meshes | Cloth chains |
|---|---|---|---|
| taila | rigged, Sketchfab CC-BY | 18 | 35 |
| kiyoko | VRoid, CC-BY | 13 | 20 |
| aria | VRoid, CC-BY | 15 | 15 |
| momo | VRoid, CC-BY | 5 | 9 |
| hikari | VRoid, CC-BY | 13 | 10 (zero-length — do not simulate) |
| miku | unrigged source, auto-rigged | 1 | 19 (hair grown) |
| mannequin | Quaternius CC0, from the animation library | 1 | 0 (no costume) |
Known-unsolved
- Peak cloth clipping in a run, slide and dash sits at ~95 mm of thigh inside the skirt. Idle, walk and fall are clean. The solver sees the contact and pushes on it every iteration; the remaining gap is a standing fight between the collision and the garment's own shape constraints.
- No foot IK. Feet do not plant on ground height, so stairs and uneven ground read as sliding.
- No strafe or backpedal clips. Direction is conveyed by yawing the hips
(
SkinnedPlayerModel._update_travel), which is capped, so a pure sidestep still runs its legs ~40° off the direction of travel.