Files
animation/.agents/plans/clothing-pipeline-unification-2026-07-31.md
T
jeremy 3ba86b2ea8 feat: clothing lane, character sources, and DCC bridges
Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:55:43 -07:00

24 KiB
Raw Blame History

Plan: Clothing-pipeline unification — one entry point, QC gates at every seam

Status: Implemented 2026-07-31 (phases 14 built; catalog JSON still deferred) · Author: Fable 5 session 2026-07-31 · Implementer: 5-agent parallel build + integration pass, 2026-07-31 — see §8

0. Context (you have no other context — read this fully)

The clothing lane turns a garment idea into an outfit part an ariki-game character wears. It has three halves that work but do not talk to each other:

  1. MD upstream (this repo): Marvelous Designer 2024, driven over a TCP bridge (tools/md_bridge.py, port 18900). Per-garment scripts in tools/tailor/ draft panels, sew, drape on the Lena avatar FBX, texture, and snapshot. Session start is a human click (Plugin > TinqsMDBridge); MD's UI freezes while a script runs.
  2. Blender downstream (this repo): clothing/garment_pipeline.py — seven config-driven headless stages (census prepare fit reduce bake skin export), each checkpointing a .blend + QA renders into work/<name>/, exporting per-slot GLBs onto the shared 65-bone Quaternius skeleton into ariki-game/assets/quaternius/outfits/<set>/.
  3. Game side (ariki-game, read-only for this repo's tooling by convention): hand-edit src/Character/OutfitCatalog.cs to register the item, force a Godot reimport, spawn ClothingTestBed via tools/game.sh, click through Idle/Walk/Dance, and eyeball the result.

The lane is PROVEN — the downloaded-dress pilot (2026-07-30) and the MD-authored kapa haka set (pari + piupiu, 2026-07-31) are in game. But both kapa haka pieces shipped with pose-dependent defects invisible to every QA artifact the pipeline produces: the pari neckline gapes open in walk/dance, and a thigh punches through the piupiu skirt. All pipeline QA renders are rest pose — the one pose that cannot fail. A separate agent owns those two weight fixes; this document is the pipeline-level response: an assessment, a design that combines the three halves into one entry point, and the QC gates that would have caught both bugs before the game.

Source of truth for how the lane works today: .agents/wiki/architecture/clothing-lane.md and clothing/README.md. This document is a proposal for how it should work; on adoption, fold the outcome into clothing-lane.md.


1. Assessment

1.1 What already works (keep it)

  • Config-driven downstream. New garment = new JSON in clothing/configs/, not new code. The schema already expresses island→part mapping, bodyshell parts, alignment, fit masks, weld bands, proxies, weight modes, textures, per-part tri budgets.
  • Checkpointed stages. 00_census.blend … 50_skin.blend mean any stage can be re-run in isolation; resume semantics exist for free.
  • Determinism fingerprint. export prints a SHA1 over all garment vertices (garment_pipeline.py:984-989).
  • The seam is crossed. MD-authored garments arrive at game budget, pre-fitted to Ariki_Female_QuatSkin.glbreduce is only hard for downloaded meshes.
  • All the automation hooks exist on the game side. game.sh spawn (multi-instance, SPAWN_BUILD=1, MOCK_ONLY=1), agent API (/health /screenshot /navigate /state /scene /ui /command /input /console), game.sh clean-import, tools/asset_pipeline.py check.

1.2 What costs time (the streamlining targets)

Cost Evidence
4× duplicated MD garment scripts md_pari.py, md_piupiu.py, md_tee_v1.py, md_skirt_v1.py share a ~100% identical shell (NewProject → ImportFBX at op.scale=10.0 → fabric → arrange → simulate → snapshot); only panels/seams differ
Config copy-paste piupiu.json vs piupiu_sb.json: 48 lines, differing in exactly 4 values (name, body, weights, export.set) + a note
Six manual CLI invocations per garment one blender --background … --stage <s> per stage, run by hand in order
Hand-authored configs read census.json, guess island→part mapping, trial-and-error alignment against QA renders
Code-edit registration 2 Add() lines + possibly a BaseDirFor case in OutfitCatalog.cs, then a C# rebuild
Blunt reimport .bin-only changes are invisible to Godot's .gltf hash; today's fix is clean-import (wipes ALL of .godot/imported/) or hand-deleting entries
Eyeball sign-off spawn test bed, click Idle/Walk/Dance, look

1.3 What is structurally unsafe

  1. Rest-pose-only QA. Every qa_*.png the pipeline writes is the fitting pose. Both shipped bugs were invisible in it. (clothing/README.md "QA renders only prove the pose that cannot fail".)
  2. Silent catalog fallback. OutfitCatalog.BaseDirFor (OutfitCatalog.cs:88-98) takes a bare-string setId and its _ => arm silently resolves to the Fantasy pack folder — a typo'd set name mis-resolves without error. Missing assets are warn-only (OutfitCatalog.cs:71-78): the character just stays naked/previous.
  3. .bin staleness. GLTF_SEPARATE export keeps geometry/UVs in the sidecar .bin; Godot hashes only the .gltf, so a vertex-only re-export silently ships the old mesh.

1.4 Corrections to the record (verified 2026-07-31)

Earlier session notes contained four errors that materially change the design:

Prior claim Truth Consequence
"FBX export from MD is manual" export_api.ExportFBX(path, op) (+ ExportOBJ/ExportZPrj/ExportZPac) is scriptable — exact flags in .claude/skills/marvelous-designer/SKILL.md:182-190 (op.bExportGarment=True; op.bExportAvatar=False); the 4 garment scripts simply never call it The entire MD half can run unattended after the one session click
ExportSnapshot3D is the mesh export It is the viewport PNG (the vision-QC loop); mesh export is ExportFBX Don't build on the wrong call
"5 duplicated MD scripts" 4 garment scripts; md_recon.py is an API-introspection tool and stays standalone Template scope
tools/vision-compare.py exists (referenced by ariki-game/tools/visual-qa.sh) It was never written The in-game gate has no judge today; VLM-vs-pixel-diff is a real decision, not wiring

One permanent constraint, confirmed: MD exposes no mesh introspection to Python (GetClothPositions() stays empty). Every upstream drape judgement is image-based, by necessity, until CLO exposes more.

Two enabling facts: the config's export block already carries gender/set/out_dir and each part carries slot, so the catalog's exact {Gender}_{Set}_{Slot}.gltf path is derivable from the config today; and the agent API's GET /console surfaces ClothingTestBed's per-slot load-failure prints, so outfit load failure is machine-detectable without vision.


2. The unified pipeline

2.1 One config, additive blocks

Keep clothing/configs/<garment>.json as the single per-garment artifact and grow it — the existing Blender schema stays byte-compatible, so garment_pipeline.py needs no migration:

{
  "extends": "piupiu.json",          // NEW: overlay inheritance
  "name": "...", "source": "...", "body": "...",

  "md": {                            // NEW: upstream draft params (what the 4 scripts differ on)
    "avatar_fbx": "...", "zfab": "...", "texture": "...",
    "panels": [...], "seams": [...], "arrangements": [...],
    "sim_frames": 300, "strengthen": [...],
    "export_basename": "lena_<garment>_v<N>"
  },

  "align": {...}, "parts": {...},    // UNCHANGED: existing Blender schema

  "export": { "gender": "Female", "set": "Kapahaka", "out_dir": "..." },  // exists

  "catalog": {                       // NEW: what Add() needs that nothing else holds
    "id": "kapahaka_legs_f", "displayName": "Piupiu",
    "charisma": 0.05, "workSpeed": 0.04, "category": "casual"
  },

  "expect": {...}                    // NEW: QC thresholds (§3)
}
  • extends is the cheapest real win: piupiu_sb.json becomes ~8 lines. Overlay = deep-merge child over parent, arrays replaced whole.
  • md block + one template script tools/tailor/draft_garment.py replaces the 4 copy-paste scripts. The identical shell becomes the template; panels/seams/arrangements come from the config. Hard-won MD lessons (mm units, +y up in 2D, whole-edge seam indices, look up arrangement points by name, SetArrangement then ResetClothArrangement) live in ONE place instead of four.
  • catalog block carries only what the C# Add(id, displayName, slot, slotSuffix, charisma, workSpeed, category, set:, gender:) signature needs and the pipeline has never held; slot/set/gender derive from existing blocks.

2.2 One orchestrator

clothing/garment.py <config> [--from <stage>] [--to <stage>] [--only <stage>] over one ordered stage list spanning all three worlds:

draft  drape  publish │ census prepare fit reduce bake skin export │ register import verify
└──── MD bridge ──────┘└──────── Blender headless ────────────────┘└──── ariki-game ─────┘

A driver, not a rewrite. Each stage shells out to the tool that already owns it:

Stage group Shells to Notes
draft/drape/publish python tools/md_bridge.py --file <generated> Script generated from the md block via draft_garment.py. --ping first; if no session, fail fast printing the human instruction ("click Plugin > TinqsMDBridge"). Raise the idle timeout (~1 min per 300 sim frames). publish = scripted ExportZPrj + ExportFBX + ExportZPac (§1.4).
census … export blender --background --python garment_pipeline.py -- --config X --stage Y Unchanged CLI; --from/--to is a loop. Standalone stage runs keep working exactly as today.
register/import/verify bash ariki-game/tools/game.sh … + agent API §2.4 and gates G6/G7/G8. Decided: orchestrator lives in tinqs/animation/clothing/, cross-repo shell-out to game.sh is acceptable.

Checkpoint numbering (00_ … 50_) already gives resume: --from fit loads 10_prepare.blend exactly as today.

2.3 The automation boundary (permanent, human-only)

  1. Session start is a click — Plugin > TinqsMDBridge, once per MD session (not per garment). The orchestrator detects (--ping) and instructs; it can never perform it.
  2. MD's UI freezes during a blocking call — no mid-drape supervision; results are visible only after Simulate returns.
  3. No mesh introspection in MD — upstream QC is image-based forever (§1.4).

Everything else — including the FBX export previously believed manual — is scriptable. That is the headline of this assessment.

2.4 Registration: emit, don't edit (catalog JSON deferred)

Decided 2026-07-31: defer the data-driven OutfitCatalog refactor.

  • Cost today is 2 Add() lines per set (+1 BaseDirFor case when a new folder appears); the refactor is not small (Initialize() is all hardcoded calls; ClothingItem has 10 init-only properties to round-trip). Revisit at ~10 sets.
  • The real risk is not typing, it is the silent setId fallback (§1.3) — gate G8 (a ~30-line lint) buys that safety without the refactor.
  • The register stage therefore emits the exact Add(...) line(s) and any needed BaseDirFor case to stdout + work/<name>/register.cs.txt for paste-in. Codegen-as-text, not codegen-as-edit — honest about who owns src/.

3. QC gates (where agents/checks slot in)

Ranked by value = known-pain-caught ÷ effort. Both shipped bugs were pose-dependent, so the ranking is driven by how early a pose-dependent failure can be caught. Thresholds live in the config's expect block.

# Gate After stage Kind
G5 Posed penetration + gape sweep — the centrepiece skin scriptable
G6 In-game motion QC import agent-with-vision (hard part scriptable)
G3 Rest-pose penetration fit scriptable
G2 Census sanity census scriptable
G8 Catalog ↔ asset lint register scriptable
G7 Targeted reimport verify import scriptable
G4 Reduce budget + silhouette reduce scriptable
G1 Drape placement drape hybrid

G5 — posed penetration + gape sweep. Headless Blender loads 50_skin.blend (it already contains BODY, RIG, and the skinned GARM_* parts). Apply real clip poses — the UAL animation packs (ariki-game/assets/quaternius/anim/UAL1.glb etc.) are authored on the same 65-bone skeleton — or synthetic extremes (arm raise, deep step, torso twist). Per sampled frame, evaluate the armature-deformed meshes and BVH-test: (i) garment verts inside the body, (ii) body verts exposed inside a part's declared coverage band. Pass/fail: expect.max_penetrating_verts (e.g. 0 verts >1 mm inside) and expect.max_gape_verts across all frames. This is the only gate that would have caught BOTH shipped bugs — deterministically, headless, pre-game, re-runnable. One risk to verify during implementation: bone-name compatibility between the UAL pack armature and the derived-body RIG (retarget or pose-copy if names drift).

G6 — in-game motion QC. SCENE=clothing_test_bed MOCK_ONLY=1 game.sh spawnPOST /navigate {"button":"Idle"|"Walk"|"Dance"}GET /screenshot per clip → judge. Two signals: hard = GET /console contains no outfit load error (machine-checkable today); soft = visual verdict on poke-through/gaping/texture. Decided 2026-07-31: VLM/agent judge for a new garment's first sign-off, then bless those frames as the baseline so re-runs become a deterministic pixel diff. This also finally gives visual-qa.sh the comparator it references but never had (§1.4). Small ariki-game enabler needed (approved): ClothingTestBed hardcodes its opening set (DefaultBodyF = "kapahaka_body_f" etc., ClothingTestBed.cs:81-84) — add an env-var/CLI override so a new set is directly reachable instead of blind Cycle Body presses.

G3 — rest-pose penetration. The same BVH test as G5, rest pose only, run right after fit — a cheap early-fail subset that saves reduce/bake/skin time on a bad fit.

G2 — census sanity. census.json already records per-island verts/tris/z/x/centroid. Check against expect.islands (count, per-mapped-island vert count and z-span tolerance). Catches the silent killer: an MD re-export reorders islands and the config maps the wrong ones.

G8 — catalog ↔ asset lint. Every catalog entry's resolved {baseDir}/{Gender}_{Set}_{Slot}.gltf exists on disk, and every outfit GLB on disk has a catalog entry. Zero unmatched either way. ~30 lines; kills the silent BaseDirFor fallback footgun (§1.3) without the JSON refactor.

G7 — targeted reimport verify. Replace blunt clean-import in the garment loop: delete only that asset's two .godot/imported/ entries (the README already prescribes exactly this by hand), run --import, then confirm via /console + refreshed .gltf.import. Closes the .bin-staleness hole (§1.3) without nuking the whole cache.

G4 — reduce budget + silhouette. Tris vs parts[].tris; pixel IoU of the HI vs reduced QA renders ≥ threshold. Low urgency: MD-authored garments arrive at budget (both kapa haka pieces never hit their ceilings) — this gate mostly guards downloaded meshes.

G1 — drape placement. The thresholds already exist as prose — md_pari.py:4 reads "QC targets: band top ~1.31 m (above bust), hem ~1.05 m (waist) ±3 cm". Move them into expect.bands, upgrade tools/tailor/qc_placement.py to read them (instead of its hardcoded landmark table), and add a vision pass on the ExportSnapshot3D PNG. That one move takes drape QC from eyeballed to gated.


4. Phased adoption

Phase Content Effort Why this order
1 extends overlay + expect block + G2 + G8 + orchestrator skeleton driving Blender stages only ~½ day Zero new subsystems, no MD, no game, no human in the loop. Immediately kills the 48-line config copy and the six-call ritual.
2 draft_garment.py template + md block + scripted ExportZPrj/FBX/ZPac in the same bridge call + G1 ~1 day Collapses 4 scripts to 1; MD half becomes unattended after the single session click.
3 G5 posed sweep (+ G3 as its rest-pose subset) ~1 day The money phase — catches the class of bug that has actually shipped, before the game.
4 G7 targeted reimport + ClothingTestBed set override + G6 motion QC + baseline blessing ~1 day Real shaders/import/AnimationTree; only meaningful after 13. Needs the small approved ariki-game edits.
5 Data-driven OutfitCatalog JSON deferred Revisit ~10 sets; G8 buys the safety now.

G4 rides along whenever convenient (it rarely bites for MD-authored garments) rather than blocking any phase.


5. Resolved decisions (Jeremy, 2026-07-31)

  1. Orchestrator home: tinqs/animation/clothing/garment.py; game stages shell out cross-repo to ariki-game/tools/game.sh.
  2. G6 judge: VLM/agent vision judge for first sign-off; bless those frames; re-runs are deterministic pixel diffs.
  3. OutfitCatalog: stay code-edit; pipeline emits Add() lines; add the G8 lint; JSON refactor deferred (~10 sets).
  4. ariki-game/src edits: small enablers approved (ClothingTestBed set override, later testbed hooks). The pari/piupiu weight-fix agent owns garment_pipeline.py's skin stage + those two configs — coordinate before touching either.

6. Leave alone

  • garment_pipeline.py's --stage CLI and config schema — additive only; the orchestrator wraps, never rewrites.
  • The skin stage internals and pari.json/piupiu.json weight fields — owned by the weight-fix agent right now.
  • md_recon.py — introspection tool, not a garment script; does not fold into the template.
  • .agents/wiki/architecture/clothing-lane.md — cite it; fold outcomes in on adoption; never fork its content.

7. Appendix — file/line references

Fact Where
Stage implementations clothing/garment_pipeline.py: census 224248, prepare 295505, fit 542598, reduce 653706, bake 712768, skin 908957 (weight modes 920944, skirt_bones 836905), export 964989 (vertex SHA1 984989)
Config parse / work dir / CLI garment_pipeline.py:1006, :36-39, :994-1008
MD bridge protocol/session tools/md_bridge.py (port/env :88, :22; run/ping :58-68), docs/md-bridge.md:8-94
Scriptable MD exports (flags) .claude/skills/marvelous-designer/SKILL.md:182-190
MD API lessons tools/tailor/md_tee_v1.py:12-28, md_skirt_v1.py:6-25
Drape QC thresholds as prose tools/tailor/md_pari.py:4; classifier tools/tailor/qc_placement.py:24-34, bands :51-80
Config diff piupiu vs piupiu_sb clothing/configs/piupiu.json / piupiu_sb.jsonname, body (_SkirtRig), parts.Piupiu.weights, export.set
Catalog path resolution + silent fallback ariki-game/src/Character/OutfitCatalog.cs:57-98 (warn-only 7178, BaseDirFor _ => 9697), Add() pattern in Initialize() 116+
Test bed buttons / defaults / load-fail HUD ariki-game/src/Testing/ClothingTestBed.cs:449-516, :81-84, :88-89 + :279-281
Spawn / engine / import / clean-import ariki-game/tools/game.sh:633-749, :39-70, :832-841 (+ asset check :848), :478-482
Agent API endpoints game.sh:215-242 (/console via AgentServer)
Visual QA harness (judge missing) ariki-game/tools/visual-qa.sh:1-77 — calls tools/vision-compare.py, which does not exist
Reimport gotcha (hand fix prescribed) clothing/README.md:143-147
Reference logic in older converters ariki-game/tools/cc_clothing_to_quaternius.py (skeleton-frame alignment 6673, KDTree copy 110127, A-pose check 198205), make_islander_outfits.py (region cuts 103149)

8. Build outcome (2026-07-31)

Built in one session by five parallel agents plus an integration pass. As-built interfaces are documented in clothing/PIPELINE-CONTRACT.md (marked AS-BUILT); the one-command usage and gate table are in clothing/README.md.

Lane Delivered
A clothing/garment.py — the orchestrator: extends resolution, deep-merge, path absolutization, 13 stages, gate scheduling, register codegen, --selftest
B gates/g2_census.py (island census sanity), gates/g8_catalog_lint.py (catalog↔asset lint, --all / --require-registered)
C gates/g5_posed_sweep.py — posed penetration + gape sweep; G3 is its --rest mode
D tools/tailor/draft_garment.py (MD script generator), tools/tailor/qc_placement.py, gates/g1_drape.py
E ariki-game/tools/targeted_reimport.sh (G7), ariki-game/tools/clothing_motion_qa.sh (G6), ClothingTestBed.ReportLoad

Acceptance test

configs/tests/piupiu_sb_test.json (scratch export target, set KapahakaSBTest) run end to end, census..export, --no-gate-stop:

Stage Time Gate
census 8.0 s G2 PASS — 1 island, 3620 v / 7104 tri, matched expect.islands
prepare 4.7 s
fit 3.3 s G3 FAIL — 1 vert, 8.0 mm deep, at rest
reduce 3.1 s
skin 3.4 s G5 FAIL — see below
export 3.3 s

G5 reproduced the shipped piupiu bug headlessly, in 14.9 s, from the real game clips (Idle_Loop from UAL1, Walk_Fwd_Loop from UAL2; 65/65 pack bones matched the RIG, the 16 skirt bones ride their parents):

rest                      1 penetrating vert   (max  8.0 mm)
Idle  @0/12/25/38/50/62  229 217 216 231 216 216   (max 75.2  85.2 mm)
Walk  @0/7/13/20/27/33   169 134  61  90 128  63   (max 70.0  87.9 mm)

That is the thigh punching through the skirt: ~1 vert visible in the pose the whole pipeline used to sign off on, and 216231 verts up to 8.5 cm deep the moment the character stands still and breathes. 13 failing frames, with front + closeup QA renders for six of them. The gate is worth its cost.

The pari neckline gape did NOT reproduce in the Blender checkpoint — it is not present in the skinned mesh G5 measures. Its origin is therefore in-game (attach, material, or LOD), which makes it G6 territory, not G5. Worth stating plainly because the pre-build assumption was that both shipped defects were the same class of bug.

Real findings the gates produced

  • G8 found an uncommitted OutfitCatalog registration hunk in the working tree — a garment registered locally and never pushed, which the lint surfaced immediately. On the test set it correctly reports KapahakaSBTest has no BaseDirFor arm and so would silently resolve into the Fantasy pack folder.
  • G1 measured the shipped garments against their own written QC targets and found both off: pari's hem sits +3.9 cm high, and the piupiu's bands +8 cm / +14 cm. Those targets had lived as prose in the garment scripts' headers since they were written; nobody had ever checked them against a render.
  • /console did not surface load failures at all until ClothingTestBed.ReportLoad was added — GD.Print does not feed the agent API's ring buffer. Every "the bed looks fine" sign-off before that fix was reading an empty channel.
  • G6's pixel diff has a measured floor: same outfit re-run moves ~0.002 of the frame, an entirely different outfit moves 0.014, and by frame 3 phase drift alone (0.0095) matches the signal. So G6 catches gross regressions (garment vanished, failed to load, swapped) and nothing subtler. G5 owns the subtle bug class — which is the right split, because G5 is headless, deterministic and 15 s.

Still deferred

Catalog-as-JSON (§2.4) — register still emits work/<name>/register.cs.txt for a human to paste, by design. Nothing in the build depends on changing that.