`reach character {logo, strip-glb, qa, qa-analyze}` replaces make_logo.py,
glb_strip_utility_nodes.py, analyze_captures.py and the run-garment-qa bash
driver. The QA configs and method doc move beside the domain (qa_configs/,
GARMENT_QA.md), and `qa` takes a config name (`reach character qa hoodie_modern`)
or a path.
Parity, from baselines taken before anything moved:
- the logo PNG is byte-identical
- a synthetic GLB with three real utility nodes strips to identical bytes
(the committed bodies strip 0 nodes, so they proved nothing)
- re-analyzing a cached capture set gives a byte-identical report.json and
summary
run-garment-qa is rewritten, not wrapped (D-263). Its decisions — which config,
which Godot ($GODOT, then ~/bin/godot4, then PATH), and whether xvfb-run is
needed — are capture_plan(), pinned by tooling/test_character.py without
launching Godot. The bash exit codes are kept: 2 for a missing config, 3 for
no Godot.
The T-1271 domain map was wrong about this domain. Six of its ten files import
bpy: convert_outfit, inspect_glb, check_hair_symmetry, check_icosphere,
render_quaternius_test and test_quaternius_raw. They are Blender payloads and
joined the carve-out as blender_* (41 payloads now). The 22 existing payloads'
docstrings still cited tooling/garment-fit/ from before T-1273; fixed.
Archived, with reasons in tooling/archive/README.md:
- setup_clothing_metadata.py wrote coverage data for five garments that no
longer exist in the 24-garment wardrobe
- wipe-bodies.sh ran raw DELETEs on systems.db
segment_reference_distribution.md moved to docs/assets/visual/.
Behaviour changes:
- The QA analyzer exited 0 whatever it found, though its own README says
clip-through "is the real defect and it gates". qa and qa-analyze now exit 1
on clip-through, and the remedy names --min-pixels (Wave 1/2 were accepted
at 150). The cached peasant set has 33 failures at the default 8 px.
- glb strip re-reported the same nodes as stripped on every re-run and
rewrote an unchanged file: it left them as orphans and then found them
again. Only nodes still linked into the graph count now, and a first pass
writes the same bytes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
2.7 KiB
Markdown
42 lines
2.7 KiB
Markdown
# garment-qa — chromakey garment-clipping QA (T-1089)
|
||
|
||
Automatically detects clothing clip-through with a **two-pass depth-proximity**
|
||
chromakey. Per view (clips × sampled frames × 4 camera yaws), the capture scene renders
|
||
the SAME frozen animation frame twice: **pass A** paints the body segments a garment
|
||
*claims to cover* (coverage.json `hides`) flat **unshaded magenta**, garment + head/hands
|
||
normal; **pass B** is identical except the garment is flat **cyan** and every garment
|
||
vertex is nudged `clip_epsilon_m` (default 3 cm) **toward the camera**. A body-key pixel
|
||
that is magenta in A but cyan in B means the ε-shifted cloth now covers it — the body sat
|
||
within ε *in front of* the cloth = poking through. This separates a true clip from a limb
|
||
merely crossing in front of the torso, an open collar, or a bare arm over background
|
||
(all of which stay magenta because the cloth behind them is > ε away).
|
||
|
||
**Run:** `reach character qa [config]` — a name from `qa_configs/` (`peasant`, the
|
||
default, `hoodie_modern`, …) or a path. It launches Godot (`$GODOT`, else `~/bin/godot4`,
|
||
`opengl3`, `xvfb-run` when headless) on the capture scene, then runs the analyzer.
|
||
Re-analyze existing captures without re-rendering: `reach character qa-analyze --config …`
|
||
(or `--dir`). **Both exit 1 when any capture clips through** — the gate the text below
|
||
describes; before T-1290 the analyzer exited 0 whatever it found.
|
||
|
||
**Config** (path passed to the scene via `GARMENT_QA_CONFIG`): `garments`
|
||
(`{item_id,slot}` catalogue items, or `{glb,covers[]}` for a raw WIP garment),
|
||
`body_types`, `clips`, `frames_per_clip`, `yaws`, `head_id/hair_id/eyebrow_id/skin_tone`,
|
||
`out_dir`.
|
||
|
||
**Read the report:** `<out_dir>/report.json` — the same summary prints to stdout. Two
|
||
metrics per capture:
|
||
- **`clip_through_pixels`** / `largest_clip_component` — key pixels the ε-shifted cloth
|
||
covers (skin ≤ ε in front of cloth). **This is the real defect and it gates:** a capture
|
||
`fail`s when its largest connected clip blob ≥ `--min-pixels` (default 8, tolerating AA
|
||
edges). `clip_through_failures` + the `by_group` `clipfail`/`worstClip` columns roll it up.
|
||
- **`exposed_skin_pixels`** — key pixels the shift did NOT cover (skin well in front of any
|
||
cloth: collar/sleeve/ankle gaps, a limb crossing the torso). Informational; does not gate.
|
||
|
||
Tune sensitivity with `clip_epsilon_m` in the config (smaller = only tighter pokes count)
|
||
and `--min-pixels` on the analyzer. Failing/flagged frames are copied to
|
||
`<out_dir>/failures/*_HL.png` with **exposed_skin lime**, **clip_through filled red**, and
|
||
each clip blob boxed.
|
||
|
||
**Note:** the Godot scene lives at `client/tools/garment_qa/chromakey_scene.gd` (not here)
|
||
because Godot `res://` paths cannot leave the client project root.
|