Files
Papay-Shooter/.claude/skills/character-pipeline/references/verification.md
T
Nicholas ButzkeandClaude Opus 5 700d0925d7 tools: measure rest poses, and diagnose momo
momo's idle plays with her arms overhead and her waist pinched, and every
assertion in the suite passes on her.

The first suspicion was the retarget's rest-relative delta: it applies "what the
clip does to the LIBRARY's rest" to THIS rig's rest, which quietly assumes the
two rests are alike. rest_pose_check.gd tests that, and disproves it — miku's
arms rest 41° off the library's and taila's 32°, and both animate correctly. The
delta retarget handles a rest-pose difference, which is what it is for. Recorded
in the reference so nobody spends that hour again.

Writing the tool reproduced this project's own recurring mistake in miniature.
Measuring "the direction from a bone to its first child" reported kiyoko's and
aria's legs 71° off the library — because a thigh's first child is as likely to
be a skirt bone as a shin, and it was measuring the hang of a skirt panel.
Pointing it at the next limb BY ROLE dropped both to 1°.

What momo actually has: `Root_001` through `Root_007` are in her driven_bones,
and they are her HAIR roots — Hair_A is dominated by Root_001_001, Root_007 and
Root_005. The animation is keying bones the spring solver is supposed to own,
which is non-negotiable #2 broken by the role resolver rather than by a clip.
The surface table corroborates it: her hair surfaces report 1.8% and 9.4% chain
share, because most of their vertices belong to bones in no chain at all.

Not fixed here. `Root_00N` matches no COSMETIC stem, and adding "root" to the
stems would cost a rig whose actual root is called `Root` its hips. The
structural fix is that a bone whose geometry is dominated by a mesh classified
`hair` is a hair bone whatever it is called — which the surface table makes
answerable at build time, and did not when momo was imported. It needs a Blender
re-run and re-verification of all six characters.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-27 15:03:33 -04:00

