# Clothing Pipeline — garment mesh → game-ready outfit part

**What this is:** a headless-Blender pipeline that takes a clothing mesh from any
source (Marvelous Designer export, marketplace FBX/OBJ/GLB, hand-modeled) and turns
it into outfit-part GLBs that ariki-game characters actually wear — fitted to the
real body, reduced to game budget, skinned to the shared 65-bone Quaternius
skeleton, and registered in the game's `OutfitCatalog`.

Built 2026-07-30 after the CC5/Reallusion clothing lane was investigated and
rejected (CC5 provably refuses clothing tools on AccuRig "Humanoid" characters —
verified via its Python API *and* GUI; see `ariki-game` session notes). Everything
here is Blender-only, deterministic, and agent-drivable end to end.

## Why each stage exists

A downloaded garment is useless to the game for four reasons; the pipeline is the
four fixes in order:

| Problem | Fix | Stage |
|---|---|---|
| Not shaped like our character | Shrinkwrap-fit to the real body GLB (inflated "inner shell" target guarantees clearance) | `fit` |
| Millions of tris vs ~1.5k/part budget | Split by material into parts, rebuild/decimate, bake lost detail to normal+AO maps | `reduce`, `bake` |
| No idea how to move | Copy skin weights from the character's own body (special cone-proxy recipe for skirts) | `skin` |
| Game has never heard of it | Export per-slot GLBs on the shared skeleton + register in `OutfitCatalog` | `export` + manual catalog entry |

## Design decisions (and where they came from)

Reviewed by GLM-5.2 and glm-4.6 before implementation
(`../garments/glm-advice-glm52.md`, `../garments/glm-advice.md`) plus web research
on Marvelous Designer game workflows. Key adopted corrections:

1. **Fit BEFORE decimate, bake AFTER** — shrinkwrapping the high-poly garment
   molds real pleat geometry; fitting a faceted low-poly loses the silhouette.
2. **Pleated skirts are not decimated** — a clean low-poly cylinder proxy is
   generated and shrinkwrapped to the high-poly skirt; pleats live in the baked
   normal map (at 1.5k tris geometric pleats read as noise).
3. **Skirt weights come from a "cone proxy"** — a leg-bridging tapered cylinder
   weighted with a Z-gradient (body weights at waist → pelvis-only at hem), then
   transferred to the skirt. Kills the hard left/right-leg seam that tears skirts
   in walk cycles. Forward-compatible with skirt bones later.
4. **Clearance via inner shell** — garments wrap to a ~4mm inflated copy of the
   body, not the body itself: mathematical clearance, not hope.
5. **Layer stacking** — bow wraps to bodice, bodice wraps to body shell
   (cumulative offsets), so layered cloth keeps separation.
6. **Weight transfer uses Data Transfer 'nearest face interpolated'**, not raw
   nearest-vertex KDTree (fails on overhangs like collars/bow tails).
7. Per-part tri budgets vary (bow ~400, bodice ~1200, skirt ~2200);
   verify in the game test bed EARLY (after skin), not at the end.

Sources also agree: if a garment came from Marvelous Designer, re-exporting from
MD with lower particle density / MD's own quad remesh skips the worst reduction
pain at the source. Prefer that when the .zprj is available.

## Files

| File | What |
|---|---|
| `garment_pipeline.py` | The whole pipeline. One Blender headless script, staged; each stage checkpoints a .blend into `work/` so stages can be re-run/iterated independently. |
| `configs/<garment>.json` | Per-garment recipe: source file, island→part mapping, slots, budgets, alignment, fit, cuts. The pipeline is data-driven; new garment = new config, not new code. |
| `work/` | Intermediates (checkpoint .blends, census data, QA renders). Disposable, gitignored. |
| `README.md` | This file. |

## Stages as-built (2026-07-30, after the dress pilot)

