docs(skill): the surface table, rig anchors, and asserting the consequence
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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
99a2ee131d
commit
1e6f3001ac
@@ -72,9 +72,69 @@ Blender is required (`BLENDER_PATH`, or auto-found under
|
||||
| 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 | `LevelMaterials.apply_character_look` | The model's own line-work not being re-lit |
|
||||
| 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
|
||||
|
||||
@@ -129,13 +189,30 @@ is a list of things that cost a debugging cycle to learn.
|
||||
## After importing
|
||||
|
||||
```bash
|
||||
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
|
||||
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 |
|
||||
@@ -149,15 +226,16 @@ What "good" looks like on Taila, for calibration:
|
||||
|
||||
## Characters currently shipping
|
||||
|
||||
| Skin | Source | Meshes | Cloth chains |
|
||||
The six in `skins.json`, with what `surface_class_check` reports:
|
||||
|
||||
| Skin | Source | Cloth chains | Surfaces |
|
||||
|---|---|---|---|
|
||||
| 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)* |
|
||||
| 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
|
||||
|
||||
@@ -170,3 +248,14 @@ What "good" looks like on Taila, for calibration:
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user