152 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Verification — and the trap that invalidated all of it
## READ THIS FIRST
**Godot restores every bone's local pose after the `SkeletonModifier3D` pass.**
So calling `force_update_all_bone_transforms()` and reading
`get_bone_global_pose()` from a `SceneTree` script, from `_process`, or anywhere
outside that pass recomputes the globals from the **animation alone**. The
shooter pose layer and the cloth solver are simply not in what you measure.
`debug/cloth_clip_check.gd` did exactly this. It reported the same ~95 mm of
leg-inside-skirt with collision fully enabled **and with the collision call
commented out**. Every number ever taken from that tool before 2026-07-26 is
void, and several rounds of "tuning did nothing" in the history were reading a
pose the solver never touched.
**To measure a pose layer, add your own `SkeletonModifier3D` as a child of the
`Skeleton3D` AFTER the one you care about, and snapshot inside its
`_process_modification()`.** The `PoseProbe` class in `cloth_clip_check.gd` and
`travel_dir_check.gd` is the pattern.
Two related traps:
- **Headless runs uncapped**, so the engine delta is sub-millisecond and anything
integrated barely moves. Set `SpringBones.fixed_delta = 1.0/60.0`.
- **A single frame of a locomotion clip measures the clip.** A run cycle twists
the torso against the hips by tens of degrees twice per stride, swamping
anything a pose layer does. Average over a stride.
## The tools
| Tool | Measures | Good |
|---|---|---|
| `spawn_smoke_test.gd` | spawn, skins, anim tree, camera, state cycling | 29 OK, 0 failures |
| `cloth_clip_check.gd` | leg-inside-cloth per movement state, per vertex | idle < 25 mm |
| `cloth_settle_check.gd` | deg/frame at a dead idle, contacts/frame | skirt < 0.1, hair < 0.01 |
| `cloth_perf_check.gd` | ms per character per frame | ~2.6 ms |
| `cloth_allow_check.gd` | how much of each limb the rest-clearance cap makes the solver blind to | 1735 mm on Taila |
| `cloth_stretch_check.gd` | mesh tearing between panels | no 3× edges |
| `travel_dir_check.gd` | stride direction vs. travel direction | < 10° except a capped sidestep |
| `limb_deform_check.gd` | joint collapse | knee ~0.99 |
| `verify_character.py` | meshes, bones, weights of a SOURCE model | several meshes, cloth bones present |
| `surface_class_check.gd` | every surface resolves from the sidecar, not the fallback | 0 fallbacks on all six skins |
| `character_picker_check.gd` | the escape-menu roster: skeleton, clips, surfaces, and that the pose MOVES | 0 failures |
| `rig_anchor_check.gd` | a grip anchor physically moves the weapon, and clears | 0 failures |
| `anim_capture.gd` / `orbit_capture.gd` | renders, for looking | — |
| `roster_capture.gd` | one photo of every character, from the picker | — |
| `ui_capture.gd` | one photo of every menu screen | — |
| `rest_pose_check.gd` | each rig's bind-pose limb directions vs. the library's | see below |
## What `rest_pose_check` actually established
It was written to test a suspicion — that the rest-relative retarget silently
assumes both rigs rest alike — and it disproved it. Miku's arms rest **41°** off
the animation library's and Taila's **32°**, and both animate correctly. The
delta retarget handles a rest-pose difference, which is what it is for. Do not
go looking there again.
It also demonstrates the measurement trap in miniature. Written as "the direction
from a bone to its FIRST CHILD", it reported kiyoko's and aria's legs 71° off —
because a thigh's first child is as likely to be a skirt bone as a shin, and it
was measuring the hang of a skirt panel. Pointing it at the next limb BY ROLE
dropped both to 1°. The same rule as everywhere else in this pipeline: resolve
roles, never take whatever the rig happens to hand you.
## Assert the consequence, not the plumbing
Three of these exist because the obvious check passes on a broken system.
- `character_picker_check` asserts the skeleton's pose CHANGES over a dozen
frames. Asking the model which clip it is playing does not work: that is a
variable the class sets on itself, and it reads `"Idle"` just as happily when
the animation tree is not ticking at all.
- `rig_anchor_check` asserts the weapon MOVES by the offset asked for. An anchor
system is easy to build so that the sliders move, the file saves and the JSON
round-trips while the gun does not budge — the value read into a variable
nobody consumed. It measures in the attachment's frame, not the world's:
the attachment tracks a bone on an animating skeleton, so a world-space delta
is mostly the idle animation.
- `surface_class_check` FAILS on a surface that falls through to the heuristic
instead of resolving from the table. A model whose names stopped matching still
renders — the fallback catches it — and quietly loses its per-class art
direction. Nothing else would report that.
And four of the last five real defects came from LOOKING, not from asserting:
a preview showing the back of the character's head, a turntable that carried on
from the previous character, an unstyled list, and momo's idle pose. Every one
passed every assertion. Run `roster_capture` and `ui_capture` and open the PNGs.
Run them:
```bash
godot --headless --path . -s res://debug/<tool>.gd
godot --headless --path . -s res://debug/<tool>.gd -- res://assets/characters/skins/<name>.glb
```
Scripts run with `-s` MUST extend `SceneTree`. A `Node` script never quits and
hangs forever.
## Measure the right quantity
`cloth_clip_check.gd` used to report "how much CLOSER the leg got than the artist
modelled it". A hem 200 mm clear of a shin legitimately comes 180 mm closer when
the leg kicks out in a slide, and counting that as a failure buried the real
clipping under motion the character is supposed to have. It now reports how far
INSIDE a capsule a cloth vertex is, over and above however far inside it was
modelled — only cloth actually within the capsule can be showing a leg through.
It also applies the collider's `from` offset, so it tests the same band of thigh
the solver is defending. Measuring the full bone tests the hip cap the solver
deliberately excludes and reports it as clipping no tuning can fix.
## What the suite still does not check
It verifies that a character is WELL-FORMED, not that it is CORRECT. Those are
different properties, and only the first was ever asserted — which is how four
characters shipped "All checks passed" while lying on their backs, seven times
too large, facing backwards, or unable to hold a gun. See `failure-modes.md`.
`posture` and `bone roles reachable at runtime` are now hard checks. Still
missing, and worth adding when a source next exposes them: facing measured on the
OUTPUT, and per-vertex validation that a generated cloth chain actually tracks
the geometry it was given.
## Diagnosing "the solver isn't working"
In order:
1. **Is the measurement inside the modifier pass?** (Above. Do this first.)
2. **Does the solver SEE the contact?** `debug_hit_report()` — bone → deepest
overlap it found. If ~0 while the mesh is deep inside a leg, the collision
hull does not cover the geometry that is clipping.
3. **Does it CONVERGE?** `debug_residual_report()` — overlap left after the
relaxation. Seen 93 mm, left 95 mm is a standing fight, not slow convergence;
quadrupling the iterations will buy nothing. Find what is pulling back.
4. **Only then, tune.**
That order was learned the hard way: the drape, the bend limits, the backstop,
the iteration count and the hull sampling were each suspected and tested, and
the answer was in step 1.
## Also run
```bash
godot --headless --path . -s res://movement/tests/run_fsm_tests.gd # 11 tests
godot --headless --path . --check-only --script res://<file>.gd # syntax
```
Autoload identifiers report false "not found" errors under `--check-only`
ignore those.