| Stage | What it does | Hard lessons baked in |
|---|---|---|
| `census` | Import garment, drop configured junk materials, weld, split into loose **islands**, render each in a distinct color + write `work/<name>/census.json` | MD material names lie (all fabric on one material; 4M faces were topstitch threads → `drop_materials`). Parts are configured as island index lists, not material names. |
| `prepare` | Import body+rig, build configured parts from islands, **bodyshell** parts cut from the body itself, sleeve pose-warp (garment-local, pre-align), anisotropic align (`scale_xy`/`scale_z` — heroic bodies are wide, not tall), radial `torso_boost`, post-align `cut_above_z`/`cut_outboard_x`, `prune_scraps_x` (drop disconnected leftovers by centroid), fabric `texture` on MD's own UVs | Fitted tops on a bust bigger than the garment shred under any shrinkwrap — use a **bodyshell** part (body region copy + normal offset, Islander technique): perfect fit and weights by construction. Cuts must run post-align in body space. MD's exported UVs already sample the fabric PNG correctly — leave `texture_offset` at zero, and judge any change in the game, since Workbench QA renders ignore shader Mapping nodes. |
| `fit` | Inflated `BODY_SHELL` (+4mm) target; shrinkwrap mode **OUTSIDE** (push out penetrating verts ONLY — never snap the whole cloth, that shreds layered garments); optional `hem_mm` adds a second OUTSIDE pass masked 0 at hip → 1 at hem for graded leg clearance on skirts | `OUTSIDE_SURFACE` destroyed multi-layer bodices; `OUTSIDE` unmasked is safe everywhere since it can't pull cloth inward. A uniform 4mm fit clips in walk cycles — hems need 15–20mm where legs travel (`hem_mm`), while the waist stays snug. |
| `reduce` | Keep `HI_<part>` copy for baking; `weld_band` fuses stacked layers (waistband/belt sandwiches) before decimation; planar dissolve → collapse to per-part budget; optional cylinder proxy for standalone skirts | Decimating coincident stacked layers = swiss cheese; protecting zones from the decimator just moves the damage elsewhere — **weld the sandwich first**. |
| `bake` | (optional, Cycles) normals+AO from HI onto a second UV map | |
| `skin` | Data Transfer (nearest-face-interpolated) from body; `weights: dress` adds pelvis-gradient below hip so the hem doesn't tear between legs; bodyshell parts keep inherited weights; smooth+normalize; bind to RIG | |
| `export` | One GLB per outfit slot (multi-part slots joined by selection), full 65-bone armature included, vertex hash printed for determinism | |

Related repo tooling this builds on (in `ariki-game/tools/`):
`cc_clothing_to_quaternius.py` (earlier one-shot converter — superseded by this
for production but its skeleton-frame alignment math is the reference),
`make_islander_outfits.py` (KDTree weight-copy precedent + tri budgets),
`make_fitted_body.py` / `race-body-fitting` skill (measure-then-warp doctrine).

## Usage

```bash
BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe"
cd /c/Users/Jeremy/tinqs/animation/clothing

# run stages in order (each saves work/<NN>_<stage>.blend + QA renders)
"$BLENDER" --background --python garment_pipeline.py -- --config configs/dress.json --stage prepare
"$BLENDER" --background --python garment_pipeline.py -- --config configs/dress.json --stage fit
"$BLENDER" --background --python garment_pipeline.py -- --config configs/dress.json --stage reduce
"$BLENDER" --background --python garment_pipeline.py -- --config configs/dress.json --stage bake    # optional, slow (Cycles)
"$BLENDER" --background --python garment_pipeline.py -- --config configs/dress.json --stage skin
"$BLENDER" --background --python garment_pipeline.py -- --config configs/dress.json --stage export
```

Outputs land in the config's `export.out_dir` (default
`ariki-game/assets/quaternius/outfits/<set>/`), one GLB per outfit slot, ready for
an `OutfitCatalog.Register` entry (see `ariki-game/src/Character/OutfitCatalog.cs`
`Initialize()` — path pattern `{Gender}_{Set}_{Slot}`).

## Shipped garments

| Config | Source | In game as | Notes |
|---|---|---|---|
| `pari.json` | `../tools/tailor/lena_pari_v3_garment.fbx` (MD, ours) | `Kapahaka` / Body | Tāniko bodice. First **MD-authored** garment through the pipeline (2026-07-31) — the upstream/downstream seam. |
| `piupiu.json` | `../tools/tailor/lena_piupiu_v2_garment.fbx` (MD, ours) | `Kapahaka` / Legs | Flax skirt, `hem_mm: 14`. |
| `dress.json` | `../garments/thin-unweld.fbx` (downloaded) | `MDDress` / Body | The pilot: 2.49M verts, 12 materials, bodice + belt/bow + pleated skirt. |

MD-authored garments are the easy case and confirm the `clothing-lane.md`
prediction: both kapa haka pieces arrived at game budget already (their 4k/8k tri
ceilings never bit) and needed no alignment beyond a `z_nudge`, because they were
draped on `Ariki_Female_QuatSkin.glb` itself. `reduce` is only the hard stage for
downloaded meshes.

Deferred on the dress: the sleeved jacket (`cape*` materials) — sleeves need a
pose-warp to T-pose (GLM advice §4 has the recipe). Unknown `Material*` slots are
excluded pending the per-material stats `prepare` prints; reassign them in the
config if they turn out to be buttons/trim.

