Files
animation/clothing
ozan be1ee94701 feat(skirt-weights): partial-coverage + pants-top modes for the ariki-game pugu lane
Two env-gated extensions, defaults unchanged (full skirts behave exactly as before):
- SKIRT_ALLOW_DEAD=1 — a pugu loincloth has OPEN sides by design; its bare side
  strands downgrade the dead-strand FATAL to a warning.
- SKIRT_PANTS_TOP=<frac> + SKIRT_PANTS_MIX (0.65) — the garment's top fraction
  is skinned like the pack's own Peasant pants (measured: crotch cloth = 49/49
  both thighs, pelvis ≈ 0 below the waistband), mixed with the strand weights
  so leg colliders keep authority; blends into pure strands below.

Consumed by ariki-game tools/build_pugu_tifi.py garments (pugu/tapa skirts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLpZsSvLufgVri2bhvmAS6
2026-08-19 03:49:25 +01:00
..

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 1520mm 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

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.

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.

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.

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:

# 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.

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.