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]>
7.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/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.
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° |
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.