Files
animation/.claude/skills/marvelous-designer/references/tooling.md
T
jeremy d1030def4e docs+tools: working-file policy, lane pruner, and a human MD explainer
Two pieces of work.

1. Keep milestones, not steps. Staged NN_*.py lanes were saving a full
   ~80 MB .blend per attempt, so Lena's lane reached 3.0 GB of which 2.2 GB
   was 30 .blend files -- five snapshots to land one crotch fix, five more
   for the bra. The .py recipes are the real history; the blends are cache.

   - .agents/rules/working-files.md: four-tier policy (KEEP / MASTER /
     SCRATCH / UNKNOWN) + how to work a lane.
   - tools/prune_lane.py: classifies a lane and prunes the scratch tier.
     Dry-run by default. Reads a per-lane .lanekeep manifest, flags
     binaries byte-identical to a registered original (canonical name
     always survives a duplicate pair), and never auto-deletes a .blend
     with no step script beside it -- those cannot be rebuilt.
   - .gitignore: scratch patterns can never be committed.

   Dry run on characters/female/lena_nude reports 2.3 GB reclaimable.
   Not applied -- that lane had a live Blender session at the time.

2. .humans/marvelous-designer.html: how we author garments in Marvelous
   Designer, written for people rather than agents -- the six-step process,
   what has been made, the traps that cost hours, and what is still
   unsolved. Matches the .humans/ HTML convention in ariki-game.

Also committing the docs the AGENTS.md knowledge map and the new page
reference, so they are not dangling: the clothing-lane architecture page,
the marvelous-designer skill, and the two screenshots the page embeds.

characters/ is deliberately untracked and stays that way -- it holds GBs of
blends and GLBs, and .gitattributes does not LFS-track .blend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:05:46 -07:00

17 KiB
Raw Blame History

MD tooling — which call to reach for, when, and why

Companion to ../SKILL.md. That file is doctrine (how to author a garment). This file is tool selection: for a given question, which API answers it, why that one rather than the obvious alternative, and what it costs you when you guess.

Everything here is verified against MD 2026 Personal by running it. Where a belief was disproved by experiment, the disproof is kept — those are the expensive entries. Published Reallusion/CLO docs are thin and several signatures in them are wrong; tools/md_bridge/api_surface.json (688 functions) is the name truth, api_docs.json holds only ~48 harvested docstrings, so read __doc__ live for anything not listed below.


0. The diagnostic loop — the part that actually determines whether you succeed

The vision loop in SKILL.md is right but incomplete. Four passes, in this order, because each one can only see what the previous one can't hide:

Pass Call Catches
1. Pre-sim, zero frames ExportSnapshot3D right after ResetClothArrangement() Upside-down panels, tangles, self-intersecting outlines, wrong start height. Physics hasn't muddied anything yet.
2. Untextured skip SetBaseTextureMapImageGivenFilePath Folds and winding. See §1.3 — this is non-negotiable for shape work.
3. The back ExportTurntableImages(4), index 2 Anything on the rear. ExportSnapshot3D cannot show the back at all (§1.1).
4. Numbers export a throwaway OBJ, parse it (§2) Placement, span, coverage. Eyeballing placement is how garments shipped 814 cm off their own written targets.

A defect that survives to production is almost always one that pass 2 or 3 would have caught. Every screenshot in this repo before 2026-07-31 was a textured front view — pass 1 only — and a twisted seam, an inside-out panel, an asymmetric shoulder fold and an exposed back neckline all shipped underneath that blind spot.


1. Looking at the garment

1.1 SetCamViewPoint HAS NO REAR VIEW

Verified by shooting all eight:

n 0 1 2 3 4 5 6 7
view bottom front ¾ front front ¾ side top side side

There is no back. So ExportSnapshot3D is structurally incapable of showing the rear of a garment, no matter how you drive it. Use:

paths = export_api.ExportTurntableImages(4)   # 0 front, 1 side, 2 BACK, 3 side
  • The (int) overload ignores any path you pass and writes into MD's own output folder (%LOCALAPPDATA%\CLO Virtual Fashion\Marvelous Designer Personal\<n>\output*.png). The return value is the only way to find the files.
  • The documented (path, count, w, h, startIndex) overload returned [] — didn't work.
  • ExportCustomViewSnapshot(folder, w, h, prefix) also returned [].
  • Turntable reuses the same output*.png names every call, so if you are sweeping variants, shutil.copyfile each frame out immediately or you lose it.
  • SetCamViewPoint(5) (top-down) is the clearest angle on a shoulder join — nothing else shows whether front and back actually meet over the shoulder.
  • SetViewPoint() does nothing. Don't confuse the two.
  • Always set the viewpoint before a snapshot so shots are comparable.

