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
@@ -19,6 +19,7 @@ the convention):
|
||||
| Cloth/hair bones carry NO animation keys; physics owns them | `export_optimize_animation_keep_anim_armature=False` |
|
||||
| Physics colliders are a small set of capsules: thighs, shins, and a big one at the waist acting as a lid | 5 capsules, measured from the mesh (`_leg_colliders`) |
|
||||
| Neighbouring skirt panels are linked sideways | 278 cross-panel distance links from shared vertices |
|
||||
| Each surface is TAGGED with what it is, so shading can differ per class | `tools/surface_map.py` writes it; `SkinSurfaces` reads it |
|
||||
| Cel shading with a ramp, plus a separate outline pass | `LevelMaterials.apply_toon_recursive` + `apply_character_look` |
|
||||
|
||||
Where we differ: their collider capsules and cloth parameters are hand-authored
|
||||
@@ -26,6 +27,38 @@ per character by a technical artist. We MEASURE them from the model's own
|
||||
geometry at build time, because there is no artist in this loop. That is the
|
||||
whole reason `<model>.rig.json` exists.
|
||||
|
||||
Where there IS an artist in the loop, there is now somewhere to put the answer:
|
||||
`debug/rig_lab.tscn` and the layered files behind it (see the SKILL). Measuring
|
||||
is the default and hand-authoring is the override, rather than the other way
|
||||
round.
|
||||
|
||||
## Separation is only half of it — the parts have to be NAMED
|
||||
|
||||
Keeping the meshes apart is structural. Knowing which is which is what lets
|
||||
anything act on the difference, and until the surface table existed nothing did:
|
||||
every surface of every character took one set of shading numbers, calibrated on
|
||||
skin, because there was no way to ask whether a surface was hair.
|
||||
|
||||
The table lives in the sidecar as `surfaces`, keyed on the MATERIAL name — mesh
|
||||
node names are `Object_7` through `Object_32` on every character in this game and
|
||||
carry no meaning, while material names survive the glTF round trip intact and are
|
||||
what the artist actually chose. It is decided three ways, in descending order of
|
||||
how much it trusts them:
|
||||
|
||||
1. **the material name.** On VRoid exports it is formal —
|
||||
`N00_000_00_Body_00_SKIN_Instance` carries its own class infix, and every
|
||||
VRoid character here uses SKIN / FACE / EYE / HAIR / CLOTH.
|
||||
2. **the weights.** Decisive when the name says nothing: a surface pulled by the
|
||||
skirt chain is a skirt whatever it is called. The threshold is deliberately
|
||||
low (5%), because VRoid welds the whole cap of the hair to the head bone and
|
||||
springs only the strands — kiyoko's hair mesh is 85% head, and a majority rule
|
||||
would call it skin.
|
||||
3. **the material flags.** These catch line-work, which is the one class that is
|
||||
not a surface of the character at all.
|
||||
|
||||
It is built from the same chains the spring solver uses, so the two can never
|
||||
disagree about which bones are a skirt.
|
||||
|
||||
## Why the separation is load-bearing
|
||||
|
||||
**Materials.** The body wants skin shading, hair wants an anisotropic-ish ramp
|
||||
|
||||
@@ -23,10 +23,17 @@ rim term lights it brightly exactly where it is supposed to read as ink.
|
||||
it flat and unshaded. "Untextured" alone is NOT the test — that made every
|
||||
flat-coloured model render as a black silhouette, because Quaternius' mannequin
|
||||
has two untextured materials (a yellow body, lilac joints) and both were hidden
|
||||
as though they were an outline shell. `_is_line_work()` asks three things
|
||||
instead: is it named `eyes*`, is it drawn front-face-culled (the classic
|
||||
inverted-hull setup), or is its albedo near-black. An ink shell is black; a
|
||||
flat-coloured character is any colour at all.
|
||||
as though they were an outline shell. The test asks three things instead: is it
|
||||
named `eyes*`, is it drawn front-face-culled (the classic inverted-hull setup),
|
||||
or is its albedo near-black. An ink shell is black; a flat-coloured character is
|
||||
any colour at all.
|
||||
|
||||
That test now runs at BUILD time (`tools/surface_map.classify_linework`) and its
|
||||
answer lives in the sidecar. `SkinSurfaces.guess()` is the same rule kept as the
|
||||
runtime fallback, for a model with no surface table — and the cull-mode half of
|
||||
it is re-run at runtime even when the table exists, because glTF has no way to
|
||||
say "draw only the backfaces" and an inverted hull cannot survive the round trip
|
||||
as a cull mode. Blender genuinely cannot see it; Godot can.
|
||||
|
||||
What it then does:
|
||||
|
||||
@@ -37,9 +44,29 @@ What it then does:
|
||||
with `HL` in the name, which is the glint in the pupil and really is white.
|
||||
|
||||
If a newly imported character comes out with a white halo, a black silhouette,
|
||||
or black eyes that should have irises, `_is_line_work()` and the name-matching
|
||||
below it are where to look. The conventions vary by source and this is the one
|
||||
place they are read.
|
||||
or black eyes that should have irises, the line-work test and the name-matching
|
||||
below it are where to look — now in `tools/surface_map.py`, mirrored by
|
||||
`SkinSurfaces.guess()`. **Change both or neither**: a model with a surface table
|
||||
would start rendering differently from one without.
|
||||
|
||||
## Per-class art direction
|
||||
|
||||
Because each surface says what it is, each class takes its own numbers
|
||||
(`LevelMaterials.CHARACTER_LOOK`). `body` is deliberately identical to what every
|
||||
surface used to get, so the calibration this was all built on does not move. The
|
||||
others are departures, each for a reason:
|
||||
|
||||
| Class | Outline | Band | Why |
|
||||
|---|---|---|---|
|
||||
| body | 5.0 mm | 0.16 | unchanged — the baseline |
|
||||
| cloth | 5.8 mm | 0.13 | a garment's silhouette is most of what separates a character from the background at range; folds need a defined terminator to read as fabric |
|
||||
| hair | 3.4 mm | 0.20 | **the one that matters.** A hair mesh is dozens of near-parallel strands millimetres apart; at the body's 5 mm each strand's hull swallows its neighbour and the head reads as one solid dark cap |
|
||||
| accessory | 6.8 mm | 0.10 | small, rigid, usually the most saturated thing on the character — meant to pop |
|
||||
|
||||
This required moving the outline from `material_overlay` on the INSTANCE to
|
||||
`next_pass` on each surface's material. Miku's body, face and hair are three
|
||||
surfaces of one mesh, so an instance-wide overlay can only ever give all three
|
||||
the same weight.
|
||||
|
||||
Taila's eyes still render as black cards rather than amber irises. Her eye
|
||||
surfaces are untextured, and the glTF import hands every untextured surface a
|
||||
|
||||
@@ -41,7 +41,36 @@ Two related traps:
|
||||
| `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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user