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]>
136 lines
6.8 KiB
Markdown
136 lines
6.8 KiB
Markdown
# 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 | 17–35 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 | — |
|
||
|
||
## 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.
|