1.2 Zoom in, and build contact sheets

The turntable renders 2500×2500. Crop to the region of interest and upscale with PIL before looking, or you will miss centimetre-scale defects. When comparing variants, tile them into one image with labels — one look at four labelled tiles beats four separate looks, and it makes the winner obvious.

1.3 Untextured renders show face orientation — the single best shape diagnostic

With the default sim fabric and no texture map, cloth renders WHITE on its front face and GREY on its back face.

  • A panel showing grey from outside is inside out.
  • A white streak on an otherwise grey panel is a fold exposing the true front face.
  • A busy motif hides both completely. A tāniko print concealed a twisted side seam through several rounds of wrong diagnosis; the untextured pass identified it in one look.

Judge shape untextured, then re-enable the texture only for the shipping render.


2. Measuring — you cannot introspect the mesh, so export and parse

GetClothPositions() is an out-param that stays empty from Python. There is no mesh access. So:

op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
export_api.ExportOBJ(throwaway_path, op)      # then parse the 'v ' lines yourself

Units are a trap. The two exporters disagree:

Path Units Conversion to metres
ExportOBJ millimetres × 0.001
ExportFBX → clothing pipeline census decimetres × 0.1 (this is what align.scale_z: 0.1 is doing)

Verified: an OBJ y of 1086.9 is 1.0869 m, checked against the known 1.777 m body. Getting this wrong wastes a full sim round-trip.

tools/tailor/qc_placement.py only works on solid silhouettes. Its pixel classifier needs a filled shape; on a strand skirt (mostly gaps) it reported a 1.71 m span, which is nonsense. For anything gappy, layered, or strand-based, measure the exported geometry instead. Useful derived numbers: z-span (band top, hem), fraction of verts above a bone-ring height, and per-height radius percentiles for ease.

GetLineLength(pattern, line) is the cheap way to confirm you are about to sew the edges you think you are. Fingerprint by expected length before every seam call — see §3.2.


3. Constructing patterns

3.1 Outlines, not partial seams

CreatePatternWithPoints with y+ = UP in 3D. Drafting y-down drapes the garment upside-down over the head; the symptom reads like a seam bug and isn't.

Cut features into the outline — neck gaps, armholes, and the teeth of a strand skirt. Partial seams twist unpredictably. A 32-strand piupiu was built as two comb-shaped panels with the strands cut into the outline and only the two band side edges sewn; the alternative (32 partial seams) is in the failure catalogue for a reason.

3.2 Line indices shift — compute them, never hardcode

Line i runs pts[i] → pts[i+1]. Inserting a point shifts every index after it. Hardcoded 0/5/7/9 broke this repo's bodice recipe twice. Build the point list with named landmarks and derive the indices:

n_arm = 1 if arm else 0
side_r = 6 + n_arm + 1
idx = {"shoulder_l": 0, "shoulder_r": 5,
       "side_r": side_r, "hem": side_r + 1, "side_l": side_r + 2}

Then verify against known lengths (shoulders ≈ 93 mm, sides ≈ 208 mm). If those drift, your index maths is wrong — not the cloth. This check turns a silent mis-sew into an immediate, obvious failure.

Paired edges must be EQUAL, not close. A strand skirt fingerprinted as [60.0, 60.075, 60.0, 60.075] and that 0.075 mm was dismissed as rounding. It wasn't: one band side edge was a true vertical and the other was slanted, because the outline gave a half-gap to the leftmost tooth but not the rightmost (the loop skipped the band-bottom step point on its first iteration, leaving that tooth flush with the panel edge and half a gap wider than every other). Sewing a flush tooth to a half-gapped one across a slanted seam notched the waistband and exposed the reverse face. Treat any inequality between paired edges as a construction bug.

3.3 Seam pairing — a decision table, because the naive rule is incomplete

AddSeamlinePairGroup(pf, lineF, pb, lineB, flagA, flagB). The two booleans reverse edge traversal. SKILL.md's rule — "same line index on mirrored front/back panels with (False, False)" — is correct but the word mirrored is load-bearing, and most recipes here draft both panels from the identical point list offset by dx, which is not mirrored.

