The skill is this project's own instructions, and three commits made it wrong:
it described a runtime that re-guessed what every surface was, a lab that only
tuned weapon holds, and a suite that could not see any of the last five defects.
It also still listed a seventh character that is not in skins.json.
Records what is new — the surface table and how it is decided, the layered
tuning files and why a fixed rotation on the weapon mount is the wrong answer,
the rig lab — and what the work taught:
* assert the CONSEQUENCE. Asking a model which clip it is playing reads a
variable it set on itself, and says "Idle" just as happily when nothing is
ticking. Asking whether an anchor saved says nothing about whether the gun
moved.
* four of the last five real defects came from looking at a PNG. Every one of
them passed every assertion.
* the line-work rule now exists twice, at build time and as the runtime
fallback, and they must be changed together or a model with a surface table
starts rendering differently from one without.
Also records momo's broken idle and the stray Icosphere under known-unsolved,
with what is worth suspecting first in each case, since neither is fixed.
Co-Authored-By: Claude Opus 5 <[email protected]>
262 lines
14 KiB
Markdown
262 lines
14 KiB
Markdown
---
|
|
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/<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:
|
|
|
|
```bash
|
|
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 |
|
|
| 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/<name>.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.<skin>._all` → `skins.<skin>.<subject>` 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 -- <dir> # every character, one shot each
|
|
godot --path . -s res://debug/ui_capture.gd -- <dir> # every menu screen
|
|
godot --path . res://debug/rig_lab.tscn -- shot <png> <skin>
|
|
```
|
|
|
|
`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.
|