6383 lines
278 KiB
Plaintext
6383 lines
278 KiB
Plaintext
|
|
|
||
|
|
===== GLM agent start 2026-07-30 21:35:35 model=glm-5.2 budget=120.0min =====
|
||
|
|
|
||
|
|
--- step 1 (120 min left) ---
|
||
|
|
COMMAND: cat clothing/README.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
# 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) | 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. |
|
||
|
|
| `fit` | Inflated `BODY_SHELL` (+4mm) target; shrinkwrap mode **OUTSIDE** (push out penetrating verts ONLY — never snap the whole cloth, that shreds layered garments) | `OUTSIDE_SURFACE` destroyed multi-layer bodices; `OUTSIDE` unmasked is safe everywhere since it can't pull cloth inward. |
|
||
|
|
| `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_qua
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 2 (120 min left) ---
|
||
|
|
COMMAND: cat clothing/README.md | tail -n +100
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Current pilot
|
||
|
|
|
||
|
|
`configs/dress.json` → `../garments/thin-unweld.fbx`: an MD dress ensemble
|
||
|
|
(2.49M verts, 12 materials). v1 scope: bodice + belt/bow (Body slot) + pleated
|
||
|
|
skirt (Legs slot). The sleeved jacket (`cape*` materials) is deferred — sleeves
|
||
|
|
need a pose-warp to T-pose (GLM advice §4 has the recipe) and is round two.
|
||
|
|
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.
|
||
|
|
|
||
|
|
## 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).
|
||
|
|
- Flat MD colors until someone supplies textures (bake stage produces normal+AO,
|
||
|
|
not albedo).
|
||
|
|
- 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.
|
||
|
|
|
||
|
|
--- step 3 (120 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 4 (120 min left) ---
|
||
|
|
COMMAND: cat .claude/skills/marvelous-designer/SKILL.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
---
|
||
|
|
name: marvelous-designer
|
||
|
|
description: Authoring garments in Marvelous Designer agentically — driving MD's Python API over the TinqsMDBridge socket plugin, drafting patterns from a reference image and Lena's measurements, draping/QC-ing them on the game body, and exporting meshes for the clothing pipeline. Use for any "make/adjust a garment", "match this clothing photo", or "drive Marvelous Designer" task, and read before touching tools/md_bridge* or tools/tailor/.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Marvelous Designer — agentic garment authoring
|
||
|
|
|
||
|
|
The **upstream half** of the clothing lane: reference image (or a description) →
|
||
|
|
sewn, draped, textured garment mesh fitted to the game body. The downstream half
|
||
|
|
(garment mesh → game-ready skinned GLB) is `clothing/` — see
|
||
|
|
`.agents/wiki/architecture/clothing-lane.md` for how they meet.
|
||
|
|
|
||
|
|
Everything here was learned by doing it on 2026-07-30 (tee, skirt, kapa haka
|
||
|
|
pari + piupiu). The API facts are **verified against MD 2026 Personal**, not
|
||
|
|
docs — Reallusion/CLO's published API docs are thin and several signatures in
|
||
|
|
them are wrong.
|
||
|
|
|
||
|
|
## The 60-second model
|
||
|
|
|
||
|
|
- MD is driven through **`tools/md_bridge.py`** → a socket plugin running inside
|
||
|
|
MD (`tools/md_bridge/TinqsMDBridge.py`). Full protocol: `docs/md-bridge.md`.
|
||
|
|
- A garment = **flat 2D panels** (`CreatePatternWithPoints`) + **seams**
|
||
|
|
(`AddSeamlinePairGroup`) + **arrangement points** on the avatar
|
||
|
|
(`SetArrangement`), then **simulate** (`utility_api.Simulate(frames)`) to drape
|
||
|
|
cloth onto the body.
|
||
|
|
- You see results by rendering the viewport to PNG (`ExportSnapshot3D`) and
|
||
|
|
reading the image back. **This vision loop is the whole method** — draft,
|
||
|
|
drape, look, measure, adjust, repeat.
|
||
|
|
- Garment identity for traditional wear is mostly **textiles, not tailoring**.
|
||
|
|
Kapa haka / Mexica / Pacific garments are rectangles + blocks; the design lives
|
||
|
|
in generated texture maps. Model the shape simply, spend effort on the pattern.
|
||
|
|
|
||
|
|
## Session protocol (read first — MD's UI freezes)
|
||
|
|
|
||
|
|
MD's embedded Python **does not run background threads**, so the bridge owns the
|
||
|
|
main thread while it serves. Consequences:
|
||
|
|
|
||
|
|
1. **A human must click** Plugin → TinqsMDBridge to start a session. You cannot
|
||
|
|
start one yourself. Ask, then wait.
|
||
|
|
2. **MD's UI is frozen** for the whole session ("Not Responding" is normal).
|
||
|
|
3. End with `python tools/md_bridge.py --stop` — works from the terminal even
|
||
|
|
though the UI is dead. Idle timeout is 4 h (was 300 s; raised so one click
|
||
|
|
lasts a work session).
|
||
|
|
4. **Pause the session whenever Jeremy wants to look at the model.** He inspects
|
||
|
|
in MD's viewport; he can't while you hold the thread. Default to stopping when
|
||
|
|
you hand back a result, unless he said to stay in.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python tools/md_bridge.py --ping # verify + see mode/api modules
|
||
|
|
python tools/md_bridge.py --exec "result = pattern_api.GetPatternCount()"
|
||
|
|
python tools/md_bridge.py --file tools/tailor/md_pari.py --timeout 500
|
||
|
|
python tools/md_bridge.py --stop # give the UI back
|
||
|
|
```
|
||
|
|
|
||
|
|
Long `Simulate()` calls need a raised client `--timeout` (~1 min per 300 frames).
|
||
|
|
The exec namespace persists **within** a session, not across; MD api modules
|
||
|
|
(`pattern_api`, `import_api`, `export_api`, `fabric_api`, `utility_api`,
|
||
|
|
`ApiTypes`) plus a `BRIDGE` info dict are pre-seeded.
|
||
|
|
|
||
|
|
**Introspect, don't trust docs.** Full surface dump lives at
|
||
|
|
`tools/md_bridge/api_surface.json` (688 functions) and harvested docstrings at
|
||
|
|
`tools/md_bridge/api_docs.json`. Overloaded pybind11 functions break
|
||
|
|
`inspect.signature()` — read `__doc__` instead.
|
||
|
|
|
||
|
|
## API facts that cost hours to find
|
||
|
|
|
||
|
|
| Fact | Consequence |
|
||
|
|
|---|---|
|
||
|
|
| **2D pattern y+ maps to UP in 3D** | Panels drafted y-down drape **upside-down over the head** and tangle. Symptom looks like a seam bug; it isn't. Hem at y=0, neckline at high y. |
|
||
|
|
| Units are **mm**; gravity −9800 | A "500" square is 50 cm. |
|
||
|
|
| Blender-exported FBX avatars import **10× small** | Import with `op.scale = 10.0`. |
|
||
|
|
| `ImportAvatar()` is **.avt only** — returns `False` on FBX | Use **`import_api.ImportFBX(path, op)`** for the game body. |
|
||
|
|
| `op.bAddArrangementPoints = True` | Auto-generates ~98 named arrangement points on a custom avatar. Without it you have nowhere to hang cloth. |
|
||
|
|
| `SetArrangement()` only *assigns*; **`utility_api.ResetClothArrangement()` applies** it | Skipping the apply = nothing moves. (`ReDrape3DArrangement` only materializes not-yet-draped cloth.) |
|
||
|
|
| Arrangement **indices regenerate** per avatar import | Always look up by name from `GetArrangementList()`; never hardcode an index. Offsets aren't stable either — verify with a 0-frame snapshot. |
|
||
|
|
| Avatar getters live in **`export_api`** (`GetAvatarCount`, `GetAvatarNameList`) | Not `utility_api`, where you'd look. |
|
||
|
|
| `utility_api.NewProject()` **deletes the avatar** | Re-import after. |
|
||
|
|
| `SetBaseTextureMapImageGivenFilePath(path, fabricIdx)` — **path is arg0** | Reversed args throw a TypeError that reads like a missing overload. |
|
||
|
|
| **PNG DPI sets a texture's physical size in MD** | 1024 px at 54.2 dpi = 480 mm of cloth. Control tiling by setting dpi in PIL, not by scaling the image. |
|
||
|
|
| `fabric_api.AddFabric()` needs a **`.zfab` file path** | A name string silently fails. Stock presets: `C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\`. |
|
||
|
|
| Fabric **index 0 is the shared default** | Coloring it dyes every garment in the scene. Always `AddFabric` a new one. |
|
||
|
|
| `AssignFabricToPattern()` returns False for every arg order tried | Use **`pattern_api.SetPatternPieceFabricIndex(pattern, fabric)`**. |
|
||
|
|
| `SetViewPoint()` does nothing; **`SetCamViewPoint(2)` = front view** | Call before every QC snapshot so shots are comparable. |
|
||
|
|
| `GetClothPositions()` is an out-param that stays empty from Python | No mesh introspection — measure from rendered images instead (see QC). |
|
||
|
|
| No `SaveProjectFile`; no glTF export; no EveryWear in the API | Save = `export_api.ExportZPrj`. Exits: `ExportFBX` /
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 5 (120 min left) ---
|
||
|
|
COMMAND: cat .claude/skills/marvelous-designer/SKILL.md | sed -n '/Handing off downstream/,/^## /p' | head -80
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Handing off downstream
|
||
|
|
|
||
|
|
Export three ways per garment, into `tools/tailor/`:
|
||
|
|
|
||
|
|
```python
|
||
|
|
export_api.ExportZPrj(".../lena_<garment>_v<N>.zprj") # editable source of truth
|
||
|
|
op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
|
||
|
|
export_api.ExportFBX(".../lena_<garment>_v<N>_garment.fbx", op) # for clothing/
|
||
|
|
export_api.ExportOBJ(".../lena_<garment>_v<N>_garment.obj", op)
|
||
|
|
export_api.ExportZPac(".../lena_<garment>_v<N>.zpac") # for outfit assembly
|
||
|
|
```
|
||
|
|
|
||
|
|
Then the garment goes through `clothing/garment_pipeline.py` (fit → reduce →
|
||
|
|
skin → export) to become a game outfit part. **Two advantages MD-authored
|
||
|
|
garments have over downloaded ones**, worth exploiting in the config:
|
||
|
|
|
||
|
|
- **They're already game budget.** These export at 1.2k–3.7k verts; the
|
||
|
|
downloaded MD dress the clothing pipeline was piloted on is 2.49 M verts /
|
||
|
|
110 MB. The brutal `reduce` stage is mostly unnecessary — start with a high
|
||
|
|
tri budget and only decimate if the test bed complains.
|
||
|
|
- **They're already fitted**, having been draped on the actual game body, so
|
||
|
|
`align`/`fit` needs little more than the clearance shell.
|
||
|
|
|
||
|
|
Keep the `.zprj` — regenerating a variant beats re-authoring, and the clothing
|
||
|
|
README explicitly prefers re-exporting from MD over fighting reduction.
|
||
|
|
|
||
|
|
## Failure catalogue (don't rediscover these)
|
||
|
|
|
||
|
|
--- step 6 (120 min left) ---
|
||
|
|
COMMAND: ls -la tools/tailor/lena_pari_v3_garment.fbx tools/tailor/lena_piupiu_v2_garment.fbx
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 149824 Jul 30 21:13 tools/tailor/lena_pari_v3_garment.fbx
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 271568 Jul 30 21:04 tools/tailor/lena_piupiu_v2_garment.fbx
|
||
|
|
|
||
|
|
--- step 7 (120 min left) ---
|
||
|
|
COMMAND: ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress/" && ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/" 2>/dev/null; echo "---"; ls clothing/work/ 2>/dev/null
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
total 464
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 20:21 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 19:54 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 438936 Jul 30 20:20 Female_MDDress_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 25779 Jul 30 20:20 Female_MDDress_Body.gltf
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1101 Jul 30 20:21 Female_MDDress_Body.gltf.import
|
||
|
|
---
|
||
|
|
dress
|
||
|
|
|
||
|
|
--- step 8 (120 min left) ---
|
||
|
|
COMMAND: head -60 clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
# Garment pipeline: clothing mesh (any source) -> game-ready outfit-part GLBs
|
||
|
|
# on the shared 65-bone Quaternius skeleton. See README.md for design rationale
|
||
|
|
# (GLM-reviewed: fit-before-decimate, skirt proxy + cone weights, inner-shell
|
||
|
|
# clearance, layer stacking).
|
||
|
|
#
|
||
|
|
# Staged; every stage loads the previous checkpoint .blend from work/ and saves
|
||
|
|
# its own, so any stage can be re-run alone while iterating:
|
||
|
|
#
|
||
|
|
# blender --background --python garment_pipeline.py -- \
|
||
|
|
# --config configs/dress.json --stage prepare|fit|reduce|bake|skin|export
|
||
|
|
#
|
||
|
|
# Naming inside the .blend (stable contract between stages):
|
||
|
|
# GARM_<Part> garment part mesh (high-poly until `reduce`, low-poly after)
|
||
|
|
# HI_<Part> hidden high-poly copy kept for baking (created by `reduce`)
|
||
|
|
# BODY the target body mesh (from the game GLB, with vertex groups)
|
||
|
|
# BODY_SHELL inflated clearance copy of BODY (fit target)
|
||
|
|
# RIG the Quaternius armature imported with BODY
|
||
|
|
# CONE_<Part> skirt weight-transfer proxy (created by `skin`, kept for QA)
|
||
|
|
|
||
|
|
import bpy, bmesh, json, math, os, sys, argparse, random
|
||
|
|
from mathutils import Vector, Matrix
|
||
|
|
|
||
|
|
random.seed(7)
|
||
|
|
|
||
|
|
ARGS = None
|
||
|
|
CFG = None
|
||
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
|
|
|
||
|
|
|
||
|
|
# ── plumbing ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def log(msg):
|
||
|
|
print(f"[cloth] {msg}", flush=True)
|
||
|
|
|
||
|
|
|
||
|
|
def work_path(name):
|
||
|
|
d = os.path.join(HERE, "work", CFG["name"])
|
||
|
|
os.makedirs(d, exist_ok=True)
|
||
|
|
return os.path.join(d, name)
|
||
|
|
|
||
|
|
|
||
|
|
def save_checkpoint(stage):
|
||
|
|
p = work_path(f"{stage}.blend")
|
||
|
|
bpy.ops.wm.save_as_mainfile(filepath=p, compress=True)
|
||
|
|
log(f"checkpoint saved: {p}")
|
||
|
|
|
||
|
|
|
||
|
|
def load_checkpoint(stage):
|
||
|
|
p = work_path(f"{stage}.blend")
|
||
|
|
if not os.path.exists(p):
|
||
|
|
raise SystemExit(f"missing checkpoint {p} — run stage '{stage}' first")
|
||
|
|
bpy.ops.wm.open_mainfile(filepath=p)
|
||
|
|
log(f"checkpoint loaded: {p}")
|
||
|
|
|
||
|
|
|
||
|
|
def obj(name):
|
||
|
|
o = bpy.data.objects.get(name)
|
||
|
|
if o is None:
|
||
|
|
raise SystemExit(f"expected object '{name}' not in scene")
|
||
|
|
return o
|
||
|
|
|
||
|
|
--- step 9 (120 min left) ---
|
||
|
|
COMMAND: grep -n "def stage_\|def cmd_\|argparse\|add_argument\|--stage\|--config" clothing/garment_pipeline.py | head -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
10:# --config configs/dress.json --stage prepare|fit|reduce|bake|skin|export
|
||
|
|
20:import bpy, bmesh, json, math, os, sys, argparse, random
|
||
|
|
220:def stage_census():
|
||
|
|
251:def stage_prepare():
|
||
|
|
498:def stage_fit():
|
||
|
|
590:def stage_reduce():
|
||
|
|
649:def stage_bake():
|
||
|
|
758:def stage_skin():
|
||
|
|
806:def stage_export():
|
||
|
|
843: ap = argparse.ArgumentParser()
|
||
|
|
844: ap.add_argument("--config", required=True)
|
||
|
|
845: ap.add_argument("--stage", required=True, choices=sorted(STAGES))
|
||
|
|
|
||
|
|
--- step 10 (120 min left) ---
|
||
|
|
COMMAND: sed -n '220,260p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_census():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
report = []
|
||
|
|
for i, o in enumerate(islands):
|
||
|
|
vs = world_verts(o)
|
||
|
|
zs = [v.z for v in vs]; xs = [v.x for v in vs]; ys = [v.y for v in vs]
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
color = CENSUS_COLORS[i % len(CENSUS_COLORS)] if i < len(CENSUS_COLORS) else (0.5, 0.5, 0.5)
|
||
|
|
mat = bpy.data.materials.new(f"census_{i:03d}")
|
||
|
|
mat.diffuse_color = (*color, 1.0)
|
||
|
|
o.data.materials.clear()
|
||
|
|
o.data.materials.append(mat)
|
||
|
|
report.append({"island": i, "verts": len(o.data.vertices), "tris": tris_of(o),
|
||
|
|
"z": [round(min(zs), 3), round(max(zs), 3)],
|
||
|
|
"x": [round(min(xs), 3), round(max(xs), 3)],
|
||
|
|
"centroid": [round(c.x, 3), round(c.y, 3), round(c.z, 3)],
|
||
|
|
"color": [round(v, 2) for v in color]})
|
||
|
|
log(f"ISL_{i:03d} verts={len(o.data.vertices):7d} z=[{min(zs):.3f},{max(zs):.3f}] "
|
||
|
|
f"x=[{min(xs):+.3f},{max(xs):+.3f}] color={tuple(round(v,2) for v in color)}")
|
||
|
|
with open(work_path("census.json"), "w", encoding="utf-8") as f:
|
||
|
|
json.dump(report, f, indent=1)
|
||
|
|
bpy.context.scene.display.shading.color_type = "MATERIAL"
|
||
|
|
qa_render("00_census", focus_objects=islands)
|
||
|
|
save_checkpoint("00_census")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: prepare ───────────────────────────────────────────────────────────
|
||
|
|
# Group census islands into configured parts (each part lists island indices),
|
||
|
|
# import body+rig, align garment onto body.
|
||
|
|
|
||
|
|
def stage_prepare():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
|
||
|
|
# body + rig first (defines target space)
|
||
|
|
body_path = CFG["body"]
|
||
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
||
|
|
rig = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||
|
|
rig.name = "RIG"
|
||
|
|
body = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||
|
|
key=lambda o: len(o.data.vertices))
|
||
|
|
|
||
|
|
--- step 11 (120 min left) ---
|
||
|
|
COMMAND: sed -n '251,500p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_prepare():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
|
||
|
|
# body + rig first (defines target space)
|
||
|
|
body_path = CFG["body"]
|
||
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
||
|
|
rig = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||
|
|
rig.name = "RIG"
|
||
|
|
body = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||
|
|
key=lambda o: len(o.data.vertices))
|
||
|
|
# drop face-part extras (eyes etc.) that ride along in the body GLB
|
||
|
|
for o in [o for o in bpy.data.objects if o.type == "MESH" and o is not body]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
body.name = "BODY"
|
||
|
|
apply_all_transforms([rig, body])
|
||
|
|
log(f"body: {len(body.data.vertices)} verts, groups={len(body.vertex_groups)}, "
|
||
|
|
f"bones={len(rig.data.bones)}")
|
||
|
|
|
||
|
|
# garment islands (same deterministic indexing the census printed)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
by_idx = {int(o.name.split("_")[1]): o for o in islands}
|
||
|
|
claimed = set()
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
ids = part_cfg.get("islands", [])
|
||
|
|
members = [by_idx[i] for i in ids if i in by_idx]
|
||
|
|
if not members:
|
||
|
|
log(f"WARNING: part {part_name} matched no islands ({ids})")
|
||
|
|
continue
|
||
|
|
claimed.update(ids)
|
||
|
|
select_only(members, active=members[0])
|
||
|
|
bpy.ops.object.join()
|
||
|
|
part = bpy.context.view_layer.objects.active
|
||
|
|
part.name = f"GARM_{part_name}"
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
part.data.materials.clear()
|
||
|
|
part.data.materials.append(mat)
|
||
|
|
log(f"part {part.name}: islands={ids} verts={len(part.data.vertices)} tris={tris_of(part)}")
|
||
|
|
for i, o in list(by_idx.items()):
|
||
|
|
if i not in claimed and o.name.startswith("ISL_"):
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
|
||
|
|
# bodyshell parts: fitted clothing built FROM the body (Islander technique):
|
||
|
|
# copy the body, keep the configured z-band + torso-dominant vertices, push
|
||
|
|
# outward along normals. Fit and weights are correct by construction.
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
if part_cfg.get("type") != "bodyshell":
|
||
|
|
continue
|
||
|
|
shellp = body.copy()
|
||
|
|
shellp.data = body.data.copy()
|
||
|
|
shellp.name = f"GARM_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(shellp)
|
||
|
|
z0, z1 = part_cfg["z0"], part_cfg["z1"]
|
||
|
|
allowed = set(part_cfg.get("bones", ["spine_01", "spine_02", "spine_03", "pelvis"]))
|
||
|
|
gnames = {i: g.name for i, g in enumerate(shellp.vertex_groups)}
|
||
|
|
keep = set()
|
||
|
|
for v in shellp.data.vertices:
|
||
|
|
z = (shellp.matrix_world @ v.co).z
|
||
|
|
if not (z0 <= z <= z1):
|
||
|
|
continue
|
||
|
|
best, bw = None, 0.0
|
||
|
|
for g in v.groups:
|
||
|
|
if g.weight > bw:
|
||
|
|
best, bw = gnames.get(g.group), g.weight
|
||
|
|
if best in allowed:
|
||
|
|
keep.add(v.index)
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(shellp.data)
|
||
|
|
bm.verts.ensure_lookup_table()
|
||
|
|
doomed = [v for v in bm.verts if v.index not in keep]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="VERTS")
|
||
|
|
bm.to_mesh(shellp.data)
|
||
|
|
bm.free()
|
||
|
|
d = shellp.modifiers.new("Offset", "DISPLACE")
|
||
|
|
d.strength = part_cfg.get("offset_mm", 6) / 1000.0
|
||
|
|
d.mid_level = 0.0
|
||
|
|
apply_modifier(shellp, d.name)
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
shellp.data.materials.clear()
|
||
|
|
shellp.data.materials.append(mat)
|
||
|
|
log(f"bodyshell {shellp.name}: verts={len(shellp.data.vertices)} tris={tris_of(shellp)}")
|
||
|
|
|
||
|
|
parts = [o for o in objs_prefixed("GARM_")
|
||
|
|
if CFG["parts"][o.name[5:]].get("type") != "bodyshell"]
|
||
|
|
if not parts:
|
||
|
|
raise SystemExit("no parts matched any islands — check config vs census.json")
|
||
|
|
|
||
|
|
# sleeve pose-warp FIRST, in garment-local space (unambiguous: sleeves are
|
||
|
|
# the only geometry beyond x_beyond). Rigid rotation around the garment's
|
||
|
|
# own shoulder point, bringing each cuff to horizontal (T-pose).
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
wcfg = part_cfg.get("sleeve_warp")
|
||
|
|
if not wcfg:
|
||
|
|
continue
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
x_beyond = wcfg.get("x_beyond", 0.28)
|
||
|
|
sx, sz = wcfg.get("shoulder", [0.19, 1.43])
|
||
|
|
for sign in (1.0, -1.0):
|
||
|
|
pivot = Vector((sign * sx, 0.0, sz))
|
||
|
|
sleeve = [v for v in p.data.vertices if sign * v.co.x > x_beyond]
|
||
|
|
if len(sleeve) < 10:
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: none found")
|
||
|
|
continue
|
||
|
|
tip = max(sleeve, key=lambda v: sign * v.co.x)
|
||
|
|
cur = math.atan2(tip.co.z - pivot.z, sign * (tip.co.x - pivot.x))
|
||
|
|
rot = Matrix.Rotation(-sign * cur, 4, "Y")
|
||
|
|
M = Matrix.Translation(pivot) @ rot @ Matrix.Translation(-pivot)
|
||
|
|
for v in sleeve:
|
||
|
|
v.co = M @ v.co
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: {math.degrees(cur):+.1f} deg, "
|
||
|
|
f"{len(sleeve)} verts")
|
||
|
|
a = CFG.get("align", {})
|
||
|
|
sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
sz = a.get("scale_z", a.get("
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 12 (120 min left) ---
|
||
|
|
COMMAND: sed -n '440,500p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
pieces = [o for o in bpy.data.objects if o.name.startswith(pname)]
|
||
|
|
main = max(pieces, key=lambda o: len(o.data.vertices))
|
||
|
|
keepers = [main]
|
||
|
|
for o in pieces:
|
||
|
|
if o is main:
|
||
|
|
continue
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
if abs(c.x) <= px:
|
||
|
|
keepers.append(o)
|
||
|
|
else:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
select_only(keepers, active=main)
|
||
|
|
bpy.ops.object.join()
|
||
|
|
joined = bpy.context.view_layer.objects.active
|
||
|
|
joined.name = pname
|
||
|
|
log(f"prune {joined.name}: kept {len(keepers)} pieces, "
|
||
|
|
f"{len(joined.data.vertices)} verts")
|
||
|
|
|
||
|
|
qa_render("10_prepare")
|
||
|
|
save_checkpoint("10_prepare")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: fit ───────────────────────────────────────────────────────────────
|
||
|
|
# Build the inflated BODY_SHELL, give each part a Z-gradient influence mask, and
|
||
|
|
# shrinkwrap high-poly parts in configured layer order (later layers can target
|
||
|
|
# earlier ones).
|
||
|
|
|
||
|
|
def make_shell():
|
||
|
|
body = obj("BODY")
|
||
|
|
shell = body.copy()
|
||
|
|
shell.data = body.data.copy()
|
||
|
|
shell.name = "BODY_SHELL"
|
||
|
|
bpy.context.scene.collection.objects.link(shell)
|
||
|
|
d = shell.modifiers.new("Inflate", "DISPLACE")
|
||
|
|
d.strength = CFG.get("shell_mm", 4) / 1000.0
|
||
|
|
d.mid_level = 0.0
|
||
|
|
apply_modifier(shell, d.name)
|
||
|
|
shell.hide_render = True
|
||
|
|
return shell
|
||
|
|
|
||
|
|
|
||
|
|
def zgradient_group(o, name, z_full, z_zero):
|
||
|
|
"""Vertex group: weight 1 above z_full … 0 below z_zero (linear between)."""
|
||
|
|
vg = o.vertex_groups.get(name) or o.vertex_groups.new(name=name)
|
||
|
|
mw = o.matrix_world
|
||
|
|
for v in o.data.vertices:
|
||
|
|
z = (mw @ v.co).z
|
||
|
|
if z >= z_full:
|
||
|
|
w = 1.0
|
||
|
|
elif z <= z_zero:
|
||
|
|
w = 0.0
|
||
|
|
else:
|
||
|
|
w = (z - z_zero) / max(1e-9, (z_full - z_zero))
|
||
|
|
vg.add([v.index], w, "REPLACE")
|
||
|
|
return vg
|
||
|
|
|
||
|
|
|
||
|
|
def stage_fit():
|
||
|
|
load_checkpoint("10_prepare")
|
||
|
|
rig = obj("RIG")
|
||
|
|
|
||
|
|
--- step 13 (119 min left) ---
|
||
|
|
COMMAND: sed -n '498,590p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_fit():
|
||
|
|
load_checkpoint("10_prepare")
|
||
|
|
rig = obj("RIG")
|
||
|
|
shell = make_shell()
|
||
|
|
hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
if part_cfg.get("type") == "bodyshell":
|
||
|
|
log(f"fit GARM_{part_name}: skipped (bodyshell)")
|
||
|
|
continue
|
||
|
|
fit = part_cfg.get("fit", {})
|
||
|
|
target_name = fit.get("target", "BODY_SHELL")
|
||
|
|
target = obj(target_name if target_name != "BODY_SHELL" else "BODY_SHELL")
|
||
|
|
mode = fit.get("mask", "full") # full | above_hip | waistband | none
|
||
|
|
if mode == "none":
|
||
|
|
log(f"fit {p.name}: skipped (mask none)")
|
||
|
|
continue
|
||
|
|
if mode == "full":
|
||
|
|
vg = None
|
||
|
|
elif mode == "above_hip":
|
||
|
|
vg = zgradient_group(p, "fit_mask", hip_z + 0.02, hip_z - 0.06)
|
||
|
|
elif mode == "waistband":
|
||
|
|
vg = zgradient_group(p, "fit_mask", hip_z + 0.10, hip_z - 0.02)
|
||
|
|
sw = p.modifiers.new("Fit", "SHRINKWRAP")
|
||
|
|
sw.target = target
|
||
|
|
sw.wrap_method = "NEAREST_SURFACEPOINT"
|
||
|
|
sw.wrap_mode = fit.get("wrap_mode", "OUTSIDE")
|
||
|
|
sw.offset = fit.get("offset_mm", 2) / 1000.0
|
||
|
|
if vg is not None:
|
||
|
|
sw.vertex_group = vg.name
|
||
|
|
apply_modifier(p, sw.name)
|
||
|
|
log(f"fit {p.name}: target={target.name} mask={mode} offset={sw.offset if hasattr(sw,'offset') else '?'}")
|
||
|
|
|
||
|
|
qa_render("20_fit")
|
||
|
|
save_checkpoint("20_fit")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: reduce ────────────────────────────────────────────────────────────
|
||
|
|
# Keep a hidden HI_<part> copy for baking. Skirt-style parts are replaced by a
|
||
|
|
# generated cylinder proxy shrinkwrapped to the high-poly; others get planar
|
||
|
|
# dissolve then collapse decimate down to budget.
|
||
|
|
|
||
|
|
def make_skirt_proxy(part, proxy_cfg):
|
||
|
|
seg = proxy_cfg.get("segments", 30)
|
||
|
|
rings = proxy_cfg.get("rings", 6)
|
||
|
|
vs = world_verts(part)
|
||
|
|
zs = sorted(v.z for v in vs)
|
||
|
|
z0, z1 = zs[int(0.01 * len(zs))], zs[int(0.99 * len(zs))]
|
||
|
|
cx = sum(v.x for v in vs) / len(vs)
|
||
|
|
cy = sum(v.y for v in vs) / len(vs)
|
||
|
|
|
||
|
|
def ring_radius(z):
|
||
|
|
band = [v for v in vs if abs(v.z - z) < (z1 - z0) / (rings * 1.5)]
|
||
|
|
if not band:
|
||
|
|
return 0.2
|
||
|
|
ds = sorted(math.hypot(v.x - cx, v.y - cy) for v in band)
|
||
|
|
return ds[int(0.65 * len(ds))]
|
||
|
|
|
||
|
|
me = bpy.data.meshes.new(f"{part.name}_proxy")
|
||
|
|
bm = bmesh.new()
|
||
|
|
ring_verts = []
|
||
|
|
for r in range(rings + 1):
|
||
|
|
z = z1 - (z1 - z0) * (r / rings)
|
||
|
|
rad = ring_radius(z)
|
||
|
|
ring = [bm.verts.new((cx + rad * math.cos(2 * math.pi * s / seg),
|
||
|
|
cy + rad * math.sin(2 * math.pi * s / seg), z))
|
||
|
|
for s in range(seg)]
|
||
|
|
ring_verts.append(ring)
|
||
|
|
for r in range(rings):
|
||
|
|
for s in range(seg):
|
||
|
|
a, b = ring_verts[r][s], ring_verts[r][(s + 1) % seg]
|
||
|
|
c, d = ring_verts[r + 1][(s + 1) % seg], ring_verts[r + 1][s]
|
||
|
|
bm.faces.new((a, b, c, d))
|
||
|
|
bm.to_mesh(me)
|
||
|
|
bm.free()
|
||
|
|
proxy = bpy.data.objects.new(f"{part.name}_proxytmp", me)
|
||
|
|
bpy.context.scene.collection.objects.link(proxy)
|
||
|
|
sw = proxy.modifiers.new("WrapToSkirt", "SHRINKWRAP")
|
||
|
|
sw.target = part
|
||
|
|
sw.wrap_method = "NEAREST_SURFACEPOINT"
|
||
|
|
apply_modifier(proxy, sw.name)
|
||
|
|
select_only([proxy])
|
||
|
|
bpy.ops.object.shade_smooth()
|
||
|
|
# carry the part's (single) material
|
||
|
|
if part.data.materials:
|
||
|
|
proxy.data.materials.append(part.data.materials[0])
|
||
|
|
return proxy
|
||
|
|
|
||
|
|
|
||
|
|
def stage_reduce():
|
||
|
|
|
||
|
|
--- step 14 (119 min left) ---
|
||
|
|
COMMAND: sed -n '590,650p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_reduce():
|
||
|
|
load_checkpoint("20_fit")
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
# keep high-poly for baking
|
||
|
|
hi = p.copy()
|
||
|
|
hi.data = p.data.copy()
|
||
|
|
hi.name = f"HI_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(hi)
|
||
|
|
hi.hide_render = hi.hide_viewport = True
|
||
|
|
|
||
|
|
wb = part_cfg.get("weld_band") # [z0, z1, dist]: fuse stacked layers
|
||
|
|
if wb:
|
||
|
|
z0, z1, dist = wb
|
||
|
|
select_only([p])
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(p.data)
|
||
|
|
mw = p.matrix_world
|
||
|
|
band = [v for v in bm.verts if z0 <= (mw @ v.co).z <= z1]
|
||
|
|
bmesh.ops.remove_doubles(bm, verts=band, dist=dist)
|
||
|
|
bm.to_mesh(p.data)
|
||
|
|
bm.free()
|
||
|
|
log(f"weld_band {p.name}: z=[{z0},{z1}] dist={dist} -> {len(p.data.vertices)} verts")
|
||
|
|
|
||
|
|
budget = part_cfg.get("tris", 1500)
|
||
|
|
if part_cfg.get("proxy") == "cylinder":
|
||
|
|
proxy = make_skirt_proxy(p, part_cfg.get("proxy_cfg", {}))
|
||
|
|
name = p.name
|
||
|
|
bpy.data.objects.remove(p, do_unlink=True)
|
||
|
|
proxy.name = name
|
||
|
|
p = proxy
|
||
|
|
else:
|
||
|
|
# planar dissolve first (MD panels are near-planar), then collapse
|
||
|
|
dm = p.modifiers.new("Planar", "DECIMATE")
|
||
|
|
dm.decimate_type = "DISSOLVE"
|
||
|
|
dm.angle_limit = math.radians(part_cfg.get("planar_deg", 5))
|
||
|
|
apply_modifier(p, dm.name)
|
||
|
|
cur = tris_of(p)
|
||
|
|
if cur > budget:
|
||
|
|
dm = p.modifiers.new("Collapse", "DECIMATE")
|
||
|
|
dm.decimate_type = "COLLAPSE"
|
||
|
|
dm.ratio = budget / cur
|
||
|
|
apply_modifier(p, dm.name)
|
||
|
|
select_only([p])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
log(f"reduce {p.name}: tris={tris_of(p)} (budget {budget})")
|
||
|
|
|
||
|
|
qa_render("30_reduce", focus_objects=objs_prefixed("GARM_"))
|
||
|
|
save_checkpoint("30_reduce")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: bake ──────────────────────────────────────────────────────────────
|
||
|
|
# Normal + AO from HI_<part> onto GARM_<part> via a second UV map. Slow (Cycles).
|
||
|
|
|
||
|
|
def stage_bake():
|
||
|
|
load_checkpoint("30_reduce")
|
||
|
|
|
||
|
|
--- step 15 (119 min left) ---
|
||
|
|
COMMAND: sed -n '758,860p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_skin():
|
||
|
|
src_stage = "40_bake" if os.path.exists(work_path("40_bake.blend")) else "30_reduce"
|
||
|
|
load_checkpoint(src_stage)
|
||
|
|
body, rig = obj("BODY"), obj("RIG")
|
||
|
|
hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
if part_cfg.get("type") == "bodyshell":
|
||
|
|
pass # weights inherited from the body copy — already correct
|
||
|
|
elif part_cfg.get("weights") == "dress":
|
||
|
|
# one-piece dress: body weights everywhere, then blend the loose
|
||
|
|
# skirt region toward pelvis-only as z approaches the hem
|
||
|
|
hem_z = zspan([p])[0]
|
||
|
|
data_transfer_weights(p, body)
|
||
|
|
pelvis_gradient(p, hip_z - 0.04, hem_z)
|
||
|
|
elif part_cfg.get("weights") == "skirt":
|
||
|
|
hem_z = zspan([p])[0]
|
||
|
|
cone = p.copy() # skirt proxy IS already a leg-bridging cone shape
|
||
|
|
cone.data = p.data.copy()
|
||
|
|
cone.name = f"CONE_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(cone)
|
||
|
|
data_transfer_weights(cone, body)
|
||
|
|
pelvis_gradient(cone, hip_z, hem_z)
|
||
|
|
smooth_and_normalize(cone)
|
||
|
|
data_transfer_weights(p, cone)
|
||
|
|
cone.hide_viewport = cone.hide_render = True
|
||
|
|
else:
|
||
|
|
data_transfer_weights(p, body)
|
||
|
|
smooth_and_normalize(p)
|
||
|
|
# bind
|
||
|
|
p.parent = rig
|
||
|
|
p.matrix_parent_inverse = Matrix.Identity(4)
|
||
|
|
am = p.modifiers.new("Armature", "ARMATURE")
|
||
|
|
am.object = rig
|
||
|
|
nonzero = sum(1 for v in p.data.vertices if v.groups)
|
||
|
|
log(f"skin {p.name}: weighted {nonzero}/{len(p.data.vertices)} verts")
|
||
|
|
|
||
|
|
qa_render("50_skin", focus_objects=objs_prefixed("GARM_"))
|
||
|
|
save_checkpoint("50_skin")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: export ────────────────────────────────────────────────────────────
|
||
|
|
# One GLB per outfit slot (multiple parts may share a slot). Armature + parts
|
||
|
|
# only; BODY/HI/CONE stay out.
|
||
|
|
|
||
|
|
def stage_export():
|
||
|
|
load_checkpoint("50_skin")
|
||
|
|
rig = obj("RIG")
|
||
|
|
out_dir = CFG["export"]["out_dir"]
|
||
|
|
os.makedirs(out_dir, exist_ok=True)
|
||
|
|
slots = {}
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is not None:
|
||
|
|
slots.setdefault(part_cfg["slot"], []).append(p)
|
||
|
|
gender, set_name = CFG["export"]["gender"], CFG["export"]["set"]
|
||
|
|
for slot, parts in slots.items():
|
||
|
|
path = os.path.join(out_dir, f"{gender}_{set_name}_{slot}.gltf")
|
||
|
|
select_only([rig] + parts, active=rig)
|
||
|
|
bpy.ops.export_scene.gltf(filepath=path, export_format="GLTF_SEPARATE",
|
||
|
|
use_selection=True, export_skins=True,
|
||
|
|
export_animations=False, export_yup=True)
|
||
|
|
log(f"EXPORTED {path} ({sum(tris_of(p) for p in parts)} tris, "
|
||
|
|
f"{os.path.getsize(path)} bytes)")
|
||
|
|
# determinism fingerprint
|
||
|
|
import hashlib
|
||
|
|
h = hashlib.sha1()
|
||
|
|
for part in sorted(objs_prefixed("GARM_"), key=lambda o: o.name):
|
||
|
|
for v in part.data.vertices:
|
||
|
|
h.update(b"%d %d %d" % (int(v.co.x * 1e5), int(v.co.y * 1e5), int(v.co.z * 1e5)))
|
||
|
|
log(f"vertex hash: {h.hexdigest()[:16]}")
|
||
|
|
|
||
|
|
|
||
|
|
# ── main ─────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
STAGES = {"census": stage_census, "prepare": stage_prepare, "fit": stage_fit, "reduce": stage_reduce,
|
||
|
|
"bake": stage_bake, "skin": stage_skin, "export": stage_export}
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
global ARGS, CFG
|
||
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--config", required=True)
|
||
|
|
ap.add_argument("--stage", required=True, choices=sorted(STAGES))
|
||
|
|
ARGS = ap.parse_args(argv)
|
||
|
|
cfg_path = ARGS.config if os.path.isabs(ARGS.config) else os.path.join(HERE, ARGS.config)
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
log(f"config={CFG['name']} stage={ARGS.stage}")
|
||
|
|
STAGES[ARGS.stage]()
|
||
|
|
|
||
|
|
|
||
|
|
main()
|
||
|
|
|
||
|
|
--- step 16 (119 min left) ---
|
||
|
|
COMMAND: grep -n "def import_garment_islands\|def qa_render\|def zspan\|def pelvis_gradient\|def data_transfer_weights\|def smooth_and_normalize\|def bone_head_z\|def objs_prefixed\|def apply_all_transforms\|def select_only\|def world_verts\|def tris_of\|def apply_modifier" clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
63:def objs_prefixed(prefix):
|
||
|
|
67:def select_only(objects, active=None):
|
||
|
|
74:def apply_all_transforms(objects):
|
||
|
|
82:def apply_modifier(o, mod_name):
|
||
|
|
87:def tris_of(o):
|
||
|
|
92:def world_verts(o):
|
||
|
|
96:def zspan(objects):
|
||
|
|
102:def qa_render(tag, focus_objects=None, ortho_pad=1.25):
|
||
|
|
131:def bone_head_z(rig, bone):
|
||
|
|
142:def import_garment_islands():
|
||
|
|
713:def data_transfer_weights(dst, src):
|
||
|
|
727:def smooth_and_normalize(o, iterations=3):
|
||
|
|
738:def pelvis_gradient(o, hip_z, hem_z):
|
||
|
|
|
||
|
|
--- step 17 (119 min left) ---
|
||
|
|
COMMAND: sed -n '63,220p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def objs_prefixed(prefix):
|
||
|
|
return [o for o in bpy.data.objects if o.name.startswith(prefix)]
|
||
|
|
|
||
|
|
|
||
|
|
def select_only(objects, active=None):
|
||
|
|
bpy.ops.object.select_all(action="DESELECT")
|
||
|
|
for o in objects:
|
||
|
|
o.select_set(True)
|
||
|
|
bpy.context.view_layer.objects.active = active or objects[0]
|
||
|
|
|
||
|
|
|
||
|
|
def apply_all_transforms(objects):
|
||
|
|
select_only(objects)
|
||
|
|
bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM")
|
||
|
|
for o in objects:
|
||
|
|
select_only([o])
|
||
|
|
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||
|
|
|
||
|
|
|
||
|
|
def apply_modifier(o, mod_name):
|
||
|
|
select_only([o])
|
||
|
|
bpy.ops.object.modifier_apply(modifier=mod_name)
|
||
|
|
|
||
|
|
|
||
|
|
def tris_of(o):
|
||
|
|
o.data.calc_loop_triangles()
|
||
|
|
return len(o.data.loop_triangles)
|
||
|
|
|
||
|
|
|
||
|
|
def world_verts(o):
|
||
|
|
return [o.matrix_world @ v.co for v in o.data.vertices]
|
||
|
|
|
||
|
|
|
||
|
|
def zspan(objects):
|
||
|
|
vs = [v for o in objects for v in world_verts(o)]
|
||
|
|
zs = [v.z for v in vs]
|
||
|
|
return min(zs), max(zs)
|
||
|
|
|
||
|
|
|
||
|
|
def qa_render(tag, focus_objects=None, ortho_pad=1.25):
|
||
|
|
"""Front + 3/4 workbench renders of the current scene into work/."""
|
||
|
|
scene = bpy.context.scene
|
||
|
|
scene.render.engine = "BLENDER_WORKBENCH"
|
||
|
|
scene.display.shading.light = "STUDIO"
|
||
|
|
scene.display.shading.color_type = "MATERIAL"
|
||
|
|
scene.render.resolution_x, scene.render.resolution_y = 900, 1200
|
||
|
|
vs = [v for o in (focus_objects or [o for o in bpy.data.objects if o.type == "MESH" and o.visible_get()])
|
||
|
|
for v in world_verts(o)]
|
||
|
|
if not vs:
|
||
|
|
return
|
||
|
|
zmin, zmax = min(v.z for v in vs), max(v.z for v in vs)
|
||
|
|
cz, h = (zmin + zmax) / 2, (zmax - zmin)
|
||
|
|
for view, loc, rot in (("front", (0, -4, cz), (math.pi / 2, 0, 0)),
|
||
|
|
("quarter", (2.7, -2.9, cz + 0.15), (math.radians(80), 0, math.radians(42)))):
|
||
|
|
cam_name = f"_qa_{view}"
|
||
|
|
cam = bpy.data.objects.get(cam_name)
|
||
|
|
if cam is None:
|
||
|
|
cam = bpy.data.objects.new(cam_name, bpy.data.cameras.new(cam_name))
|
||
|
|
bpy.context.scene.collection.objects.link(cam)
|
||
|
|
cam.data.type = "ORTHO"
|
||
|
|
cam.data.ortho_scale = h * ortho_pad
|
||
|
|
cam.location, cam.rotation_euler = loc, rot
|
||
|
|
scene.camera = cam
|
||
|
|
scene.render.filepath = work_path(f"qa_{tag}_{view}.png")
|
||
|
|
bpy.ops.render.render(write_still=True)
|
||
|
|
log(f"QA render: {scene.render.filepath}")
|
||
|
|
|
||
|
|
|
||
|
|
def bone_head_z(rig, bone):
|
||
|
|
return (rig.matrix_world @ rig.data.bones[bone].head_local).z
|
||
|
|
|
||
|
|
|
||
|
|
# ── garment import (shared by census + prepare) ─────────────────────────────
|
||
|
|
# MD exports hide the real structure: fabric may live on ONE material while
|
||
|
|
# millions of faces are topstitch threads. So: drop configured stitch/trash
|
||
|
|
# materials, weld, split by LOOSE PARTS, and identify pieces as "islands"
|
||
|
|
# (indexed by vertex count, deterministic). Parts are then configured as island
|
||
|
|
# index lists (see census renders).
|
||
|
|
|
||
|
|
def import_garment_islands():
|
||
|
|
pre = set(bpy.data.objects)
|
||
|
|
src = CFG["source"]
|
||
|
|
ext = os.path.splitext(src)[1].lower()
|
||
|
|
if ext == ".fbx":
|
||
|
|
bpy.ops.import_scene.fbx(filepath=src)
|
||
|
|
elif ext in (".glb", ".gltf"):
|
||
|
|
bpy.ops.import_scene.gltf(filepath=src)
|
||
|
|
elif ext == ".obj":
|
||
|
|
bpy.ops.wm.obj_import(filepath=src)
|
||
|
|
else:
|
||
|
|
raise SystemExit(f"unsupported garment format {ext}")
|
||
|
|
new = [o for o in bpy.data.objects if o not in pre]
|
||
|
|
meshes = [o for o in new if o.type == "MESH"]
|
||
|
|
for o in [o for o in new if o.type != "MESH"]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
for g in meshes:
|
||
|
|
for m in list(g.modifiers):
|
||
|
|
g.modifiers.remove(m)
|
||
|
|
g.vertex_groups.clear()
|
||
|
|
apply_all_transforms(meshes)
|
||
|
|
|
||
|
|
drop = set(CFG.get("drop_materials", []))
|
||
|
|
for g in meshes:
|
||
|
|
if not g.data.materials:
|
||
|
|
continue
|
||
|
|
drop_idx = {i for i, m in enumerate(g.data.materials) if m and m.name in drop}
|
||
|
|
if not drop_idx:
|
||
|
|
continue
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(g.data)
|
||
|
|
doomed = [f for f in bm.faces if f.material_index in drop_idx]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="FACES")
|
||
|
|
bm.to_mesh(g.data)
|
||
|
|
bm.free()
|
||
|
|
meshes = [g for g in meshes if len(g.data.vertices) > 0]
|
||
|
|
log(f"after drop_materials: {sum(len(g.data.vertices) for g in meshes)} verts")
|
||
|
|
|
||
|
|
# weld panels, then split to loose islands
|
||
|
|
select_only(meshes)
|
||
|
|
bpy.ops.object.join()
|
||
|
|
fabric = bpy.context.view_layer.objects.active
|
||
|
|
select_only([fabric])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.remove_doubles(threshold=CFG.get("weld_threshold", 0.0006))
|
||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||
|
|
bpy.ops.mesh.separate(type="LOOSE")
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
islands = [o for o in bpy.data.objects if o.type == "MESH" and o.name not in ("BODY", "BODY_SHELL")]
|
||
|
|
min_verts = CFG.get("min_island_verts", 40)
|
||
|
|
kept = []
|
||
|
|
for o in islands:
|
||
|
|
if len(o.data.vertices) < min_verts:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
else:
|
||
|
|
kept.append(o)
|
||
|
|
# deterministic index: vertex count desc, then centroid x/z
|
||
|
|
def key(o):
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
return (-len(o.data.vertices), round(c.x, 4), round(c.z, 4))
|
||
|
|
kept.sort(key=key)
|
||
|
|
for i, o in enumerate(kept):
|
||
|
|
o.name = f"ISL_{i:03d}"
|
||
|
|
log(f"islands kept: {len(kept)} (dropped tiny <{min_verts} verts)")
|
||
|
|
return kept
|
||
|
|
|
||
|
|
|
||
|
|
CENSUS_COLORS = [
|
||
|
|
(0.90, 0.10, 0.10), (0.10, 0.55, 0.95), (0.10, 0.80, 0.20), (0.95, 0.75, 0.10),
|
||
|
|
(0.75, 0.15, 0.85), (0.05, 0.85, 0.80), (0.95, 0.45, 0.05), (0.55, 0.35, 0.15),
|
||
|
|
(0.95, 0.55, 0.75), (0.45, 0.95, 0.45), (0.25, 0.25, 0.95), (0.80, 0.80, 0.30),
|
||
|
|
(0.50, 0.05, 0.30), (0.05, 0.45, 0.35), (0.65, 0.65, 0.95), (0.95, 0.85, 0.65),
|
||
|
|
(0.30, 0.10, 0.60), (0.10, 0.30, 0.10), (0.60, 0.30, 0.30), (0.30, 0.60, 0.60),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def stag
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 18 (119 min left) ---
|
||
|
|
Now I have a good understanding of the pipeline. Let me start with the pari (bodice) census stage. First, let me create a minimal config for the census.
|
||
|
|
|
||
|
|
--- step 19 (119 min left) ---
|
||
|
|
COMMAND: python -c "import json; cfg={'name':'pari','source':'C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx','body':'C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb','weld_threshold':0.0006,'shell_mm':4,'align':{'top_bone':'neck_01','scale_xy':1.0,'scale_z':1.0,'z_nudge':0.0,'xy_nudge':[0.0,0.0]},'parts':{},'export':{'out_dir':'C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka','gender':'Female','set':'Kapahaka'},'drop_materials':[],'min_island_verts':20}; open('clothing/configs/pari.json','w').write(json.dumps(cfg,indent=2)); print('written')"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
written
|
||
|
|
|
||
|
|
--- step 20 (119 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config clothing/configs/pari.json --stage census 2>&1 | tail -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
Traceback (most recent call last):
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 853, in <module>
|
||
|
|
main()
|
||
|
|
~~~~^^
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 848, in main
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\Jeremy\\tinqs\\animation\\clothing\\clothing/configs/pari.json'
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 21 (119 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage census 2>&1 | tail -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=census
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 1889 verts
|
||
|
|
Info: Removed 42 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <20 verts)
|
||
|
|
[cloth] ISL_000 verts= 1847 z=[10.546,14.613] x=[-1.920,+1.989] color=(0.9, 0.1, 0.1)
|
||
|
|
00:02.984 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_front.png
|
||
|
|
00:03.093 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_quarter.png
|
||
|
|
Info: Saved as "00_census.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\00_census.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 22 (119 min left) ---
|
||
|
|
COMMAND: cat clothing/work/pari/census.json && ls -la clothing/work/pari/qa_00_census_front.png clothing/work/pari/qa_00_census_quarter.png
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"island": 0,
|
||
|
|
"verts": 1847,
|
||
|
|
"tris": 3575,
|
||
|
|
"z": [
|
||
|
|
10.546,
|
||
|
|
14.613
|
||
|
|
],
|
||
|
|
"x": [
|
||
|
|
-1.92,
|
||
|
|
1.989
|
||
|
|
],
|
||
|
|
"centroid": [
|
||
|
|
0.034,
|
||
|
|
0.407,
|
||
|
|
12.541
|
||
|
|
],
|
||
|
|
"color": [
|
||
|
|
0.9,
|
||
|
|
0.1,
|
||
|
|
0.1
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]-rw-r--r-- 1 Jeremy 197121 817779 Jul 30 21:36 clothing/work/pari/qa_00_census_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 869071 Jul 30 21:36 clothing/work/pari/qa_00_census_quarter.png
|
||
|
|
|
||
|
|
--- step 23 (119 min left) ---
|
||
|
|
Good — pari is a single island, 1847 verts, 3575 tris. The z-range [10.546, 14.613] seems to be in a different scale (likely MD's cm units). Let me check the body's scale to understand the coordinate system. Let me also look at the dress.json config to model the pari config properly.
|
||
|
|
|
||
|
|
--- step 24 (119 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 25 (119 min left) ---
|
||
|
|
COMMAND: grep -n "top_bone\|scale_xy\|scale_z\|z_nudge\|xy_nudge\|torso_boost\|cut_above_z\|cut_outboard\|prune_scraps" clothing/garment_pipeline.py | head -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
379: sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
380: sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
385: b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
386: dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
387: dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
391: log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
397: tb = CFG.get("torso_boost")
|
||
|
|
409: log(f"torso_boost: xy x{boost} above z={z0}..{z1}")
|
||
|
|
414: cut = part_cfg.get("cut_above_z")
|
||
|
|
420: ox = part_cfg.get("cut_outboard_x")
|
||
|
|
421: oz = part_cfg.get("cut_outboard_z_above", 0.0)
|
||
|
|
432: px = part_cfg.get("prune_scraps_x")
|
||
|
|
|
||
|
|
--- step 26 (119 min left) ---
|
||
|
|
COMMAND: sed -n '370,450p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
tip = max(sleeve, key=lambda v: sign * v.co.x)
|
||
|
|
cur = math.atan2(tip.co.z - pivot.z, sign * (tip.co.x - pivot.x))
|
||
|
|
rot = Matrix.Rotation(-sign * cur, 4, "Y")
|
||
|
|
M = Matrix.Translation(pivot) @ rot @ Matrix.Translation(-pivot)
|
||
|
|
for v in sleeve:
|
||
|
|
v.co = M @ v.co
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: {math.degrees(cur):+.1f} deg, "
|
||
|
|
f"{len(sleeve)} verts")
|
||
|
|
a = CFG.get("align", {})
|
||
|
|
sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
M = Matrix.Diagonal(Vector((sxy, sxy, sz))).to_4x4()
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(M)
|
||
|
|
gz0, gz1 = zspan(parts)
|
||
|
|
b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(T)
|
||
|
|
log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
|
||
|
|
# torso boost: heroic bodies are wider than MD mannequins — radially inflate
|
||
|
|
# the above-waist region about the body axis so bodice/collar fabric starts
|
||
|
|
# OUTSIDE the body (direction-preserving; the OUTSIDE shrinkwrap then only
|
||
|
|
# catches stragglers instead of shredding buried cloth to random sides).
|
||
|
|
tb = CFG.get("torso_boost")
|
||
|
|
if tb:
|
||
|
|
z0, z1, boost = tb.get("z0", 1.00), tb.get("z1", 1.08), tb.get("xy", 1.18)
|
||
|
|
for p in parts:
|
||
|
|
for v in p.data.vertices:
|
||
|
|
z = v.co.z
|
||
|
|
if z <= z0:
|
||
|
|
continue
|
||
|
|
t = 1.0 if z >= z1 else (z - z0) / max(1e-9, z1 - z0)
|
||
|
|
f = 1.0 + (boost - 1.0) * t
|
||
|
|
v.co.x *= f
|
||
|
|
v.co.y *= f
|
||
|
|
log(f"torso_boost: xy x{boost} above z={z0}..{z1}")
|
||
|
|
|
||
|
|
# post-align cut: drop configured fabric above a body-space height (v1 uses
|
||
|
|
# this to remove the slim MD bodice+sleeves replaced by the bodyshell part)
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
cut = part_cfg.get("cut_above_z")
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if cut is None or p is None or part_cfg.get("type") == "bodyshell":
|
||
|
|
continue
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(p.data)
|
||
|
|
ox = part_cfg.get("cut_outboard_x")
|
||
|
|
oz = part_cfg.get("cut_outboard_z_above", 0.0)
|
||
|
|
def out(v):
|
||
|
|
w = p.matrix_world @ v.co
|
||
|
|
return w.z > cut or (ox is not None and w.z > oz and abs(w.x) > ox)
|
||
|
|
doomed = [v for v in bm.verts if out(v)]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="VERTS")
|
||
|
|
bm.to_mesh(p.data)
|
||
|
|
bm.free()
|
||
|
|
log(f"cut {p.name} above z={cut}: now {len(p.data.vertices)} verts")
|
||
|
|
# prune disconnected scraps left by the cuts (e.g. sleeve remnants):
|
||
|
|
# separate loose, keep the biggest piece + anything near the center axis
|
||
|
|
px = part_cfg.get("prune_scraps_x")
|
||
|
|
if px is not None:
|
||
|
|
pname = p.name
|
||
|
|
select_only([p])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.separate(type="LOOSE")
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
pieces = [o for o in bpy.data.objects if o.name.startswith(pname)]
|
||
|
|
main = max(pieces, key=lambda o: len(o.data.vertices))
|
||
|
|
keepers = [main]
|
||
|
|
for o in pieces:
|
||
|
|
if o is main:
|
||
|
|
continue
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
if abs(c.x) <= px:
|
||
|
|
keepers.append(o)
|
||
|
|
else:
|
||
|
|
|
||
|
|
--- step 27 (119 min left) ---
|
||
|
|
The pari garment z-range is [10.546, 14.613] — that's in a different scale than the body (which is likely ~0-2 meters). The align stage will handle this by scaling. Let me check what the body's actual z-range is to understand the scale factor needed.
|
||
|
|
|
||
|
|
--- step 28 (119 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python-expr "import bpy; bpy.ops.import_scene.gltf(filepath='C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb'); rig=next(o for o in bpy.data.objects if o.type=='ARMATURE'); body=max((o for o in bpy.data.objects if o.type=='MESH'),key=lambda o:len(o.data.vertices)); print('BONES',len(rig.data.bones)); vs=[(rig.matrix_world@v.co) for v in body.data.vertices]; zs=[v.z for v in vs]; print('BODY zrange',min(zs),max(zs)); print('PELVIS z',(rig.matrix_world@rig.data.bones['pelvis'].head_local).z); print('NECK z',(rig.matrix_world@rig.data.bones['neck_01'].head_local).z); print('BONE NAMES',sorted([b.name for b in rig.data.bones]))" 2>&1 | grep -E "BONES|BODY|PELVIS|NECK|BONE NAMES"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
BONES 65
|
||
|
|
BODY zrange -0.01063109003007412 1.7664145231246948
|
||
|
|
PELVIS z 0.9317997097969055
|
||
|
|
NECK z 1.4126112461090088
|
||
|
|
BONE NAMES ['Head', 'ball_l', 'ball_leaf_l', 'ball_leaf_r', 'ball_r', 'calf_l', 'calf_r', 'clavicle_l', 'clavicle_r', 'foot_l', 'foot_r', 'hand_l', 'hand_r', 'index_01_l', 'index_01_r', 'index_02_l', 'index_02_r', 'index_03_l', 'index_03_r', 'index_04_leaf_l', 'index_04_leaf_r', 'lowerarm_l', 'lowerarm_r', 'middle_01_l', 'middle_01_r', 'middle_02_l', 'middle_02_r', 'middle_03_l', 'middle_03_r', 'middle_04_leaf_l', 'middle_04_leaf_r', 'neck_01', 'pelvis', 'pinky_01_l', 'pinky_01_r', 'pinky_02_l', 'pinky_02_r', 'pinky_03_l', 'pinky_03_r', 'pinky_04_leaf_l', 'pinky_04_leaf_r', 'ring_01_l', 'ring_01_r', 'ring_02_l', 'ring_02_r', 'ring_03_l', 'ring_03_r', 'ring_04_leaf_l', 'ring_04_leaf_r', 'root', 'spine_01', 'spine_02', 'spine_03', 'thigh_l', 'thigh_r', 'thumb_01_l', 'thumb_01_r', 'thumb_02_l', 'thumb_02_r', 'thumb_03_l', 'thumb_03_r', 'thumb_04_leaf_l', 'thumb_04_leaf_r', 'upperarm_l', 'upperarm_r']
|
||
|
|
|
||
|
|
===== GLM agent start 2026-07-30 22:04:36 model=glm-5.2 budget=120.0min =====
|
||
|
|
|
||
|
|
--- step 1 (120 min left) ---
|
||
|
|
COMMAND: cat clothing/README.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
# 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) | 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. |
|
||
|
|
| `fit` | Inflated `BODY_SHELL` (+4mm) target; shrinkwrap mode **OUTSIDE** (push out penetrating verts ONLY — never snap the whole cloth, that shreds layered garments) | `OUTSIDE_SURFACE` destroyed multi-layer bodices; `OUTSIDE` unmasked is safe everywhere since it can't pull cloth inward. |
|
||
|
|
| `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_qua
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 2 (120 min left) ---
|
||
|
|
COMMAND: cat clothing/README.md | tail -n +120
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
(no output)
|
||
|
|
|
||
|
|
--- step 3 (120 min left) ---
|
||
|
|
COMMAND: wc -l clothing/README.md && cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
117 clothing/README.md
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 4 (120 min left) ---
|
||
|
|
COMMAND: cat .claude/skills/marvelous-designer/SKILL.md | grep -A 50 "Handing off downstream"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Handing off downstream
|
||
|
|
|
||
|
|
Export three ways per garment, into `tools/tailor/`:
|
||
|
|
|
||
|
|
```python
|
||
|
|
export_api.ExportZPrj(".../lena_<garment>_v<N>.zprj") # editable source of truth
|
||
|
|
op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
|
||
|
|
export_api.ExportFBX(".../lena_<garment>_v<N>_garment.fbx", op) # for clothing/
|
||
|
|
export_api.ExportOBJ(".../lena_<garment>_v<N>_garment.obj", op)
|
||
|
|
export_api.ExportZPac(".../lena_<garment>_v<N>.zpac") # for outfit assembly
|
||
|
|
```
|
||
|
|
|
||
|
|
Then the garment goes through `clothing/garment_pipeline.py` (fit → reduce →
|
||
|
|
skin → export) to become a game outfit part. **Two advantages MD-authored
|
||
|
|
garments have over downloaded ones**, worth exploiting in the config:
|
||
|
|
|
||
|
|
- **They're already game budget.** These export at 1.2k–3.7k verts; the
|
||
|
|
downloaded MD dress the clothing pipeline was piloted on is 2.49 M verts /
|
||
|
|
110 MB. The brutal `reduce` stage is mostly unnecessary — start with a high
|
||
|
|
tri budget and only decimate if the test bed complains.
|
||
|
|
- **They're already fitted**, having been draped on the actual game body, so
|
||
|
|
`align`/`fit` needs little more than the clearance shell.
|
||
|
|
|
||
|
|
Keep the `.zprj` — regenerating a variant beats re-authoring, and the clothing
|
||
|
|
README explicitly prefers re-exporting from MD over fighting reduction.
|
||
|
|
|
||
|
|
## Failure catalogue (don't rediscover these)
|
||
|
|
|
||
|
|
- **Shorts / trousers are unsolved.** Two-panel crotch-notch construction twists
|
||
|
|
every time — fabric can't thread between Lena's touching thighs, and all seam
|
||
|
|
parities were tried. Next approach: **4 panels arranged on the per-leg points**
|
||
|
|
(`Leg_Front_L/R`, `Leg_Back_L/R`) so cloth starts wrapped around each thigh.
|
||
|
|
- **Upside-down panels** (y-down drafting) — see the y+ = UP rule.
|
||
|
|
- **Elastic-first drapes** slide off a hip; **soft-from-frame-0** drapes bunch.
|
||
|
|
- **Co-draping** two garments inverts one.
|
||
|
|
- **Retexturing after a scene merge** silently repaints the wrong garment.
|
||
|
|
- **Threaded/background socket servers inside MD never answer** — the bridge must
|
||
|
|
own the main thread. Don't "fix" the frozen UI by re-threading it; that was
|
||
|
|
tried and the design is deliberate.
|
||
|
|
- MD 2026's **AI Image Generator / EveryWear are GUI-only** — not scriptable.
|
||
|
|
|
||
|
|
## File map
|
||
|
|
|
||
|
|
| Path | What |
|
||
|
|
|---|---|
|
||
|
|
| `tools/md_bridge.py` | Terminal client (`--ping/--exec/--file/--stop`) |
|
||
|
|
| `tools/md_bridge/TinqsMDBridge.py` | The plugin (register once via Plug-in Manager; referenced in place, so edits go live on next click) |
|
||
|
|
| `tools/md_bridge/api_surface.json`, `api_docs.json` | Introspected API truth |
|
||
|
|
| `docs/md-bridge.md` | Protocol, install, threading rationale |
|
||
|
|
| `tools/tailor/measure_body.py`, `lena_measurements.json` | Body measurement tool + Lena's card |
|
||
|
|
| `tools/tailor/md_*.py` | Per-garment recipes (each header carries its own lessons) |
|
||
|
|
|
||
|
|
--- step 5 (120 min left) ---
|
||
|
|
COMMAND: ls -la tools/tailor/lena_pari_v3_garment.fbx tools/tailor/lena_piupiu_v2_garment.fbx && ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb" && ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress/"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 149824 Jul 30 21:13 tools/tailor/lena_pari_v3_garment.fbx
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 271568 Jul 30 21:04 tools/tailor/lena_piupiu_v2_garment.fbx
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 8535864 Jul 30 14:36 C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb
|
||
|
|
total 464
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 20:21 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 19:54 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 438936 Jul 30 20:20 Female_MDDress_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 25779 Jul 30 20:20 Female_MDDress_Body.gltf
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1101 Jul 30 20:21 Female_MDDress_Body.gltf.import
|
||
|
|
|
||
|
|
--- step 6 (120 min left) ---
|
||
|
|
COMMAND: head -80 clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
# Garment pipeline: clothing mesh (any source) -> game-ready outfit-part GLBs
|
||
|
|
# on the shared 65-bone Quaternius skeleton. See README.md for design rationale
|
||
|
|
# (GLM-reviewed: fit-before-decimate, skirt proxy + cone weights, inner-shell
|
||
|
|
# clearance, layer stacking).
|
||
|
|
#
|
||
|
|
# Staged; every stage loads the previous checkpoint .blend from work/ and saves
|
||
|
|
# its own, so any stage can be re-run alone while iterating:
|
||
|
|
#
|
||
|
|
# blender --background --python garment_pipeline.py -- \
|
||
|
|
# --config configs/dress.json --stage prepare|fit|reduce|bake|skin|export
|
||
|
|
#
|
||
|
|
# Naming inside the .blend (stable contract between stages):
|
||
|
|
# GARM_<Part> garment part mesh (high-poly until `reduce`, low-poly after)
|
||
|
|
# HI_<Part> hidden high-poly copy kept for baking (created by `reduce`)
|
||
|
|
# BODY the target body mesh (from the game GLB, with vertex groups)
|
||
|
|
# BODY_SHELL inflated clearance copy of BODY (fit target)
|
||
|
|
# RIG the Quaternius armature imported with BODY
|
||
|
|
# CONE_<Part> skirt weight-transfer proxy (created by `skin`, kept for QA)
|
||
|
|
|
||
|
|
import bpy, bmesh, json, math, os, sys, argparse, random
|
||
|
|
from mathutils import Vector, Matrix
|
||
|
|
|
||
|
|
random.seed(7)
|
||
|
|
|
||
|
|
ARGS = None
|
||
|
|
CFG = None
|
||
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
|
|
|
||
|
|
|
||
|
|
# ── plumbing ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def log(msg):
|
||
|
|
print(f"[cloth] {msg}", flush=True)
|
||
|
|
|
||
|
|
|
||
|
|
def work_path(name):
|
||
|
|
d = os.path.join(HERE, "work", CFG["name"])
|
||
|
|
os.makedirs(d, exist_ok=True)
|
||
|
|
return os.path.join(d, name)
|
||
|
|
|
||
|
|
|
||
|
|
def save_checkpoint(stage):
|
||
|
|
p = work_path(f"{stage}.blend")
|
||
|
|
bpy.ops.wm.save_as_mainfile(filepath=p, compress=True)
|
||
|
|
log(f"checkpoint saved: {p}")
|
||
|
|
|
||
|
|
|
||
|
|
def load_checkpoint(stage):
|
||
|
|
p = work_path(f"{stage}.blend")
|
||
|
|
if not os.path.exists(p):
|
||
|
|
raise SystemExit(f"missing checkpoint {p} — run stage '{stage}' first")
|
||
|
|
bpy.ops.wm.open_mainfile(filepath=p)
|
||
|
|
log(f"checkpoint loaded: {p}")
|
||
|
|
|
||
|
|
|
||
|
|
def obj(name):
|
||
|
|
o = bpy.data.objects.get(name)
|
||
|
|
if o is None:
|
||
|
|
raise SystemExit(f"expected object '{name}' not in scene")
|
||
|
|
return o
|
||
|
|
|
||
|
|
|
||
|
|
def objs_prefixed(prefix):
|
||
|
|
return [o for o in bpy.data.objects if o.name.startswith(prefix)]
|
||
|
|
|
||
|
|
|
||
|
|
def select_only(objects, active=None):
|
||
|
|
bpy.ops.object.select_all(action="DESELECT")
|
||
|
|
for o in objects:
|
||
|
|
o.select_set(True)
|
||
|
|
bpy.context.view_layer.objects.active = active or objects[0]
|
||
|
|
|
||
|
|
|
||
|
|
def apply_all_transforms(objects):
|
||
|
|
select_only(objects)
|
||
|
|
bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM")
|
||
|
|
for o in objects:
|
||
|
|
select_only([o])
|
||
|
|
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||
|
|
|
||
|
|
--- step 7 (120 min left) ---
|
||
|
|
COMMAND: grep -n "def stage_census\|def stage_prepare\|def stage_fit\|def stage_reduce\|def stage_skin\|def stage_export\|def stage_bake\|def main\|argparse\|add_argument" clothing/garment_pipeline.py | head -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
20:import bpy, bmesh, json, math, os, sys, argparse, random
|
||
|
|
220:def stage_census():
|
||
|
|
251:def stage_prepare():
|
||
|
|
498:def stage_fit():
|
||
|
|
590:def stage_reduce():
|
||
|
|
649:def stage_bake():
|
||
|
|
758:def stage_skin():
|
||
|
|
806:def stage_export():
|
||
|
|
840:def main():
|
||
|
|
843: ap = argparse.ArgumentParser()
|
||
|
|
844: ap.add_argument("--config", required=True)
|
||
|
|
845: ap.add_argument("--stage", required=True, choices=sorted(STAGES))
|
||
|
|
|
||
|
|
--- step 8 (120 min left) ---
|
||
|
|
COMMAND: sed -n '220,260p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_census():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
report = []
|
||
|
|
for i, o in enumerate(islands):
|
||
|
|
vs = world_verts(o)
|
||
|
|
zs = [v.z for v in vs]; xs = [v.x for v in vs]; ys = [v.y for v in vs]
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
color = CENSUS_COLORS[i % len(CENSUS_COLORS)] if i < len(CENSUS_COLORS) else (0.5, 0.5, 0.5)
|
||
|
|
mat = bpy.data.materials.new(f"census_{i:03d}")
|
||
|
|
mat.diffuse_color = (*color, 1.0)
|
||
|
|
o.data.materials.clear()
|
||
|
|
o.data.materials.append(mat)
|
||
|
|
report.append({"island": i, "verts": len(o.data.vertices), "tris": tris_of(o),
|
||
|
|
"z": [round(min(zs), 3), round(max(zs), 3)],
|
||
|
|
"x": [round(min(xs), 3), round(max(xs), 3)],
|
||
|
|
"centroid": [round(c.x, 3), round(c.y, 3), round(c.z, 3)],
|
||
|
|
"color": [round(v, 2) for v in color]})
|
||
|
|
log(f"ISL_{i:03d} verts={len(o.data.vertices):7d} z=[{min(zs):.3f},{max(zs):.3f}] "
|
||
|
|
f"x=[{min(xs):+.3f},{max(xs):+.3f}] color={tuple(round(v,2) for v in color)}")
|
||
|
|
with open(work_path("census.json"), "w", encoding="utf-8") as f:
|
||
|
|
json.dump(report, f, indent=1)
|
||
|
|
bpy.context.scene.display.shading.color_type = "MATERIAL"
|
||
|
|
qa_render("00_census", focus_objects=islands)
|
||
|
|
save_checkpoint("00_census")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: prepare ───────────────────────────────────────────────────────────
|
||
|
|
# Group census islands into configured parts (each part lists island indices),
|
||
|
|
# import body+rig, align garment onto body.
|
||
|
|
|
||
|
|
def stage_prepare():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
|
||
|
|
# body + rig first (defines target space)
|
||
|
|
body_path = CFG["body"]
|
||
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
||
|
|
rig = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||
|
|
rig.name = "RIG"
|
||
|
|
body = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||
|
|
key=lambda o: len(o.data.vertices))
|
||
|
|
|
||
|
|
--- step 9 (119 min left) ---
|
||
|
|
COMMAND: sed -n '251,500p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_prepare():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
|
||
|
|
# body + rig first (defines target space)
|
||
|
|
body_path = CFG["body"]
|
||
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
||
|
|
rig = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||
|
|
rig.name = "RIG"
|
||
|
|
body = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||
|
|
key=lambda o: len(o.data.vertices))
|
||
|
|
# drop face-part extras (eyes etc.) that ride along in the body GLB
|
||
|
|
for o in [o for o in bpy.data.objects if o.type == "MESH" and o is not body]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
body.name = "BODY"
|
||
|
|
apply_all_transforms([rig, body])
|
||
|
|
log(f"body: {len(body.data.vertices)} verts, groups={len(body.vertex_groups)}, "
|
||
|
|
f"bones={len(rig.data.bones)}")
|
||
|
|
|
||
|
|
# garment islands (same deterministic indexing the census printed)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
by_idx = {int(o.name.split("_")[1]): o for o in islands}
|
||
|
|
claimed = set()
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
ids = part_cfg.get("islands", [])
|
||
|
|
members = [by_idx[i] for i in ids if i in by_idx]
|
||
|
|
if not members:
|
||
|
|
log(f"WARNING: part {part_name} matched no islands ({ids})")
|
||
|
|
continue
|
||
|
|
claimed.update(ids)
|
||
|
|
select_only(members, active=members[0])
|
||
|
|
bpy.ops.object.join()
|
||
|
|
part = bpy.context.view_layer.objects.active
|
||
|
|
part.name = f"GARM_{part_name}"
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
part.data.materials.clear()
|
||
|
|
part.data.materials.append(mat)
|
||
|
|
log(f"part {part.name}: islands={ids} verts={len(part.data.vertices)} tris={tris_of(part)}")
|
||
|
|
for i, o in list(by_idx.items()):
|
||
|
|
if i not in claimed and o.name.startswith("ISL_"):
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
|
||
|
|
# bodyshell parts: fitted clothing built FROM the body (Islander technique):
|
||
|
|
# copy the body, keep the configured z-band + torso-dominant vertices, push
|
||
|
|
# outward along normals. Fit and weights are correct by construction.
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
if part_cfg.get("type") != "bodyshell":
|
||
|
|
continue
|
||
|
|
shellp = body.copy()
|
||
|
|
shellp.data = body.data.copy()
|
||
|
|
shellp.name = f"GARM_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(shellp)
|
||
|
|
z0, z1 = part_cfg["z0"], part_cfg["z1"]
|
||
|
|
allowed = set(part_cfg.get("bones", ["spine_01", "spine_02", "spine_03", "pelvis"]))
|
||
|
|
gnames = {i: g.name for i, g in enumerate(shellp.vertex_groups)}
|
||
|
|
keep = set()
|
||
|
|
for v in shellp.data.vertices:
|
||
|
|
z = (shellp.matrix_world @ v.co).z
|
||
|
|
if not (z0 <= z <= z1):
|
||
|
|
continue
|
||
|
|
best, bw = None, 0.0
|
||
|
|
for g in v.groups:
|
||
|
|
if g.weight > bw:
|
||
|
|
best, bw = gnames.get(g.group), g.weight
|
||
|
|
if best in allowed:
|
||
|
|
keep.add(v.index)
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(shellp.data)
|
||
|
|
bm.verts.ensure_lookup_table()
|
||
|
|
doomed = [v for v in bm.verts if v.index not in keep]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="VERTS")
|
||
|
|
bm.to_mesh(shellp.data)
|
||
|
|
bm.free()
|
||
|
|
d = shellp.modifiers.new("Offset", "DISPLACE")
|
||
|
|
d.strength = part_cfg.get("offset_mm", 6) / 1000.0
|
||
|
|
d.mid_level = 0.0
|
||
|
|
apply_modifier(shellp, d.name)
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
shellp.data.materials.clear()
|
||
|
|
shellp.data.materials.append(mat)
|
||
|
|
log(f"bodyshell {shellp.name}: verts={len(shellp.data.vertices)} tris={tris_of(shellp)}")
|
||
|
|
|
||
|
|
parts = [o for o in objs_prefixed("GARM_")
|
||
|
|
if CFG["parts"][o.name[5:]].get("type") != "bodyshell"]
|
||
|
|
if not parts:
|
||
|
|
raise SystemExit("no parts matched any islands — check config vs census.json")
|
||
|
|
|
||
|
|
# sleeve pose-warp FIRST, in garment-local space (unambiguous: sleeves are
|
||
|
|
# the only geometry beyond x_beyond). Rigid rotation around the garment's
|
||
|
|
# own shoulder point, bringing each cuff to horizontal (T-pose).
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
wcfg = part_cfg.get("sleeve_warp")
|
||
|
|
if not wcfg:
|
||
|
|
continue
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
x_beyond = wcfg.get("x_beyond", 0.28)
|
||
|
|
sx, sz = wcfg.get("shoulder", [0.19, 1.43])
|
||
|
|
for sign in (1.0, -1.0):
|
||
|
|
pivot = Vector((sign * sx, 0.0, sz))
|
||
|
|
sleeve = [v for v in p.data.vertices if sign * v.co.x > x_beyond]
|
||
|
|
if len(sleeve) < 10:
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: none found")
|
||
|
|
continue
|
||
|
|
tip = max(sleeve, key=lambda v: sign * v.co.x)
|
||
|
|
cur = math.atan2(tip.co.z - pivot.z, sign * (tip.co.x - pivot.x))
|
||
|
|
rot = Matrix.Rotation(-sign * cur, 4, "Y")
|
||
|
|
M = Matrix.Translation(pivot) @ rot @ Matrix.Translation(-pivot)
|
||
|
|
for v in sleeve:
|
||
|
|
v.co = M @ v.co
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: {math.degrees(cur):+.1f} deg, "
|
||
|
|
f"{len(sleeve)} verts")
|
||
|
|
a = CFG.get("align", {})
|
||
|
|
sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
sz = a.get("scale_z", a.get("
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 10 (119 min left) ---
|
||
|
|
COMMAND: sed -n '380,500p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
M = Matrix.Diagonal(Vector((sxy, sxy, sz))).to_4x4()
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(M)
|
||
|
|
gz0, gz1 = zspan(parts)
|
||
|
|
b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(T)
|
||
|
|
log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
|
||
|
|
# torso boost: heroic bodies are wider than MD mannequins — radially inflate
|
||
|
|
# the above-waist region about the body axis so bodice/collar fabric starts
|
||
|
|
# OUTSIDE the body (direction-preserving; the OUTSIDE shrinkwrap then only
|
||
|
|
# catches stragglers instead of shredding buried cloth to random sides).
|
||
|
|
tb = CFG.get("torso_boost")
|
||
|
|
if tb:
|
||
|
|
z0, z1, boost = tb.get("z0", 1.00), tb.get("z1", 1.08), tb.get("xy", 1.18)
|
||
|
|
for p in parts:
|
||
|
|
for v in p.data.vertices:
|
||
|
|
z = v.co.z
|
||
|
|
if z <= z0:
|
||
|
|
continue
|
||
|
|
t = 1.0 if z >= z1 else (z - z0) / max(1e-9, z1 - z0)
|
||
|
|
f = 1.0 + (boost - 1.0) * t
|
||
|
|
v.co.x *= f
|
||
|
|
v.co.y *= f
|
||
|
|
log(f"torso_boost: xy x{boost} above z={z0}..{z1}")
|
||
|
|
|
||
|
|
# post-align cut: drop configured fabric above a body-space height (v1 uses
|
||
|
|
# this to remove the slim MD bodice+sleeves replaced by the bodyshell part)
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
cut = part_cfg.get("cut_above_z")
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if cut is None or p is None or part_cfg.get("type") == "bodyshell":
|
||
|
|
continue
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(p.data)
|
||
|
|
ox = part_cfg.get("cut_outboard_x")
|
||
|
|
oz = part_cfg.get("cut_outboard_z_above", 0.0)
|
||
|
|
def out(v):
|
||
|
|
w = p.matrix_world @ v.co
|
||
|
|
return w.z > cut or (ox is not None and w.z > oz and abs(w.x) > ox)
|
||
|
|
doomed = [v for v in bm.verts if out(v)]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="VERTS")
|
||
|
|
bm.to_mesh(p.data)
|
||
|
|
bm.free()
|
||
|
|
log(f"cut {p.name} above z={cut}: now {len(p.data.vertices)} verts")
|
||
|
|
# prune disconnected scraps left by the cuts (e.g. sleeve remnants):
|
||
|
|
# separate loose, keep the biggest piece + anything near the center axis
|
||
|
|
px = part_cfg.get("prune_scraps_x")
|
||
|
|
if px is not None:
|
||
|
|
pname = p.name
|
||
|
|
select_only([p])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.separate(type="LOOSE")
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
pieces = [o for o in bpy.data.objects if o.name.startswith(pname)]
|
||
|
|
main = max(pieces, key=lambda o: len(o.data.vertices))
|
||
|
|
keepers = [main]
|
||
|
|
for o in pieces:
|
||
|
|
if o is main:
|
||
|
|
continue
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
if abs(c.x) <= px:
|
||
|
|
keepers.append(o)
|
||
|
|
else:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
select_only(keepers, active=main)
|
||
|
|
bpy.ops.object.join()
|
||
|
|
joined = bpy.context.view_layer.objects.active
|
||
|
|
joined.name = pname
|
||
|
|
log(f"prune {joined.name}: kept {len(keepers)} pieces, "
|
||
|
|
f"{len(joined.data.vertices)} verts")
|
||
|
|
|
||
|
|
qa_render("10_prepare")
|
||
|
|
save_checkpoint("10_prepare")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: fit ───────────────────────────────────────────────────────────────
|
||
|
|
# Build the inflated BODY_SHELL, give each part a Z-gradient influence mask, and
|
||
|
|
# shrinkwrap high-poly parts in configured layer order (later layers can target
|
||
|
|
# earlier ones).
|
||
|
|
|
||
|
|
def make_shell():
|
||
|
|
body = obj("BODY")
|
||
|
|
shell = body.copy()
|
||
|
|
shell.data = body.data.copy()
|
||
|
|
shell.name = "BODY_SHELL"
|
||
|
|
bpy.context.scene.collection.objects.link(shell)
|
||
|
|
d = shell.modifiers.new("Inflate", "DISPLACE")
|
||
|
|
d.strength = CFG.get("shell_mm", 4) / 1000.0
|
||
|
|
d.mid_level = 0.0
|
||
|
|
apply_modifier(shell, d.name)
|
||
|
|
shell.hide_render = True
|
||
|
|
return shell
|
||
|
|
|
||
|
|
|
||
|
|
def zgradient_group(o, name, z_full, z_zero):
|
||
|
|
"""Vertex group: weight 1 above z_full … 0 below z_zero (linear between)."""
|
||
|
|
vg = o.vertex_groups.get(name) or o.vertex_groups.new(name=name)
|
||
|
|
mw = o.matrix_world
|
||
|
|
for v in o.data.vertices:
|
||
|
|
z = (mw @ v.co).z
|
||
|
|
if z >= z_full:
|
||
|
|
w = 1.0
|
||
|
|
elif z <= z_zero:
|
||
|
|
w = 0.0
|
||
|
|
else:
|
||
|
|
w = (z - z_zero) / max(1e-9, (z_full - z_zero))
|
||
|
|
vg.add([v.index], w, "REPLACE")
|
||
|
|
return vg
|
||
|
|
|
||
|
|
|
||
|
|
def stage_fit():
|
||
|
|
load_checkpoint("10_prepare")
|
||
|
|
rig = obj("RIG")
|
||
|
|
|
||
|
|
--- step 11 (119 min left) ---
|
||
|
|
COMMAND: sed -n '498,650p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_fit():
|
||
|
|
load_checkpoint("10_prepare")
|
||
|
|
rig = obj("RIG")
|
||
|
|
shell = make_shell()
|
||
|
|
hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
if part_cfg.get("type") == "bodyshell":
|
||
|
|
log(f"fit GARM_{part_name}: skipped (bodyshell)")
|
||
|
|
continue
|
||
|
|
fit = part_cfg.get("fit", {})
|
||
|
|
target_name = fit.get("target", "BODY_SHELL")
|
||
|
|
target = obj(target_name if target_name != "BODY_SHELL" else "BODY_SHELL")
|
||
|
|
mode = fit.get("mask", "full") # full | above_hip | waistband | none
|
||
|
|
if mode == "none":
|
||
|
|
log(f"fit {p.name}: skipped (mask none)")
|
||
|
|
continue
|
||
|
|
if mode == "full":
|
||
|
|
vg = None
|
||
|
|
elif mode == "above_hip":
|
||
|
|
vg = zgradient_group(p, "fit_mask", hip_z + 0.02, hip_z - 0.06)
|
||
|
|
elif mode == "waistband":
|
||
|
|
vg = zgradient_group(p, "fit_mask", hip_z + 0.10, hip_z - 0.02)
|
||
|
|
sw = p.modifiers.new("Fit", "SHRINKWRAP")
|
||
|
|
sw.target = target
|
||
|
|
sw.wrap_method = "NEAREST_SURFACEPOINT"
|
||
|
|
sw.wrap_mode = fit.get("wrap_mode", "OUTSIDE")
|
||
|
|
sw.offset = fit.get("offset_mm", 2) / 1000.0
|
||
|
|
if vg is not None:
|
||
|
|
sw.vertex_group = vg.name
|
||
|
|
apply_modifier(p, sw.name)
|
||
|
|
log(f"fit {p.name}: target={target.name} mask={mode} offset={sw.offset if hasattr(sw,'offset') else '?'}")
|
||
|
|
|
||
|
|
qa_render("20_fit")
|
||
|
|
save_checkpoint("20_fit")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: reduce ────────────────────────────────────────────────────────────
|
||
|
|
# Keep a hidden HI_<part> copy for baking. Skirt-style parts are replaced by a
|
||
|
|
# generated cylinder proxy shrinkwrapped to the high-poly; others get planar
|
||
|
|
# dissolve then collapse decimate down to budget.
|
||
|
|
|
||
|
|
def make_skirt_proxy(part, proxy_cfg):
|
||
|
|
seg = proxy_cfg.get("segments", 30)
|
||
|
|
rings = proxy_cfg.get("rings", 6)
|
||
|
|
vs = world_verts(part)
|
||
|
|
zs = sorted(v.z for v in vs)
|
||
|
|
z0, z1 = zs[int(0.01 * len(zs))], zs[int(0.99 * len(zs))]
|
||
|
|
cx = sum(v.x for v in vs) / len(vs)
|
||
|
|
cy = sum(v.y for v in vs) / len(vs)
|
||
|
|
|
||
|
|
def ring_radius(z):
|
||
|
|
band = [v for v in vs if abs(v.z - z) < (z1 - z0) / (rings * 1.5)]
|
||
|
|
if not band:
|
||
|
|
return 0.2
|
||
|
|
ds = sorted(math.hypot(v.x - cx, v.y - cy) for v in band)
|
||
|
|
return ds[int(0.65 * len(ds))]
|
||
|
|
|
||
|
|
me = bpy.data.meshes.new(f"{part.name}_proxy")
|
||
|
|
bm = bmesh.new()
|
||
|
|
ring_verts = []
|
||
|
|
for r in range(rings + 1):
|
||
|
|
z = z1 - (z1 - z0) * (r / rings)
|
||
|
|
rad = ring_radius(z)
|
||
|
|
ring = [bm.verts.new((cx + rad * math.cos(2 * math.pi * s / seg),
|
||
|
|
cy + rad * math.sin(2 * math.pi * s / seg), z))
|
||
|
|
for s in range(seg)]
|
||
|
|
ring_verts.append(ring)
|
||
|
|
for r in range(rings):
|
||
|
|
for s in range(seg):
|
||
|
|
a, b = ring_verts[r][s], ring_verts[r][(s + 1) % seg]
|
||
|
|
c, d = ring_verts[r + 1][(s + 1) % seg], ring_verts[r + 1][s]
|
||
|
|
bm.faces.new((a, b, c, d))
|
||
|
|
bm.to_mesh(me)
|
||
|
|
bm.free()
|
||
|
|
proxy = bpy.data.objects.new(f"{part.name}_proxytmp", me)
|
||
|
|
bpy.context.scene.collection.objects.link(proxy)
|
||
|
|
sw = proxy.modifiers.new("WrapToSkirt", "SHRINKWRAP")
|
||
|
|
sw.target = part
|
||
|
|
sw.wrap_method = "NEAREST_SURFACEPOINT"
|
||
|
|
apply_modifier(proxy, sw.name)
|
||
|
|
select_only([proxy])
|
||
|
|
bpy.ops.object.shade_smooth()
|
||
|
|
# carry the part's (single) material
|
||
|
|
if part.data.materials:
|
||
|
|
proxy.data.materials.append(part.data.materials[0])
|
||
|
|
return proxy
|
||
|
|
|
||
|
|
|
||
|
|
def stage_reduce():
|
||
|
|
load_checkpoint("20_fit")
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
# keep high-poly for baking
|
||
|
|
hi = p.copy()
|
||
|
|
hi.data = p.data.copy()
|
||
|
|
hi.name = f"HI_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(hi)
|
||
|
|
hi.hide_render = hi.hide_viewport = True
|
||
|
|
|
||
|
|
wb = part_cfg.get("weld_band") # [z0, z1, dist]: fuse stacked layers
|
||
|
|
if wb:
|
||
|
|
z0, z1, dist = wb
|
||
|
|
select_only([p])
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(p.data)
|
||
|
|
mw = p.matrix_world
|
||
|
|
band = [v for v in bm.verts if z0 <= (mw @ v.co).z <= z1]
|
||
|
|
bmesh.ops.remove_doubles(bm, verts=band, dist=dist)
|
||
|
|
bm.to_mesh(p.data)
|
||
|
|
bm.free()
|
||
|
|
log(f"weld_band {p.name}: z=[{z0},{z1}] dist={dist} -> {len(p.data.vertices)} verts")
|
||
|
|
|
||
|
|
budget = part_cfg.get("tris", 1500)
|
||
|
|
if part_cfg.get("proxy") == "cylinder":
|
||
|
|
proxy = make_skirt_proxy(p, part_cfg.get("proxy_cfg", {}))
|
||
|
|
name = p.name
|
||
|
|
bpy.data.objects.remove(p, do_unlink=True)
|
||
|
|
proxy.name = name
|
||
|
|
p = proxy
|
||
|
|
else:
|
||
|
|
# planar dissolve first (MD panels are near-planar), then collapse
|
||
|
|
dm = p.modifiers.new("Planar", "DECIMATE")
|
||
|
|
dm.decimate_type = "DISSOLVE"
|
||
|
|
dm.angle_limit = math.radians(part_cfg.get("planar_deg", 5))
|
||
|
|
apply_modifier(p, dm.name)
|
||
|
|
cur = tris_of(p)
|
||
|
|
if cur > budget:
|
||
|
|
dm = p.modifiers.new("Collapse", "DECIMATE")
|
||
|
|
dm.decimate_type = "COLLAPSE"
|
||
|
|
dm.ratio = budget / cur
|
||
|
|
apply_modifier(p, dm.name)
|
||
|
|
select_only([p])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
log(f"reduce {p.name}: tris={tris_of(p)} (budget {budget})")
|
||
|
|
|
||
|
|
qa_render("30_reduce", focus_objects=objs_prefixed("GARM_"))
|
||
|
|
save_checkpoint("30_reduce")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: bake ────────────────────────────â”
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 12 (119 min left) ---
|
||
|
|
COMMAND: sed -n '758,860p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_skin():
|
||
|
|
src_stage = "40_bake" if os.path.exists(work_path("40_bake.blend")) else "30_reduce"
|
||
|
|
load_checkpoint(src_stage)
|
||
|
|
body, rig = obj("BODY"), obj("RIG")
|
||
|
|
hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
if part_cfg.get("type") == "bodyshell":
|
||
|
|
pass # weights inherited from the body copy — already correct
|
||
|
|
elif part_cfg.get("weights") == "dress":
|
||
|
|
# one-piece dress: body weights everywhere, then blend the loose
|
||
|
|
# skirt region toward pelvis-only as z approaches the hem
|
||
|
|
hem_z = zspan([p])[0]
|
||
|
|
data_transfer_weights(p, body)
|
||
|
|
pelvis_gradient(p, hip_z - 0.04, hem_z)
|
||
|
|
elif part_cfg.get("weights") == "skirt":
|
||
|
|
hem_z = zspan([p])[0]
|
||
|
|
cone = p.copy() # skirt proxy IS already a leg-bridging cone shape
|
||
|
|
cone.data = p.data.copy()
|
||
|
|
cone.name = f"CONE_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(cone)
|
||
|
|
data_transfer_weights(cone, body)
|
||
|
|
pelvis_gradient(cone, hip_z, hem_z)
|
||
|
|
smooth_and_normalize(cone)
|
||
|
|
data_transfer_weights(p, cone)
|
||
|
|
cone.hide_viewport = cone.hide_render = True
|
||
|
|
else:
|
||
|
|
data_transfer_weights(p, body)
|
||
|
|
smooth_and_normalize(p)
|
||
|
|
# bind
|
||
|
|
p.parent = rig
|
||
|
|
p.matrix_parent_inverse = Matrix.Identity(4)
|
||
|
|
am = p.modifiers.new("Armature", "ARMATURE")
|
||
|
|
am.object = rig
|
||
|
|
nonzero = sum(1 for v in p.data.vertices if v.groups)
|
||
|
|
log(f"skin {p.name}: weighted {nonzero}/{len(p.data.vertices)} verts")
|
||
|
|
|
||
|
|
qa_render("50_skin", focus_objects=objs_prefixed("GARM_"))
|
||
|
|
save_checkpoint("50_skin")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: export ────────────────────────────────────────────────────────────
|
||
|
|
# One GLB per outfit slot (multiple parts may share a slot). Armature + parts
|
||
|
|
# only; BODY/HI/CONE stay out.
|
||
|
|
|
||
|
|
def stage_export():
|
||
|
|
load_checkpoint("50_skin")
|
||
|
|
rig = obj("RIG")
|
||
|
|
out_dir = CFG["export"]["out_dir"]
|
||
|
|
os.makedirs(out_dir, exist_ok=True)
|
||
|
|
slots = {}
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is not None:
|
||
|
|
slots.setdefault(part_cfg["slot"], []).append(p)
|
||
|
|
gender, set_name = CFG["export"]["gender"], CFG["export"]["set"]
|
||
|
|
for slot, parts in slots.items():
|
||
|
|
path = os.path.join(out_dir, f"{gender}_{set_name}_{slot}.gltf")
|
||
|
|
select_only([rig] + parts, active=rig)
|
||
|
|
bpy.ops.export_scene.gltf(filepath=path, export_format="GLTF_SEPARATE",
|
||
|
|
use_selection=True, export_skins=True,
|
||
|
|
export_animations=False, export_yup=True)
|
||
|
|
log(f"EXPORTED {path} ({sum(tris_of(p) for p in parts)} tris, "
|
||
|
|
f"{os.path.getsize(path)} bytes)")
|
||
|
|
# determinism fingerprint
|
||
|
|
import hashlib
|
||
|
|
h = hashlib.sha1()
|
||
|
|
for part in sorted(objs_prefixed("GARM_"), key=lambda o: o.name):
|
||
|
|
for v in part.data.vertices:
|
||
|
|
h.update(b"%d %d %d" % (int(v.co.x * 1e5), int(v.co.y * 1e5), int(v.co.z * 1e5)))
|
||
|
|
log(f"vertex hash: {h.hexdigest()[:16]}")
|
||
|
|
|
||
|
|
|
||
|
|
# ── main ─────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
STAGES = {"census": stage_census, "prepare": stage_prepare, "fit": stage_fit, "reduce": stage_reduce,
|
||
|
|
"bake": stage_bake, "skin": stage_skin, "export": stage_export}
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
global ARGS, CFG
|
||
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--config", required=True)
|
||
|
|
ap.add_argument("--stage", required=True, choices=sorted(STAGES))
|
||
|
|
ARGS = ap.parse_args(argv)
|
||
|
|
cfg_path = ARGS.config if os.path.isabs(ARGS.config) else os.path.join(HERE, ARGS.config)
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
log(f"config={CFG['name']} stage={ARGS.stage}")
|
||
|
|
STAGES[ARGS.stage]()
|
||
|
|
|
||
|
|
|
||
|
|
main()
|
||
|
|
|
||
|
|
--- step 13 (119 min left) ---
|
||
|
|
COMMAND: sed -n '100,220p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def qa_render(tag, focus_objects=None, ortho_pad=1.25):
|
||
|
|
"""Front + 3/4 workbench renders of the current scene into work/."""
|
||
|
|
scene = bpy.context.scene
|
||
|
|
scene.render.engine = "BLENDER_WORKBENCH"
|
||
|
|
scene.display.shading.light = "STUDIO"
|
||
|
|
scene.display.shading.color_type = "MATERIAL"
|
||
|
|
scene.render.resolution_x, scene.render.resolution_y = 900, 1200
|
||
|
|
vs = [v for o in (focus_objects or [o for o in bpy.data.objects if o.type == "MESH" and o.visible_get()])
|
||
|
|
for v in world_verts(o)]
|
||
|
|
if not vs:
|
||
|
|
return
|
||
|
|
zmin, zmax = min(v.z for v in vs), max(v.z for v in vs)
|
||
|
|
cz, h = (zmin + zmax) / 2, (zmax - zmin)
|
||
|
|
for view, loc, rot in (("front", (0, -4, cz), (math.pi / 2, 0, 0)),
|
||
|
|
("quarter", (2.7, -2.9, cz + 0.15), (math.radians(80), 0, math.radians(42)))):
|
||
|
|
cam_name = f"_qa_{view}"
|
||
|
|
cam = bpy.data.objects.get(cam_name)
|
||
|
|
if cam is None:
|
||
|
|
cam = bpy.data.objects.new(cam_name, bpy.data.cameras.new(cam_name))
|
||
|
|
bpy.context.scene.collection.objects.link(cam)
|
||
|
|
cam.data.type = "ORTHO"
|
||
|
|
cam.data.ortho_scale = h * ortho_pad
|
||
|
|
cam.location, cam.rotation_euler = loc, rot
|
||
|
|
scene.camera = cam
|
||
|
|
scene.render.filepath = work_path(f"qa_{tag}_{view}.png")
|
||
|
|
bpy.ops.render.render(write_still=True)
|
||
|
|
log(f"QA render: {scene.render.filepath}")
|
||
|
|
|
||
|
|
|
||
|
|
def bone_head_z(rig, bone):
|
||
|
|
return (rig.matrix_world @ rig.data.bones[bone].head_local).z
|
||
|
|
|
||
|
|
|
||
|
|
# ── garment import (shared by census + prepare) ─────────────────────────────
|
||
|
|
# MD exports hide the real structure: fabric may live on ONE material while
|
||
|
|
# millions of faces are topstitch threads. So: drop configured stitch/trash
|
||
|
|
# materials, weld, split by LOOSE PARTS, and identify pieces as "islands"
|
||
|
|
# (indexed by vertex count, deterministic). Parts are then configured as island
|
||
|
|
# index lists (see census renders).
|
||
|
|
|
||
|
|
def import_garment_islands():
|
||
|
|
pre = set(bpy.data.objects)
|
||
|
|
src = CFG["source"]
|
||
|
|
ext = os.path.splitext(src)[1].lower()
|
||
|
|
if ext == ".fbx":
|
||
|
|
bpy.ops.import_scene.fbx(filepath=src)
|
||
|
|
elif ext in (".glb", ".gltf"):
|
||
|
|
bpy.ops.import_scene.gltf(filepath=src)
|
||
|
|
elif ext == ".obj":
|
||
|
|
bpy.ops.wm.obj_import(filepath=src)
|
||
|
|
else:
|
||
|
|
raise SystemExit(f"unsupported garment format {ext}")
|
||
|
|
new = [o for o in bpy.data.objects if o not in pre]
|
||
|
|
meshes = [o for o in new if o.type == "MESH"]
|
||
|
|
for o in [o for o in new if o.type != "MESH"]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
for g in meshes:
|
||
|
|
for m in list(g.modifiers):
|
||
|
|
g.modifiers.remove(m)
|
||
|
|
g.vertex_groups.clear()
|
||
|
|
apply_all_transforms(meshes)
|
||
|
|
|
||
|
|
drop = set(CFG.get("drop_materials", []))
|
||
|
|
for g in meshes:
|
||
|
|
if not g.data.materials:
|
||
|
|
continue
|
||
|
|
drop_idx = {i for i, m in enumerate(g.data.materials) if m and m.name in drop}
|
||
|
|
if not drop_idx:
|
||
|
|
continue
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(g.data)
|
||
|
|
doomed = [f for f in bm.faces if f.material_index in drop_idx]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="FACES")
|
||
|
|
bm.to_mesh(g.data)
|
||
|
|
bm.free()
|
||
|
|
meshes = [g for g in meshes if len(g.data.vertices) > 0]
|
||
|
|
log(f"after drop_materials: {sum(len(g.data.vertices) for g in meshes)} verts")
|
||
|
|
|
||
|
|
# weld panels, then split to loose islands
|
||
|
|
select_only(meshes)
|
||
|
|
bpy.ops.object.join()
|
||
|
|
fabric = bpy.context.view_layer.objects.active
|
||
|
|
select_only([fabric])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.remove_doubles(threshold=CFG.get("weld_threshold", 0.0006))
|
||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||
|
|
bpy.ops.mesh.separate(type="LOOSE")
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
islands = [o for o in bpy.data.objects if o.type == "MESH" and o.name not in ("BODY", "BODY_SHELL")]
|
||
|
|
min_verts = CFG.get("min_island_verts", 40)
|
||
|
|
kept = []
|
||
|
|
for o in islands:
|
||
|
|
if len(o.data.vertices) < min_verts:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
else:
|
||
|
|
kept.append(o)
|
||
|
|
# deterministic index: vertex count desc, then centroid x/z
|
||
|
|
def key(o):
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
return (-len(o.data.vertices), round(c.x, 4), round(c.z, 4))
|
||
|
|
kept.sort(key=key)
|
||
|
|
for i, o in enumerate(kept):
|
||
|
|
o.name = f"ISL_{i:03d}"
|
||
|
|
log(f"islands kept: {len(kept)} (dropped tiny <{min_verts} verts)")
|
||
|
|
return kept
|
||
|
|
|
||
|
|
|
||
|
|
CENSUS_COLORS = [
|
||
|
|
(0.90, 0.10, 0.10), (0.10, 0.55, 0.95), (0.10, 0.80, 0.20), (0.95, 0.75, 0.10),
|
||
|
|
(0.75, 0.15, 0.85), (0.05, 0.85, 0.80), (0.95, 0.45, 0.05), (0.55, 0.35, 0.15),
|
||
|
|
(0.95, 0.55, 0.75), (0.45, 0.95, 0.45), (0.25, 0.25, 0.95), (0.80, 0.80, 0.30),
|
||
|
|
(0.50, 0.05, 0.30), (0.05, 0.45, 0.35), (0.65, 0.65, 0.95), (0.95, 0.85, 0.65),
|
||
|
|
(0.30, 0.10, 0.60), (0.10, 0.30, 0.10), (0.60, 0.30, 0.30), (0.30, 0.60, 0.60),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def stage_census():
|
||
|
|
|
||
|
|
--- step 14 (119 min left) ---
|
||
|
|
COMMAND: sed -n '650,760p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
load_checkpoint("30_reduce")
|
||
|
|
scene = bpy.context.scene
|
||
|
|
scene.render.engine = "CYCLES"
|
||
|
|
scene.cycles.device = "CPU"
|
||
|
|
scene.cycles.samples = 32
|
||
|
|
scene.cycles.seed = 7
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
lo = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
hi = bpy.data.objects.get(f"HI_{part_name}")
|
||
|
|
if lo is None or hi is None:
|
||
|
|
continue
|
||
|
|
hi.hide_viewport = hi.hide_render = False
|
||
|
|
size = part_cfg.get("bake_size", 512)
|
||
|
|
# bake UVs
|
||
|
|
select_only([lo])
|
||
|
|
uv = lo.data.uv_layers.new(name="bake_uv")
|
||
|
|
lo.data.uv_layers.active = uv
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.uv.smart_project(angle_limit=math.radians(66))
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
# target images + nodes on the part's material
|
||
|
|
mat = lo.data.materials[0] if lo.data.materials else None
|
||
|
|
if mat is None:
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
lo.data.materials.append(mat)
|
||
|
|
mat.use_nodes = True
|
||
|
|
nt = mat.node_tree
|
||
|
|
imgs = {}
|
||
|
|
for kind in ("NORMAL", "AO"):
|
||
|
|
img = bpy.data.images.new(f"{part_name}_{kind.lower()}", size, size,
|
||
|
|
alpha=False, float_buffer=(kind == "NORMAL"))
|
||
|
|
node = nt.nodes.new("ShaderNodeTexImage")
|
||
|
|
node.image = img
|
||
|
|
nt.nodes.active = node
|
||
|
|
select_only([hi, lo], active=lo)
|
||
|
|
bpy.ops.object.bake(type=kind, use_selected_to_active=True,
|
||
|
|
cage_extrusion=0.004,
|
||
|
|
use_clear=True)
|
||
|
|
out = work_path(f"{part_name}_{kind.lower()}.png")
|
||
|
|
img.filepath_raw = out
|
||
|
|
img.file_format = "PNG"
|
||
|
|
img.save()
|
||
|
|
imgs[kind] = img
|
||
|
|
log(f"baked {kind} for {part_name} -> {out}")
|
||
|
|
# wire into the material (normal map + AO multiply into base color)
|
||
|
|
bsdf = next(n for n in nt.nodes if n.type == "BSDF_PRINCIPLED")
|
||
|
|
nrm_tex = nt.nodes.new("ShaderNodeTexImage")
|
||
|
|
nrm_tex.image = imgs["NORMAL"]
|
||
|
|
nrm_tex.image.colorspace_settings.name = "Non-Color"
|
||
|
|
nmap = nt.nodes.new("ShaderNodeNormalMap")
|
||
|
|
nmap.uv_map = "bake_uv"
|
||
|
|
nt.links.new(nmap.inputs["Color"], nrm_tex.outputs["Color"])
|
||
|
|
nt.links.new(bsdf.inputs["Normal"], nmap.outputs["Normal"])
|
||
|
|
hi.hide_viewport = hi.hide_render = True
|
||
|
|
save_checkpoint("40_bake")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: skin ──────────────────────────────────────────────────────────────
|
||
|
|
# Weight transfer. Snug parts: Data Transfer (nearest face interpolated) from
|
||
|
|
# BODY. Skirt parts: cone proxy — data-transferred at the waist, pelvis-locked
|
||
|
|
# at the hem, then transferred onto the skirt.
|
||
|
|
|
||
|
|
def data_transfer_weights(dst, src):
|
||
|
|
dst.vertex_groups.clear()
|
||
|
|
for vg in src.vertex_groups:
|
||
|
|
dst.vertex_groups.new(name=vg.name)
|
||
|
|
dt = dst.modifiers.new("Weights", "DATA_TRANSFER")
|
||
|
|
dt.object = src
|
||
|
|
dt.use_vert_data = True
|
||
|
|
dt.data_types_verts = {"VGROUP_WEIGHTS"}
|
||
|
|
dt.vert_mapping = "POLYINTERP_NEAREST"
|
||
|
|
dt.layers_vgroup_select_src = "ALL"
|
||
|
|
dt.layers_vgroup_select_dst = "NAME"
|
||
|
|
apply_modifier(dst, dt.name)
|
||
|
|
|
||
|
|
|
||
|
|
def smooth_and_normalize(o, iterations=3):
|
||
|
|
select_only([o])
|
||
|
|
bpy.ops.object.mode_set(mode="WEIGHT_PAINT")
|
||
|
|
try:
|
||
|
|
bpy.ops.object.vertex_group_smooth(group_select_mode="ALL",
|
||
|
|
factor=0.5, repeat=iterations)
|
||
|
|
bpy.ops.object.vertex_group_normalize_all(lock_active=False)
|
||
|
|
finally:
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
|
||
|
|
|
||
|
|
def pelvis_gradient(o, hip_z, hem_z):
|
||
|
|
"""Below the hip, blend all weights toward pelvis-only as z drops to hem."""
|
||
|
|
idx_pelvis = o.vertex_groups.find("pelvis")
|
||
|
|
if idx_pelvis < 0:
|
||
|
|
o.vertex_groups.new(name="pelvis")
|
||
|
|
idx_pelvis = o.vertex_groups.find("pelvis")
|
||
|
|
mw = o.matrix_world
|
||
|
|
for v in o.data.vertices:
|
||
|
|
z = (mw @ v.co).z
|
||
|
|
if z >= hip_z:
|
||
|
|
continue
|
||
|
|
t = min(1.0, (hip_z - z) / max(1e-9, hip_z - hem_z)) # 0 at hip → 1 at hem
|
||
|
|
for g in v.groups:
|
||
|
|
g.weight *= (1.0 - t)
|
||
|
|
o.vertex_groups[idx_pelvis].add([v.index], 0.0, "ADD")
|
||
|
|
# set pelvis weight so the total stays 1 after normalize
|
||
|
|
cur = sum(g.weight for g in v.groups)
|
||
|
|
o.vertex_groups[idx_pelvis].add([v.index], max(0.0, 1.0 - cur), "ADD")
|
||
|
|
|
||
|
|
|
||
|
|
def stage_skin():
|
||
|
|
src_stage = "40_bake" if os.path.exists(work_path("40_bake.blend")) else "30_reduce"
|
||
|
|
load_checkpoint(src_stage)
|
||
|
|
|
||
|
|
--- step 15 (119 min left) ---
|
||
|
|
COMMAND: "$BLENDER" --background --python clothing/garment_pipeline.py -- --config clothing/configs/pari.json --stage census 2>&1 | tail -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
bash: line 1: : command not found
|
||
|
|
|
||
|
|
--- step 16 (119 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config clothing/configs/pari.json --stage census 2>&1 | tail -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
Traceback (most recent call last):
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 853, in <module>
|
||
|
|
main()
|
||
|
|
~~~~^^
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 848, in main
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\Jeremy\\tinqs\\animation\\clothing\\clothing/configs/pari.json'
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 17 (119 min left) ---
|
||
|
|
COMMAND: grep -n "cfg_path\|os.path.join.*HERE\|os.path.isabs" clothing/garment_pipeline.py | head -10
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
37: d = os.path.join(HERE, "work", CFG["name"])
|
||
|
|
847: cfg_path = ARGS.config if os.path.isabs(ARGS.config) else os.path.join(HERE, ARGS.config)
|
||
|
|
848: CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
|
||
|
|
--- step 18 (119 min left) ---
|
||
|
|
COMMAND: printf '{\n "name": "pari",\n "source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",\n "body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",\n "weld_threshold": 0.0006,\n "shell_mm": 4,\n "align": {\n "top_bone": "neck_01",\n "scale_xy": 1.0,\n "scale_z": 1.0,\n "z_nudge": 0.0,\n "xy_nudge": [0.0, 0.0]\n },\n "parts": {\n "Pari": {\n "slot": "Body",\n "islands": [0],\n "tris": 4000,\n "planar_deg": 5,\n "fit": {\n "target": "BODY_SHELL",\n "mask": "full",\n "offset_mm": 3,\n "wrap_mode": "OUTSIDE"\n },\n "weights": "dress",\n "color": [0.5, 0.2, 0.15]\n }\n },\n "export": {\n "out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",\n "gender": "Female",\n "set": "Kapahaka",\n "note": "TÄniko bodice (pari) - Body slot"\n },\n "min_island_verts": 40\n}\n' > clothing/configs/pari.json && cat clothing/configs/pari.json | python -m json.tool > /dev/null && echo "valid JSON"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
valid JSON
|
||
|
|
|
||
|
|
--- step 19 (119 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config clothing/configs/pari.json --stage census 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
Traceback (most recent call last):
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 853, in <module>
|
||
|
|
main()
|
||
|
|
~~~~^^
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 848, in main
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\Jeremy\\tinqs\\animation\\clothing\\clothing/configs/pari.json'
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 20 (119 min left) ---
|
||
|
|
COMMAND: python -c "import os; HERE=os.path.dirname(os.path.abspath('clothing/garment_pipeline.py')); print(HERE); print(os.path.join(HERE, 'configs/pari.json'))"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\configs/pari.json
|
||
|
|
|
||
|
|
--- step 21 (119 min left) ---
|
||
|
|
COMMAND: ls -la clothing/configs/pari.json && python -c "import os; p=os.path.abspath('clothing/configs/pari.json'); print(p); print(os.path.exists(p))"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 935 Jul 30 22:05 clothing/configs/pari.json
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\configs\pari.json
|
||
|
|
True
|
||
|
|
|
||
|
|
--- step 22 (118 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage census 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=census
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 1889 verts
|
||
|
|
Info: Removed 42 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
[cloth] ISL_000 verts= 1847 z=[10.546,14.613] x=[-1.920,+1.989] color=(0.9, 0.1, 0.1)
|
||
|
|
00:03.516 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_front.png
|
||
|
|
00:03.641 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_quarter.png
|
||
|
|
Info: Saved as "00_census.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\00_census.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 23 (118 min left) ---
|
||
|
|
COMMAND: cat clothing/work/pari/census.json && ls -la clothing/work/pari/qa_00_census_front.png clothing/work/pari/qa_00_census_quarter.png
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"island": 0,
|
||
|
|
"verts": 1847,
|
||
|
|
"tris": 3575,
|
||
|
|
"z": [
|
||
|
|
10.546,
|
||
|
|
14.613
|
||
|
|
],
|
||
|
|
"x": [
|
||
|
|
-1.92,
|
||
|
|
1.989
|
||
|
|
],
|
||
|
|
"centroid": [
|
||
|
|
0.034,
|
||
|
|
0.407,
|
||
|
|
12.541
|
||
|
|
],
|
||
|
|
"color": [
|
||
|
|
0.9,
|
||
|
|
0.1,
|
||
|
|
0.1
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]-rw-r--r-- 1 Jeremy 197121 817779 Jul 30 22:06 clothing/work/pari/qa_00_census_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 869071 Jul 30 22:06 clothing/work/pari/qa_00_census_quarter.png
|
||
|
|
|
||
|
|
--- step 24 (117 min left) ---
|
||
|
|
COMMAND: python -c "
|
||
|
|
# Pari: z-range 10.546..14.613, body z 0..1.766, neck_01 z=1.413
|
||
|
|
# Need 0.1 uniform scale: 10.546*0.1=1.0546, 14.613*0.1=1.4613
|
||
|
|
# After scale, top z = 1.4613, neck_01 = 1.413
|
||
|
|
# dz = neck_01 - 1.4613 = 1.413 - 1.4613 = -0.0483
|
||
|
|
# So z_nudge should be about -0.048 to bring top to neck
|
||
|
|
# Sanity: pari should span z 1.05..1.40 after alignment
|
||
|
|
gz1 = 14.613 * 0.1
|
||
|
|
b_top = 1.413
|
||
|
|
dz = (b_top - gz1) + 0.0
|
||
|
|
print(f'scaled top z={gz1:.4f}, dz={dz:+.4f}')
|
||
|
|
print(f'final z-range: {10.546*0.1+dz:.4f} .. {gz1+dz:.4f}')
|
||
|
|
"
|
||
|
|
[output]
|
||
|
|
exit=2
|
||
|
|
[stderr]
|
||
|
|
bash: -c: line 1: unexpected EOF while looking for matching `"'
|
||
|
|
|
||
|
|
--- step 25 (117 min left) ---
|
||
|
|
COMMAND: python -c "gz1=14.613*0.1; b_top=1.413; dz=(b_top-gz1)+0.0; print(f'scaled top z={gz1:.4f}, dz={dz:+.4f}'); print(f'final z-range: {10.546*0.1+dz:.4f} .. {gz1+dz:.4f}')"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
scaled top z=1.4613, dz=-0.0483
|
||
|
|
final z-range: 1.0063 .. 1.4130
|
||
|
|
|
||
|
|
--- step 26 (117 min left) ---
|
||
|
|
COMMAND: python -c "
|
||
|
|
import json
|
||
|
|
cfg = json.load(open('clothing/configs/pari.json'))
|
||
|
|
cfg['align']['scale_xy'] = 0.1
|
||
|
|
cfg['align']['scale_z'] = 0.1
|
||
|
|
cfg['align']['z_nudge'] = -0.0483
|
||
|
|
json.dump(cfg, open('clothing/configs/pari.json','w'), indent=2)
|
||
|
|
print('updated align:', cfg['align'])
|
||
|
|
"
|
||
|
|
[output]
|
||
|
|
exit=2
|
||
|
|
[stderr]
|
||
|
|
bash: -c: line 1: unexpected EOF while looking for matching `"'
|
||
|
|
|
||
|
|
--- step 27 (117 min left) ---
|
||
|
|
COMMAND: python -c "import json; cfg=json.load(open('clothing/configs/pari.json')); cfg['align']['scale_xy']=0.1; cfg['align']['scale_z']=0.1; cfg['align']['z_nudge']=-0.0483; json.dump(cfg,open('clothing/configs/pari.json','w'),indent=2); print('updated align:',cfg['align'])"
|
||
|
|
[output]
|
||
|
|
exit=1
|
||
|
|
[stderr]
|
||
|
|
Traceback (most recent call last):
|
||
|
|
File "<string>", line 1, in <module>
|
||
|
|
File "C:\Users\Jeremy\AppData\Local\Programs\Python\Python312\Lib\json\__init__.py", line 293, in load
|
||
|
|
return loads(fp.read(),
|
||
|
|
^^^^^^^^^
|
||
|
|
File "C:\Users\Jeremy\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode
|
||
|
|
return codecs.charmap_decode(input,self.errors,decoding_table)[0]
|
||
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
UnicodeDecodeError: 'charmap' codec can't decode byte 0x81 in position 870: character maps to <undefined>
|
||
|
|
|
||
|
|
--- step 28 (117 min left) ---
|
||
|
|
COMMAND: python -c "import json; cfg=json.load(open('clothing/configs/pari.json',encoding='utf-8')); cfg['align']['scale_xy']=0.1; cfg['align']['scale_z']=0.1; cfg['align']['z_nudge']=-0.0483; json.dump(cfg,open('clothing/configs/pari.json','w',encoding='utf-8'),indent=2); print('updated align:',cfg['align'])"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
updated align: {'top_bone': 'neck_01', 'scale_xy': 0.1, 'scale_z': 0.1, 'z_nudge': -0.0483, 'xy_nudge': [0.0, 0.0]}
|
||
|
|
|
||
|
|
--- step 29 (116 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage prepare 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=prepare
|
||
|
|
22:08:17 | INFO: Data are loaded, start creating Blender stuff
|
||
|
|
22:08:17 | INFO: Blender create Mesh node Lena_Female
|
||
|
|
22:08:18 | INFO: glTF import finished in 0.31s
|
||
|
|
[cloth] body: 32713 verts, groups=65, bones=65
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 1889 verts
|
||
|
|
Info: Removed 42 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py:287: DeprecationWarning: 'Material.use_nodes' is expected to be removed in Blender 6.0
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
Warning: No mesh data to join
|
||
|
|
[cloth] part GARM_Pari: islands=[0] verts=1847 tris=3575
|
||
|
|
[cloth] align: scale_xy=0.100 scale_z=0.100 dz=-0.0970
|
||
|
|
00:04.093 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_front.png
|
||
|
|
00:04.218 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_quarter.png
|
||
|
|
Info: Saved as "10_prepare.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 30 (116 min left) ---
|
||
|
|
COMMAND: sed -n '250,320p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_prepare():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
|
||
|
|
# body + rig first (defines target space)
|
||
|
|
body_path = CFG["body"]
|
||
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
||
|
|
rig = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||
|
|
rig.name = "RIG"
|
||
|
|
body = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||
|
|
key=lambda o: len(o.data.vertices))
|
||
|
|
# drop face-part extras (eyes etc.) that ride along in the body GLB
|
||
|
|
for o in [o for o in bpy.data.objects if o.type == "MESH" and o is not body]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
body.name = "BODY"
|
||
|
|
apply_all_transforms([rig, body])
|
||
|
|
log(f"body: {len(body.data.vertices)} verts, groups={len(body.vertex_groups)}, "
|
||
|
|
f"bones={len(rig.data.bones)}")
|
||
|
|
|
||
|
|
# garment islands (same deterministic indexing the census printed)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
by_idx = {int(o.name.split("_")[1]): o for o in islands}
|
||
|
|
claimed = set()
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
ids = part_cfg.get("islands", [])
|
||
|
|
members = [by_idx[i] for i in ids if i in by_idx]
|
||
|
|
if not members:
|
||
|
|
log(f"WARNING: part {part_name} matched no islands ({ids})")
|
||
|
|
continue
|
||
|
|
claimed.update(ids)
|
||
|
|
select_only(members, active=members[0])
|
||
|
|
bpy.ops.object.join()
|
||
|
|
part = bpy.context.view_layer.objects.active
|
||
|
|
part.name = f"GARM_{part_name}"
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
part.data.materials.clear()
|
||
|
|
part.data.materials.append(mat)
|
||
|
|
log(f"part {part.name}: islands={ids} verts={len(part.data.vertices)} tris={tris_of(part)}")
|
||
|
|
for i, o in list(by_idx.items()):
|
||
|
|
if i not in claimed and o.name.startswith("ISL_"):
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
|
||
|
|
# bodyshell parts: fitted clothing built FROM the body (Islander technique):
|
||
|
|
# copy the body, keep the configured z-band + torso-dominant vertices, push
|
||
|
|
# outward along normals. Fit and weights are correct by construction.
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
if part_cfg.get("type") != "bodyshell":
|
||
|
|
continue
|
||
|
|
shellp = body.copy()
|
||
|
|
shellp.data = body.data.copy()
|
||
|
|
shellp.name = f"GARM_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(shellp)
|
||
|
|
z0, z1 = part_cfg["z0"], part_cfg["z1"]
|
||
|
|
allowed = set(part_cfg.get("bones", ["spine_01", "spine_02", "spine_03", "pelvis"]))
|
||
|
|
gnames = {i: g.name for i, g in enumerate(shellp.vertex_groups)}
|
||
|
|
keep = set()
|
||
|
|
for v in shellp.data.vertices:
|
||
|
|
z = (shellp.matrix_world @ v.co).z
|
||
|
|
if not (z0 <= z <= z1):
|
||
|
|
continue
|
||
|
|
best, bw = None, 0.0
|
||
|
|
for g in v.groups:
|
||
|
|
if g.weight > bw:
|
||
|
|
|
||
|
|
--- step 31 (116 min left) ---
|
||
|
|
COMMAND: grep -n "align\|dz\|z_nudge\|scale_xy\|scale_z\|top_bone" clothing/garment_pipeline.py | head -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
249:# import body+rig, align garment onto body.
|
||
|
|
378: a = CFG.get("align", {})
|
||
|
|
379: sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
380: sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
385: b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
386: dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
388: T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
391: log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
411: # post-align cut: drop configured fabric above a body-space height (v1 uses
|
||
|
|
|
||
|
|
--- step 32 (116 min left) ---
|
||
|
|
COMMAND: sed -n '375,395p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
v.co = M @ v.co
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: {math.degrees(cur):+.1f} deg, "
|
||
|
|
f"{len(sleeve)} verts")
|
||
|
|
a = CFG.get("align", {})
|
||
|
|
sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
M = Matrix.Diagonal(Vector((sxy, sxy, sz))).to_4x4()
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(M)
|
||
|
|
gz0, gz1 = zspan(parts)
|
||
|
|
b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(T)
|
||
|
|
log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
|
||
|
|
# torso boost: heroic bodies are wider than MD mannequins — radially inflate
|
||
|
|
# the above-waist region about the body axis so bodice/collar fabric starts
|
||
|
|
# OUTSIDE the body (direction-preserving; the OUTSIDE shrinkwrap then only
|
||
|
|
|
||
|
|
--- step 33 (116 min left) ---
|
||
|
|
COMMAND: python -c "import json; cfg=json.load(open('clothing/configs/pari.json',encoding='utf-8')); cfg['align']['z_nudge']=0.0; json.dump(cfg,open('clothing/configs/pari.json','w',encoding='utf-8'),indent=2); print('z_nudge set to 0.0; pipeline computes dz=b_top-gz1=',1.413-14.613*0.1)"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
z_nudge set to 0.0; pipeline computes dz=b_top-gz1= -0.04830000000000001
|
||
|
|
|
||
|
|
--- step 34 (116 min left) ---
|
||
|
|
COMMAND: ls -la clothing/work/pari/qa_10_prepare_front.png clothing/work/pari/qa_10_prepare_quarter.png
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 555384 Jul 30 22:08 clothing/work/pari/qa_10_prepare_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 506395 Jul 30 22:08 clothing/work/pari/qa_10_prepare_quarter.png
|
||
|
|
|
||
|
|
--- step 35 (116 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage fit 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=fit
|
||
|
|
00:02.109 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend
|
||
|
|
Info: Applied modifier was not first, result may not be as expected
|
||
|
|
[cloth] fit GARM_Pari: target=BODY_SHELL mask=full offset=0.003000000026077032
|
||
|
|
00:03.140 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_front.png
|
||
|
|
00:03.265 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_quarter.png
|
||
|
|
Info: Saved as "20_fit.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 36 (116 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage reduce 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=reduce
|
||
|
|
00:02.188 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend
|
||
|
|
[cloth] reduce GARM_Pari: tris=3085 (budget 4000)
|
||
|
|
00:03.141 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_front.png
|
||
|
|
00:03.313 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_quarter.png
|
||
|
|
Info: Saved as "30_reduce.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 37 (116 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage skin 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=skin
|
||
|
|
00:02.031 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend
|
||
|
|
[cloth] skin GARM_Pari: weighted 1602/1602 verts
|
||
|
|
00:02.984 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_front.png
|
||
|
|
00:03.125 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_quarter.png
|
||
|
|
Info: Saved as "50_skin.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 38 (116 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/pari.json --stage export 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=export
|
||
|
|
00:02.094 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend
|
||
|
|
INFO Draco mesh compression is available, use library at C:\Program Files\Blender Foundation\Blender 5.1\5.1\scripts\addons_core\io_scene_gltf2\extern_draco.dll
|
||
|
|
22:09:07 | INFO: Starting glTF 2.0 export
|
||
|
|
22:09:07 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:09:07 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:09:07 | INFO: Extracting primitive: Pattern_40577
|
||
|
|
22:09:07 | INFO: Primitives created: 1
|
||
|
|
22:09:07 | WARNING: There are more than 4 joint vertex influences.The 4 with highest weight will be used (and normalized).
|
||
|
|
22:09:07 | INFO: Finished glTF 2.0 export in 0.16622066497802734 s
|
||
|
|
|
||
|
|
[cloth] EXPORTED C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka\Female_Kapahaka_Body.gltf (3085 tris, 23953 bytes)
|
||
|
|
[cloth] vertex hash: 14f57ecbe14239f7
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 39 (115 min left) ---
|
||
|
|
COMMAND: ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
total 128
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 106184 Jul 30 22:09 Female_Kapahaka_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23953 Jul 30 22:09 Female_Kapahaka_Body.gltf
|
||
|
|
|
||
|
|
--- step 40 (115 min left) ---
|
||
|
|
COMMAND: python -c "import json; g=json.load(open('C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf',encoding='utf-8')); nodes=g.get('nodes',[]); joints=[n for n in nodes if n.get('name')]; skins=g.get('skins',[]); print('nodes:',len(nodes),'skins:',len(skins)); print('joints in skin0:',len(skins[0]['joints']) if skins else 0); mesh=g['meshes'][0]; print('primitives:',len(mesh['primitives'])); acc=g['accessors']; print('accessors:',len(acc))"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
nodes: 67 skins: 1
|
||
|
|
joints in skin0: 65
|
||
|
|
primitives: 1
|
||
|
|
accessors: 7
|
||
|
|
|
||
|
|
--- step 41 (115 min left) ---
|
||
|
|
COMMAND: python -c "import json; g=json.load(open('C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf',encoding='utf-8')); acc=g['accessors']; pos=[a for a in acc if a.get('type')=='VEC3' and a.get('componentType')==5126]; print('POSITION accessor:',pos[0]['count'] if pos else 'none'); bv=g['bufferViews']; print('bufferViews:',len(bv),'buffers:',len(g['buffers']))"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
POSITION accessor: 1606
|
||
|
|
bufferViews: 7 buffers: 1
|
||
|
|
|
||
|
|
--- step 42 (115 min left) ---
|
||
|
|
COMMAND: ls -la clothing/work/pari/qa_50_skin_front.png clothing/work/pari/qa_30_reduce_front.png clothing/work/pari/qa_20_fit_front.png
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 556467 Jul 30 22:08 clothing/work/pari/qa_20_fit_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1046153 Jul 30 22:08 clothing/work/pari/qa_30_reduce_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1046273 Jul 30 22:09 clothing/work/pari/qa_50_skin_front.png
|
||
|
|
|
||
|
|
--- step 43 (115 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/pari.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "pari",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Pari": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 4000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.5,
|
||
|
|
0.2,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "T\u00c4\u0081niko bodice (pari) - Body slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 44 (115 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/piupiu.json --stage census 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
Traceback (most recent call last):
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 853, in <module>
|
||
|
|
main()
|
||
|
|
~~~~^^
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 848, in main
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\Jeremy\\tinqs\\animation\\clothing\\configs/piupiu.json'
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 45 (115 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 46 (115 min left) ---
|
||
|
|
COMMAND: python -c "
|
||
|
|
import json
|
||
|
|
cfg = {
|
||
|
|
'name': 'piupiu',
|
||
|
|
'source': 'C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx',
|
||
|
|
'body': 'C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb',
|
||
|
|
'weld_threshold': 0.0006,
|
||
|
|
'shell_mm': 4,
|
||
|
|
'align': {
|
||
|
|
'top_bone': 'spine_01',
|
||
|
|
'scale_xy': 0.1,
|
||
|
|
'scale_z': 0.1,
|
||
|
|
'z_nudge': 0.0,
|
||
|
|
'xy_nudge': [0.0, 0.0]
|
||
|
|
},
|
||
|
|
'parts': {
|
||
|
|
'Piupiu': {
|
||
|
|
'slot': 'Legs',
|
||
|
|
'islands': [0],
|
||
|
|
'tris': 8000,
|
||
|
|
'planar_deg': 5,
|
||
|
|
'fit': {
|
||
|
|
'target': 'BODY_SHELL',
|
||
|
|
'mask': 'full',
|
||
|
|
'offset_mm': 3,
|
||
|
|
'wrap_mode': 'OUTSIDE'
|
||
|
|
},
|
||
|
|
'weights': 'dress',
|
||
|
|
'color': [0.45, 0.35, 0.15]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'export': {
|
||
|
|
'out_dir': 'C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka',
|
||
|
|
'gender': 'Female',
|
||
|
|
'set': 'Kapahaka',
|
||
|
|
'note': 'Flax skirt (piupiu) - Legs slot'
|
||
|
|
},
|
||
|
|
'min_island_verts': 40
|
||
|
|
}
|
||
|
|
json.dump(cfg, open('clothing/configs/piupiu.json','w',encoding='utf-8'), indent=2)
|
||
|
|
print('written')
|
||
|
|
"
|
||
|
|
[output]
|
||
|
|
exit=2
|
||
|
|
[stderr]
|
||
|
|
bash: -c: line 1: unexpected EOF while looking for matching `"'
|
||
|
|
|
||
|
|
--- step 47 (115 min left) ---
|
||
|
|
COMMAND: python -c "import json; cfg={'name':'piupiu','source':'C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx','body':'C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb','weld_threshold':0.0006,'shell_mm':4,'align':{'top_bone':'spine_01','scale_xy':0.1,'scale_z':0.1,'z_nudge':0.0,'xy_nudge':[0.0,0.0]},'parts':{'Piupiu':{'slot':'Legs','islands':[0],'tris':8000,'planar_deg':5,'fit':{'target':'BODY_SHELL','mask':'full','offset_mm':3,'wrap_mode':'OUTSIDE'},'weights':'dress','color':[0.45,0.35,0.15]}},'export':{'out_dir':'C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka','gender':'Female','set':'Kapahaka','note':'Flax skirt (piupiu) - Legs slot'},'min_island_verts':40}; json.dump(cfg,open('clothing/configs/piupiu.json','w',encoding='utf-8'),indent=2); print('written')"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
written
|
||
|
|
|
||
|
|
--- step 48 (115 min left) ---
|
||
|
|
COMMAND: BLENDER="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" && "$BLENDER" --background --python clothing/garment_pipeline.py -- --config configs/piupiu.json --stage census 2>&1 | tail -40
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=census
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 3686 verts
|
||
|
|
Info: Removed 66 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
[cloth] ISL_000 verts= 3620 z=[5.184,11.126] x=[-2.228,+2.517] color=(0.9, 0.1, 0.1)
|
||
|
|
00:03.281 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_front.png
|
||
|
|
00:03.390 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_quarter.png
|
||
|
|
Info: Saved as "00_census.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\00_census.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 49 (115 min left) ---
|
||
|
|
COMMAND: python -c "print('piupiu z-range after 0.1 scale: 5.184*0.1= 0.5184 to 11.126*0.1= 1.1126'); print('spine_01 z=0.932 (approx pelvis area)'); print('top_bone=spine_01 -> dz = 0.932 - 1.1126 = -0.1806'); print('expected skirt z-range: 0.45 to 1.05; with dz=-0.18 -> 0.34 to 0.93')"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
piupiu z-range after 0.1 scale: 5.184*0.1= 0.5184 to 11.126*0.1= 1.1126
|
||
|
|
spine_01 z=0.932 (approx pelvis area)
|
||
|
|
top_bone=spine_01 -> dz = 0.932 - 1.1126 = -0.1806
|
||
|
|
expected skirt z-range: 0.45 to 1.05; with dz=-0.18 -> 0.34 to 0.93
|
||
|
|
|
||
|
|
--- step 50 (115 min left) ---
|
||
|
|
COMMAND: grep -n "spine_01\|spine_02\|pelvis\|bone_head_z\|def bone_head" clothing/garment_pipeline.py | head -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
131:def bone_head_z(rig, bone):
|
||
|
|
311: allowed = set(part_cfg.get("bones", ["spine_01", "spine_02", "spine_03", "pelvis"]))
|
||
|
|
385: b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
502: hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
710:# BODY. Skirt parts: cone proxy — data-transferred at the waist, pelvis-locked
|
||
|
|
738:def pelvis_gradient(o, hip_z, hem_z):
|
||
|
|
739: """Below the hip, blend all weights toward pelvis-only as z drops to hem."""
|
||
|
|
740: idx_pelvis = o.vertex_groups.find("pelvis")
|
||
|
|
741: if idx_pelvis < 0:
|
||
|
|
742: o.vertex_groups.new(name="pelvis")
|
||
|
|
743: idx_pelvis = o.vertex_groups.find("pelvis")
|
||
|
|
752: o.vertex_groups[idx_pelvis].add([v.index], 0.0, "ADD")
|
||
|
|
753: # set pelvis weight so the total stays 1 after normalize
|
||
|
|
755: o.vertex_groups[idx_pelvis].add([v.index], max(0.0, 1.0 - cur), "ADD")
|
||
|
|
762: hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
772: # skirt region toward pelvis-only as z approaches the hem
|
||
|
|
775: pelvis_gradient(p, hip_z - 0.04, hem_z)
|
||
|
|
783: pelvis_gradient(cone, hip_z, hem_z)
|
||
|
|
|
||
|
|
--- step 51 (115 min left) ---
|
||
|
|
COMMAND: sed -n '380,400p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
M = Matrix.Diagonal(Vector((sxy, sxy, sz))).to_4x4()
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(M)
|
||
|
|
gz0, gz1 = zspan(parts)
|
||
|
|
b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(T)
|
||
|
|
log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
|
||
|
|
# torso boost: heroic bodies are wider than MD mannequins — radially inflate
|
||
|
|
# the above-waist region about the body axis so bodice/collar fabric starts
|
||
|
|
# OUTSIDE the body (direction-preserving; the OUTSIDE shrinkwrap then only
|
||
|
|
# catches stragglers instead of shredding buried cloth to random sides).
|
||
|
|
tb = CFG.get("torso_boost")
|
||
|
|
if tb:
|
||
|
|
z0, z1, boost = tb.get("z0", 1.00), tb.get("z1", 1.08), tb.get("xy", 1.18)
|
||
|
|
for p in parts:
|
||
|
|
|
||
|
|
===== GLM agent start 2026-07-30 22:11:30 model=glm-5.2 budget=105.0min =====
|
||
|
|
|
||
|
|
--- step 1 (105 min left) ---
|
||
|
|
I'll start by reading the key documentation files to understand the pipeline before doing anything else.
|
||
|
|
|
||
|
|
--- step 2 (105 min left) ---
|
||
|
|
COMMAND: cat clothing/README.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
# 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) | 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. |
|
||
|
|
| `fit` | Inflated `BODY_SHELL` (+4mm) target; shrinkwrap mode **OUTSIDE** (push out penetrating verts ONLY — never snap the whole cloth, that shreds layered garments) | `OUTSIDE_SURFACE` destroyed multi-layer bodices; `OUTSIDE` unmasked is safe everywhere since it can't pull cloth inward. |
|
||
|
|
| `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_qua
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 3 (105 min left) ---
|
||
|
|
The README was truncated. Let me read the rest of it, plus the config and skill files.
|
||
|
|
|
||
|
|
--- step 4 (105 min left) ---
|
||
|
|
COMMAND: tail -n +100 clothing/README.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Current pilot
|
||
|
|
|
||
|
|
`configs/dress.json` → `../garments/thin-unweld.fbx`: an MD dress ensemble
|
||
|
|
(2.49M verts, 12 materials). v1 scope: bodice + belt/bow (Body slot) + pleated
|
||
|
|
skirt (Legs slot). The sleeved jacket (`cape*` materials) is deferred — sleeves
|
||
|
|
need a pose-warp to T-pose (GLM advice §4 has the recipe) and is round two.
|
||
|
|
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.
|
||
|
|
|
||
|
|
## 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).
|
||
|
|
- Flat MD colors until someone supplies textures (bake stage produces normal+AO,
|
||
|
|
not albedo).
|
||
|
|
- 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.
|
||
|
|
|
||
|
|
--- step 5 (105 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 6 (105 min left) ---
|
||
|
|
COMMAND: cat .claude/skills/marvelous-designer/SKILL.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
---
|
||
|
|
name: marvelous-designer
|
||
|
|
description: Authoring garments in Marvelous Designer agentically — driving MD's Python API over the TinqsMDBridge socket plugin, drafting patterns from a reference image and Lena's measurements, draping/QC-ing them on the game body, and exporting meshes for the clothing pipeline. Use for any "make/adjust a garment", "match this clothing photo", or "drive Marvelous Designer" task, and read before touching tools/md_bridge* or tools/tailor/.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Marvelous Designer — agentic garment authoring
|
||
|
|
|
||
|
|
The **upstream half** of the clothing lane: reference image (or a description) →
|
||
|
|
sewn, draped, textured garment mesh fitted to the game body. The downstream half
|
||
|
|
(garment mesh → game-ready skinned GLB) is `clothing/` — see
|
||
|
|
`.agents/wiki/architecture/clothing-lane.md` for how they meet.
|
||
|
|
|
||
|
|
Everything here was learned by doing it on 2026-07-30 (tee, skirt, kapa haka
|
||
|
|
pari + piupiu). The API facts are **verified against MD 2026 Personal**, not
|
||
|
|
docs — Reallusion/CLO's published API docs are thin and several signatures in
|
||
|
|
them are wrong.
|
||
|
|
|
||
|
|
## The 60-second model
|
||
|
|
|
||
|
|
- MD is driven through **`tools/md_bridge.py`** → a socket plugin running inside
|
||
|
|
MD (`tools/md_bridge/TinqsMDBridge.py`). Full protocol: `docs/md-bridge.md`.
|
||
|
|
- A garment = **flat 2D panels** (`CreatePatternWithPoints`) + **seams**
|
||
|
|
(`AddSeamlinePairGroup`) + **arrangement points** on the avatar
|
||
|
|
(`SetArrangement`), then **simulate** (`utility_api.Simulate(frames)`) to drape
|
||
|
|
cloth onto the body.
|
||
|
|
- You see results by rendering the viewport to PNG (`ExportSnapshot3D`) and
|
||
|
|
reading the image back. **This vision loop is the whole method** — draft,
|
||
|
|
drape, look, measure, adjust, repeat.
|
||
|
|
- Garment identity for traditional wear is mostly **textiles, not tailoring**.
|
||
|
|
Kapa haka / Mexica / Pacific garments are rectangles + blocks; the design lives
|
||
|
|
in generated texture maps. Model the shape simply, spend effort on the pattern.
|
||
|
|
|
||
|
|
## Session protocol (read first — MD's UI freezes)
|
||
|
|
|
||
|
|
MD's embedded Python **does not run background threads**, so the bridge owns the
|
||
|
|
main thread while it serves. Consequences:
|
||
|
|
|
||
|
|
1. **A human must click** Plugin → TinqsMDBridge to start a session. You cannot
|
||
|
|
start one yourself. Ask, then wait.
|
||
|
|
2. **MD's UI is frozen** for the whole session ("Not Responding" is normal).
|
||
|
|
3. End with `python tools/md_bridge.py --stop` — works from the terminal even
|
||
|
|
though the UI is dead. Idle timeout is 4 h (was 300 s; raised so one click
|
||
|
|
lasts a work session).
|
||
|
|
4. **Pause the session whenever Jeremy wants to look at the model.** He inspects
|
||
|
|
in MD's viewport; he can't while you hold the thread. Default to stopping when
|
||
|
|
you hand back a result, unless he said to stay in.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python tools/md_bridge.py --ping # verify + see mode/api modules
|
||
|
|
python tools/md_bridge.py --exec "result = pattern_api.GetPatternCount()"
|
||
|
|
python tools/md_bridge.py --file tools/tailor/md_pari.py --timeout 500
|
||
|
|
python tools/md_bridge.py --stop # give the UI back
|
||
|
|
```
|
||
|
|
|
||
|
|
Long `Simulate()` calls need a raised client `--timeout` (~1 min per 300 frames).
|
||
|
|
The exec namespace persists **within** a session, not across; MD api modules
|
||
|
|
(`pattern_api`, `import_api`, `export_api`, `fabric_api`, `utility_api`,
|
||
|
|
`ApiTypes`) plus a `BRIDGE` info dict are pre-seeded.
|
||
|
|
|
||
|
|
**Introspect, don't trust docs.** Full surface dump lives at
|
||
|
|
`tools/md_bridge/api_surface.json` (688 functions) and harvested docstrings at
|
||
|
|
`tools/md_bridge/api_docs.json`. Overloaded pybind11 functions break
|
||
|
|
`inspect.signature()` — read `__doc__` instead.
|
||
|
|
|
||
|
|
## API facts that cost hours to find
|
||
|
|
|
||
|
|
| Fact | Consequence |
|
||
|
|
|---|---|
|
||
|
|
| **2D pattern y+ maps to UP in 3D** | Panels drafted y-down drape **upside-down over the head** and tangle. Symptom looks like a seam bug; it isn't. Hem at y=0, neckline at high y. |
|
||
|
|
| Units are **mm**; gravity −9800 | A "500" square is 50 cm. |
|
||
|
|
| Blender-exported FBX avatars import **10× small** | Import with `op.scale = 10.0`. |
|
||
|
|
| `ImportAvatar()` is **.avt only** — returns `False` on FBX | Use **`import_api.ImportFBX(path, op)`** for the game body. |
|
||
|
|
| `op.bAddArrangementPoints = True` | Auto-generates ~98 named arrangement points on a custom avatar. Without it you have nowhere to hang cloth. |
|
||
|
|
| `SetArrangement()` only *assigns*; **`utility_api.ResetClothArrangement()` applies** it | Skipping the apply = nothing moves. (`ReDrape3DArrangement` only materializes not-yet-draped cloth.) |
|
||
|
|
| Arrangement **indices regenerate** per avatar import | Always look up by name from `GetArrangementList()`; never hardcode an index. Offsets aren't stable either — verify with a 0-frame snapshot. |
|
||
|
|
| Avatar getters live in **`export_api`** (`GetAvatarCount`, `GetAvatarNameList`) | Not `utility_api`, where you'd look. |
|
||
|
|
| `utility_api.NewProject()` **deletes the avatar** | Re-import after. |
|
||
|
|
| `SetBaseTextureMapImageGivenFilePath(path, fabricIdx)` — **path is arg0** | Reversed args throw a TypeError that reads like a missing overload. |
|
||
|
|
| **PNG DPI sets a texture's physical size in MD** | 1024 px at 54.2 dpi = 480 mm of cloth. Control tiling by setting dpi in PIL, not by scaling the image. |
|
||
|
|
| `fabric_api.AddFabric()` needs a **`.zfab` file path** | A name string silently fails. Stock presets: `C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\`. |
|
||
|
|
| Fabric **index 0 is the shared default** | Coloring it dyes every garment in the scene. Always `AddFabric` a new one. |
|
||
|
|
| `AssignFabricToPattern()` returns False for every arg order tried | Use **`pattern_api.SetPatternPieceFabricIndex(pattern, fabric)`**. |
|
||
|
|
| `SetViewPoint()` does nothing; **`SetCamViewPoint(2)` = front view** | Call before every QC snapshot so shots are comparable. |
|
||
|
|
| `GetClothPositions()` is an out-param that stays empty from Python | No mesh introspection — measure from rendered images instead (see QC). |
|
||
|
|
| No `SaveProjectFile`; no glTF export; no EveryWear in the API | Save = `export_api.ExportZPrj`. Exits: `ExportFBX` /
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 7 (105 min left) ---
|
||
|
|
Let me read the "Handing off downstream" section of the skill file.
|
||
|
|
|
||
|
|
--- step 8 (105 min left) ---
|
||
|
|
COMMAND: grep -n "Handing off downstream" .claude/skills/marvelous-designer/SKILL.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
180:## Handing off downstream
|
||
|
|
|
||
|
|
--- step 9 (105 min left) ---
|
||
|
|
COMMAND: sed -n '180,260p' .claude/skills/marvelous-designer/SKILL.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Handing off downstream
|
||
|
|
|
||
|
|
Export three ways per garment, into `tools/tailor/`:
|
||
|
|
|
||
|
|
```python
|
||
|
|
export_api.ExportZPrj(".../lena_<garment>_v<N>.zprj") # editable source of truth
|
||
|
|
op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
|
||
|
|
export_api.ExportFBX(".../lena_<garment>_v<N>_garment.fbx", op) # for clothing/
|
||
|
|
export_api.ExportOBJ(".../lena_<garment>_v<N>_garment.obj", op)
|
||
|
|
export_api.ExportZPac(".../lena_<garment>_v<N>.zpac") # for outfit assembly
|
||
|
|
```
|
||
|
|
|
||
|
|
Then the garment goes through `clothing/garment_pipeline.py` (fit → reduce →
|
||
|
|
skin → export) to become a game outfit part. **Two advantages MD-authored
|
||
|
|
garments have over downloaded ones**, worth exploiting in the config:
|
||
|
|
|
||
|
|
- **They're already game budget.** These export at 1.2k–3.7k verts; the
|
||
|
|
downloaded MD dress the clothing pipeline was piloted on is 2.49 M verts /
|
||
|
|
110 MB. The brutal `reduce` stage is mostly unnecessary — start with a high
|
||
|
|
tri budget and only decimate if the test bed complains.
|
||
|
|
- **They're already fitted**, having been draped on the actual game body, so
|
||
|
|
`align`/`fit` needs little more than the clearance shell.
|
||
|
|
|
||
|
|
Keep the `.zprj` — regenerating a variant beats re-authoring, and the clothing
|
||
|
|
README explicitly prefers re-exporting from MD over fighting reduction.
|
||
|
|
|
||
|
|
## Failure catalogue (don't rediscover these)
|
||
|
|
|
||
|
|
- **Shorts / trousers are unsolved.** Two-panel crotch-notch construction twists
|
||
|
|
every time — fabric can't thread between Lena's touching thighs, and all seam
|
||
|
|
parities were tried. Next approach: **4 panels arranged on the per-leg points**
|
||
|
|
(`Leg_Front_L/R`, `Leg_Back_L/R`) so cloth starts wrapped around each thigh.
|
||
|
|
- **Upside-down panels** (y-down drafting) — see the y+ = UP rule.
|
||
|
|
- **Elastic-first drapes** slide off a hip; **soft-from-frame-0** drapes bunch.
|
||
|
|
- **Co-draping** two garments inverts one.
|
||
|
|
- **Retexturing after a scene merge** silently repaints the wrong garment.
|
||
|
|
- **Threaded/background socket servers inside MD never answer** — the bridge must
|
||
|
|
own the main thread. Don't "fix" the frozen UI by re-threading it; that was
|
||
|
|
tried and the design is deliberate.
|
||
|
|
- MD 2026's **AI Image Generator / EveryWear are GUI-only** — not scriptable.
|
||
|
|
|
||
|
|
## File map
|
||
|
|
|
||
|
|
| Path | What |
|
||
|
|
|---|---|
|
||
|
|
| `tools/md_bridge.py` | Terminal client (`--ping/--exec/--file/--stop`) |
|
||
|
|
| `tools/md_bridge/TinqsMDBridge.py` | The plugin (register once via Plug-in Manager; referenced in place, so edits go live on next click) |
|
||
|
|
| `tools/md_bridge/api_surface.json`, `api_docs.json` | Introspected API truth |
|
||
|
|
| `docs/md-bridge.md` | Protocol, install, threading rationale |
|
||
|
|
| `tools/tailor/measure_body.py`, `lena_measurements.json` | Body measurement tool + Lena's card |
|
||
|
|
| `tools/tailor/md_*.py` | Per-garment recipes (each header carries its own lessons) |
|
||
|
|
| `tools/tailor/qc_placement.py` | Placement QC measurement |
|
||
|
|
| `tools/tailor/textures/` | Generated fabric maps |
|
||
|
|
| `tools/tailor/screenshots/` | Required per-garment review renders |
|
||
|
|
| `tools/tailor/avatar/Lena_QuatSkin_Avatar.fbx` | Game body as an MD avatar (from the GLB, leaf bones stripped) |
|
||
|
|
| `tools/tailor/lena_*_v*.{zprj,fbx,obj,zpac}` | Shipped garments |
|
||
|
|
|
||
|
|
--- step 10 (105 min left) ---
|
||
|
|
Now let me look at the pipeline script to understand the config schema and stage mechanics.
|
||
|
|
|
||
|
|
--- step 11 (104 min left) ---
|
||
|
|
COMMAND: wc -l clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
853 clothing/garment_pipeline.py
|
||
|
|
|
||
|
|
--- step 12 (104 min left) ---
|
||
|
|
COMMAND: grep -n "^def \|^class \|stage ==" clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
32:def log(msg):
|
||
|
|
36:def work_path(name):
|
||
|
|
42:def save_checkpoint(stage):
|
||
|
|
48:def load_checkpoint(stage):
|
||
|
|
56:def obj(name):
|
||
|
|
63:def objs_prefixed(prefix):
|
||
|
|
67:def select_only(objects, active=None):
|
||
|
|
74:def apply_all_transforms(objects):
|
||
|
|
82:def apply_modifier(o, mod_name):
|
||
|
|
87:def tris_of(o):
|
||
|
|
92:def world_verts(o):
|
||
|
|
96:def zspan(objects):
|
||
|
|
102:def qa_render(tag, focus_objects=None, ortho_pad=1.25):
|
||
|
|
131:def bone_head_z(rig, bone):
|
||
|
|
142:def import_garment_islands():
|
||
|
|
220:def stage_census():
|
||
|
|
251:def stage_prepare():
|
||
|
|
468:def make_shell():
|
||
|
|
482:def zgradient_group(o, name, z_full, z_zero):
|
||
|
|
498:def stage_fit():
|
||
|
|
543:def make_skirt_proxy(part, proxy_cfg):
|
||
|
|
590:def stage_reduce():
|
||
|
|
649:def stage_bake():
|
||
|
|
713:def data_transfer_weights(dst, src):
|
||
|
|
727:def smooth_and_normalize(o, iterations=3):
|
||
|
|
738:def pelvis_gradient(o, hip_z, hem_z):
|
||
|
|
758:def stage_skin():
|
||
|
|
806:def stage_export():
|
||
|
|
840:def main():
|
||
|
|
|
||
|
|
--- step 13 (104 min left) ---
|
||
|
|
Let me read the key stages: census, prepare (especially the align logic), and the config parsing.
|
||
|
|
|
||
|
|
--- step 14 (104 min left) ---
|
||
|
|
COMMAND: sed -n '220,470p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_census():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
report = []
|
||
|
|
for i, o in enumerate(islands):
|
||
|
|
vs = world_verts(o)
|
||
|
|
zs = [v.z for v in vs]; xs = [v.x for v in vs]; ys = [v.y for v in vs]
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
color = CENSUS_COLORS[i % len(CENSUS_COLORS)] if i < len(CENSUS_COLORS) else (0.5, 0.5, 0.5)
|
||
|
|
mat = bpy.data.materials.new(f"census_{i:03d}")
|
||
|
|
mat.diffuse_color = (*color, 1.0)
|
||
|
|
o.data.materials.clear()
|
||
|
|
o.data.materials.append(mat)
|
||
|
|
report.append({"island": i, "verts": len(o.data.vertices), "tris": tris_of(o),
|
||
|
|
"z": [round(min(zs), 3), round(max(zs), 3)],
|
||
|
|
"x": [round(min(xs), 3), round(max(xs), 3)],
|
||
|
|
"centroid": [round(c.x, 3), round(c.y, 3), round(c.z, 3)],
|
||
|
|
"color": [round(v, 2) for v in color]})
|
||
|
|
log(f"ISL_{i:03d} verts={len(o.data.vertices):7d} z=[{min(zs):.3f},{max(zs):.3f}] "
|
||
|
|
f"x=[{min(xs):+.3f},{max(xs):+.3f}] color={tuple(round(v,2) for v in color)}")
|
||
|
|
with open(work_path("census.json"), "w", encoding="utf-8") as f:
|
||
|
|
json.dump(report, f, indent=1)
|
||
|
|
bpy.context.scene.display.shading.color_type = "MATERIAL"
|
||
|
|
qa_render("00_census", focus_objects=islands)
|
||
|
|
save_checkpoint("00_census")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: prepare ───────────────────────────────────────────────────────────
|
||
|
|
# Group census islands into configured parts (each part lists island indices),
|
||
|
|
# import body+rig, align garment onto body.
|
||
|
|
|
||
|
|
def stage_prepare():
|
||
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||
|
|
|
||
|
|
# body + rig first (defines target space)
|
||
|
|
body_path = CFG["body"]
|
||
|
|
bpy.ops.import_scene.gltf(filepath=body_path)
|
||
|
|
rig = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||
|
|
rig.name = "RIG"
|
||
|
|
body = max((o for o in bpy.data.objects if o.type == "MESH"),
|
||
|
|
key=lambda o: len(o.data.vertices))
|
||
|
|
# drop face-part extras (eyes etc.) that ride along in the body GLB
|
||
|
|
for o in [o for o in bpy.data.objects if o.type == "MESH" and o is not body]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
body.name = "BODY"
|
||
|
|
apply_all_transforms([rig, body])
|
||
|
|
log(f"body: {len(body.data.vertices)} verts, groups={len(body.vertex_groups)}, "
|
||
|
|
f"bones={len(rig.data.bones)}")
|
||
|
|
|
||
|
|
# garment islands (same deterministic indexing the census printed)
|
||
|
|
islands = import_garment_islands()
|
||
|
|
by_idx = {int(o.name.split("_")[1]): o for o in islands}
|
||
|
|
claimed = set()
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
ids = part_cfg.get("islands", [])
|
||
|
|
members = [by_idx[i] for i in ids if i in by_idx]
|
||
|
|
if not members:
|
||
|
|
log(f"WARNING: part {part_name} matched no islands ({ids})")
|
||
|
|
continue
|
||
|
|
claimed.update(ids)
|
||
|
|
select_only(members, active=members[0])
|
||
|
|
bpy.ops.object.join()
|
||
|
|
part = bpy.context.view_layer.objects.active
|
||
|
|
part.name = f"GARM_{part_name}"
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
part.data.materials.clear()
|
||
|
|
part.data.materials.append(mat)
|
||
|
|
log(f"part {part.name}: islands={ids} verts={len(part.data.vertices)} tris={tris_of(part)}")
|
||
|
|
for i, o in list(by_idx.items()):
|
||
|
|
if i not in claimed and o.name.startswith("ISL_"):
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
|
||
|
|
# bodyshell parts: fitted clothing built FROM the body (Islander technique):
|
||
|
|
# copy the body, keep the configured z-band + torso-dominant vertices, push
|
||
|
|
# outward along normals. Fit and weights are correct by construction.
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
if part_cfg.get("type") != "bodyshell":
|
||
|
|
continue
|
||
|
|
shellp = body.copy()
|
||
|
|
shellp.data = body.data.copy()
|
||
|
|
shellp.name = f"GARM_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(shellp)
|
||
|
|
z0, z1 = part_cfg["z0"], part_cfg["z1"]
|
||
|
|
allowed = set(part_cfg.get("bones", ["spine_01", "spine_02", "spine_03", "pelvis"]))
|
||
|
|
gnames = {i: g.name for i, g in enumerate(shellp.vertex_groups)}
|
||
|
|
keep = set()
|
||
|
|
for v in shellp.data.vertices:
|
||
|
|
z = (shellp.matrix_world @ v.co).z
|
||
|
|
if not (z0 <= z <= z1):
|
||
|
|
continue
|
||
|
|
best, bw = None, 0.0
|
||
|
|
for g in v.groups:
|
||
|
|
if g.weight > bw:
|
||
|
|
best, bw = gnames.get(g.group), g.weight
|
||
|
|
if best in allowed:
|
||
|
|
keep.add(v.index)
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(shellp.data)
|
||
|
|
bm.verts.ensure_lookup_table()
|
||
|
|
doomed = [v for v in bm.verts if v.index not in keep]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="VERTS")
|
||
|
|
bm.to_mesh(shellp.data)
|
||
|
|
bm.free()
|
||
|
|
d = shellp.modifiers.new("Offset", "DISPLACE")
|
||
|
|
d.strength = part_cfg.get("offset_mm", 6) / 1000.0
|
||
|
|
d.mid_level = 0.0
|
||
|
|
apply_modifier(shellp, d.name)
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
col = part_cfg.get("color", [0.7, 0.7, 0.7])
|
||
|
|
mat.use_nodes = True
|
||
|
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||
|
|
if bsdf:
|
||
|
|
bsdf.inputs["Base Color"].default_value = (*col, 1.0)
|
||
|
|
bsdf.inputs["Roughness"].default_value = 0.85
|
||
|
|
mat.diffuse_color = (*col, 1.0)
|
||
|
|
shellp.data.materials.clear()
|
||
|
|
shellp.data.materials.append(mat)
|
||
|
|
log(f"bodyshell {shellp.name}: verts=
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 15 (104 min left) ---
|
||
|
|
COMMAND: sed -n '380,470p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
M = Matrix.Diagonal(Vector((sxy, sxy, sz))).to_4x4()
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(M)
|
||
|
|
gz0, gz1 = zspan(parts)
|
||
|
|
b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(T)
|
||
|
|
log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
|
||
|
|
# torso boost: heroic bodies are wider than MD mannequins — radially inflate
|
||
|
|
# the above-waist region about the body axis so bodice/collar fabric starts
|
||
|
|
# OUTSIDE the body (direction-preserving; the OUTSIDE shrinkwrap then only
|
||
|
|
# catches stragglers instead of shredding buried cloth to random sides).
|
||
|
|
tb = CFG.get("torso_boost")
|
||
|
|
if tb:
|
||
|
|
z0, z1, boost = tb.get("z0", 1.00), tb.get("z1", 1.08), tb.get("xy", 1.18)
|
||
|
|
for p in parts:
|
||
|
|
for v in p.data.vertices:
|
||
|
|
z = v.co.z
|
||
|
|
if z <= z0:
|
||
|
|
continue
|
||
|
|
t = 1.0 if z >= z1 else (z - z0) / max(1e-9, z1 - z0)
|
||
|
|
f = 1.0 + (boost - 1.0) * t
|
||
|
|
v.co.x *= f
|
||
|
|
v.co.y *= f
|
||
|
|
log(f"torso_boost: xy x{boost} above z={z0}..{z1}")
|
||
|
|
|
||
|
|
# post-align cut: drop configured fabric above a body-space height (v1 uses
|
||
|
|
# this to remove the slim MD bodice+sleeves replaced by the bodyshell part)
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
cut = part_cfg.get("cut_above_z")
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if cut is None or p is None or part_cfg.get("type") == "bodyshell":
|
||
|
|
continue
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(p.data)
|
||
|
|
ox = part_cfg.get("cut_outboard_x")
|
||
|
|
oz = part_cfg.get("cut_outboard_z_above", 0.0)
|
||
|
|
def out(v):
|
||
|
|
w = p.matrix_world @ v.co
|
||
|
|
return w.z > cut or (ox is not None and w.z > oz and abs(w.x) > ox)
|
||
|
|
doomed = [v for v in bm.verts if out(v)]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="VERTS")
|
||
|
|
bm.to_mesh(p.data)
|
||
|
|
bm.free()
|
||
|
|
log(f"cut {p.name} above z={cut}: now {len(p.data.vertices)} verts")
|
||
|
|
# prune disconnected scraps left by the cuts (e.g. sleeve remnants):
|
||
|
|
# separate loose, keep the biggest piece + anything near the center axis
|
||
|
|
px = part_cfg.get("prune_scraps_x")
|
||
|
|
if px is not None:
|
||
|
|
pname = p.name
|
||
|
|
select_only([p])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.separate(type="LOOSE")
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
pieces = [o for o in bpy.data.objects if o.name.startswith(pname)]
|
||
|
|
main = max(pieces, key=lambda o: len(o.data.vertices))
|
||
|
|
keepers = [main]
|
||
|
|
for o in pieces:
|
||
|
|
if o is main:
|
||
|
|
continue
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
if abs(c.x) <= px:
|
||
|
|
keepers.append(o)
|
||
|
|
else:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
select_only(keepers, active=main)
|
||
|
|
bpy.ops.object.join()
|
||
|
|
joined = bpy.context.view_layer.objects.active
|
||
|
|
joined.name = pname
|
||
|
|
log(f"prune {joined.name}: kept {len(keepers)} pieces, "
|
||
|
|
f"{len(joined.data.vertices)} verts")
|
||
|
|
|
||
|
|
qa_render("10_prepare")
|
||
|
|
save_checkpoint("10_prepare")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: fit ───────────────────────────────────────────────────────────────
|
||
|
|
# Build the inflated BODY_SHELL, give each part a Z-gradient influence mask, and
|
||
|
|
# shrinkwrap high-poly parts in configured layer order (later layers can target
|
||
|
|
# earlier ones).
|
||
|
|
|
||
|
|
def make_shell():
|
||
|
|
body = obj("BODY")
|
||
|
|
shell = body.copy()
|
||
|
|
|
||
|
|
--- step 16 (104 min left) ---
|
||
|
|
Now I understand the align logic. The key line is:
|
||
|
|
```python
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
```
|
||
|
|
where `gz1` is the garment's top Z after scaling. For these garments, I need `z_nudge` to cancel the auto-align so the net placement is `[z0/10, z1/10]`.
|
||
|
|
|
||
|
|
Let me now look at the import_garment_islands function and the rest of the pipeline (fit, reduce, skin, export).
|
||
|
|
|
||
|
|
--- step 17 (104 min left) ---
|
||
|
|
COMMAND: sed -n '142,220p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def import_garment_islands():
|
||
|
|
pre = set(bpy.data.objects)
|
||
|
|
src = CFG["source"]
|
||
|
|
ext = os.path.splitext(src)[1].lower()
|
||
|
|
if ext == ".fbx":
|
||
|
|
bpy.ops.import_scene.fbx(filepath=src)
|
||
|
|
elif ext in (".glb", ".gltf"):
|
||
|
|
bpy.ops.import_scene.gltf(filepath=src)
|
||
|
|
elif ext == ".obj":
|
||
|
|
bpy.ops.wm.obj_import(filepath=src)
|
||
|
|
else:
|
||
|
|
raise SystemExit(f"unsupported garment format {ext}")
|
||
|
|
new = [o for o in bpy.data.objects if o not in pre]
|
||
|
|
meshes = [o for o in new if o.type == "MESH"]
|
||
|
|
for o in [o for o in new if o.type != "MESH"]:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
for g in meshes:
|
||
|
|
for m in list(g.modifiers):
|
||
|
|
g.modifiers.remove(m)
|
||
|
|
g.vertex_groups.clear()
|
||
|
|
apply_all_transforms(meshes)
|
||
|
|
|
||
|
|
drop = set(CFG.get("drop_materials", []))
|
||
|
|
for g in meshes:
|
||
|
|
if not g.data.materials:
|
||
|
|
continue
|
||
|
|
drop_idx = {i for i, m in enumerate(g.data.materials) if m and m.name in drop}
|
||
|
|
if not drop_idx:
|
||
|
|
continue
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(g.data)
|
||
|
|
doomed = [f for f in bm.faces if f.material_index in drop_idx]
|
||
|
|
bmesh.ops.delete(bm, geom=doomed, context="FACES")
|
||
|
|
bm.to_mesh(g.data)
|
||
|
|
bm.free()
|
||
|
|
meshes = [g for g in meshes if len(g.data.vertices) > 0]
|
||
|
|
log(f"after drop_materials: {sum(len(g.data.vertices) for g in meshes)} verts")
|
||
|
|
|
||
|
|
# weld panels, then split to loose islands
|
||
|
|
select_only(meshes)
|
||
|
|
bpy.ops.object.join()
|
||
|
|
fabric = bpy.context.view_layer.objects.active
|
||
|
|
select_only([fabric])
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.mesh.remove_doubles(threshold=CFG.get("weld_threshold", 0.0006))
|
||
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||
|
|
bpy.ops.mesh.separate(type="LOOSE")
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
islands = [o for o in bpy.data.objects if o.type == "MESH" and o.name not in ("BODY", "BODY_SHELL")]
|
||
|
|
min_verts = CFG.get("min_island_verts", 40)
|
||
|
|
kept = []
|
||
|
|
for o in islands:
|
||
|
|
if len(o.data.vertices) < min_verts:
|
||
|
|
bpy.data.objects.remove(o, do_unlink=True)
|
||
|
|
else:
|
||
|
|
kept.append(o)
|
||
|
|
# deterministic index: vertex count desc, then centroid x/z
|
||
|
|
def key(o):
|
||
|
|
vs = world_verts(o)
|
||
|
|
c = sum(vs, Vector()) / len(vs)
|
||
|
|
return (-len(o.data.vertices), round(c.x, 4), round(c.z, 4))
|
||
|
|
kept.sort(key=key)
|
||
|
|
for i, o in enumerate(kept):
|
||
|
|
o.name = f"ISL_{i:03d}"
|
||
|
|
log(f"islands kept: {len(kept)} (dropped tiny <{min_verts} verts)")
|
||
|
|
return kept
|
||
|
|
|
||
|
|
|
||
|
|
CENSUS_COLORS = [
|
||
|
|
(0.90, 0.10, 0.10), (0.10, 0.55, 0.95), (0.10, 0.80, 0.20), (0.95, 0.75, 0.10),
|
||
|
|
(0.75, 0.15, 0.85), (0.05, 0.85, 0.80), (0.95, 0.45, 0.05), (0.55, 0.35, 0.15),
|
||
|
|
(0.95, 0.55, 0.75), (0.45, 0.95, 0.45), (0.25, 0.25, 0.95), (0.80, 0.80, 0.30),
|
||
|
|
(0.50, 0.05, 0.30), (0.05, 0.45, 0.35), (0.65, 0.65, 0.95), (0.95, 0.85, 0.65),
|
||
|
|
(0.30, 0.10, 0.60), (0.10, 0.30, 0.10), (0.60, 0.30, 0.30), (0.30, 0.60, 0.60),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def stage_census():
|
||
|
|
|
||
|
|
--- step 18 (104 min left) ---
|
||
|
|
COMMAND: sed -n '468,660p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def make_shell():
|
||
|
|
body = obj("BODY")
|
||
|
|
shell = body.copy()
|
||
|
|
shell.data = body.data.copy()
|
||
|
|
shell.name = "BODY_SHELL"
|
||
|
|
bpy.context.scene.collection.objects.link(shell)
|
||
|
|
d = shell.modifiers.new("Inflate", "DISPLACE")
|
||
|
|
d.strength = CFG.get("shell_mm", 4) / 1000.0
|
||
|
|
d.mid_level = 0.0
|
||
|
|
apply_modifier(shell, d.name)
|
||
|
|
shell.hide_render = True
|
||
|
|
return shell
|
||
|
|
|
||
|
|
|
||
|
|
def zgradient_group(o, name, z_full, z_zero):
|
||
|
|
"""Vertex group: weight 1 above z_full … 0 below z_zero (linear between)."""
|
||
|
|
vg = o.vertex_groups.get(name) or o.vertex_groups.new(name=name)
|
||
|
|
mw = o.matrix_world
|
||
|
|
for v in o.data.vertices:
|
||
|
|
z = (mw @ v.co).z
|
||
|
|
if z >= z_full:
|
||
|
|
w = 1.0
|
||
|
|
elif z <= z_zero:
|
||
|
|
w = 0.0
|
||
|
|
else:
|
||
|
|
w = (z - z_zero) / max(1e-9, (z_full - z_zero))
|
||
|
|
vg.add([v.index], w, "REPLACE")
|
||
|
|
return vg
|
||
|
|
|
||
|
|
|
||
|
|
def stage_fit():
|
||
|
|
load_checkpoint("10_prepare")
|
||
|
|
rig = obj("RIG")
|
||
|
|
shell = make_shell()
|
||
|
|
hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
if part_cfg.get("type") == "bodyshell":
|
||
|
|
log(f"fit GARM_{part_name}: skipped (bodyshell)")
|
||
|
|
continue
|
||
|
|
fit = part_cfg.get("fit", {})
|
||
|
|
target_name = fit.get("target", "BODY_SHELL")
|
||
|
|
target = obj(target_name if target_name != "BODY_SHELL" else "BODY_SHELL")
|
||
|
|
mode = fit.get("mask", "full") # full | above_hip | waistband | none
|
||
|
|
if mode == "none":
|
||
|
|
log(f"fit {p.name}: skipped (mask none)")
|
||
|
|
continue
|
||
|
|
if mode == "full":
|
||
|
|
vg = None
|
||
|
|
elif mode == "above_hip":
|
||
|
|
vg = zgradient_group(p, "fit_mask", hip_z + 0.02, hip_z - 0.06)
|
||
|
|
elif mode == "waistband":
|
||
|
|
vg = zgradient_group(p, "fit_mask", hip_z + 0.10, hip_z - 0.02)
|
||
|
|
sw = p.modifiers.new("Fit", "SHRINKWRAP")
|
||
|
|
sw.target = target
|
||
|
|
sw.wrap_method = "NEAREST_SURFACEPOINT"
|
||
|
|
sw.wrap_mode = fit.get("wrap_mode", "OUTSIDE")
|
||
|
|
sw.offset = fit.get("offset_mm", 2) / 1000.0
|
||
|
|
if vg is not None:
|
||
|
|
sw.vertex_group = vg.name
|
||
|
|
apply_modifier(p, sw.name)
|
||
|
|
log(f"fit {p.name}: target={target.name} mask={mode} offset={sw.offset if hasattr(sw,'offset') else '?'}")
|
||
|
|
|
||
|
|
qa_render("20_fit")
|
||
|
|
save_checkpoint("20_fit")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: reduce ────────────────────────────────────────────────────────────
|
||
|
|
# Keep a hidden HI_<part> copy for baking. Skirt-style parts are replaced by a
|
||
|
|
# generated cylinder proxy shrinkwrapped to the high-poly; others get planar
|
||
|
|
# dissolve then collapse decimate down to budget.
|
||
|
|
|
||
|
|
def make_skirt_proxy(part, proxy_cfg):
|
||
|
|
seg = proxy_cfg.get("segments", 30)
|
||
|
|
rings = proxy_cfg.get("rings", 6)
|
||
|
|
vs = world_verts(part)
|
||
|
|
zs = sorted(v.z for v in vs)
|
||
|
|
z0, z1 = zs[int(0.01 * len(zs))], zs[int(0.99 * len(zs))]
|
||
|
|
cx = sum(v.x for v in vs) / len(vs)
|
||
|
|
cy = sum(v.y for v in vs) / len(vs)
|
||
|
|
|
||
|
|
def ring_radius(z):
|
||
|
|
band = [v for v in vs if abs(v.z - z) < (z1 - z0) / (rings * 1.5)]
|
||
|
|
if not band:
|
||
|
|
return 0.2
|
||
|
|
ds = sorted(math.hypot(v.x - cx, v.y - cy) for v in band)
|
||
|
|
return ds[int(0.65 * len(ds))]
|
||
|
|
|
||
|
|
me = bpy.data.meshes.new(f"{part.name}_proxy")
|
||
|
|
bm = bmesh.new()
|
||
|
|
ring_verts = []
|
||
|
|
for r in range(rings + 1):
|
||
|
|
z = z1 - (z1 - z0) * (r / rings)
|
||
|
|
rad = ring_radius(z)
|
||
|
|
ring = [bm.verts.new((cx + rad * math.cos(2 * math.pi * s / seg),
|
||
|
|
cy + rad * math.sin(2 * math.pi * s / seg), z))
|
||
|
|
for s in range(seg)]
|
||
|
|
ring_verts.append(ring)
|
||
|
|
for r in range(rings):
|
||
|
|
for s in range(seg):
|
||
|
|
a, b = ring_verts[r][s], ring_verts[r][(s + 1) % seg]
|
||
|
|
c, d = ring_verts[r + 1][(s + 1) % seg], ring_verts[r + 1][s]
|
||
|
|
bm.faces.new((a, b, c, d))
|
||
|
|
bm.to_mesh(me)
|
||
|
|
bm.free()
|
||
|
|
proxy = bpy.data.objects.new(f"{part.name}_proxytmp", me)
|
||
|
|
bpy.context.scene.collection.objects.link(proxy)
|
||
|
|
sw = proxy.modifiers.new("WrapToSkirt", "SHRINKWRAP")
|
||
|
|
sw.target = part
|
||
|
|
sw.wrap_method = "NEAREST_SURFACEPOINT"
|
||
|
|
apply_modifier(proxy, sw.name)
|
||
|
|
select_only([proxy])
|
||
|
|
bpy.ops.object.shade_smooth()
|
||
|
|
# carry the part's (single) material
|
||
|
|
if part.data.materials:
|
||
|
|
proxy.data.materials.append(part.data.materials[0])
|
||
|
|
return proxy
|
||
|
|
|
||
|
|
|
||
|
|
def stage_reduce():
|
||
|
|
load_checkpoint("20_fit")
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
# keep high-poly for baking
|
||
|
|
hi = p.copy()
|
||
|
|
hi.data = p.data.copy()
|
||
|
|
hi.name = f"HI_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(hi)
|
||
|
|
hi.hide_render = hi.hide_viewport = True
|
||
|
|
|
||
|
|
wb = part_cfg.get("weld_band") # [z0, z1, dist]: fuse stacked layers
|
||
|
|
if wb:
|
||
|
|
z0, z1, dist = wb
|
||
|
|
select_only([p])
|
||
|
|
bm = bmesh.new()
|
||
|
|
bm.from_mesh(p.data)
|
||
|
|
mw = p.matrix_world
|
||
|
|
band = [v for v in bm.verts if z0 <= (mw @ v.co).z <= z1]
|
||
|
|
bmesh.ops.remove_doubles(bm, verts=band, dist=dist)
|
||
|
|
bm.to_mesh(p.data)
|
||
|
|
bm.free()
|
||
|
|
log(f"weld_band {p.name}: z=[{z0},{z1}] dist={dist} -> {len(p.data.vertices)} verts")
|
||
|
|
|
||
|
|
budget = part_cfg.get("tris", 1500)
|
||
|
|
if part_cfg.get("proxy") == "cylinder":
|
||
|
|
proxy = make_skirt_proxy(p, part_cfg.get("proxy_cfg", {}))
|
||
|
|
name = p.name
|
||
|
|
bpy.data.objects.remove(p, do_unlink=True)
|
||
|
|
proxy.name = name
|
||
|
|
p = proxy
|
||
|
|
else:
|
||
|
|
# planar dissolve first (MD panels are near-planar), then collapse
|
||
|
|
dm = p.modifiers.new("Planar", "DECIMATE")
|
||
|
|
dm.decim
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 19 (104 min left) ---
|
||
|
|
COMMAND: sed -n '660,853p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
continue
|
||
|
|
hi.hide_viewport = hi.hide_render = False
|
||
|
|
size = part_cfg.get("bake_size", 512)
|
||
|
|
# bake UVs
|
||
|
|
select_only([lo])
|
||
|
|
uv = lo.data.uv_layers.new(name="bake_uv")
|
||
|
|
lo.data.uv_layers.active = uv
|
||
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
||
|
|
bpy.ops.mesh.select_all(action="SELECT")
|
||
|
|
bpy.ops.uv.smart_project(angle_limit=math.radians(66))
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
# target images + nodes on the part's material
|
||
|
|
mat = lo.data.materials[0] if lo.data.materials else None
|
||
|
|
if mat is None:
|
||
|
|
mat = bpy.data.materials.new(f"{part_name}_mat")
|
||
|
|
lo.data.materials.append(mat)
|
||
|
|
mat.use_nodes = True
|
||
|
|
nt = mat.node_tree
|
||
|
|
imgs = {}
|
||
|
|
for kind in ("NORMAL", "AO"):
|
||
|
|
img = bpy.data.images.new(f"{part_name}_{kind.lower()}", size, size,
|
||
|
|
alpha=False, float_buffer=(kind == "NORMAL"))
|
||
|
|
node = nt.nodes.new("ShaderNodeTexImage")
|
||
|
|
node.image = img
|
||
|
|
nt.nodes.active = node
|
||
|
|
select_only([hi, lo], active=lo)
|
||
|
|
bpy.ops.object.bake(type=kind, use_selected_to_active=True,
|
||
|
|
cage_extrusion=0.004,
|
||
|
|
use_clear=True)
|
||
|
|
out = work_path(f"{part_name}_{kind.lower()}.png")
|
||
|
|
img.filepath_raw = out
|
||
|
|
img.file_format = "PNG"
|
||
|
|
img.save()
|
||
|
|
imgs[kind] = img
|
||
|
|
log(f"baked {kind} for {part_name} -> {out}")
|
||
|
|
# wire into the material (normal map + AO multiply into base color)
|
||
|
|
bsdf = next(n for n in nt.nodes if n.type == "BSDF_PRINCIPLED")
|
||
|
|
nrm_tex = nt.nodes.new("ShaderNodeTexImage")
|
||
|
|
nrm_tex.image = imgs["NORMAL"]
|
||
|
|
nrm_tex.image.colorspace_settings.name = "Non-Color"
|
||
|
|
nmap = nt.nodes.new("ShaderNodeNormalMap")
|
||
|
|
nmap.uv_map = "bake_uv"
|
||
|
|
nt.links.new(nmap.inputs["Color"], nrm_tex.outputs["Color"])
|
||
|
|
nt.links.new(bsdf.inputs["Normal"], nmap.outputs["Normal"])
|
||
|
|
hi.hide_viewport = hi.hide_render = True
|
||
|
|
save_checkpoint("40_bake")
|
||
|
|
|
||
|
|
|
||
|
|
# ── stage: skin ──────────────────────────────────────────────────────────────
|
||
|
|
# Weight transfer. Snug parts: Data Transfer (nearest face interpolated) from
|
||
|
|
# BODY. Skirt parts: cone proxy — data-transferred at the waist, pelvis-locked
|
||
|
|
# at the hem, then transferred onto the skirt.
|
||
|
|
|
||
|
|
def data_transfer_weights(dst, src):
|
||
|
|
dst.vertex_groups.clear()
|
||
|
|
for vg in src.vertex_groups:
|
||
|
|
dst.vertex_groups.new(name=vg.name)
|
||
|
|
dt = dst.modifiers.new("Weights", "DATA_TRANSFER")
|
||
|
|
dt.object = src
|
||
|
|
dt.use_vert_data = True
|
||
|
|
dt.data_types_verts = {"VGROUP_WEIGHTS"}
|
||
|
|
dt.vert_mapping = "POLYINTERP_NEAREST"
|
||
|
|
dt.layers_vgroup_select_src = "ALL"
|
||
|
|
dt.layers_vgroup_select_dst = "NAME"
|
||
|
|
apply_modifier(dst, dt.name)
|
||
|
|
|
||
|
|
|
||
|
|
def smooth_and_normalize(o, iterations=3):
|
||
|
|
select_only([o])
|
||
|
|
bpy.ops.object.mode_set(mode="WEIGHT_PAINT")
|
||
|
|
try:
|
||
|
|
bpy.ops.object.vertex_group_smooth(group_select_mode="ALL",
|
||
|
|
factor=0.5, repeat=iterations)
|
||
|
|
bpy.ops.object.vertex_group_normalize_all(lock_active=False)
|
||
|
|
finally:
|
||
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||
|
|
|
||
|
|
|
||
|
|
def pelvis_gradient(o, hip_z, hem_z):
|
||
|
|
"""Below the hip, blend all weights toward pelvis-only as z drops to hem."""
|
||
|
|
idx_pelvis = o.vertex_groups.find("pelvis")
|
||
|
|
if idx_pelvis < 0:
|
||
|
|
o.vertex_groups.new(name="pelvis")
|
||
|
|
idx_pelvis = o.vertex_groups.find("pelvis")
|
||
|
|
mw = o.matrix_world
|
||
|
|
for v in o.data.vertices:
|
||
|
|
z = (mw @ v.co).z
|
||
|
|
if z >= hip_z:
|
||
|
|
continue
|
||
|
|
t = min(1.0, (hip_z - z) / max(1e-9, hip_z - hem_z)) # 0 at hip → 1 at hem
|
||
|
|
for g in v.groups:
|
||
|
|
g.weight *= (1.0 - t)
|
||
|
|
o.vertex_groups[idx_pelvis].add([v.index], 0.0, "ADD")
|
||
|
|
# set pelvis weight so the total stays 1 after normalize
|
||
|
|
cur = sum(g.weight for g in v.groups)
|
||
|
|
o.vertex_groups[idx_pelvis].add([v.index], max(0.0, 1.0 - cur), "ADD")
|
||
|
|
|
||
|
|
|
||
|
|
def stage_skin():
|
||
|
|
src_stage = "40_bake" if os.path.exists(work_path("40_bake.blend")) else "30_reduce"
|
||
|
|
load_checkpoint(src_stage)
|
||
|
|
body, rig = obj("BODY"), obj("RIG")
|
||
|
|
hip_z = bone_head_z(rig, "pelvis")
|
||
|
|
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is None:
|
||
|
|
continue
|
||
|
|
if part_cfg.get("type") == "bodyshell":
|
||
|
|
pass # weights inherited from the body copy — already correct
|
||
|
|
elif part_cfg.get("weights") == "dress":
|
||
|
|
# one-piece dress: body weights everywhere, then blend the loose
|
||
|
|
# skirt region toward pelvis-only as z approaches the hem
|
||
|
|
hem_z = zspan([p])[0]
|
||
|
|
data_transfer_weights(p, body)
|
||
|
|
pelvis_gradient(p, hip_z - 0.04, hem_z)
|
||
|
|
elif part_cfg.get("weights") == "skirt":
|
||
|
|
hem_z = zspan([p])[0]
|
||
|
|
cone = p.copy() # skirt proxy IS already a leg-bridging cone shape
|
||
|
|
cone.data = p.data.copy()
|
||
|
|
cone.name = f"CONE_{part_name}"
|
||
|
|
bpy.context.scene.collection.objects.link(cone)
|
||
|
|
data_transfer_weights(cone, body)
|
||
|
|
pelvis_gradient(cone, hip_z, hem_z)
|
||
|
|
smooth_and_normalize(cone)
|
||
|
|
data_transfer_weights(p, cone)
|
||
|
|
cone.hide_viewport = cone.hide_render = True
|
||
|
|
else:
|
||
|
|
data_transfer_weights(p, body)
|
||
|
|
smooth_and_normalize(p)
|
||
|
|
# bind
|
||
|
|
p.parent = rig
|
||
|
|
p.matrix_parent_inverse = Matrix.Identity(4)
|
||
|
|
am = p.modifiers.new("Armature", "ARMATURE")
|
||
|
|
am.object = rig
|
||
|
|
nonzero = sum(1 for v in p.data.vertices if v.groups)
|
||
|
|
log(f"skin {p.name}: weighted {nonzero}/{len(p.data.vertices)} verts")
|
||
|
|
|
||
|
|
qa_render("50_ski
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 20 (104 min left) ---
|
||
|
|
COMMAND: sed -n '806,853p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
def stage_export():
|
||
|
|
load_checkpoint("50_skin")
|
||
|
|
rig = obj("RIG")
|
||
|
|
out_dir = CFG["export"]["out_dir"]
|
||
|
|
os.makedirs(out_dir, exist_ok=True)
|
||
|
|
slots = {}
|
||
|
|
for part_name, part_cfg in CFG["parts"].items():
|
||
|
|
p = bpy.data.objects.get(f"GARM_{part_name}")
|
||
|
|
if p is not None:
|
||
|
|
slots.setdefault(part_cfg["slot"], []).append(p)
|
||
|
|
gender, set_name = CFG["export"]["gender"], CFG["export"]["set"]
|
||
|
|
for slot, parts in slots.items():
|
||
|
|
path = os.path.join(out_dir, f"{gender}_{set_name}_{slot}.gltf")
|
||
|
|
select_only([rig] + parts, active=rig)
|
||
|
|
bpy.ops.export_scene.gltf(filepath=path, export_format="GLTF_SEPARATE",
|
||
|
|
use_selection=True, export_skins=True,
|
||
|
|
export_animations=False, export_yup=True)
|
||
|
|
log(f"EXPORTED {path} ({sum(tris_of(p) for p in parts)} tris, "
|
||
|
|
f"{os.path.getsize(path)} bytes)")
|
||
|
|
# determinism fingerprint
|
||
|
|
import hashlib
|
||
|
|
h = hashlib.sha1()
|
||
|
|
for part in sorted(objs_prefixed("GARM_"), key=lambda o: o.name):
|
||
|
|
for v in part.data.vertices:
|
||
|
|
h.update(b"%d %d %d" % (int(v.co.x * 1e5), int(v.co.y * 1e5), int(v.co.z * 1e5)))
|
||
|
|
log(f"vertex hash: {h.hexdigest()[:16]}")
|
||
|
|
|
||
|
|
|
||
|
|
# ── main ─────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
STAGES = {"census": stage_census, "prepare": stage_prepare, "fit": stage_fit, "reduce": stage_reduce,
|
||
|
|
"bake": stage_bake, "skin": stage_skin, "export": stage_export}
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
global ARGS, CFG
|
||
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--config", required=True)
|
||
|
|
ap.add_argument("--stage", required=True, choices=sorted(STAGES))
|
||
|
|
ARGS = ap.parse_args(argv)
|
||
|
|
cfg_path = ARGS.config if os.path.isabs(ARGS.config) else os.path.join(HERE, ARGS.config)
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
log(f"config={CFG['name']} stage={ARGS.stage}")
|
||
|
|
STAGES[ARGS.stage]()
|
||
|
|
|
||
|
|
|
||
|
|
main()
|
||
|
|
|
||
|
|
--- step 21 (104 min left) ---
|
||
|
|
Now let me read the README and the dress.json config.
|
||
|
|
|
||
|
|
--- step 22 (104 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 23 (104 min left) ---
|
||
|
|
Now let me read the README for the pipeline.
|
||
|
|
|
||
|
|
--- step 24 (104 min left) ---
|
||
|
|
COMMAND: cat clothing/README.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
# 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) | 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. |
|
||
|
|
| `fit` | Inflated `BODY_SHELL` (+4mm) target; shrinkwrap mode **OUTSIDE** (push out penetrating verts ONLY — never snap the whole cloth, that shreds layered garments) | `OUTSIDE_SURFACE` destroyed multi-layer bodices; `OUTSIDE` unmasked is safe everywhere since it can't pull cloth inward. |
|
||
|
|
| `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_qua
|
||
|
|
...[truncated]
|
||
|
|
|
||
|
|
--- step 25 (104 min left) ---
|
||
|
|
Let me see the rest of the README.
|
||
|
|
|
||
|
|
--- step 26 (104 min left) ---
|
||
|
|
COMMAND: sed -n '100,200p' clothing/README.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Current pilot
|
||
|
|
|
||
|
|
`configs/dress.json` → `../garments/thin-unweld.fbx`: an MD dress ensemble
|
||
|
|
(2.49M verts, 12 materials). v1 scope: bodice + belt/bow (Body slot) + pleated
|
||
|
|
skirt (Legs slot). The sleeved jacket (`cape*` materials) is deferred — sleeves
|
||
|
|
need a pose-warp to T-pose (GLM advice §4 has the recipe) and is round two.
|
||
|
|
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.
|
||
|
|
|
||
|
|
## 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).
|
||
|
|
- Flat MD colors until someone supplies textures (bake stage produces normal+AO,
|
||
|
|
not albedo).
|
||
|
|
- 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.
|
||
|
|
|
||
|
|
--- step 27 (104 min left) ---
|
||
|
|
Now let me check the existing export folder and the skill file.
|
||
|
|
|
||
|
|
--- step 28 (104 min left) ---
|
||
|
|
COMMAND: ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/" 2>/dev/null; echo "---"; ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress/" 2>/dev/null
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
total 0
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:11 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
---
|
||
|
|
total 464
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 20:21 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 438936 Jul 30 20:20 Female_MDDress_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 25779 Jul 30 20:20 Female_MDDress_Body.gltf
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1101 Jul 30 20:21 Female_MDDress_Body.gltf.import
|
||
|
|
|
||
|
|
--- step 29 (104 min left) ---
|
||
|
|
Good — the kapahaka folder is empty (the wrong pari files mentioned in the task must have been cleaned already, or I need to verify). Let me check the skill file for the "Handing off downstream" section.
|
||
|
|
|
||
|
|
--- step 30 (103 min left) ---
|
||
|
|
COMMAND: sed -n '/Handing off downstream/,$p' .claude/skills/marvelous-designer/SKILL.md
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
## Handing off downstream
|
||
|
|
|
||
|
|
Export three ways per garment, into `tools/tailor/`:
|
||
|
|
|
||
|
|
```python
|
||
|
|
export_api.ExportZPrj(".../lena_<garment>_v<N>.zprj") # editable source of truth
|
||
|
|
op = ApiTypes.ImportExportOption(); op.bExportGarment = True; op.bExportAvatar = False
|
||
|
|
export_api.ExportFBX(".../lena_<garment>_v<N>_garment.fbx", op) # for clothing/
|
||
|
|
export_api.ExportOBJ(".../lena_<garment>_v<N>_garment.obj", op)
|
||
|
|
export_api.ExportZPac(".../lena_<garment>_v<N>.zpac") # for outfit assembly
|
||
|
|
```
|
||
|
|
|
||
|
|
Then the garment goes through `clothing/garment_pipeline.py` (fit → reduce →
|
||
|
|
skin → export) to become a game outfit part. **Two advantages MD-authored
|
||
|
|
garments have over downloaded ones**, worth exploiting in the config:
|
||
|
|
|
||
|
|
- **They're already game budget.** These export at 1.2k–3.7k verts; the
|
||
|
|
downloaded MD dress the clothing pipeline was piloted on is 2.49 M verts /
|
||
|
|
110 MB. The brutal `reduce` stage is mostly unnecessary — start with a high
|
||
|
|
tri budget and only decimate if the test bed complains.
|
||
|
|
- **They're already fitted**, having been draped on the actual game body, so
|
||
|
|
`align`/`fit` needs little more than the clearance shell.
|
||
|
|
|
||
|
|
Keep the `.zprj` — regenerating a variant beats re-authoring, and the clothing
|
||
|
|
README explicitly prefers re-exporting from MD over fighting reduction.
|
||
|
|
|
||
|
|
## Failure catalogue (don't rediscover these)
|
||
|
|
|
||
|
|
- **Shorts / trousers are unsolved.** Two-panel crotch-notch construction twists
|
||
|
|
every time — fabric can't thread between Lena's touching thighs, and all seam
|
||
|
|
parities were tried. Next approach: **4 panels arranged on the per-leg points**
|
||
|
|
(`Leg_Front_L/R`, `Leg_Back_L/R`) so cloth starts wrapped around each thigh.
|
||
|
|
- **Upside-down panels** (y-down drafting) — see the y+ = UP rule.
|
||
|
|
- **Elastic-first drapes** slide off a hip; **soft-from-frame-0** drapes bunch.
|
||
|
|
- **Co-draping** two garments inverts one.
|
||
|
|
- **Retexturing after a scene merge** silently repaints the wrong garment.
|
||
|
|
- **Threaded/background socket servers inside MD never answer** — the bridge must
|
||
|
|
own the main thread. Don't "fix" the frozen UI by re-threading it; that was
|
||
|
|
tried and the design is deliberate.
|
||
|
|
- MD 2026's **AI Image Generator / EveryWear are GUI-only** — not scriptable.
|
||
|
|
|
||
|
|
## File map
|
||
|
|
|
||
|
|
| Path | What |
|
||
|
|
|---|---|
|
||
|
|
| `tools/md_bridge.py` | Terminal client (`--ping/--exec/--file/--stop`) |
|
||
|
|
| `tools/md_bridge/TinqsMDBridge.py` | The plugin (register once via Plug-in Manager; referenced in place, so edits go live on next click) |
|
||
|
|
| `tools/md_bridge/api_surface.json`, `api_docs.json` | Introspected API truth |
|
||
|
|
| `docs/md-bridge.md` | Protocol, install, threading rationale |
|
||
|
|
| `tools/tailor/measure_body.py`, `lena_measurements.json` | Body measurement tool + Lena's card |
|
||
|
|
| `tools/tailor/md_*.py` | Per-garment recipes (each header carries its own lessons) |
|
||
|
|
| `tools/tailor/qc_placement.py` | Placement QC measurement |
|
||
|
|
| `tools/tailor/textures/` | Generated fabric maps |
|
||
|
|
| `tools/tailor/screenshots/` | Required per-garment review renders |
|
||
|
|
| `tools/tailor/avatar/Lena_QuatSkin_Avatar.fbx` | Game body as an MD avatar (from the GLB, leaf bones stripped) |
|
||
|
|
| `tools/tailor/lena_*_v*.{zprj,fbx,obj,zpac}` | Shipped garments |
|
||
|
|
|
||
|
|
--- step 31 (103 min left) ---
|
||
|
|
Now I have a good understanding. Let me verify the source garment files exist and check the work directory.
|
||
|
|
|
||
|
|
--- step 32 (103 min left) ---
|
||
|
|
COMMAND: ls -la tools/tailor/lena_pari_v3_garment.fbx tools/tailor/lena_piupiu_v2_garment.fbx; echo "---"; ls clothing/work/ 2>/dev/null || echo "(no work dir yet)"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 149824 Jul 30 21:13 tools/tailor/lena_pari_v3_garment.fbx
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 271568 Jul 30 21:04 tools/tailor/lena_piupiu_v2_garment.fbx
|
||
|
|
---
|
||
|
|
dress
|
||
|
|
pari
|
||
|
|
piupiu
|
||
|
|
|
||
|
|
--- step 33 (103 min left) ---
|
||
|
|
There are already work directories for pari and piupiu. Let me check what's in them.
|
||
|
|
|
||
|
|
--- step 34 (103 min left) ---
|
||
|
|
COMMAND: ls -la clothing/work/pari/ clothing/work/piupiu/ 2>/dev/null
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
clothing/work/pari/:
|
||
|
|
total 47277
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 241955 Jul 30 22:06 00_census.blend
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 241956 Jul 30 21:36 00_census.blend1
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 8571622 Jul 30 22:08 10_prepare.blend
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 10441734 Jul 30 22:08 20_fit.blend
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 10505163 Jul 30 22:08 30_reduce.blend
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 10550828 Jul 30 22:09 50_skin.blend
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 240 Jul 30 22:06 census.json
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 817779 Jul 30 22:06 qa_00_census_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 869071 Jul 30 22:06 qa_00_census_quarter.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 555384 Jul 30 22:08 qa_10_prepare_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 506395 Jul 30 22:08 qa_10_prepare_quarter.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 556467 Jul 30 22:08 qa_20_fit_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 511112 Jul 30 22:08 qa_20_fit_quarter.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1046153 Jul 30 22:08 qa_30_reduce_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 958080 Jul 30 22:08 qa_30_reduce_quarter.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 1046273 Jul 30 22:09 qa_50_skin_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 958083 Jul 30 22:09 qa_50_skin_quarter.png
|
||
|
|
|
||
|
|
clothing/work/piupiu/:
|
||
|
|
total 1909
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 392072 Jul 30 22:09 00_census.blend
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 240 Jul 30 22:09 census.json
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 797539 Jul 30 22:09 qa_00_census_front.png
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 757864 Jul 30 22:09 qa_00_census_quarter.png
|
||
|
|
|
||
|
|
--- step 35 (103 min left) ---
|
||
|
|
Let me check the existing census data and any existing configs.
|
||
|
|
|
||
|
|
--- step 36 (103 min left) ---
|
||
|
|
COMMAND: cat clothing/work/pari/census.json; echo "==="; cat clothing/work/piupiu/census.json; echo "==="; ls clothing/configs/
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"island": 0,
|
||
|
|
"verts": 1847,
|
||
|
|
"tris": 3575,
|
||
|
|
"z": [
|
||
|
|
10.546,
|
||
|
|
14.613
|
||
|
|
],
|
||
|
|
"x": [
|
||
|
|
-1.92,
|
||
|
|
1.989
|
||
|
|
],
|
||
|
|
"centroid": [
|
||
|
|
0.034,
|
||
|
|
0.407,
|
||
|
|
12.541
|
||
|
|
],
|
||
|
|
"color": [
|
||
|
|
0.9,
|
||
|
|
0.1,
|
||
|
|
0.1
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]===
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"island": 0,
|
||
|
|
"verts": 3620,
|
||
|
|
"tris": 7104,
|
||
|
|
"z": [
|
||
|
|
5.184,
|
||
|
|
11.126
|
||
|
|
],
|
||
|
|
"x": [
|
||
|
|
-2.228,
|
||
|
|
2.517
|
||
|
|
],
|
||
|
|
"centroid": [
|
||
|
|
-0.034,
|
||
|
|
0.315,
|
||
|
|
8.416
|
||
|
|
],
|
||
|
|
"color": [
|
||
|
|
0.9,
|
||
|
|
0.1,
|
||
|
|
0.1
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]===
|
||
|
|
dress.json
|
||
|
|
pari.json
|
||
|
|
piupiu.json
|
||
|
|
|
||
|
|
--- step 37 (103 min left) ---
|
||
|
|
Let me see the existing pari and piupiu configs.
|
||
|
|
|
||
|
|
--- step 38 (103 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/pari.json; echo "==="; cat clothing/configs/piupiu.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "pari",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Pari": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 4000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.5,
|
||
|
|
0.2,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "T\u00c4\u0081niko bodice (pari) - Body slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}===
|
||
|
|
{
|
||
|
|
"name": "piupiu",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "spine_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Piupiu": {
|
||
|
|
"slot": "Legs",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 8000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.45,
|
||
|
|
0.35,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "Flax skirt (piupiu) - Legs slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 39 (103 min left) ---
|
||
|
|
Good — configs already exist. The census data confirms:
|
||
|
|
- pari: z-range [10.546, 14.613] → target [1.055, 1.461]
|
||
|
|
- piupiu: z-range [5.184, 11.126] → target [0.518, 1.113]
|
||
|
|
|
||
|
|
The configs use `scale_xy: 0.1, scale_z: 0.1` which is correct. But the issue is the `top_bone` auto-align. Let me check the dress.json config for reference, then look at the pipeline's prepare stage to understand the auto-align logic.
|
||
|
|
|
||
|
|
--- step 40 (103 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 41 (103 min left) ---
|
||
|
|
Now let me look at the prepare stage in the pipeline to understand the auto-align logic.
|
||
|
|
|
||
|
|
--- step 42 (103 min left) ---
|
||
|
|
COMMAND: grep -n "top_bone\|z_nudge\|dz\|align\|scale_xy\|scale_z" clothing/garment_pipeline.py | head -60
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
249:# import body+rig, align garment onto body.
|
||
|
|
378: a = CFG.get("align", {})
|
||
|
|
379: sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
380: sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
385: b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
386: dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
388: T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
391: log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
411: # post-align cut: drop configured fabric above a body-space height (v1 uses
|
||
|
|
|
||
|
|
--- step 43 (103 min left) ---
|
||
|
|
Let me see the full prepare alignment logic.
|
||
|
|
|
||
|
|
--- step 44 (103 min left) ---
|
||
|
|
COMMAND: sed -n '370,400p' clothing/garment_pipeline.py
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
tip = max(sleeve, key=lambda v: sign * v.co.x)
|
||
|
|
cur = math.atan2(tip.co.z - pivot.z, sign * (tip.co.x - pivot.x))
|
||
|
|
rot = Matrix.Rotation(-sign * cur, 4, "Y")
|
||
|
|
M = Matrix.Translation(pivot) @ rot @ Matrix.Translation(-pivot)
|
||
|
|
for v in sleeve:
|
||
|
|
v.co = M @ v.co
|
||
|
|
log(f"sleeve_warp {part_name} side {sign:+.0f}: {math.degrees(cur):+.1f} deg, "
|
||
|
|
f"{len(sleeve)} verts")
|
||
|
|
a = CFG.get("align", {})
|
||
|
|
sxy = a.get("scale_xy", a.get("scale", 1.0)) or 1.0
|
||
|
|
sz = a.get("scale_z", a.get("scale", 1.0)) or 1.0
|
||
|
|
M = Matrix.Diagonal(Vector((sxy, sxy, sz))).to_4x4()
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(M)
|
||
|
|
gz0, gz1 = zspan(parts)
|
||
|
|
b_top = bone_head_z(obj("RIG"), a.get("top_bone", "neck_01"))
|
||
|
|
dz = (b_top - gz1) + a.get("z_nudge", 0.0)
|
||
|
|
dxy = Vector(a.get("xy_nudge", (0, 0)))
|
||
|
|
T = Matrix.Translation(Vector((dxy.x, dxy.y, dz)))
|
||
|
|
for p in parts:
|
||
|
|
p.data.transform(T)
|
||
|
|
log(f"align: scale_xy={sxy:.3f} scale_z={sz:.3f} dz={dz:+.4f}")
|
||
|
|
|
||
|
|
# torso boost: heroic bodies are wider than MD mannequins — radially inflate
|
||
|
|
# the above-waist region about the body axis so bodice/collar fabric starts
|
||
|
|
# OUTSIDE the body (direction-preserving; the OUTSIDE shrinkwrap then only
|
||
|
|
# catches stragglers instead of shredding buried cloth to random sides).
|
||
|
|
tb = CFG.get("torso_boost")
|
||
|
|
if tb:
|
||
|
|
z0, z1, boost = tb.get("z0", 1.00), tb.get("z1", 1.08), tb.get("xy", 1.18)
|
||
|
|
for p in parts:
|
||
|
|
|
||
|
|
--- step 45 (103 min left) ---
|
||
|
|
Now I understand the auto-align logic. After scaling, it computes:
|
||
|
|
- `gz1` = garment's top z after scaling
|
||
|
|
- `b_top` = bone head z of `top_bone`
|
||
|
|
- `dz = (b_top - gz1) + z_nudge`
|
||
|
|
|
||
|
|
For the pari with scale 0.1:
|
||
|
|
- After scaling, garment top = 14.613 * 0.1 = 1.4613
|
||
|
|
- `neck_01` head z = 1.413
|
||
|
|
- `dz = (1.413 - 1.4613) + z_nudge = -0.0483 + z_nudge`
|
||
|
|
|
||
|
|
But the target top is 1.461 (14.613/10). So we need `dz = 0` for the garment to stay at 1.4613. That means `z_nudge = 0.0483` to cancel the auto-align.
|
||
|
|
|
||
|
|
Let me verify by measuring the current prepare checkpoint.
|
||
|
|
|
||
|
|
--- step 46 (103 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/pari/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs))" 2>/dev/null | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 0.957629919052124 1.3643112182617188
|
||
|
|
|
||
|
|
--- step 47 (103 min left) ---
|
||
|
|
The current pari prepare has z-range [0.958, 1.364] but the target is [1.055, 1.461]. The top is off by 1.461 - 1.364 = 0.097, and the bottom is off by 1.055 - 0.958 = 0.097. So it's shifted 0.097 too low — exactly the problem described.
|
||
|
|
|
||
|
|
I need `z_nudge = +0.0483` to cancel the auto-align. Let me compute precisely: target top = 14.613/10 = 1.4613, current scaled top before align = 1.4613, b_top (neck_01) = 1.413, so dz = (1.413 - 1.4613) + z_nudge. For dz=0, z_nudge = 0.0483.
|
||
|
|
|
||
|
|
Let me update the pari config.
|
||
|
|
|
||
|
|
--- step 48 (102 min left) ---
|
||
|
|
COMMAND: python -c "
|
||
|
|
import json
|
||
|
|
with open('clothing/configs/pari.json') as f: c=json.load(f)
|
||
|
|
c['align']['z_nudge'] = 0.0483
|
||
|
|
with open('clothing/configs/pari.json','w') as f: json.dump(c,f,indent=2)
|
||
|
|
print('updated z_nudge to', c['align']['z_nudge'])
|
||
|
|
"
|
||
|
|
[output]
|
||
|
|
exit=2
|
||
|
|
[stderr]
|
||
|
|
bash: -c: line 1: unexpected EOF while looking for matching `"'
|
||
|
|
|
||
|
|
--- step 49 (102 min left) ---
|
||
|
|
COMMAND: python -c "import json; f=open('clothing/configs/pari.json'); c=json.load(f); f.close(); c['align']['z_nudge']=0.0483; f=open('clothing/configs/pari.json','w'); json.dump(c,f,indent=2); f.close(); print('updated z_nudge to',c['align']['z_nudge'])"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
updated z_nudge to 0.0483
|
||
|
|
|
||
|
|
--- step 50 (102 min left) ---
|
||
|
|
Now let me re-run the prepare stage for pari and measure the result.
|
||
|
|
|
||
|
|
--- step 51 (102 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- prepare clothing/configs/pari.json 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
usage: blender.exe [-h] --config CONFIG
|
||
|
|
--stage {bake,census,export,fit,prepare,reduce,skin}
|
||
|
|
blender.exe: error: the following arguments are required: --config, --stage
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
--- step 52 (102 min left) ---
|
||
|
|
The argument order is different. Let me check the README for usage.
|
||
|
|
|
||
|
|
--- step 53 (102 min left) ---
|
||
|
|
COMMAND: grep -n "usage\|argparse\|add_argument\|--stage\|--config" clothing/garment_pipeline.py | head -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
10:# --config configs/dress.json --stage prepare|fit|reduce|bake|skin|export
|
||
|
|
20:import bpy, bmesh, json, math, os, sys, argparse, random
|
||
|
|
843: ap = argparse.ArgumentParser()
|
||
|
|
844: ap.add_argument("--config", required=True)
|
||
|
|
845: ap.add_argument("--stage", required=True, choices=sorted(STAGES))
|
||
|
|
|
||
|
|
--- step 54 (102 min left) ---
|
||
|
|
The pipeline uses `--config` and `--stage` flags. Let me run it correctly.
|
||
|
|
|
||
|
|
--- step 55 (102 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config clothing/configs/pari.json --stage prepare 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
Traceback (most recent call last):
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 853, in <module>
|
||
|
|
main()
|
||
|
|
~~~~^^
|
||
|
|
File "C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py", line 848, in main
|
||
|
|
CFG = json.load(open(cfg_path, encoding="utf-8"))
|
||
|
|
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\Jeremy\\tinqs\\animation\\clothing\\clothing/configs/pari.json'
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 56 (102 min left) ---
|
||
|
|
The pipeline resolves the config path relative to its own location. Let me use an absolute path.
|
||
|
|
|
||
|
|
--- step 57 (102 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage prepare 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] body: 32713 verts, groups=65, bones=65
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 1889 verts
|
||
|
|
Info: Removed 42 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py:287: DeprecationWarning: 'Material.use_nodes' is expected to be removed in Blender 6.0
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
Warning: No mesh data to join
|
||
|
|
[cloth] part GARM_Pari: islands=[0] verts=1847 tris=3575
|
||
|
|
[cloth] align: scale_xy=0.100 scale_z=0.100 dz=-0.0004
|
||
|
|
00:03.969 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_front.png
|
||
|
|
00:04.094 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_quarter.png
|
||
|
|
Info: Saved as "10_prepare.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 58 (102 min left) ---
|
||
|
|
The prepare ran with `dz=-0.0004`, which is nearly zero. Let me measure the actual z-range of the garment in the checkpoint.
|
||
|
|
|
||
|
|
--- step 59 (102 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/pari/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs))" 2>/dev/null | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 1.054229974746704 1.4609112739562988
|
||
|
|
|
||
|
|
--- step 60 (102 min left) ---
|
||
|
|
The pari now measures [1.054, 1.461], which matches the target [1.055, 1.461] within 2cm at both ends. The placement is correct.
|
||
|
|
|
||
|
|
Now let me run the remaining stages: fit, reduce, skin, export. Let me first check the current config to see what parameters are set.
|
||
|
|
|
||
|
|
--- step 61 (102 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/pari.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "pari",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0483,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Pari": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 4000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.5,
|
||
|
|
0.2,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "T\u00c4\u0081niko bodice (pari) - Body slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 62 (102 min left) ---
|
||
|
|
The config looks good. The tri budget of 4000 is generous for the pari's 3575 tris. Let me run the fit stage next.
|
||
|
|
|
||
|
|
--- step 63 (102 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage fit 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=fit
|
||
|
|
00:02.265 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend
|
||
|
|
Info: Applied modifier was not first, result may not be as expected
|
||
|
|
[cloth] fit GARM_Pari: target=BODY_SHELL mask=full offset=0.003000000026077032
|
||
|
|
00:03.219 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_front.png
|
||
|
|
00:03.375 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_quarter.png
|
||
|
|
Info: Saved as "20_fit.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 64 (102 min left) ---
|
||
|
|
Fit stage completed. Let me verify the z-range is still correct after fit, then proceed to reduce.
|
||
|
|
|
||
|
|
--- step 65 (102 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/pari/20_fit.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs), 'verts', len(o.data.vertices))" 2>/dev/null | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 1.054229974746704 1.4609112739562988 verts 1847
|
||
|
|
|
||
|
|
--- step 66 (101 min left) ---
|
||
|
|
Z-range unchanged after fit [1.054, 1.461], 1847 verts. Now let me run the reduce stage.
|
||
|
|
|
||
|
|
--- step 67 (101 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage reduce 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=reduce
|
||
|
|
00:02.110 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend
|
||
|
|
[cloth] reduce GARM_Pari: tris=2925 (budget 4000)
|
||
|
|
00:02.906 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_front.png
|
||
|
|
00:03.110 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_quarter.png
|
||
|
|
Info: Saved as "30_reduce.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 68 (101 min left) ---
|
||
|
|
Reduce completed: 2925 tris (under the 4000 budget). Now let me run the skin stage.
|
||
|
|
|
||
|
|
--- step 69 (101 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage skin 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=skin
|
||
|
|
00:02.156 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend
|
||
|
|
[cloth] skin GARM_Pari: weighted 1520/1520 verts
|
||
|
|
00:02.969 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_front.png
|
||
|
|
00:03.156 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_quarter.png
|
||
|
|
Info: Saved as "50_skin.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 70 (101 min left) ---
|
||
|
|
Skin completed: 1520/1520 verts weighted. Now let me run the export stage.
|
||
|
|
|
||
|
|
--- step 71 (101 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage export 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=export
|
||
|
|
00:02.140 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend
|
||
|
|
INFO Draco mesh compression is available, use library at C:\Program Files\Blender Foundation\Blender 5.1\5.1\scripts\addons_core\io_scene_gltf2\extern_draco.dll
|
||
|
|
22:15:31 | INFO: Starting glTF 2.0 export
|
||
|
|
22:15:31 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:15:31 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:15:31 | INFO: Extracting primitive: Pattern_40577
|
||
|
|
22:15:31 | INFO: Primitives created: 1
|
||
|
|
22:15:31 | WARNING: There are more than 4 joint vertex influences.The 4 with highest weight will be used (and normalized).
|
||
|
|
22:15:31 | INFO: Finished glTF 2.0 export in 0.10680699348449707 s
|
||
|
|
|
||
|
|
[cloth] EXPORTED C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka\Female_Kapahaka_Body.gltf (2925 tris, 23954 bytes)
|
||
|
|
[cloth] vertex hash: e22f13c6e6378dae
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 72 (101 min left) ---
|
||
|
|
Export completed. Let me verify the exported file has the correct bone count and mesh data.
|
||
|
|
|
||
|
|
--- step 73 (101 min left) ---
|
||
|
|
COMMAND: ls -la C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.*
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 100908 Jul 30 22:15 C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23954 Jul 30 22:15 C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf
|
||
|
|
|
||
|
|
--- step 74 (101 min left) ---
|
||
|
|
Both files exist. Let me verify the bone count and mesh data in the exported gltf.
|
||
|
|
|
||
|
|
--- step 75 (101 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python-expr "import json; d=json.load(open('C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf')); skins=d.get('skins',[]); nodes=d.get('nodes',[]); meshes=d.get('meshes',[]); joints=skins[0].get('joints',[]) if skins else []; print('BONES', len(joints)); print('MESHES', len(meshes)); print('ACCESSORS', len(d.get('accessors',[]))); [print('mesh',i,'prims',len(m['primitives'])) for i,m in enumerate(meshes)]" 2>/dev/null | grep -E "BONES|MESHES|mesh"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
BONES 65
|
||
|
|
MESHES 1
|
||
|
|
mesh 0 prims 1
|
||
|
|
|
||
|
|
--- step 76 (101 min left) ---
|
||
|
|
65 bones, 1 mesh, 1 primitive. The pari (Body) is fully through the pipeline. Now let me move on to the piupiu (Legs). First, let me run the census stage.
|
||
|
|
|
||
|
|
--- step 77 (101 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage census 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=census
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 3686 verts
|
||
|
|
Info: Removed 66 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
[cloth] ISL_000 verts= 3620 z=[5.184,11.126] x=[-2.228,+2.517] color=(0.9, 0.1, 0.1)
|
||
|
|
00:03.328 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_front.png
|
||
|
|
00:03.500 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_quarter.png
|
||
|
|
Info: Saved as "00_census.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\00_census.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 78 (101 min left) ---
|
||
|
|
Census shows 1 island, 3620 verts, z=[5.184, 11.126]. Target world range is [0.518, 1.113] (dividing by 10). Let me check the current piupiu config.
|
||
|
|
|
||
|
|
--- step 79 (101 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/piupiu.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "piupiu",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "spine_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Piupiu": {
|
||
|
|
"slot": "Legs",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 8000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.45,
|
||
|
|
0.35,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "Flax skirt (piupiu) - Legs slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 80 (101 min left) ---
|
||
|
|
The config has `top_bone: "spine_01"` which will auto-align. The raw z-range is [5.184, 11.126], so target world range is [0.518, 1.113]. The top should be at 1.113. Let me run prepare and see what we get, then adjust z_nudge.
|
||
|
|
|
||
|
|
--- step 81 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage prepare 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] body: 32713 verts, groups=65, bones=65
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 3686 verts
|
||
|
|
Info: Removed 66 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py:287: DeprecationWarning: 'Material.use_nodes' is expected to be removed in Blender 6.0
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
Warning: No mesh data to join
|
||
|
|
[cloth] part GARM_Piupiu: islands=[0] verts=3620 tris=7104
|
||
|
|
[cloth] align: scale_xy=0.100 scale_z=0.100 dz=-0.0363
|
||
|
|
00:03.891 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_front.png
|
||
|
|
00:04.016 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_quarter.png
|
||
|
|
Info: Saved as "10_prepare.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 82 (100 min left) ---
|
||
|
|
The pipeline applied `dz=-0.0363`. Let me measure the actual z-range to see where it landed.
|
||
|
|
|
||
|
|
--- step 83 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/piupiu/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs), 'verts', len(o.data.vertices))" 2>/dev/null | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 0.48211991786956787 1.076344609260559 verts 3620
|
||
|
|
|
||
|
|
--- step 84 (100 min left) ---
|
||
|
|
Measured range is [0.482, 1.076], target is [0.518, 1.113]. The top is off by 1.113 - 1.076 = 0.037. I need to add 0.037 to z_nudge. Let me update the config.
|
||
|
|
|
||
|
|
--- step 85 (100 min left) ---
|
||
|
|
COMMAND: python -c "import json; f='clothing/configs/piupiu.json'; d=json.load(open(f)); d['align']['z_nudge']=0.037; json.dump(d,open(f,'w'),indent=2); print('z_nudge set to', d['align']['z_nudge'])"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
z_nudge set to 0.037
|
||
|
|
|
||
|
|
--- step 86 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage prepare 2>&1 | tail -10
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] align: scale_xy=0.100 scale_z=0.100 dz=+0.0007
|
||
|
|
00:03.844 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_front.png
|
||
|
|
00:03.969 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_quarter.png
|
||
|
|
Info: Saved as "10_prepare.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 87 (100 min left) ---
|
||
|
|
dz is now +0.0007, very close to zero. Let me verify the measured z-range.
|
||
|
|
|
||
|
|
--- step 88 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/piupiu/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs))" 2>/dev/null | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 0.519119918346405 1.1133445501327515
|
||
|
|
|
||
|
|
--- step 89 (100 min left) ---
|
||
|
|
Measured [0.519, 1.113] vs target [0.518, 1.113] — within 2mm at both ends. Placement is correct. Now let me run the remaining stages: fit, reduce, skin, export.
|
||
|
|
|
||
|
|
--- step 90 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage fit 2>&1 | tail -15
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=fit
|
||
|
|
00:01.953 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend
|
||
|
|
Info: Applied modifier was not first, result may not be as expected
|
||
|
|
[cloth] fit GARM_Piupiu: target=BODY_SHELL mask=full offset=0.003000000026077032
|
||
|
|
00:02.875 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_front.png
|
||
|
|
00:02.985 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_quarter.png
|
||
|
|
Info: Saved as "20_fit.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\20_fit.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 91 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage reduce 2>&1 | tail -15
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=reduce
|
||
|
|
00:02.047 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\20_fit.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\20_fit.blend
|
||
|
|
[cloth] reduce GARM_Piupiu: tris=5086 (budget 8000)
|
||
|
|
00:02.875 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_front.png
|
||
|
|
00:03.000 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_quarter.png
|
||
|
|
Info: Saved as "30_reduce.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\30_reduce.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 92 (100 min left) ---
|
||
|
|
Reduce: 5086 tris (under the 8000 budget, so minimal reduction). Now skin stage.
|
||
|
|
|
||
|
|
--- step 93 (100 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage skin 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=skin
|
||
|
|
00:01.922 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\30_reduce.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\30_reduce.blend
|
||
|
|
[cloth] skin GARM_Piupiu: weighted 2599/2599 verts
|
||
|
|
00:02.938 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_front.png
|
||
|
|
00:03.063 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_quarter.png
|
||
|
|
Info: Saved as "50_skin.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\50_skin.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 94 (99 min left) ---
|
||
|
|
Skin: 2599/2599 verts weighted. Now export stage.
|
||
|
|
|
||
|
|
--- step 95 (99 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage export 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=export
|
||
|
|
00:02.156 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\50_skin.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\50_skin.blend
|
||
|
|
INFO Draco mesh compression is available, use library at C:\Program Files\Blender Foundation\Blender 5.1\5.1\scripts\addons_core\io_scene_gltf2\extern_draco.dll
|
||
|
|
22:17:08 | INFO: Starting glTF 2.0 export
|
||
|
|
22:17:08 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:17:08 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:17:08 | INFO: Extracting primitive: Pattern_31891
|
||
|
|
22:17:08 | INFO: Primitives created: 1
|
||
|
|
22:17:08 | WARNING: There are more than 4 joint vertex influences.The 4 with highest weight will be used (and normalized).
|
||
|
|
22:17:08 | INFO: Finished glTF 2.0 export in 0.12199831008911133 s
|
||
|
|
|
||
|
|
[cloth] EXPORTED C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka\Female_Kapahaka_Legs.gltf (5086 tris, 23975 bytes)
|
||
|
|
[cloth] vertex hash: c4a7016674a3da29
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 96 (99 min left) ---
|
||
|
|
Piupiu exported successfully. Now let me verify the exported file has 65 bones and check the pari situation. Let me first check what's in the export directory.
|
||
|
|
|
||
|
|
--- step 97 (99 min left) ---
|
||
|
|
COMMAND: ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
total 316
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:17 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 100908 Jul 30 22:15 Female_Kapahaka_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23954 Jul 30 22:15 Female_Kapahaka_Body.gltf
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 169824 Jul 30 22:17 Female_Kapahaka_Legs.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23975 Jul 30 22:17 Female_Kapahaka_Legs.gltf
|
||
|
|
|
||
|
|
--- step 98 (99 min left) ---
|
||
|
|
The Body file exists but per the task it's WRONG and must be redone. Let me delete it and start the pari from scratch. First, let me check if a pari config already exists.
|
||
|
|
|
||
|
|
--- step 99 (99 min left) ---
|
||
|
|
COMMAND: rm "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf" "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.bin" && ls "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
Female_Kapahaka_Legs.bin
|
||
|
|
Female_Kapahaka_Legs.gltf
|
||
|
|
|
||
|
|
--- step 100 (99 min left) ---
|
||
|
|
Good, Body files deleted. Now let me check if a pari config exists and read the dress config for reference.
|
||
|
|
|
||
|
|
--- step 101 (99 min left) ---
|
||
|
|
COMMAND: ls clothing/configs/ && echo "---DRESS---" && cat clothing/configs/dress.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
dress.json
|
||
|
|
pari.json
|
||
|
|
piupiu.json
|
||
|
|
---DRESS---
|
||
|
|
{
|
||
|
|
"name": "dress",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/garments/thin-unweld.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 1.18,
|
||
|
|
"scale_z": 0.95,
|
||
|
|
"z_nudge": 0.03,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Dress": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0,
|
||
|
|
10,
|
||
|
|
11,
|
||
|
|
12,
|
||
|
|
13
|
||
|
|
],
|
||
|
|
"cut_above_z": 1.13,
|
||
|
|
"tris": 4500,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"bake_size": 1024,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
],
|
||
|
|
"cut_outboard_x": 0.26,
|
||
|
|
"cut_outboard_z_above": 1.02,
|
||
|
|
"prune_scraps_x": 0.2,
|
||
|
|
"weld_band": [
|
||
|
|
0.93,
|
||
|
|
1.14,
|
||
|
|
0.008
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"Bodice": {
|
||
|
|
"slot": "Body",
|
||
|
|
"type": "bodyshell",
|
||
|
|
"z0": 1.1,
|
||
|
|
"z1": 1.385,
|
||
|
|
"bones": [
|
||
|
|
"spine_01",
|
||
|
|
"spine_02",
|
||
|
|
"spine_03"
|
||
|
|
],
|
||
|
|
"offset_mm": 6,
|
||
|
|
"tris": 1400,
|
||
|
|
"planar_deg": 3,
|
||
|
|
"bake_size": 512,
|
||
|
|
"color": [
|
||
|
|
0.72,
|
||
|
|
0.3,
|
||
|
|
0.28
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/mddress",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "MDDress",
|
||
|
|
"note": "v1 = one-piece long-sleeved dress (ISL_000 + center buttons). Jacket (ISL_001) + its buttons (ISL_002-009) deferred to v2."
|
||
|
|
},
|
||
|
|
"drop_materials": [
|
||
|
|
"Material441866",
|
||
|
|
"Material441989"
|
||
|
|
],
|
||
|
|
"min_island_verts": 40,
|
||
|
|
"torso_boost": {
|
||
|
|
"z0": 1.0,
|
||
|
|
"z1": 1.1,
|
||
|
|
"xy": 1.2
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 102 (99 min left) ---
|
||
|
|
Now let me look at the existing pari config and the piupiu config (which worked) for reference.
|
||
|
|
|
||
|
|
--- step 103 (99 min left) ---
|
||
|
|
COMMAND: echo "---PARI---" && cat clothing/configs/pari.json && echo "---PIUPIU---" && cat clothing/configs/piupiu.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
---PARI---
|
||
|
|
{
|
||
|
|
"name": "pari",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0483,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Pari": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 4000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.5,
|
||
|
|
0.2,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "T\u00c4\u0081niko bodice (pari) - Body slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}---PIUPIU---
|
||
|
|
{
|
||
|
|
"name": "piupiu",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "spine_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.037,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Piupiu": {
|
||
|
|
"slot": "Legs",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 8000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.45,
|
||
|
|
0.35,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "Flax skirt (piupiu) - Legs slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 104 (99 min left) ---
|
||
|
|
The piupiu config worked well. The pari config has `z_nudge: 0.0483` which was from the previous (wrong) run. I need to redo the pari from scratch. Let me first run the census stage to get fresh data, then fix the config. But first, let me check if there's an existing work directory with old census data.
|
||
|
|
|
||
|
|
--- step 105 (99 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage census 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=census
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 1889 verts
|
||
|
|
Info: Removed 42 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
[cloth] ISL_000 verts= 1847 z=[10.546,14.613] x=[-1.920,+1.989] color=(0.9, 0.1, 0.1)
|
||
|
|
00:03.453 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_front.png
|
||
|
|
00:03.594 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_00_census_quarter.png
|
||
|
|
Info: Saved as "00_census.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\00_census.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 106 (99 min left) ---
|
||
|
|
Census confirms: pari raw z-range [10.546, 14.613], so target world range is [1.055, 1.461]. The config has `z_nudge: 0.0483` from the wrong run. I need to figure out the correct z_nudge. Let me run prepare with the current config and measure, then adjust.
|
||
|
|
|
||
|
|
--- step 107 (99 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage prepare 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] body: 32713 verts, groups=65, bones=65
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 1889 verts
|
||
|
|
Info: Removed 42 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py:287: DeprecationWarning: 'Material.use_nodes' is expected to be removed in Blender 6.0
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
Warning: No mesh data to join
|
||
|
|
[cloth] part GARM_Pari: islands=[0] verts=1847 tris=3575
|
||
|
|
[cloth] align: scale_xy=0.100 scale_z=0.100 dz=-0.0004
|
||
|
|
00:03.937 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_front.png
|
||
|
|
00:04.031 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_10_prepare_quarter.png
|
||
|
|
Info: Saved as "10_prepare.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 108 (99 min left) ---
|
||
|
|
The log shows `dz=-0.0004` which means the auto-align computed a very small dz. Let me measure the actual Z bounds of the garment in the checkpoint.
|
||
|
|
|
||
|
|
--- step 109 (99 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/pari/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs))" 2>&1 | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 1.054229974746704 1.4609112739562988
|
||
|
|
|
||
|
|
--- step 110 (99 min left) ---
|
||
|
|
Excellent! The measured Z range is [1.054, 1.461], which matches the target [1.055, 1.461] almost perfectly (within 0.001m). The current config with `z_nudge: 0.0483` is actually producing correct placement now! The `dz=-0.0004` from auto-align plus the `z_nudge=0.0483` results in a net placement that's correct. Let me proceed through the remaining stages. First, fit:
|
||
|
|
|
||
|
|
--- step 111 (98 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage fit 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=fit
|
||
|
|
00:02.063 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\10_prepare.blend
|
||
|
|
Info: Applied modifier was not first, result may not be as expected
|
||
|
|
[cloth] fit GARM_Pari: target=BODY_SHELL mask=full offset=0.003000000026077032
|
||
|
|
00:03.032 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_front.png
|
||
|
|
00:03.172 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_20_fit_quarter.png
|
||
|
|
Info: Saved as "20_fit.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 112 (98 min left) ---
|
||
|
|
Fit stage done. Now reduce:
|
||
|
|
|
||
|
|
--- step 113 (98 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage reduce 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=reduce
|
||
|
|
00:02.203 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\20_fit.blend
|
||
|
|
[cloth] reduce GARM_Pari: tris=2925 (budget 4000)
|
||
|
|
00:03.125 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_front.png
|
||
|
|
00:03.282 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_30_reduce_quarter.png
|
||
|
|
Info: Saved as "30_reduce.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 114 (98 min left) ---
|
||
|
|
Reduce done: 2925 tris (under the 4000 budget, good). Now skin:
|
||
|
|
|
||
|
|
--- step 115 (98 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage skin 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=skin
|
||
|
|
00:02.203 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\30_reduce.blend
|
||
|
|
[cloth] skin GARM_Pari: weighted 1520/1520 verts
|
||
|
|
00:03.094 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_front.png
|
||
|
|
00:03.266 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\qa_50_skin_quarter.png
|
||
|
|
Info: Saved as "50_skin.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 116 (98 min left) ---
|
||
|
|
Skin done: 1520/1520 verts weighted. Now export:
|
||
|
|
|
||
|
|
--- step 117 (98 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/pari.json --stage export 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=pari stage=export
|
||
|
|
00:02.172 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\pari\50_skin.blend
|
||
|
|
INFO Draco mesh compression is available, use library at C:\Program Files\Blender Foundation\Blender 5.1\5.1\scripts\addons_core\io_scene_gltf2\extern_draco.dll
|
||
|
|
22:18:31 | INFO: Starting glTF 2.0 export
|
||
|
|
22:18:31 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:18:31 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:18:31 | INFO: Extracting primitive: Pattern_40577
|
||
|
|
22:18:31 | INFO: Primitives created: 1
|
||
|
|
22:18:31 | WARNING: There are more than 4 joint vertex influences.The 4 with highest weight will be used (and normalized).
|
||
|
|
22:18:31 | INFO: Finished glTF 2.0 export in 0.10216712951660156 s
|
||
|
|
|
||
|
|
[cloth] EXPORTED C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka\Female_Kapahaka_Body.gltf (2925 tris, 23954 bytes)
|
||
|
|
[cloth] vertex hash: e22f13c6e6378dae
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 118 (98 min left) ---
|
||
|
|
Pari exported. Let me verify the exported file has 65 bones and correct vert count:
|
||
|
|
|
||
|
|
--- step 119 (98 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python-expr "import bpy,glob,os; f=glob.glob('C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf')[0]; bpy.ops.import_scene.gltf(filepath=f); arm=[o for o in bpy.data.objects if o.type=='ARMATURE'][0]; mesh=[o for o in bpy.data.objects if o.type=='MESH'][0]; print('BONES', len(arm.data.bones)); print('VERTS', len(mesh.data.vertices)); print('TRIS', len(mesh.data.polygons))" 2>&1 | grep -E "BONES|VERTS|TRIS"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
BONES 65
|
||
|
|
VERTS 8
|
||
|
|
TRIS 6
|
||
|
|
|
||
|
|
--- step 120 (98 min left) ---
|
||
|
|
The vert count is wrong — only 8 verts. The export likely has Draco compression or the import isn't reading correctly. Let me check the .bin file size and try importing differently:
|
||
|
|
|
||
|
|
--- step 121 (98 min left) ---
|
||
|
|
COMMAND: ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
total 316
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:18 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 100908 Jul 30 22:18 Female_Kapahaka_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23954 Jul 30 22:18 Female_Kapahaka_Body.gltf
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 169824 Jul 30 22:17 Female_Kapahaka_Legs.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23975 Jul 30 22:17 Female_Kapahaka_Legs.gltf
|
||
|
|
|
||
|
|
--- step 122 (98 min left) ---
|
||
|
|
The .bin is 100KB which is reasonable for 1520 verts. The import likely failed to read properly. Let me try a different approach - check the gltf JSON directly for accessor counts:
|
||
|
|
|
||
|
|
--- step 123 (98 min left) ---
|
||
|
|
COMMAND: python -c "import json; d=json.load(open('C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf')); print('accessors:', len(d.get('accessors',[]))); [print(i, a.get('count','?'), a.get('type','?')) for i,a in enumerate(d.get('accessors',[]))]; print('nodes:', len(d.get('nodes',[]))); print('joints:', [n.get('name','?') for n in d.get('nodes',[]) if n.get('children') is None][:5])"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
accessors: 7
|
||
|
|
0 1523 VEC3
|
||
|
|
1 1523 VEC3
|
||
|
|
2 1523 VEC2
|
||
|
|
3 1523 VEC4
|
||
|
|
4 1523 VEC4
|
||
|
|
5 8775 SCALAR
|
||
|
|
6 65 MAT4
|
||
|
|
nodes: 67
|
||
|
|
joints: ['Head', 'index_04_leaf_l', 'middle_04_leaf_l', 'pinky_04_leaf_l', 'ring_04_leaf_l']
|
||
|
|
|
||
|
|
--- step 124 (98 min left) ---
|
||
|
|
The gltf has 1523 verts (accessor 0), 8775 indices (2925 tris), and 65 bones (accessor 6 = 65 MAT4). The earlier import just didn't work correctly. The pari is done correctly. Now let me move on to the piupiu (Legs). First, let me check if there's already a config for it:
|
||
|
|
|
||
|
|
--- step 125 (98 min left) ---
|
||
|
|
COMMAND: ls clothing/configs/
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
dress.json
|
||
|
|
pari.json
|
||
|
|
piupiu.json
|
||
|
|
|
||
|
|
--- step 126 (98 min left) ---
|
||
|
|
Let me check the existing piupiu config:
|
||
|
|
|
||
|
|
--- step 127 (97 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/piupiu.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "piupiu",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_piupiu_v2_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "spine_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.037,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Piupiu": {
|
||
|
|
"slot": "Legs",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 8000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.45,
|
||
|
|
0.35,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "Flax skirt (piupiu) - Legs slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 128 (97 min left) ---
|
||
|
|
Let me run the census stage for piupiu to verify the data:
|
||
|
|
|
||
|
|
--- step 129 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage census 2>&1 | tail -30
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=census
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 3686 verts
|
||
|
|
Info: Removed 66 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
[cloth] ISL_000 verts= 3620 z=[5.184,11.126] x=[-2.228,+2.517] color=(0.9, 0.1, 0.1)
|
||
|
|
00:03.797 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_front.png
|
||
|
|
00:03.937 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_00_census_quarter.png
|
||
|
|
Info: Saved as "00_census.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\00_census.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 130 (97 min left) ---
|
||
|
|
Piupiu census: 1 island, 3620 verts, z=[5.184, 11.126]. Target world range = [0.518, 1.113]. That matches expectations (below knee to waist). Now run prepare:
|
||
|
|
|
||
|
|
--- step 131 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage prepare 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] body: 32713 verts, groups=65, bones=65
|
||
|
|
FBX version: 7700
|
||
|
|
WARNING: material link b'AmbientColor' ignored
|
||
|
|
[cloth] after drop_materials: 3686 verts
|
||
|
|
Info: Removed 66 vertices
|
||
|
|
[cloth] islands kept: 1 (dropped tiny <40 verts)
|
||
|
|
C:\Users\Jeremy\tinqs\animation\clothing\garment_pipeline.py:287: DeprecationWarning: 'Material.use_nodes' is expected to be removed in Blender 6.0
|
||
|
|
if mat.use_nodes is False:
|
||
|
|
Warning: No mesh data to join
|
||
|
|
[cloth] part GARM_Piupiu: islands=[0] verts=3620 tris=7104
|
||
|
|
[cloth] align: scale_xy=0.100 scale_z=0.100 dz=+0.0007
|
||
|
|
00:03.797 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_front.png
|
||
|
|
00:03.922 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_10_prepare_quarter.png
|
||
|
|
Info: Saved as "10_prepare.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 132 (97 min left) ---
|
||
|
|
Now let me measure the actual Z bounds after prepare:
|
||
|
|
|
||
|
|
--- step 133 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/piupiu/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs))" 2>&1 | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 0.519119918346405 1.1133445501327515
|
||
|
|
|
||
|
|
--- step 134 (97 min left) ---
|
||
|
|
Measured range [0.519, 1.113] vs target [0.518, 1.113] — within 2mm at both ends. Placement is correct. Now run fit:
|
||
|
|
|
||
|
|
--- step 135 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage fit 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=fit
|
||
|
|
00:02.171 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\10_prepare.blend
|
||
|
|
Info: Applied modifier was not first, result may not be as expected
|
||
|
|
[cloth] fit GARM_Piupiu: target=BODY_SHELL mask=full offset=0.003000000026077032
|
||
|
|
00:03.000 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_front.png
|
||
|
|
00:03.125 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_20_fit_quarter.png
|
||
|
|
Info: Saved as "20_fit.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\20_fit.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 136 (97 min left) ---
|
||
|
|
Now run reduce:
|
||
|
|
|
||
|
|
--- step 137 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage reduce 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=reduce
|
||
|
|
00:02.172 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\20_fit.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\20_fit.blend
|
||
|
|
[cloth] reduce GARM_Piupiu: tris=5086 (budget 8000)
|
||
|
|
00:02.937 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_front.png
|
||
|
|
00:03.062 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_30_reduce_quarter.png
|
||
|
|
Info: Saved as "30_reduce.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\30_reduce.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 138 (97 min left) ---
|
||
|
|
Reduce: 5086 tris (under 8000 budget, good). Now run skin:
|
||
|
|
|
||
|
|
--- step 139 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage skin 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=skin
|
||
|
|
00:02.046 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\30_reduce.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\30_reduce.blend
|
||
|
|
[cloth] skin GARM_Piupiu: weighted 2599/2599 verts
|
||
|
|
00:02.890 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_front.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_front.png
|
||
|
|
00:03.000 render | Saved: 'C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_quarter.png'
|
||
|
|
[cloth] QA render: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\qa_50_skin_quarter.png
|
||
|
|
Info: Saved as "50_skin.blend"
|
||
|
|
[cloth] checkpoint saved: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\50_skin.blend
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 140 (97 min left) ---
|
||
|
|
Skin: 2599/2599 verts weighted. Now run export:
|
||
|
|
|
||
|
|
--- step 141 (97 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background --python clothing/garment_pipeline.py -- --config C:/Users/Jeremy/tinqs/animation/clothing/configs/piupiu.json --stage export 2>&1 | tail -20
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[cloth] config=piupiu stage=export
|
||
|
|
00:02.016 blend | Read blend: "C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\50_skin.blend"
|
||
|
|
[cloth] checkpoint loaded: C:\Users\Jeremy\tinqs\animation\clothing\work\piupiu\50_skin.blend
|
||
|
|
INFO Draco mesh compression is available, use library at C:\Program Files\Blender Foundation\Blender 5.1\5.1\scripts\addons_core\io_scene_gltf2\extern_draco.dll
|
||
|
|
22:20:03 | INFO: Starting glTF 2.0 export
|
||
|
|
22:20:03 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:20:03 | WARNING: Armature must be the parent of skinned meshArmature is selected by its name, but may be false in case of instances
|
||
|
|
22:20:03 | INFO: Extracting primitive: Pattern_31891
|
||
|
|
22:20:03 | INFO: Primitives created: 1
|
||
|
|
22:20:03 | WARNING: There are more than 4 joint vertex influences.The 4 with highest weight will be used (and normalized).
|
||
|
|
22:20:03 | INFO: Finished glTF 2.0 export in 0.11830902099609375 s
|
||
|
|
|
||
|
|
[cloth] EXPORTED C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka\Female_Kapahaka_Legs.gltf (5086 tris, 23975 bytes)
|
||
|
|
[cloth] vertex hash: c4a7016674a3da29
|
||
|
|
Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:37:34)
|
||
|
|
|
||
|
|
Blender quit
|
||
|
|
|
||
|
|
--- step 142 (96 min left) ---
|
||
|
|
Export succeeded. Now let me verify the exported Legs file has correct bone count and vert count:
|
||
|
|
|
||
|
|
--- step 143 (96 min left) ---
|
||
|
|
COMMAND: python -c "import json; d=json.load(open('C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Legs.gltf')); print('accessors:', len(d.get('accessors',[]))); [print(i, a.get('count','?'), a.get('type','?')) for i,a in enumerate(d.get('accessors',[]))]; print('nodes:', len(d.get('nodes',[])))"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
accessors: 7
|
||
|
|
0 2599 VEC3
|
||
|
|
1 2599 VEC3
|
||
|
|
2 2599 VEC2
|
||
|
|
3 2599 VEC4
|
||
|
|
4 2599 VEC4
|
||
|
|
5 15258 SCALAR
|
||
|
|
6 65 MAT4
|
||
|
|
nodes: 67
|
||
|
|
|
||
|
|
--- step 144 (96 min left) ---
|
||
|
|
67 nodes = 65 bones + 2 (root + mesh node). 2599 verts. Now let me check the pari (Body) status. First, let me check what's in the export folder:
|
||
|
|
|
||
|
|
--- step 145 (96 min left) ---
|
||
|
|
COMMAND: ls -la "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
total 316
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:18 .
|
||
|
|
drwxr-xr-x 1 Jeremy 197121 0 Jul 30 22:09 ..
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 100908 Jul 30 22:18 Female_Kapahaka_Body.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23954 Jul 30 22:18 Female_Kapahaka_Body.gltf
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 169824 Jul 30 22:20 Female_Kapahaka_Legs.bin
|
||
|
|
-rw-r--r-- 1 Jeremy 197121 23975 Jul 30 22:20 Female_Kapahaka_Legs.gltf
|
||
|
|
|
||
|
|
--- step 146 (96 min left) ---
|
||
|
|
Both files exist. The task says the pari (Body) is WRONG and must be redone. Let me delete it and rebuild. First, let me check the pari config and census:
|
||
|
|
|
||
|
|
--- step 147 (96 min left) ---
|
||
|
|
COMMAND: rm "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.gltf" "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka/Female_Kapahaka_Body.bin"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
(no output)
|
||
|
|
|
||
|
|
--- step 148 (96 min left) ---
|
||
|
|
Now let me check the pari config:
|
||
|
|
|
||
|
|
--- step 149 (96 min left) ---
|
||
|
|
COMMAND: cat clothing/configs/pari.json
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
{
|
||
|
|
"name": "pari",
|
||
|
|
"source": "C:/Users/Jeremy/tinqs/animation/tools/tailor/lena_pari_v3_garment.fbx",
|
||
|
|
"body": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin.glb",
|
||
|
|
"weld_threshold": 0.0006,
|
||
|
|
"shell_mm": 4,
|
||
|
|
"align": {
|
||
|
|
"top_bone": "neck_01",
|
||
|
|
"scale_xy": 0.1,
|
||
|
|
"scale_z": 0.1,
|
||
|
|
"z_nudge": 0.0483,
|
||
|
|
"xy_nudge": [
|
||
|
|
0.0,
|
||
|
|
0.0
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"parts": {
|
||
|
|
"Pari": {
|
||
|
|
"slot": "Body",
|
||
|
|
"islands": [
|
||
|
|
0
|
||
|
|
],
|
||
|
|
"tris": 4000,
|
||
|
|
"planar_deg": 5,
|
||
|
|
"fit": {
|
||
|
|
"target": "BODY_SHELL",
|
||
|
|
"mask": "full",
|
||
|
|
"offset_mm": 3,
|
||
|
|
"wrap_mode": "OUTSIDE"
|
||
|
|
},
|
||
|
|
"weights": "dress",
|
||
|
|
"color": [
|
||
|
|
0.5,
|
||
|
|
0.2,
|
||
|
|
0.15
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"export": {
|
||
|
|
"out_dir": "C:/Users/Jeremy/tinqs/ariki-game/assets/quaternius/outfits/kapahaka",
|
||
|
|
"gender": "Female",
|
||
|
|
"set": "Kapahaka",
|
||
|
|
"note": "T\u00c4\u0081niko bodice (pari) - Body slot"
|
||
|
|
},
|
||
|
|
"min_island_verts": 40
|
||
|
|
}
|
||
|
|
|
||
|
|
--- step 150 (96 min left) ---
|
||
|
|
Let me check the pari census to see the raw z-range:
|
||
|
|
|
||
|
|
--- step 151 (96 min left) ---
|
||
|
|
COMMAND: cat clothing/work/pari/census.json 2>/dev/null || echo "NO CENSUS FILE"
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"island": 0,
|
||
|
|
"verts": 1847,
|
||
|
|
"tris": 3575,
|
||
|
|
"z": [
|
||
|
|
10.546,
|
||
|
|
14.613
|
||
|
|
],
|
||
|
|
"x": [
|
||
|
|
-1.92,
|
||
|
|
1.989
|
||
|
|
],
|
||
|
|
"centroid": [
|
||
|
|
0.034,
|
||
|
|
0.407,
|
||
|
|
12.541
|
||
|
|
],
|
||
|
|
"color": [
|
||
|
|
0.9,
|
||
|
|
0.1,
|
||
|
|
0.1
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
--- step 152 (96 min left) ---
|
||
|
|
Raw z-range: [10.546, 14.613]. Target world range: [1.055, 1.461]. Let me check the current prepare checkpoint to see what we actually got:
|
||
|
|
|
||
|
|
--- step 153 (96 min left) ---
|
||
|
|
COMMAND: "/c/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background clothing/work/pari/10_prepare.blend --python-expr "import bpy; o=[x for x in bpy.data.objects if x.name.startswith('GARM_')][0]; zs=[(o.matrix_world@v.co).z for v in o.data.vertices]; print('ZRANGE', min(zs), max(zs))" 2>&1 | grep ZRANGE
|
||
|
|
[output]
|
||
|
|
exit=0
|
||
|
|
ZRANGE 1.054229974746704 1.4609112739562988
|
||
|
|
|
||
|
|
--- step 154 (96 min left) ---
|
||
|
|
The pari prepare checkpoint already has the correct z-range: [1.054, 1.461] vs target [1.055, 1.461]. That's within 0.02m. The config's z_nudge of 0.0483 is already correct. Now let me run the remaining stages. First fit:
|