## QA renders only prove the pose that cannot fail

Every `qa_*` render this pipeline writes is the **rest pose** — the pose the
garment was fitted in. A garment that clears the body perfectly there can still
open at the neckline or let a limb through the moment the skeleton moves, because
the weights (not the fit) decide that. Both kapa haka pieces did exactly this: the
Blender QA showed a closed scoop neck and a hem covering both thighs; in the game
bed, the neckline opens over the sternum and a thigh comes through the skirt.

So `skin` is not the last checkpoint — **the game test bed is**, and it has to be
driven through more than one clip (`Idle` / `Walk` / `Dance` buttons in
`ClothingTestBed`; the AnimationTree used to override them, fixed 2026-07-31).
Treat a rest-pose-only sign-off as unverified.

## Unified orchestrator + QC gates (2026-07-31)

The stage-at-a-time invocations above still work and still own the logic. What
changed is that you no longer have to type them, and that each seam now has a
check that fails **before** the defect ships.

```bash
cd /c/Users/Jeremy/tinqs/animation
python clothing/garment.py configs/piupiu.json --from census --to export
```

`garment.py` is a driver, not a rewrite: every stage shells out to the tool that
already owns it (`garment_pipeline.py` for Blender, `md_bridge.py` for MD, agent
E's scripts for the game). It resolves the config's `extends` chain, absolutizes
its paths and writes `work/<name>/resolved.json` — every stage and gate is then
handed that one file, never the raw config.

| | Stage | Runs where |
|---|---|---|
| upstream | `draft` `drape` `publish` | Marvelous Designer, over the MD bridge |
| core | `census` `prepare` `fit` `reduce` `bake` `skin` `export` | Blender headless |
| downstream | `register` `import` `verify` | ariki-game |

Default range is `census..export` — the Blender half. MD and game stages run only
when you ask for them by name (`--from`/`--to`/`--only`). Other flags worth
knowing: `--list` (what would run, and which gates are armed), `--dry-run` (print
the exact commands), `--no-gate-stop` / `--gate-stop` (override the QC mode's
stop-on-failure behaviour), `--selftest`.

### QC modes — `--qc light` (default) and `--qc deep`

Two ways to use the pipeline, two costs.

```bash
python clothing/garment.py configs/x.json --from census --to export        # light
python clothing/garment.py configs/x.json --qc deep --from census --to export
```

**light** is the iterate-on-a-garment loop: one clip (`Walk`), two frames, one
anim pack, two QA renders, and a failing gate **reports instead of stopping** so
one run tells you everything that is wrong with a work-in-progress garment.
**deep** is the sign-off sweep: `Idle Walk Dance`, eight frames each, both packs,
the synthetic extremes appended on top, twelve renders, and a failing gate stops
the run. Measured end to end on `configs/tests/piupiu_sb_test.json`,
`--from census --to export` (Blender 5.1, this box):

| | census | g2 | prepare | fit | **G3** | reduce | skin | **G5** | export | **total** |
|---|---|---|---|---|---|---|---|---|---|---|
| light | 3.3s | 0.1s | 4.2s | 3.3s | 2.9s | 3.2s | 3.6s | **9.9s** | 2.8s | **33.3s** |
| deep | 3.1s | 1.1s | 4.0s | 3.1s | 2.9s | 3.1s | 3.4s | **19.1s** | 2.8s | **42.6s** |

All of the difference is G5: 13 s of gate time in light (1 rest + 2 Walk frames,
UAL2 only) against 23 s in deep (1 rest + 24 clip frames + 4 synthetic extremes,
both packs). ~7 s of light's G5 is importing the one anim pack — loading UAL1 as
well costs another ~7 s and buys nothing when only `Walk` is sampled, and `Walk`
is aliased onto **UAL2's** `Walk_Fwd_Loop`. Light still catches the shipped
thigh-poke on both of its Walk frames (168 and 90 verts inside the body, ~80 mm
deep) — the bug fires on every frame, which is exactly why two are enough to
iterate against. Deep is what tells you the whole shape of it: worst frame 231
verts (Idle), the Dance clip light never samples, and a synthetic `step_thigh_60`
extreme at 187 verts / 95 mm.

A mode only ever changes **how hard the pipeline looks** — the sampling and cost
knobs. It can never touch a threshold (`depth_mm`, `max_verts`, gape budgets):
those are the config's, so a garment that passes light and fails deep failed on
frames light did not sample, never on a moved goalpost. Per-garment overrides go
in the config's `qc` block; full precedence and the key whitelist are in
[`PIPELINE-CONTRACT.md`](PIPELINE-CONTRACT.md#qc-modes-as-built).

Omitting `--qc` gives you light plus a one-line reminder at the end of the run
(`QC mode: light -- run --qc deep before sign-off`). Run deep before you bless a
baseline or register a set.

### The gates

A gate runs after its stage, and only when the config carries the `expect` block
that gives it thresholds — no thresholds, no opinion. All of them write
`work/<name>/qc/<gate>.json` and speak one exit code: **0 pass · 2 fail · 3 could
not evaluate**.

| Gate | After | What it catches |
|---|---|---|
| G1 | `drape` | Band placement in the MD snapshot — hem/top edge off its target in metres. Found the shipped pari hem sitting **+3.9 cm** and the piupiu bands **+8/+14 cm** above their own written targets. Needs `expect.bands` (+ a `drape.mask` palette; the default heuristic counts shaded skin as fabric). |
| G2 | `census` | An MD re-export renumbering islands, so `parts[*].islands` now points at the wrong geometry and every later stage builds the wrong garment. Needs `expect.islands` — in **census units** (pre-`align.scale_*`), read straight off `census.json`. |
| G3 | `fit` | Rest-pose clearance. Same script as G5 (`g5_posed_sweep.py --rest`) — there is no `g3_*.py` — and it writes **`qc/g3.json`**. Needs `expect.penetration`. |
| G5 | `skin` | **The one that matters.** Poses the skinned checkpoint with the real game clips and counts garment verts inside the body, per frame. This is the gate that turns "QA renders only prove the pose that cannot fail" (above) into a number. Needs `expect.penetration`. |
| G8 | `register` | Catalog↔asset lint: a missing `.gltf` (warn-only at runtime — the character just goes naked), a set with no `BaseDirFor` arm (silently resolves to the *Fantasy* pack folder), duplicate ids, unregistered exports. Always runs. |
| G7 | `import` | `targeted_reimport.sh` — deletes just this asset's `.godot/imported/` entries, re-imports, and verifies they came back fresh. The blunt fix for the `.bin`-hash problem in "Known limitations" below, without a 17k-entry full reimport. |
| G6 | `verify` | `clothing_motion_qa.sh` — spawns `ClothingTestBed` on the set, drives Idle/Walk/Dance, captures frames, and fails on any outfit load error. **Spawns with `SPAWN_BUILD=0`** — after editing game C# (catalog/bed entries), run `bash tools/game.sh build` first or G6 silently QAs the stale binary (its fallback messages will quote constants your edit already changed — that's the tell). |

G5 is the only gate that runs under Blender; the rest are plain `python`.

### First sign-off and blessing (G6)

G6's hard signal is deterministic (an outfit that failed to load says so). Its
soft signal is a pixel diff against a blessed baseline, and that has a measured
floor: re-running the same outfit moves ~0.002 of the frame, swapping the outfit
**entirely** moves 0.014. So the diff is a regression tripwire for "the garment
vanished or grossly changed" — it will not see a gaping neckline. G5 owns that
class of bug.

Which means the first sign-off on a new garment is a human or VLM looking at the
frames, and the flow is deliberately two-step:

```bash
# 1. no baseline yet -> captures evidence, no diff, and tells you to go look
python clothing/garment.py configs/piupiu.json --only verify

# 2. once the frames are actually good, freeze them
GARMENT_BLESS=1 python clothing/garment.py configs/piupiu.json --only verify
#   (or --bless-baseline)

# 3. every later run diffs against that baseline automatically
python clothing/garment.py configs/piupiu.json --only verify
```

Baselines live in `clothing/baselines/<set-lowercased>/`. Blessing before looking
freezes a defect as the thing every future run is compared to — that is why step 1
refuses to bless for you.

Interfaces, schemas and the full `expect` reference:
[`PIPELINE-CONTRACT.md`](PIPELINE-CONTRACT.md).

## Known limitations (v1)

- Skirt will still favor standing/dance poses; deep leg swings compress the hem
  (no skirt bones yet — the Z-gradient weights are ready for them later).
- Albedo is the MD fabric PNG re-applied over MD's own exported UVs (part
  `texture` in the config); the bake stage still only produces normal+AO. Parts
  without a `texture` fall back to the flat `color`.
- Body stays under garments (no hide-mask system in game yet) — clearance is the
  only poke-through defense; report tunnel-through in the test bed per animation.
- Re-export alone does not reach the game. `GLTF_SEPARATE` keeps geometry and UVs
  in the sidecar `.bin`, but Godot's reimport check hashes only the `.gltf`, so a
  vertex- or UV-only change is silently skipped. Delete that asset's entries in
  `.godot/imported/` (those two files only — never the folder) and run
  `godot --headless --import --path .` to force it.