Panels Pairing Flags Result
Mirrored same index (False, False) correct (the documented case)
Identical, not mirrored same index (True, True) correct — both edges reversed
Identical, not mirrored same index (False,False) / (0,1) / (1,0) twists the panel; part turns inside out and rucks up
Mirrored coords, list order kept same index any garment falls off — mirroring permutes the indices
Mirrored coords + remapped indices (shoulder_lshoulder_r, side_rside_l) crossed (False, False) correct, and the only thing that fixes an inside-out back panel
Cross every pair (sides and shoulders) crossed any slides to the hips — cross-wired straps cancel and it falls off the shoulders

Proven by sweeping all four flag combinations at fixed geometry. Two lessons worth internalising: a twisted seam and surplus cloth look identical when textured, and mirroring is only correct if you remap the pair indices with it.

3.4 Shape rules that are geometry, not physics

  • Bodices must taper. A rectangular panel cut for the bust carries its full bust width down to a hem sitting on a much smaller waist (1083 mm bust vs 675 mm waist here — ~400 mm of surplus) and the excess can only fold. Take it off each side edge at the hem. Use the same taper on both panels: side-seam length is sqrt(taper² + side_y²), so equal tapers keep whole-edge pairing matched and unequal ones skew it.
  • A pelvis-parented skirt must contain the whole leg-swing envelope. At a hem 0.5 m below the hip pivot, 30° of hip flexion sweeps the leg ~25 cm forward — more than any believable silhouette clears. Clearance alone cannot fix a knee-length skirt; discrete strands or a runtime bone rig has to do the rest.
  • Straps exist to cover the target body's baked-in underwear. If a neckline or scoop drops below the body's painted-on tank, the tank shows and reads as the garment failing to connect. Check the back neckline specifically — nothing was looking at it.

4. Fit and physics levers

Goal Call Why this one
Stand cloth off the skin pattern_api.SetAddlThicknessCollision(p, mm) The sim resolves the clearance, so it holds everywhere. Better than the downstream Blender normal-push, which self-intersects in concave regions — exactly between touching thighs. Set it before the settle.
Mesh / sim resolution SetParticleDistanceOfPattern(p, mm) Drives vert count and how many rows sit between skeleton joints. 20 mm gave ~29 rows on a 530 mm strand; 15 mm blew the vert budget.
Stop crumpling during the settle SetPatternStrengthen(p, True) → relax at the end Conditional — see §6.
Hold a waistband up SetPatternPieceElastic(p, line, bool) + SetPatternPieceElasticTotalLength(p, line, mm) Apply mid-settle, never from frame 0. Not needed if the band is cut smaller than the hips — the hip blocks it.
Fabric stiffness a different .zfab fabric_api has no physics setters at all — it is textures and metadata only. Stiffness ladder: V2_Woven_Canvas_1 < V2_Woven_Denim_1 < V2_Non-Fabric_Tyvek_1. Stock presets in C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\.

Signatures verified live (all (patternIdx, …), and the elastic family takes a line index as its second arg):

SetAddlThicknessCollision(int, float) -> None      GetAddlThicknessCollisionValue(int) -> float
SetParticleDistanceOfPattern(int, float) -> None   SetPatternPieceSolidifyStrengthen(int, float) -> None
SetPatternPieceElastic(int, int, bool) -> None     SetPatternPieceElasticTotalLength(int, int, float) -> None
SetPatternPieceElasticStrength(int, int, float)    SetPatternPieceElasticSegmentLength(int, int, float)
SetSimulationSelfCollisionAvoidanceStiffness(float)  SetAvatarSoftBodyStiffness(int, float)
GetLineLength(int, int) -> float                   ImportZprj(str, ApiTypes.ImportZPRJOption) -> bool

5. Arrangement — placement is a separate system from physics

