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