d1030def4e
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>
311 lines
17 KiB
Markdown
311 lines
17 KiB
Markdown
# 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 8–14 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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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_l`↔`shoulder_r`, `side_r`↔`side_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 **8–14 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.
|
||
|
||
```python
|
||
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.
|