--- name: character-pipeline description: 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 ```bash # From an already-rigged local model (the normal case) python tools/pipeline.py --input assets/characters/incoming/.glb --name --rigged # From a Sketchfab UID (needs SKETCHFAB_API_TOKEN) python tools/pipeline.py --uid --name # From an unrigged mesh — auto-rigs, and accepts the quality loss python tools/pipeline.py --input --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 --name --rigged --grow-cloth ``` Then, once, so Godot sees the new files: ```bash godot --headless --path . --import ``` The result is `assets/characters/skins/.glb` + `.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 | | Classify every SURFACE | `tools/surface_map.py` | The runtime never re-guessing what a surface is | | Write the rig sidecar | `retarget.py::describe_rig` | The runtime never re-guessing anatomy | | Cel look, per surface class | `LevelMaterials.apply_character_look` | Hair not reading as a solid dark cap | | Cloth + hair | `characters/spring_bones.gd` | Clothes reading as clothes | | Per-character judgement calls | `characters/tuning_store.gd` | Art direction not becoming another constant | ## The surface table The sidecar carries a `surfaces` list saying what each mesh surface IS — `body`, `cloth`, `hair`, `accessory`, or `linework` (the model's own ink shell, which is not a surface of the character at all). It is keyed on the MATERIAL name, because every character in this game arrives with its meshes called `Object_7` through `Object_32` while material names survive the glTF round trip intact. `SkinSurfaces` reads it and `apply_character_look` acts on it: hair takes a much thinner outline than the body, cloth a heavier one and a crisper terminator, accessories the heaviest. `SkinnedPlayerModel.surfaces_of(cls)` answers the question for anything else that needs it. Backfill a character that predates it, without re-importing: ```bash blender --background --python tools/surface_map.py -- \ assets/characters/skins/.glb ``` New imports get it from `describe_rig`, built from the same chains the solver uses, so the surface table and the cloth solver can never disagree about which bones are a skirt. ## Per-character judgement Anything derivable from the skeleton is derived. What is left is genuinely an artist's call, and it lives in layered JSON rather than in a constant: | File | Scope | Class | |---|---|---| | `assets/characters/weapon_holds.json` | character + weapon | `WeaponHoldTuning` | | `assets/characters/rig_anchors.json` | character | `RigAnchors` | Both layer `defaults` → `skins.._all` → `skins..` through `TuningStore`. An absent file means "use what the code derives", so nothing here is required for the game to run. Adding a knob is adding a row to a `KNOBS` spec table — the lab builds its whole UI from those. `RigAnchors` is where "the grip sits here in the palm" lives. A hand bone's origin is the WRIST; how far down the palm a grip belongs depends on the character's hand and cannot be derived. It defaults to identity, and identity is exactly the derived mount. Do **not** put a fixed rotation on the weapon mount instead — a bone attachment is expressed in the BONE's axes, no two rigs agree on those, and that constant is why the hand mount points were once wrong on every character. ## The rig lab ```bash godot --path . res://debug/rig_lab.tscn ``` Pick a character, a weapon, a pose or a single clip. Drag sliders for the HOLD (character + weapon) and the ANCHORS (character), and save. Click a surface class to isolate it — that is how the classifier gets checked: click `hair` and anything else still standing was misclassified. ## 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 1. **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. 2. **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. 3. **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 in `retarget.py` says why. 4. **Never run `SkinLegRepair` on authored weights.** It snaps weights and deletes triangles. It exists only to undo auto-rigging. It is gated on `weights_authored`, which is MEASURED, not assumed. 5. **Measure from inside the modifier pass.** See `references/verification.md`. 6. **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`. 7. **Never look a bone up by name.** `tools/rig_map.py` resolves roles and writes them to the sidecar so nothing downstream has to guess. Any hardcoded spelling — `_find_bone(["RightHand", ...])`, a `thigh`/`shin` substring 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 ```bash godot --headless --path . -s res://debug/spawn_smoke_test.gd # 29 checks godot --headless --path . -s res://debug/surface_class_check.gd # every surface classified godot --headless --path . -s res://debug/character_picker_check.gd # the escape-menu roster godot --headless --path . -s res://debug/rig_anchor_check.gd # anchors move the weapon 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 ``` And LOOK at it, which is where four of the last five real defects were found: ```bash godot --path . -s res://debug/roster_capture.gd -- # every character, one shot each godot --path . -s res://debug/ui_capture.gd -- # every menu screen godot --path . res://debug/rig_lab.tscn -- shot ``` `surface_class_check` fails on any surface that falls through to the heuristic rather than resolving from the table. That is deliberate: a model whose names stopped matching still RENDERS, because the fallback catches it — it just quietly loses its per-class art direction, which is exactly the kind of regression nothing else would report. 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 The six in `skins.json`, with what `surface_class_check` reports: | Skin | Source | Cloth chains | Surfaces | |---|---|---|---| | taila | rigged, Sketchfab CC-BY | 35 | 18 — body 6, cloth 7, hair 1, linework 4 | | kiyoko | VRoid, CC-BY | 20 | 13 — body 8, cloth 3, hair 2 | | aria | VRoid, CC-BY | 15 | 15 — body 9, cloth 4, hair 2 | | momo | VRoid, CC-BY | 9 | 5 — body 2, cloth 1, hair 2 | | miku | unrigged source, auto-rigged | 0 | 4 — body 3, hair 1 *(one mesh, four surfaces)* | | mannequin | Quaternius CC0, from the animation library | 0 | 2 — body 2 | ## 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. - **momo's idle pose is wrong** — arms overhead and a pinched waist. Every assertion passes on her: she loads, animates, classifies and mounts a weapon. It shows up only in `roster_capture`. Her source rig is the reason to suspect — its bones are auto-named (`Item_O_Sphere_005`, `Unused_Noname_004`, `Root_001_001`), so the role resolver has almost nothing to go on, and a limb role claimed by the wrong bone would look exactly like this. Start by dumping her resolved roles against her skeleton. - **A stray `Icosphere` ships inside every skin GLB** — 42 vertices, no parent, no vertex groups. It rides in from the animation library. Harmless, and now skipped by construction rather than by name in `surface_map`, but the export should not be producing it.