Fact Consequence
SetArrangement() only assigns; utility_api.ResetClothArrangement() applies Skipping the apply = nothing moves.
Arrangement indices regenerate per avatar import Always look up by name from GetArrangementList(). Never hardcode.
SetArrangementPosition takes 4 ints A float raises TypeError.
The x argument's correct value DEPENDS ON THE ARRANGEMENT POINT FAMILY — verified both ways by sweep Body_*_Center_1 (bodice): the two panels must use the SAME x. Front 50 / back 0 folds one shoulder only and exposes the reverse face. Leg_Skirt_Front/Back (skirt): the two panels must use DIFFERENT x. 50/0 and 0/50 both drape correctly (band top 1.093 / 1.095) while 50/50 and 0/0 drop the skirt on the floor (0.10 / 0.14). So the split is load-bearing for skirts and a bug for bodices. Never harmonise the two recipes — and sweep with a control before changing this value on a new garment.
Skirts belong on Leg_Skirt_Front / Leg_Skirt_Back, y=92 Using Body_*_Waist instead put a skirt 814 cm high. Switching fixed placement to within 9 mm.
A light garment does not slide into place MD materialises cloth at the arrangement point; it does not simulate donning. A heavy solid panel settles onto the hips, but a 60 mm waistband with light strands stays exactly where it is arranged — the first strand-skirt drape sat at the chest. For light garments, arrangement height IS the placement.
utility_api.NewProject() deletes the avatar Re-import after.

6. Rules in SKILL.md that need a condition attached

  • "Soft-from-frame-0 drapes bunch" → true, and strengthening does flatten a bunched back. But SetPatternStrengthen rotates an under-constrained garment: a bodice whose sides are sewn only over the lower half and whose straps are 90 mm wide came out flat and twisted, with an uneven hem and skin showing. Pre-sim was clean, so it develops during the settle. The rule applies to garments whose sides are fully sewn. Where it doesn't, remove the surplus instead of stiffening.
  • "Pair the same line index with (False, False)" → only for mirrored panels. See the table in §3.3.

7. Harness patterns for iterating

Sweep variants in ONE bridge call. Each --file round trip costs a session turn; a loop with NewProject() per iteration costs one. Four 250-frame sims ≈ 4 minutes and answers a question that four guesses would not.

for value in SWEEP:
    utility_api.NewProject(); import_api.ImportFBX(AVATAR_FBX, op)   # avatar first
    ...build, sew, arrange, Simulate(250)...
    shots = export_api.ExportTurntableImages(4)
    shutil.copyfile(shots[2], f"...{value}_back.png")    # names are reused — copy now

Read pass/fail from GEOMETRY, not images, where you can. A garment on the floor measures band_top < 0.3 m. Have the sweep parse its own probe OBJ and self-report which cells even stayed on the body — then you only open images for the survivors.

Always include a control case that reproduces the known-good result. A sweep whose control also fails tells you the harness is broken, not the geometry — tools/tailor/md_pari_armsweep.py dropped the garment on the floor in all four cells including its control, so every one of its results was void. md_pari_seamsweep.py had a valid control and produced a decisive answer. Without a control you cannot tell those two situations apart, and you will "learn" something false.

Guard the export. Keep EXPORT = False until a snapshot looks right, so a look-first run cannot overwrite shipped files.

Timeouts: roughly 1 minute per 300 simulated frames; pass --timeout 880 for anything with several sims.

Session mechanics: a human must click Plugin → TinqsMDBridge; MD's UI is frozen for the whole session ("Not Responding" is normal); python tools/md_bridge.py --stop hands it back and works even though the UI is dead. Stop when you hand back a result — Jeremy inspects in the viewport and can't while the bridge owns the thread.

Writing recipe files from Python: these files contain box-drawing and em-dash characters. Always io.open(path, encoding='utf-8'). A default-codec round trip (cp1252) corrupted a recipe and the bridge then failed to read it at all.


8. Exits

ExportZPrj (editable source of truth), ExportFBX, ExportOBJ, ExportZPac (outfit assembly), ExportAlembic / ExportUSD, ExportAnimationVideo.

No glTF export. No SaveProjectFile — saving is ExportZPrj. EveryWear and the AI Image Generator are GUI-only, absent from the API, so the downstream Blender half of the lane cannot be replaced by them.

Avatar getters live in export_api (GetAvatarCount, GetAvatarNameList), not utility_api where you would look for them.

There is an animation surface worth knowing about but unverified: SetStartAnimationFrame / SetEndAnimationFrame / SetCurrentAnimationFrame / RunAnimationRecording / GetAnimationLayerFrameRange, plus ExportAlembic. If a game clip's motion can be driven onto the avatar, this yields a per-frame cloth cache with zero penetration by construction — the reference a skinned garment should be scored against. Not yet tested: whether ImportFBX's options can bring animation in with the avatar is unknown.