feat: clothing lane, character sources, and DCC bridges

Bulk import of the working lanes that were living untracked on the PC.

Content:
- characters/  Lena/male body lanes, bakes, texture work, run logs
- clothing/    garment pipeline, configs, gates, contract docs
- garments/    MD-authored garment sources (.zprj/.zpac)
- UAL-Lib/     Universal Animation Library 2 source (.blend/.fbx/.glb)
- tools/       blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent
- docs/, plans/, dev/, .agents/plans/

Repo hygiene:
- .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the
  Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the
  ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out
  on purpose — ~250 are already tracked raw and converting them would
  rewrite every one without shrinking history.
- .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable
  from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies
  dropped by a `>/dev/null` redirect on Windows).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:55:43 -07:00
parent 3363209cac
commit 3ba86b2ea8
558 changed files with 68622 additions and 8 deletions
+304
View File
@@ -0,0 +1,304 @@
# FBX → Quaternius Pipeline — Formal Plan & Specification
**Doc id:** fbx-pipeline-plan-2026-07-21 · **Status:** APPROVED direction, pre-implementation
**Owner:** Jeremy · **Author:** Claude (5-agent research audit, 2026-07-21)
**Supersedes:** ad-hoc `tools/*` pipeline docs scattered across skills/READMEs.
---
## 1. Purpose & scope
After the FBX-vs-GLB study (2026-07-21) we adopted **Recommendation 1**: the game keeps
GLB clips on the shared Quaternius skeleton; the conversion pipeline gets consolidated.
**In scope**
- S1. Relocate the FBX→Quaternius pipeline into a single folder `pipeline/` at the
animation-repo root, runnable end-to-end on this Windows machine and on the Mac.
- S2. Accept *any* registered FBX source ("regular fbx"), not only iClone exports.
- S3. House the iClone integration in the pipeline, driven by an export script over the
TinqsBridge API (localhost 18800).
- S4. Define and enforce a standardized bone/rig contract for all incoming FBX.
- S5. A reviewed, regression-gated migration (baseline before/after parity).
**Out of scope (non-goals)**
- N1. No change to ariki-game runtime code, ClipCatalog, dance JSON, or shipped GLBs.
- N2. No skeleton change (CC-skeleton option was evaluated and rejected).
- N3. No FBX-in-Godot ingestion (Option 2 spike may be scheduled separately).
- N4. Video-mocap tools (`mocap_retarget.py`) and the reverse prop bridge
(`export_boat_prop.py`) stay in `tools/` unchanged.
**Asset flow decision:** all assets continue to run through **`exchange/`** (§6). The
pipeline reads and writes only `exchange/` stage folders; no new asset roots.
---
## 2. Definitions
| Term | Meaning |
|---|---|
| **Canonical skeleton** | The game's Quaternius 65-bone UE4-named rig (§4.1). Sole retarget target. |
| **Rig family** | A named source-skeleton convention with a registered bone map (§4.2). |
| **Take** | One FBX file containing one baked animation clip + skinned mesh. |
| **Pack / Clip** | GLB basename / animation name baked into the GLB; game ref is `pack/Clip`. |
| **Intake contract** | The validity rules every FBX must satisfy before retargeting (§4.3). |
| **Stage folder** | A subfolder of `exchange/` representing one pipeline stage (§6). |
| **Bridge** | TinqsBridge iClone plugin + `iclone_bridge.py` client (verified 2026-07-21). |
---
## 3. Requirements
### Functional
- **FR-1** One command converts a take: intake validation → retarget → ping-pong bake →
loop QC (both variants) → per-take report. Partial failures stop at the failing gate.
- **FR-2** Rig family is auto-detected; a take on an unregistered rig is rejected with
the complete list of unmapped bones and a nonzero exit code.
- **FR-3** Adding a rig family requires only a new bone-map file + registry entry
(no retargeter code change).
- **FR-4** iClone export is scriptable: `export_take.py` applies the standard export
block (§4.4) and lands the FBX + metadata sidecar in `exchange/incoming-fbx/`.
- **FR-5** All existing outputs remain reproducible: for each rig family, the unified
retargeter's output matches the legacy tool's output within tolerance (§9).
- **FR-6** The pipeline runs identically from Windows and macOS; Blender executable is
resolved via `$BLENDER` → per-OS default paths, never hardcoded in commands.
- **FR-7** The driver surfaces the next free `nd_##` id by parsing
`docs/dances/REGISTRY.md`, and refuses output names that collide with registry rows.
### Non-functional
- **NFR-1 Fail loud:** every gate exits nonzero on failure; no silent partial output.
A failed stage must not leave a half-written GLB in a stage folder (write to temp,
rename on success).
- **NFR-2 Provenance:** every produced GLB gets a sibling JSON recording source file
hash, rig family, tool versions (pipeline git rev, Blender version), settings, and
QC verdicts.
- **NFR-3 Determinism:** same input + same pipeline rev ⇒ byte-stable animation curves
(float tolerance per §9).
- **NFR-4 Compatibility window:** legacy tools remain runnable (moved to
`tools/legacy/`) until the Phase-2 regression matrix passes; deleted in a separate,
single-purpose commit.
- **NFR-5 3.8 constraint:** any code executed *inside* iClone stays Python 3.8.8
compatible. Pipeline-side code targets the repo's standard Python 3.
- **NFR-6 LFS hygiene:** no new large-binary patterns; `.fbx/.glb` already ride LFS.
---
## 4. Standards (the contract)
### 4.1 Canonical skeleton
Quaternius 65-bone rig, UE4-style names: `root, pelvis, spine_01..03, neck_01, Head`
(capital H); per side `clavicle_, upperarm_, lowerarm_, hand_, thigh_, calf_, foot_,
ball_, ball_leaf_`; fingers `thumb_01..03`, `index/middle/ring/pinky_01`, `*_04_leaf`.
- Pinned snapshot committed at `pipeline/retarget/rigs/target/quaternius_ue.gltf`
(mesh-stripped copy of ariki-game `Superhero_Male_FullBody.gltf`).
- `verify_target.py` compares pinned rig vs the local ariki-game checkout (bone names +
rest transforms, tolerance 1e-5) and is run automatically by the driver; drift ⇒ hard
fail with instructions to re-pin deliberately.
### 4.2 Rig-family registry — `pipeline/retarget/rigs/*.json`
One JSON per family:
```json
{
"family": "cc_base",
"version": 1,
"probe_bones": ["CC_Base_Hip", "CC_Base_L_Upperarm"],
"char_frame": ["CC_Base_Hip", "CC_Base_Head", "CC_Base_L_Upperarm"],
"bone_map": { "CC_Base_Hip": "pelvis", "...": "..." },
"notes": "iClone 8 / CC4 exports; twist bones intentionally unmapped (ride parent)"
}
```
Registered at launch:
| Family | Probe | Source | Seed map |
|---|---|---|---|
| `cc_base` | `CC_Base_Hip` + `CC_Base_L_Upperarm` | iClone/CC exports (primary) | `cc_retarget.py` BONE_MAP |
| `bstyle` | `B-hips` + `B-upperArm.L` | Kevin Iglesias packs (`HumanM@*`, `BasicMotions@*`) | `kevin_retarget.py` BONE_MAP |
| `mixamo` | `mixamorig` prefix (strip-variants handled) | Mixamo | `mixamo_retarget.py` BONE_MAP |
| `quaternius_ue` | `pelvis` + `spine_01` | Quaternius vendor FBX (UAL) | identity pass-through |
Detection order: most-specific probe first; ambiguous or no match ⇒ reject (FR-2).
Bone-map edits bump `version`; provenance sidecars record it (NFR-2).
### 4.3 Intake contract (validated by `validate_fbx.py`, pre-Blender)
| # | Rule | Check | On violation |
|---|---|---|---|
| IC-1 | Registered rig family | probe-bone scan of FBX node names | REJECT, list unmapped bones |
| IC-2 | Baked motion present, >5 frames | anim-stack scan (`TempMotion` guard retained) | REJECT: "exported WITHOUT motion" |
| IC-3 | T-pose bind preferred | `cc_base`: sidecar says `t_pose: true`, else WARN "A-pose — arm QA required" | WARN (blocks nothing; flags QA) |
| IC-4 | In-place motion | not statically checkable; enforced downstream by loop-QC root-drift metric | n/a (QC gate) |
| IC-5 | One take per file | anim-stack count == 1 | REJECT |
| IC-6 | Mesh + skin weights present | at least one skinned mesh node | REJECT (bind pose needs weighted mesh) |
| IC-7 | Textures absent or ≤512px | embedded-texture scan | WARN (size hygiene) |
| IC-8 | Metadata sidecar present for iClone takes | `<take>.json` next to FBX | WARN |
| IC-9 | Output name valid + free | registry parse (FR-7) | REJECT on collision |
### 4.4 iClone standard export block (enforced by `export_take.py`)
Target Tool Preset **Blender** · Export Range **All** · FPS **60** ·
**Preserve Bone Names (CC Base)** ON · **Use T-Pose As Bind Pose** ON ·
**Reset Bone Scale** ON · Embed Textures OFF/≤512 ·
precondition: `Animation > Flatten All Motion with Constraint`.
The script writes the settings actually used into the metadata sidecar (IC-8), so QA
can distinguish "contract says" from "export did".
---
## 5. Target layout
```
pipeline/
README.md # operator doc; contract §4 + runbook. Source of truth.
convert.py # FR-1 driver
validate_fbx.py # IC-1..IC-9 gate (no Blender; raw FBX scan; seconds)
blender_env.py # FR-6 Blender resolver ($BLENDER → per-OS defaults)
retarget/
retarget.py # unified retargeter (replaces cc/kevin/mixamo_retarget.py)
rigs/ # §4.2 registry
cc_base.json bstyle.json mixamo.json quaternius_ue.json
target/quaternius_ue.gltf target/verify_target.py
qc/loop_qc.py # moved unchanged (then FBX-input tolerant later if needed)
fix/loop_fix.py # moved
fix/pingpong_bake.py # moved
post/rename_clip.py # moved
post/dance_profile.py # moved
iclone/
export_take.py # FR-4 (bridge client side)
bridge/ # moved from tools/iclone_bridge/ (plugin src + installer)
tools/
legacy/ # NFR-4: cc_retarget.py, kevin_retarget.py,
# mixamo_retarget.py until Phase-2 matrix passes
export_boat_prop.py mocap_retarget.py bake_run_punch.mjs # out of scope (N4)
```
## 6. Exchange data-flow contract (assets run through `exchange/` — decided)
```
exchange/
incoming-fbx/ STAGE 0 raw takes + .fbm textures + <take>.json sidecars
converted-glb/ STAGE 1 retargeted GLB (+ provenance .json) [convert.py]
pingpong-glb/ STAGE 2 <pack>_pp.glb loop variants (+ provenance)[convert.py]
looped-glb/ STAGE 2b optional loop_fix outputs (recreated on demand)
reports/ STAGE 3 per-take markdown QC report [convert.py] ← NEW
reference-video/ QA reference footage (unchanged)
workingfiles-iclone/ iClone .iProject staging (unchanged)
outgoing-props/ reverse direction, game→iClone (unchanged, out of scope)
```
Rules:
- E-1 Stages only move forward; a stage folder never contains hand-edited files.
- E-2 Ship step remains manual+reviewed: copy STAGE 1/2 GLBs into ariki-game
`assets/quaternius/dancegen/` with dance JSON, per existing skill workflow.
- E-3 Cleanup only after Jeremy confirms in-game (existing etiquette, unchanged).
- E-4 Everything in `exchange/` is LFS-tracked already; sidecars/reports are plain text.
## 7. Component contracts
| Component | CLI (canonical) | Exit codes |
|---|---|---|
| `validate_fbx.py` | `python pipeline/validate_fbx.py <fbx> [--name <Clip>]` | 0 pass / 1 reject / 2 warn-only / 3 error |
| `retarget.py` | `<blender> --background --python pipeline/retarget/retarget.py -- --src <fbx…> --out <glb> --name <Clip> [--family <f>] [--target <gltf>]` | 0 ok / 1 reject (unmapped/no-motion) / 2 error |
| `loop_qc.py` | unchanged: `-- --src <glb>` | 0 SMOOTH / 1 NOT SMOOTH / 2 error (unchanged) |
| `pingpong_bake.py`, `loop_fix.py`, `rename_clip.py` | unchanged flags, new paths | unchanged |
| `convert.py` | `python pipeline/convert.py --src exchange/incoming-fbx/<take>.fbx --id nd_02` | 0 all gates green / 1 QC not smooth / 2 rejected at intake / 3 error |
| `export_take.py` | `python pipeline/iclone/export_take.py --take <name> [--project <.iProject>]` | 0 exported+valid / 1 bridge down / 2 export invalid vs §4.3 |
Driver behavior (FR-1): temp-write + atomic rename (NFR-1); provenance sidecars
(NFR-2); report to `exchange/reports/<id>-<date>.md` containing IC results, QC metrics
for base and `_pp`, worst-offending bones, and the ship checklist.
### 7.1 QC architecture (the QC element)
QC is layered, script-based, and mandatory — `convert.py` will not emit a shippable
GLB past a red gate:
| Gate | Tool | Runs | Catches |
|---|---|---|---|
| **QC-0 Intake** | `validate_fbx.py` (IC-1..9) | before Blender, seconds | wrong rig, missing motion, A-pose flag, name collisions |
| **QC-1 Target integrity** | `verify_target.py` | every `convert.py` run | pinned skeleton drifting from the game's rig |
| **QC-2 Loop seam** | `qc/loop_qc.py` on STAGE 1 GLB *and* the `_pp` variant | every take | pose gap / velocity gap / root drift at the loop seam, worst-offending bones |
| **QC-3 Report + human eyeball** | `exchange/reports/<id>.md` + test-bed viewing | every take | everything numbers can't catch (arm orientation, style, reversal reading as moonwalk) |
| **QC-R Regression** | §9 matrix | on retargeter/bone-map changes | math/output drift vs known-good clips |
QC-3 stays human by policy: the boat-prop lesson (all asserts green, boat upside down)
is why no automated verdict ships a clip without an eyeball in the test bed.
### 7.2 Model & token usage
**The pipeline itself calls no AI models and consumes zero tokens.** Every component in
§7 is deterministic Python/Blender (required by NFR-3 — determinism and model calls are
mutually exclusive here). There are no LLM API calls, no cloud services; the only
network-adjacent piece is the TinqsBridge localhost socket to iClone.
Tokens are spent only when a **Claude agent operates** the pipeline, and only in the
session doing the operating:
| Consumer | Model | Typical cost | Notes |
|---|---|---|---|
| Scripts (`convert.py`, retarget, QC, bakes, `export_take.py`) | none | 0 tokens | pure Python/Blender |
| `loop-qc` agent (`.claude/agents/loop-qc.md`) | inherits the session's model (no override pinned) | ~515k tokens per clip; ~2040k per small batch | convenience wrapper that runs `loop_qc.py` and interprets numbers |
| Claude running a full convert batch interactively | session model | ~50150k tokens per batch today | `convert.py` + reports are designed to shrink this: one command + one readable report instead of many tool round-trips |
Cost-control decisions baked into this plan: QC verdicts live in `exchange/reports/`
markdown so future sessions read a summary instead of re-running tools; `validate_fbx.py`
fails bad takes in seconds before any expensive step; and the `loop-qc` agent SHOULD be
pinned to a small model (`model: haiku`) in its frontmatter during P1 — it only runs a
script and relays numbers, which doesn't need a frontier model.
## 8. Phases, gates, deliverables
| Phase | Work | Entry gate | Exit gate (must pass) | Est |
|---|---|---|---|---|
| **P0 Baseline review** | Run CURRENT tools on Windows (explicit Blender path) on `hakadance1.fbx`; curve-diff vs shipped `war_dance_01.glb`; write `plans/fbx-pipeline-baseline-2026-07-21.md` incl. per-tool audit table | plan approved | Windows/Mac parity verdict documented; blockers listed | 0.5d |
| **P1 Move & wire** | `git mv` per §5; `blender_env.py`; fix kevin hardcoded path; sweep docs/skills (grep checklist: `tools/`, `/Applications/Blender`) | P0 pass | P0 run repeats green from new paths; grep sweep clean | 0.5d |
| **P2 Unified retargeter** | `retarget.py` + rigs registry; port maps from legacy tools | P1 done | Regression matrix §9 green; legacy tools moved to `tools/legacy/` | 1d |
| **P3 iClone export script** | Verify RLPy FBX-export API coverage of §4.4 FIRST; implement `export_take.py`; else checklist-fallback mode; T-pose QA on first export | P2 done; bridge pings | One take exported from iClone passes `validate_fbx.py` with `t_pose: true`; arm QA eyeballed in test bed | 1d |
| **P4 Driver + gates** | `convert.py`, `validate_fbx.py`, reports, registry-id logic (FR-7) | P2 done (parallel w/ P3) | `convert.py` on a cc_base take: green end-to-end, report emitted | 0.51d |
| **P5 Docs + pilot** | `pipeline/README.md`; skills point at it; pilot batch: 4× `HumanM@Mining*.fbx` (bstyle) | P2P4 done | Pilot converted + QC'd + staged; registry updated | 0.5d |
| **P6 Retire legacy** | Delete `tools/legacy/` in a single-purpose commit | 2 real batches through new pipeline w/o legacy fallback | commit merged | 0.1d |
Total ≈ 4 days. P3 and P4 can run in parallel.
## 9. Regression & test matrix (P2 exit gate)
| Case | Family | Legacy tool | Comparison |
|---|---|---|---|
| `aloha1.fbx` | cc_base | `cc_retarget.py` | per-bone quaternion curves, max abs delta ≤ 1e-4; pelvis translation ≤ 1e-4 m; frame count equal |
| `HumanM@Mining01 - Loop Ground.fbx` | bstyle | `kevin_retarget.py` (target path fixed) | same |
| `UAL1_Standard.fbx` (one clip) | quaternius_ue | n/a (new capability) | output plays on pinned rig; bone set ⊆ canonical 65 |
| mixamo | mixamo | `mixamo_retarget.py` | **no source FBX on disk** — gate on any newly downloaded Mixamo take; if none by P2, mark family "ported, unverified" in README |
| Negative: unknown rig | — | — | reject w/ unmapped-bone list (FR-2) |
| Negative: 2-frame TempMotion | cc_base | — | IC-2 reject message |
| `verify_target.py` | — | — | pinned rig == ariki-game rig |
Float-tolerance rationale: Blender 5.1 (Win) vs 5.1.2 (Mac) may differ in low-order
bits; 1e-4 is far below visible thresholds but catches real math changes.
## 10. Risk register
| # | Risk | L×I | Mitigation / trigger |
|---|---|---|---|
| R1 | RLPy export API lacks §4.4 settings | M×M | P3 verifies API surface before coding; fallback = scene-prep + human-clicked checklist; contract still enforced by `validate_fbx.py` |
| R2 | T-pose-bind switch shifts cc_base output vs shipped clips | M×M | Deliberate: new takes only; shipped GLBs untouched; first T-pose take gets arm QA (P3 exit gate) |
| R3 | Blender Win/Mac version drift | L×M | P0 curve-diff gate; pin "Blender 5.1.x" in README; upgrade both sides together |
| R4 | Pinned target rig drifts from game skeleton | L×H | `verify_target.py` auto-run by driver (hard fail) |
| R5 | Doc/skill path rot after move | H×L | P1 grep checklist is a named exit gate |
| R6 | Unified retargeter regression on edge takes | M×H | §9 matrix + NFR-4 legacy window + P6 requires 2 clean real batches |
| R7 | Registry parse (FR-7) breaks on table format change | M×L | Parser tolerant to column additions; on parse failure driver degrades to WARN, never blocks |
| R8 | Mixamo family unverifiable at P2 | M×L | Explicitly labeled "unverified" until a take arrives; not a launch blocker |
## 11. Acceptance criteria (project-level)
1. `python pipeline/convert.py --src exchange/incoming-fbx/<take>.fbx --id nd_##` on
this Windows machine produces STAGE 1+2 GLBs, provenance sidecars, and a QC report —
zero Mac involvement, zero manual Blender paths.
2. All rig families in §4.2 convert; §9 matrix green (mixamo allowed "unverified").
3. Unknown-rig and no-motion FBX fail fast with actionable messages.
4. `export_take.py` (or its documented fallback) yields an intake-valid, T-pose iClone
FBX with settings-recording sidecar.
5. Legacy tools deleted (P6) after two clean production batches.
6. Skills/docs reference only `pipeline/` paths; `pipeline/README.md` is the runbook.
## 12. Open questions (tracked, non-blocking)
- Q1 RLPy FBX-export settings coverage (resolves in P3).
- Q2 Should `loop_qc.py` gain FBX input for pre-retarget QC? (defer; not needed for launch)
- Q3 Option-2 spike (Godot import-time retarget of FBX) — separate plan if/when wanted.
- Q4 `hakadance1_static.glb` orphan in dancegen — cleanup during next ariki-game touch.
+161
View File
@@ -0,0 +1,161 @@
# TASK: Repair Lena's HIGH-DENSITY sculpt into a nude body (CD cup, round, with cleavage)
You are building a nude variant of the Lena character from her original high-density Tripo
sculpt. The finished model must read as a naked stylized woman with **round CD cup breasts and
clear cleavage**, **no nipples**, a **smooth featureless crotch** (doll-like), and **no trace of
the underwear** in geometry or texture. This has already been done successfully at game density
(32k verts); your job is the same repair at full sculpt density. Every measured fact you need is
below — trust these numbers, they were measured this week on these exact files.
## Source file — READ-ONLY
`characters/originals/female/lena_sculpt_glb_v01.glb`
(974,478 verts, 1,907,931 tris, ~55 MB; byte-identical alias of `female_lena_tripo.glb`).
**NEVER modify, resave, or re-export the original.** It is checksummed in
`characters/REGISTRY.md` and guarded. First action: copy it into your work directory and only
ever open the copy.
## Facts about the source (measured, verified)
- glTF is Y-up; **Blender's importer converts to Z-up**. After import: feet at z=0, height
**0.9792 units**; multiply by **1.8147** to get real metres on Lena's 1.777 m game body.
**Front = Y.** Single mesh object named `tripo_node_9701d6b7-…`, identity transform, one
material, three 4096² textures: `lena+glb_basecolor.jpg`, `lena+glb_normal.jpg`,
`lena+glb_rm.jpg`. One UV map. UNRIGGED (no armature) — keep it that way.
- **The underwear IS the skin.** Ray columns through chest and hips return exactly 2 surface
crossings (front wall + back wall) almost everywhere: there is NO body surface hidden under
the bra or briefs. **Deleting garment faces opens holes.** The repair verbs are FLATTEN,
SMOOTH, BRIDGE, WELD — never delete.
- The bra is a sports-bra shape whose hems are raised lips: the shell sits ~0.006 units
(1.1 cm real) proud at the hems, and at the neckline the edge is an overlapping flap (a ray
there returns 3 crossings). The briefs have the same raised waistband/leg-opening rims.
- **The mesh is NOT watertight: 40,769 open boundary edges**, concentrated exactly at the
garment hems — z≈0.637 (bra band, 6,220 edges), 0.685 (armholes, 4,726), 0.734 (neckline,
3,002), 0.588 (briefs waistband, 2,842), 0.490 (leg openings, 2,592). Expect UV-seam splits
everywhere too: `merge by distance` (~1e-5) before any topology reasoning, or seam splits
masquerade as boundaries.
- Body landmarks (source units | real m): acromion 0.784 | 1.423 · neckline notch 0.723 |
1.312 · **bust apex 0.688 | 1.248** · underbust 0.641 | 1.163 · waist (narrowest) 0.606 |
1.100 · briefs waistband 0.578 | 1.049 · hip/trochanter ≈0.49 | ≈0.89.
- Existing breast anatomy under the bra: apexes at x = ±0.0300.035 (±6 cm real), projection
including the bra shell 0.023 units (4.1 cm), bare flesh ≈0.017 (3.0 cm); base ≈0.070 units
(12.7 cm) diameter; the intermammary valley is BRIDGED by the bra (centre sits only 0.002
units behind the apex plane). Torso depth at apex level: 0.1565 units.
- The basecolor paints the bra/briefs matte beige, lighter and much less saturated than skin,
with painted seam/shadow lines at the hems and a stray orange seam artifact on the left strap.
## Target shape — exact acceptance numbers (all in source units, Z-up, after import)
SHAPE REFERENCE: Jeremy supplied a reference image (archived at
`characters/female/lena_nude/reference_breasts.jpg` — you cannot view it; these numbers ARE its
translation): near-hemispherical ROUND breasts whose apex projection is close to the base
radius (not a shallow dome), full upper AND lower pole, a defined inframammary fold, apexes
pointing slightly outward, and a NARROW cleavage where the mounds nearly touch medially.
1. **Breasts: round CD cup.** Apex projection off the repaired chest wall between **0.036 and
0.044 units** (6.58.0 cm real), targeting projection ≈ 0.91.0× the base RADIUS. Apex
height z = 0.688 ± 0.010; apexes at x = ±0.0330.038. Base diameter 0.0750.090 units.
ROUND profile: the y(z) profile through the apex approximately symmetric within 20% above
vs below the apex; if you shape a falloff curve, use a fattened mid-slope (e.g. gamma 0.75
on normalized height), never a thin cone.
2. **Cleavage.** NARROW: medial dead zone half-width ~0.007 units with a ~0.013 falloff — the
mounds nearly touch. At apex height the sternum line (x=0) must still sit at least
**0.015 units** behind the apex plane, a smooth U-valley, two clearly separate forms.
3. **No nipples**: the apex region must be smooth (no bump smaller than 0.01 units radius
protruding off the breast surface).
4. **Crotch**: match the MALE game body's crotch construction (Jeremy's directive — the male is
already the smooth doll-like build, and the female must match it). Donor:
`C:\Users\Jeremy\tinqs\ariki-game\assets\quaternius\derived-bodies\Ariki_Male_QuatBody_D_RigCandidate.glb`
(READ-ONLY). Method: extract the male's crotch patch (pubic triangle + perineum, the region
between the leg roots), align it to the female pelvis by landmarks — crotch saddle point
(lowest point of the torso between the legs) and hip half-width scale — then project the
female crotch-window verts onto the aligned donor surface (subdivide the donor patch 2
levels first so the target is smooth), blending at the window rim. Note the scale: the male
GLB is in METRES (~1.75 m tall) while this sculpt is 0.9792 units tall — scale donor by the
ratio of heights, then refine by hip width. No rim lips, no anatomical detail anywhere in
the window.
5. **No garment trace in geometry**: boundary-edge count in each hem z-band (0.4850.495,
0.5830.593, 0.6320.642, 0.6800.690, 0.7290.739) reduced to the background level of
plain-skin bands (measure a control band at mid-thigh z≈0.30 and match its order of
magnitude). No y(z) step greater than **0.002 units between adjacent 0.005-unit samples**
along the sternum column or the apex column, except across the breast contour itself.
6. **No garment trace in texture**: garment-keyed texels repainted to locally-matched skin.
Calibrate the colour key yourself: sample faces at briefs centre (z 0.500.56, |x|<0.05,
front) vs mid-thigh skin (z 0.280.34) on the basecolor, then pick thresholds that separate
them; after repaint, a face-level census over the old garment regions must classify ≥99% as
skin. Fill tone must come from NEARBY-ON-BODY skin (nearest skin vertices in 3D), never from
a global average — atlas-local fills produce wrong tones and visible island seams.
## Method notes (hard-won this week at game density — same traps apply)
- **NEVER use `python -c` or inline one-liner scripts with nested quotes** — your previous run
burned 600+ steps wedged on shell-quoting errors in exactly that pattern. ALWAYS write
scripts to FILES first: `printf '%s\n' "line1" "line2" > script.py` or a heredoc, then run
the file. One command writes the file, the next runs it.
- Work in Blender headless: `"/c/Program Files/Blender Foundation/Blender 5.1/blender.exe"
--background --python <script.py> -- <args>`. numpy IS available inside Blender's python.
System `python` has PIL but NO numpy. Each of your shell commands has a 15-minute timeout —
split heavy work into staged scripts and save intermediate `.blend`/`.glb` checkpoints in the
work dir so a timeout never loses progress. 974k verts is heavy: prefer numpy `foreach_get`/
`foreach_set` over per-vertex python loops wherever possible.
- **Donor mesh, if present (check before sculpting):** if `exchange/INBOX/cc_female_donor.fbx`
exists, it is a Character Creator base female exported with the DESIRED CD breast shape.
Prefer it over the pure procedural field: import it, auto-detect its breast apexes (front-most
points in its chest band) and chest wall, similarity-align it to this sculpt's landmarks
(apex z=0.688, apexes x=±0.033, chest-wall depth), then project the chest-region vertices
onto the donor's breast surface (BVH ray-cast along vertex normals, outward then inward,
reject hits whose normal opposes the ray), blending to the repaired chest wall at the root.
The acceptance gates below apply unchanged. If the file does not exist, use the procedural
field described next.
- Suggested pipeline (adapt as needed): copy source → merge-by-distance → weld/close hem rims
(weld small gaps rather than filling holes with new faces; new faces need invented UVs) →
flatten hem lips into the surrounding skin (narrow smoothing band along each hem; the garment
interior IS the anatomy — do NOT smooth the whole briefs region or you erase her hips/butt)
→ chest: smooth the bra shell down to a chest wall, then sculpt the two mounds (procedural
displacement field along vertex normals is proven: elliptical root, radial falloff with zero
slope at the root; for ROUND use a profile like (1u²)^1.4 with mild vertical asymmetry) →
repaint basecolor garment regions (3D-nearest-skin fill + reinject high-frequency grain
sampled from real skin so the patch isn't a smooth decal) → also neutralise the garment in
`lena+glb_normal.jpg` and `lena+glb_rm.jpg` over the same masks (flat normal 128,128,255;
rm matched to skin) → export.
- The UV atlas may mirror or pack islands unintuitively — never assume atlas-adjacent means
body-adjacent. When sampling textures from raw glTF UV data remember glTF v runs top-down
while Blender image rows run bottom-up (flip v); UVs already imported through bpy are
pre-flipped.
- Verify NUMERICALLY after every stage (you cannot look at renders): ray-crossing census,
boundary-edge counts per z-band, y(z)/y(x) cross-section profiles, texture colour census.
Print the numbers; compare against the gates above.
- ALSO render review images for the human (front/back/side/¾, textured AND untextured clay,
plus chest and hip closeups) into `characters/female/lena_nude/hires_work/review/`. Clay
renders are how humans catch what numbers miss.
## Output & NAMING LAW (follow exactly — this is a hard requirement)
Naming follows `characters/REGISTRY.md` (read it). For this artifact:
- Final model: `characters/female/lena_nude/lena_nude_sculpt_glb_v01.glb`
— grammar `<character>_<stage>_<format>_v<NN>`: character `lena_nude` (registered), stage
`sculpt` (unrigged full-res), format `glb`, first version `v01`. All lowercase snake_case,
`[a-z0-9_]` only. No dates, no status words, no hyphens in the name.
- **PACK all three textures inside the GLB** (self-contained) — loose texture names would
collide with the game-density `lena_nude_basecolor.png` already in that folder.
- **NEVER create any file named `Ariki_*`** — PascalCase ship names belong to the game repo
only; the rename happens once, at release (registry rule 8).
- Never touch `characters/originals/**` except to COPY from it. Never modify
`lena_nude_quatskin_glb_v01.glb` or `lena_nude_basecolor.png` (the shipped game-density
variant lives beside your output).
- When the model passes the gates, append ONE row to the "Derived artifacts" table in
`characters/REGISTRY.md`: name, character `lena_nude`, parent `lena_sculpt_glb_v01.glb`,
stage `sculpt`, tool (your script path), date, status `wip`, ship name blank ("—"), notes
with the sha256 (`sha256sum`) plus final vert/tri counts and the apex-projection number you
achieved. Do not git-commit anything.
- Work dir for everything intermediate: `characters/female/lena_nude/hires_work/` (create it).
Keep your stage scripts there too — they are the record of how the model was made.
## Definition of done
Emit `DONE:` only when: (a) the output GLB exists at the exact path above with textures packed,
(b) every numeric gate in "Target shape" prints PASS from your verification script, (c) review
renders exist, (d) the registry row is appended. If blocked, emit `DONE:` with the precise
blocker and the numbers you got vs the gates.
+191
View File
@@ -0,0 +1,191 @@
# Plan: iClone 8 Python API bridge — control iClone from the terminal
**Status:** ready for implementation · **Author:** Fable 5 session 2026-07-21 · **Implementer:** GLM session
## 0. Context (you have no other context — read this fully)
You are on Jeremy's **Windows 11 PC**. This repo (`C:\Users\Jeremy\tinqs\animation`) is the
animation bridge for the ariki-game project: animations are authored in iClone 8, exported
as FBX into `exchange/`, and retargeted with `tools/cc_retarget.py`. Today that pipeline is
entirely manual on the iClone side (Jeremy clicks Export in the UI).
**Goal:** establish a programmatic connection to iClone 8 so terminal agents (Claude, GLM,
scripts) can drive it — load motions, query the scene, trigger FBX exports — without Jeremy
clicking through the UI each time.
**Why a bridge is needed:** iClone 8 has an embedded Python API (`RLPy` module) but **no
external/network API of any kind**. Scripts only run *inside* iClone, loaded either from the
Script menu or as auto-loading plugins. The standard pattern (used by MotionLIVE etc.) is a
plugin that runs a localhost socket server inside iClone and executes commands sent to it.
That is what you will build.
### Verified environment facts (checked 2026-07-21 — trust these)
- iClone 8 install: `A:\Program Files (x86)\iClone 8\` (note: **A: drive**, not C:).
- Plugin auto-load folder: `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\`
**verified writable without elevation**. Currently contains only stock plugins
(AIStudio, MotionLIVE, VideoMocap). Do not touch those.
- API stub for reference: `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` — this file
lists every RLPy class/method with docstrings. **It is your API ground truth**; grep it
before using any RLPy call. Do not trust API names from memory or old forum posts.
- Embedded Python: `Bin64` contains BOTH `python38.dll` and `python310.dll` — do not
assume which one hosts plugins. Write the plugin in **3.8-compatible syntax** (no
`match`, no `X | Y` unions, no 3.9+ stdlib) and report the real version via the bridge
itself (`sys.version`) in your results.
- Qt: iClone embeds Qt 5.15 / PySide2 (`shiboken2` available in-process).
- Client-side Python: 3.12.10 on PATH (`python`). Client must be **stdlib-only**.
- iClone was RUNNING when this plan was written. A restart is required to load the new
plugin — **Jeremy must do the restart himself** (he may have unsaved work). Never kill
the iClone process.
### Reference docs (fetch only if stuck; the RLPy.py stub usually suffices)
- Plugin structure: https://wiki.reallusion.com/IC_Python_API:Your_First_iClone_Python_Plugin
- IC8 API wiki: https://wiki.reallusion.com/IC8_Python_API
- Official samples (QTimer/menu/PySide2 patterns): https://github.com/reallusion/iClone
## 1. Architecture
```
┌─ this repo ──────────────────┐ ┌─ inside iClone 8 (Qt main thread) ─────────┐
│ tools/iclone_bridge.py │ TCP │ OpenPlugin\TinqsBridge\main.py │
│ (client, py3.12, stdlib) │◄──────►│ socket thread: accept + read/write ONLY │
│ │ 127.0. │ queue → QTimer(50ms, main thread) drains, │
│ tools/iclone_bridge/ │ 0.1: │ exec()s code with RLPy in namespace, │
│ TinqsBridge/main.py (source)│ 18800 │ posts JSON reply back to socket thread │
│ tools/install_iclone_bridge │ │ │
│ .ps1 (copies to OpenPlugin) │ └────────────────────────────────────────────┘
└──────────────────────────────┘
```
**Non-negotiable threading rule:** RLPy is not thread-safe and must only be called on
iClone's Qt main thread. The socket thread NEVER touches RLPy. It pushes requests onto a
`queue.Queue`; a PySide2 `QTimer` created in `initialize_plugin()` (which runs on the main
thread) fires every ~50 ms, drains the queue, executes, and hands the response back (a
second queue or per-request `threading.Event` — your choice).
## 2. Wire protocol (keep it this simple)
Newline-delimited JSON over TCP, localhost only, one request/response pair at a time.
- Request: `{"id": 1, "code": "<python source>"}`
- Success: `{"id": 1, "ok": true, "result": <json>, "stdout": "<captured prints>"}`
- Failure: `{"id": 1, "ok": false, "error": "<full traceback>", "stdout": "..."}`
Execution semantics:
- `exec(code, ns, ns)` — one **persistent** namespace dict `ns` shared across all requests
for the life of the iClone session, pre-seeded with `RLPy` imported. Same dict as globals
AND locals (avoids the exec-scoping trap where nested functions can't see top-level names).
- If the code sets a variable named `result`, that is the response `result`
(JSON-serialize; on `TypeError` fall back to `repr()`). Otherwise `result` is null.
Clear `ns["result"]` before each exec so stale values don't leak.
- Capture stdout/stderr during exec (`contextlib.redirect_stdout/stderr`) into `stdout`.
- Any exception → `ok:false` with `traceback.format_exc()`; the bridge itself must survive
and serve the next request.
## 3. Deliverables
1. **`tools/iclone_bridge/TinqsBridge/main.py`** — the plugin (source of truth lives in the
repo; the installer copies it out). Requirements:
- `initialize_plugin()` (mandatory — iClone refuses to load the plugin without it).
Wrap its entire body in try/except that writes `traceback.format_exc()` to the log
file — plugin load failures are otherwise near-silent.
- TCP server on `127.0.0.1:18800`, `SO_REUSEADDR`, port overridable via env var
`TINQS_ICLONE_BRIDGE_PORT` read inside iClone. If the bind fails, log it clearly and
return without crashing the plugin loader.
- Socket-accept loop on a `daemon=True` thread; QTimer executor per §1. Keep
module-level references to the timer and thread (a GC'd QTimer silently stops).
- Log to `%TEMP%\tinqs_iclone_bridge.log` (append, timestamped lines: startup, bind
result, each request id + ok/error, shutdown).
- Optional nice-to-have, skip if it costs you more than ~20 min: a **Plugins ▸ Tinqs
Bridge** menu entry showing status (port, requests served) via `RLPy.RUi.AddMenu` +
`shiboken2.wrapInstance` (see the GitHub samples). The log file is the required
status surface; the menu is garnish.
2. **`tools/iclone_bridge.py`** — client, Python 3.12, stdlib only. API:
`run(code, port=18800, timeout=30.0) -> dict` and `ping(port, timeout) -> dict`.
CLI (match the repo's argparse style):
```
python tools/iclone_bridge.py --ping
python tools/iclone_bridge.py --exec "result = 1 + 1"
python tools/iclone_bridge.py --file some_script.py
python tools/iclone_bridge.py --port 18801 --timeout 120 --exec "..."
```
`--ping` sends a snippet returning `{"product": ..., "version": ..., "python": sys.version}`
— find the real product/version getters by grepping `RLPy.py` (look at `RApplication`
and `RGlobal`; do not guess). Exit 0 on ok:true, 1 on ok:false or connection failure,
with a readable message either way (mention "is iClone running? was it restarted after
install?" on connection-refused).
3. **`tools/install_iclone_bridge.ps1`** — copies `tools/iclone_bridge/TinqsBridge/` →
`A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\` (overwrite), prints
what it did and reminds that iClone must be restarted. Idempotent.
4. **`docs/iclone-bridge.md`** — short usage doc: what it is, install/update procedure,
CLI examples, the `result` convention, the persistent-namespace behavior, the log file
location, the threading rule for anyone extending the plugin, and the "long-running
code freezes the iClone UI" warning.
5. **Results report `plans/iclone-api-bridge-results-2026-07-21.md`**: actual embedded
Python version discovered, RLPy calls used for ping, smoke-test transcript (§5), any
deviations from this plan with reasons, and known limitations.
## 4. Implementation order
1. Grep `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` for the calls you need
(`RApplication`, `RGlobal`, `RScene`, `RFileIO`) and note exact signatures.
2. Write plugin + client + installer.
3. Run the installer. Verify the files landed in OpenPlugin.
4. **Stop and ask Jeremy to save his iClone work and restart iClone.** Do not proceed to
smoke tests until he confirms. Never kill or restart the process yourself.
5. Run smoke tests (§5). If the plugin didn't load: check the log file first, then
iClone's **Script ▸ Console Log** window (ask Jeremy to read it out if needed).
6. Write docs + results report.
## 5. Acceptance criteria (all must pass, transcript goes in the results report)
1. `python tools/iclone_bridge.py --ping` → ok, prints product name, iClone version, and
embedded Python version.
2. Scene query: `--exec` a snippet listing scene object names via `RScene` (e.g. avatars +
props; exact API from the stub) returns a JSON list without error.
3. Namespace persistence: `--exec "x = 41"` then `--exec "result = x + 1"``42`.
4. Error resilience: `--exec "1/0"``ok:false` with a ZeroDivisionError traceback, AND
a follow-up `--ping` still succeeds.
5. stdout capture: `--exec "print('hi'); result = True"``stdout` contains `hi`.
6. Bridge survives ≥ 2 sequential client connections (each CLI call is a new connection).
## 6. Guardrails — do NOT
- Do not kill, restart, or send input to the running iClone process. Restarts are
Jeremy's job (step 4.4).
- Do not modify anything in `OpenPlugin\` other than creating/updating `TinqsBridge\`.
- Do not modify existing repo tools (`cc_retarget.py`, `loop_qc.py`, `loop_fix.py`,
`dance_profile.py`, etc.), anything in `.claude/`, `docs/` (except the new
`docs/iclone-bridge.md`), or `exchange/`.
- Do not bind to anything other than `127.0.0.1`. This is an arbitrary-code-execution
endpoint by design; localhost-only is the security model.
- Do not install any packages — plugin uses iClone's embedded stdlib + PySide2; client
uses Python 3.12 stdlib.
- Do not commit or push. Leave everything in the working tree for Jeremy's review.
## 7. Known traps
- **RLPy off the main thread** crashes or corrupts iClone. The QTimer pattern in §1 is
mandatory; the socket thread only moves bytes.
- **QTimer lifetime:** keep a module-level reference; also create it *in*
`initialize_plugin()` (main thread), never in the socket thread.
- `initialize_plugin()` missing, misnamed, or raising → plugin silently fails to load.
Hence the try/except-to-logfile around everything.
- **exec scoping:** pass the same dict as globals and locals, or nested
functions/comprehensions in user code will throw NameError on top-level names.
- Commands run synchronously on the Qt main thread → **iClone's UI freezes for the
duration**. Fine for seconds-long operations (an FBX export blocks anyway); document it.
- JSON can't serialize RLPy objects — always try/except the dumps and fall back to repr.
- Stale port from a previous iClone instance: `SO_REUSEADDR` + a clear log line beats a
cryptic bind exception.
- Windows Firewall shouldn't prompt for a 127.0.0.1 bind, but if a prompt appears Jeremy
should allow it (note in docs).
- Socket recv: requests may arrive fragmented — read until `\n`, decode UTF-8.
- Both python38.dll and python310.dll exist in Bin64 — write for 3.8, verify via ping.
- Old wiki pages mix iClone 7 and 8 APIs; several IC7 calls were renamed/removed. The
local `RLPy.py` stub outranks every web source.
- After the bridge is up once, you can iterate on *executor* behavior by exec-ing new
code through the bridge itself (hot-swap) — but changes to `main.py` on disk still need
an iClone restart to load. Batch your plugin edits to minimize restart requests.
@@ -0,0 +1,218 @@
# Results: iClone 8 Python API bridge — 2026-07-21
Implements `plans/iclone-api-bridge-plan-2026-07-21.md`. All acceptance criteria in
plan §5 pass. Written after Jeremy restarted iClone with the TinqsBridge plugin
installed at `A:\Program Files (x86)\iClone 8\Bin64\OpenPlugin\TinqsBridge\main.py`.
## Embedded Python version discovered
```
3.8.8 (tags/v3.8.8:024d805, Feb 19 2021, 13:18:16) [MSC v.1928 64 bit (AMD64)]
```
Confirms iClone 8 hosts plugins on **python38.dll**, not python310.dll (Bin64 ships
both). The 3.8-compatible-syntax requirement in the plan was the right call.
## RLPy calls used
Found by grepping `A:\Program Files (x86)\iClone 8\Bin64\RLPy.py` directly (not from
memory or web sources), per the plan's ground-truth rule:
- **Ping** — `RApplication` (stub at line 15328), both `@staticmethod`:
- `RLPy.RApplication.GetProductName()``"iClone"`
- `RLPy.RApplication.GetProductVersion()``[8, 73, 5509]`
- **Scene query** — `RScene` (stub at line 14872) + `RIObject` base class (stub at
line 5142):
- `RLPy.RScene.GetAvatars()``@staticmethod`, returns avatar objects
- `RLPy.RScene.GetProps()``@staticmethod`, returns prop objects
- `RIObject.GetName()` (inherited by both) — display name of each returned object
Both `GetAvatars`/`GetProps` stubs are argument-less in the common call form (the
SWIG stub shows `GetAvatars(*args)` for an overload iClone doesn't require callers to
use); no arguments were needed for a flat name listing.
## Smoke-test transcript (plan §5, all six items)
Log file: `C:\Users\Jeremy\AppData\Local\Temp\tinqs_iclone_bridge.log` (this is the
resolved `%TEMP%` for Jeremy's user session — not `C:\Windows\Temp`). Full contents
after the run:
```
[2026-07-21 13:22:10] ============================================================
[2026-07-21 13:22:10] TinqsBridge initialize_plugin() starting, requested port 18800
[2026-07-21 13:22:10] bound OK on 127.0.0.1:18800
[2026-07-21 13:22:10] accept thread started
[2026-07-21 13:22:10] QTimer executor started (50 ms poll)
[2026-07-21 13:22:10] TinqsBridge ready on 127.0.0.1:18800. Python: 3.8.8 (tags/v3.8.8:024d805, Feb 19 2021, 13:18:16) [MSC v.1928 64 bit (AMD64)]
[2026-07-21 13:22:54] request id=1784665374837 ok=true
[2026-07-21 13:23:39] request id=1784665419368 ok=true
[2026-07-21 13:24:53] request id=1784665493184 ok=true
[2026-07-21 13:24:53] request id=1784665493333 ok=true
[2026-07-21 13:25:44] request id=1784665544442 ok=true
[2026-07-21 13:25:44] request id=1784665544559 ok=true
[2026-07-21 13:26:31] request id=1784665591763 ok=false: ZeroDivisionError: division by zero
[2026-07-21 13:26:31] request id=1784665591935 ok=true
[2026-07-21 13:26:32] request id=1784665592046 ok=true
[2026-07-21 13:26:42] request id=1784665602593 ok=true
[2026-07-21 13:26:42] request id=1784665602745 ok=true
```
### 1. `--ping`
```
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
$ echo $?
0
```
**PASS** — product name, iClone version, and embedded Python version all present.
### 2. Scene query via RScene
```
$ python tools/iclone_bridge.py --exec "result = [o.GetName() for o in RLPy.RScene.GetAvatars()] + [o.GetName() for o in RLPy.RScene.GetProps()]"
[
"Shadow Catcher"
]
$ echo $?
0
```
**PASS** — valid JSON list, no error. (Scene had no avatars loaded at test time, just
the default "Shadow Catcher" prop — expected for whatever project state iClone was in
after restart; the query mechanism itself is what's under test.)
### 3. Namespace persistence
```
$ python tools/iclone_bridge.py --exec "x = 41"
null
$ echo $?
0
$ python tools/iclone_bridge.py --exec "result = x + 1"
42
$ echo $?
0
```
**PASS**`x` set in one connection was visible from a second, separate connection;
result was exactly `42`.
### 4. Error resilience
```
$ python tools/iclone_bridge.py --exec "1/0"
error: Traceback (most recent call last):
File "A:/Program Files (x86)/iClone 8/Bin64/OpenPlugin/TinqsBridge/main.py", line 120, in _execute
exec(code, _ns, _ns)
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
$ echo $?
1
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
$ echo $?
0
```
**PASS**`ok:false` with a full ZeroDivisionError traceback, exit code 1; the
follow-up `--ping` succeeded immediately after, confirming the bridge survives an
exec-time exception.
### 5. stdout capture
```
$ python tools/iclone_bridge.py --exec "print('hi'); result = True"
hi
true
$ echo $?
0
```
**PASS**`stdout` contained `hi` (client prints stdout above the result line); JSON
result `true`.
### 6. ≥2 sequential connections
```
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
$ python tools/iclone_bridge.py --ping
ok: iClone [8, 73, 5509] (embedded python 3.8.8)
```
**PASS** — two back-to-back separate client invocations (separate TCP connections)
both succeeded. In total the log shows 11 requests served across 9 distinct CLI
invocations plus 2 earlier ad-hoc pings during initial verification, all without the
bridge needing a restart.
**All six acceptance criteria: PASS.**
## Notable event during this session: "incompatible plugin" dialog report
Partway through this session, Jeremy reported that on the iClone restart he saw a
dialog reading the plugin name as "simbridge" (almost certainly TinqsBridge) and
stating it was "not compatible with this iClone." This raised a real concern that the
plugin had failed to load.
**Investigation found no evidence of a load failure.** At the time that report came
in:
- The log file already existed and showed a completely clean startup sequence
(bind OK, accept thread started, QTimer started) timestamped `13:22:10`.
- A `--ping` sent immediately before the report arrived had already returned a
correct, real response (`iClone [8, 73, 5509]`, embedded Python `3.8.8`) — data
that can only come from a running `RApplication` call inside iClone.
- Every subsequent test in this session (all six acceptance criteria) passed cleanly
against the same running instance with no further restart.
Root cause of the dialog was not conclusively identified, but the plugin was **not**
affected by it: `initialize_plugin()` ran, bound the socket, and served requests
successfully throughout. Plausible explanations (unverified): the dialog belonged to
one of the stock plugins (AIStudio/MotionLIVE/VideoMocap) and was misattributed by
name similarity in a quick read, or it's a generic Reallusion update/compatibility
notice unrelated to plugin load success. No code changes were made in response to
this report since the evidence contradicted the failure hypothesis; no second restart
was requested. Recommend Jeremy re-check next time whether the dialog names
"TinqsBridge" specifically, and if so, ask him to read the exact wording verbatim
before further diagnosis.
## Deviations from the plan
- Skipped the optional **Plugins ▸ Tinqs Bridge** menu entry (`RUi.AddMenu` /
`shiboken2`) — plan explicitly allowed skipping if it costs more than ~20 minutes.
The log file (required) fully covers the status surface.
- Added `dispose_plugin()` (not explicitly requested) for clean socket/timer teardown
on unload — logs shutdown + total requests served. Low-risk addition consistent
with the guardrail against silent failures.
- Used `str.format()` instead of f-strings throughout `main.py`, specifically to
avoid relying on Python 3.12-only relaxed f-string grammar that could differ under
3.8 — a conservative choice made before the actual embedded version (3.8.8) was
confirmed by this session's ping.
- A stray `__pycache__/*.pyc` (generated locally by `py_compile` under host Python
3.12) was copied by the installer on its first run; it was harmless (iClone never
reads a 3.12 pyc) but was deleted from both the repo source and the installed copy
before the final install, and the installer was re-run to confirm a clean,
single-file (`main.py`) payload.
## Known limitations
- **UI freeze during exec**: submitted code runs synchronously on iClone's Qt main
thread; iClone's UI (and the bridge's own responsiveness to other requests) blocks
for the duration of any `--exec`/`--file` call. Fine for short operations; avoid
long sleeps/loops.
- **No auth beyond localhost bind**: any process on the machine can connect to
`127.0.0.1:18800` and execute arbitrary Python with full RLPy access. This is the
documented security model (see `docs/iclone-bridge.md`) — do not bind to anything
other than `127.0.0.1`.
- **One request per TCP connection**: the wire protocol is not pipelined; each CLI
invocation opens a fresh socket. This is by design (plan §2) and was exercised by
acceptance criterion 6.
- **JSON-serialization fallback**: RLPy objects generally are not JSON-serializable;
`result` falls back to `repr()` in that case, which is often not round-trippable —
fine for human-readable output, not for structured chaining of RLPy object handles
across separate `--exec` calls (though the persistent namespace means an object can
still be *referenced by variable name* across calls without ever being serialized).
- **Stale port handling**: `SO_REUSEADDR` plus a clear bind-failure log line are in
place, but a truly stuck listener from a crashed prior iClone process would still
require killing that process manually (never automated per guardrails).
- **Scene query test result was trivial**: only one prop ("Shadow Catcher") was in
the scene during smoke testing — no avatars were loaded, so the acceptance test
validates the query *mechanism* (valid JSON list, no error) rather than exercising
avatar-name output specifically. Worth re-running with an avatar loaded before
relying on this for real motion/scene-driving work.
@@ -0,0 +1,534 @@
# Handoff: kapa haka garments — MD side done, downstream not started
**From:** MD session, 2026-07-31 · **Repo:** `C:\Users\Jeremy\tinqs\animation`
**Read first:** `.claude/skills/marvelous-designer/SKILL.md`, `.agents/wiki/architecture/clothing-lane.md`, `clothing/README.md`
> ### ⚠️ NAMING CHANGED — no cultural names on test garments (Jeremy, 2026-07-31)
> The garments this doc calls **piupiu v3** and **pari v4** ship as **`bottom_test1_f`** and
> **`top_test1_f`**. Cultural names (piupiu, tāniko, pari, pareu, kapa haka) are phased out of
> **outfit IDs, set names, asset filenames, textures and UI labels** from here on: a
> first-pass test asset carrying a real cultural name asserts an accuracy it does not have,
> and the name then propagates into places that are expensive to walk back. A garment can
> earn its real name once the form is right and someone with the standing to say so signs
> off. Canonical statement: `ariki-game/src/Character/OutfitCatalog.cs` class doc.
>
> **Applies to the delivery, not to history.** MD source filenames
> (`lena_piupiu_v3.*`, `md_piupiu_v3.py`, `lena_pari_v4.*`) stay as they are — they are
> versioned, referenced by the recipes and by `backup/2026-07-31-pre-v3/MANIFEST.md`, and
> `tools/tailor/` is untracked, so that backup is the only safety net. Rename nothing there.
> New files from here take the new scheme.
>
> **Case matters — two conventions, don't conflate them:**
> item IDs are **snake_case + gender suffix** (`bottom_test1_f`); set ids are **PascalCase**
> (`Test1`) because they compose asset filenames as `{Gender}_{Set}_{Slot}.gltf`; configs,
> textures and set folders are **lowercase** (`bottom_test1.json`, `outfits/test1/`).
>
> Concretely, for §2.A/§2.B below:
> - config `clothing/configs/bottom_test1.json` (not `piupiu_v3.json`), `top_test1.json`
> - `export.set` → **`Test1`** · `export.out_dir` → `…/ariki-game/assets/quaternius/outfits/test1`
> - so outputs are `Female_Test1_Legs.gltf` / `Female_Test1_Body.gltf`
> - texture → `bottom_test1.png` (was `piupiu.png`)
> - the `register` stage should emit
> `Add("bottom_test1_f", "Bottom Test 1", OutfitSlot.Legs, "Legs", …, set: "Test1", gender: 1);`
> - ariki-game catalog entries `top_test1_f` / `bottom_test1_f` are already written and
> commented out in `OutfitCatalog.cs`, awaiting these files
## 🔁 REPRODUCING THIS, AND THE BODY QUESTION — 2026-08-01
### How to rebuild it: `python clothing/reproduce_test1.py`
`garment.py` **cannot** express this build. The skirt's weights are owned by
`skirt_garment_weights.py`, which must run **after `export` and before `import`**;
re-running the pipeline the obvious way (`--from census --to import`) silently ships
`garment_pipeline.py`'s 2-segment blend and reads as a rig regression. The new script is
the enforcement, and it reads the tuning from the config's `post_export` block so the four
env vars can't drift or be forgotten. `--dry-run` prints the sequence; `--texture`
regenerates the texture first.
### How this body differs from the regular Lena — it is ONLY the skeleton
| | regular `Ariki_Female_QuatSkin.glb` | `..._SkirtRig_4seg.glb` |
|---|---|---|
| nodes | 67 | 99 |
| skin joints | 65 | **97** |
| skirt bones | **0** | **32** (8 strands × 4 segments) |
| mesh | `Lena_Female`, 32713 v | `Lena_Female`, 32733 v |
| material / images | `MI_Body_Lena`, 1 | identical |
Same mesh, same material, same texture. The **only** difference is 32 extra bones
parented to `pelvis` plus the matching joint list. Consequences:
- **`BODY_OVERRIDE` is mandatory.** The garment's skin references `skirt_NN_0M` by name; on
the standard body those bones do not exist, so the hem gets no weights and collapses.
- **It is a side file on purpose** (their wiki): a garment's skin must carry every bone it
references, so body and garments have to change segment count together. There are now
three variants — `_SkirtRig` (16), `_SkirtRig_4seg` (32), `_SkirtRig_32x5` (160).
- **To ship on real characters the ring must be promoted onto the canonical body.** Until
then Test1 is test-bed-only. `_SkirtRig_32x5` (32 strands × 5 segments) is newer than the
wiki and unreferenced by the catalog — **ask the rig session** whether it supersedes 4seg
before building against it; our garment's 32 geometric strands would map 1:1 to it.
### Look: what was fixed, and what is left
**FIXED — the texture must be V-only.** `bottom_test1.png` was authored for a solid panel:
it painted its own vertical strand lines (now doubled up with the geometry) and its band
region varied along U (measured horizontal stdev **6883** per row vs 013 on clean rows).
Every geometric strand samples a different U slice, so the woven bands misaligned
strand-to-strand and read as noise. Now generated by
`tools/tailor/textures/make_bottom_test1.py`, which collapses each row to its median
(keeping the vertical rhythm, killing the horizontal variance) and adds back low-contrast
fibre striation: **mean horizontal stdev per row 50.2 → 5.9**. In game the bands now read
as continuous woven rows. Original kept as `bottom_test1_prestrand.png`.
**The rule generalises: any strand/gappy garment needs a V-only texture.**
**CHECKED, NOT A BUG — the "translucency".** At 1:1 it is *gaps*, not alpha: strands are
opaque and you see body and the inside of the back strands through the spaces. Materials
are correct — `doubleSided: true`, no `alphaMode` (opaque), textures RGB with no alpha.
**STILL OPEN**
1. **The top's neckline gape** — the biggest remaining visual defect. Present in game, not
in MD, so it is skinning/deformation (G6 class), and G5 does not catch it.
2. Fibre striation reads as faint dashes at extreme zoom — lower `STRIATION` or raise
`STRIATION_PERIOD` if it ever matters.
3. `top_test1.png` has NOT been given the same treatment; the top is a solid panel so
V-only does not apply, but its 39-vert rest penetration is unexamined.
---
## ✅ LEG CLIPPING FIXED IN THE BED — 2026-08-01
Jeremy: "the legs are clipping through the skirt still … what should happen is the skirt
moves with the leg but you never see the leg skin." Fixed; verified across Walk **and**
Dance, front and back.
**My first read was wrong.** I looked at a small render and called it "strands parting
correctly". Zoomed in, the thigh was punching straight through and being drawn on top of
the cloth. It was real clipping.
**Root cause was mine, and it was not the strand rebuild.** The rebuild changed
CONSTRUCTION but never added CLEARANCE — measured cloth radius 0.190 hip / 0.201 thigh /
0.202 knee against a body of 0.180.22, i.e. still the 13 cm ease the skirt-rig session
flagged at the very start. Strands made the gaps *legitimate*; they did not move cloth off
the leg.
**Two changes, both needed:**
1. **`fit.hem_mm` 14 → 80.** Gives the skirt a real BELL the leg swings *inside*, instead
of cloth lying on the thigh for the leg to punch through. Graded 0 at hip → full at hem,
so the waistband still rides the hip. **45 was not enough; 80 holds.**
2. **Thigh follow 0.128 → 0.698**, via `SKIRT_HEM_FREE=0.02 SKIRT_FOLLOW_MAX=0.70
SKIRT_CONTACT_R=0.22 SKIRT_FALLOFF=0.14`. `HEM_FREE`'s default of 0.55 put this skirt's
whole KNEE region (hem y 0.451, knee y 0.517) inside the no-follow band. This is the
influence that makes the skirt *travel with the knee* — exactly what Jeremy described.
**⚠️ TOLD THE SKIRT-RIG SESSION:** I added **env overrides** to their
`clothing/skirt_garment_weights.py` (`FOLLOW_MAX / CONTACT_R / FALLOFF / HEM_FREE`).
**Defaults are unchanged**, so `kapahaka_legs_sb_f` and anything else is unaffected — but
they own that file and should know. Their comment explains `FOLLOW_MAX` was cut 0.55 → 0.20
because the hem rode up on the swing; that tuning was against the SHORT continuous piupiu,
and this longer strand skirt needs different numbers. Per-garment tuning is why the
overrides exist rather than a changed default.
**⚠️ ORDER-SENSITIVE MANUAL STEP.** The weights command is recorded in
`configs/bottomTest1.json` under `post_export`. It must run AFTER `export` and BEFORE
`import`, and re-running the pipeline without re-running it silently ships the 2-segment
blend. Nothing enforces this — `garment.py` has no stage for it.
**Not chased:** the cloth reads slightly translucent in-game (legs faintly visible through
strands) — likely a material/alpha or single-sided issue, not clipping. And the **top's
neckline gape is still open** (below).
---
## ✅ IN GAME — downstream complete 2026-07-31
Both garments are through the pipeline and **on Lena in the clothing test bed**, all three
clips. Configs: `clothing/configs/bottomTest1.json`, `topTest1.json`. Set `Test1` →
`ariki-game/assets/quaternius/outfits/test1/Female_Test1_{Legs,Body}.gltf`.
Spawn (BODY_OVERRIDE is mandatory — the 4-segment ring only exists on that body):
```bash
SESSION_ID=md-clothing MOCK_ONLY=1 SCENE=clothing_test_bed BED_GENDER=1 SKIRT_RIG_DEBUG=1 BODY_OVERRIDE=res://assets/quaternius/derived-bodies/Ariki_Female_QuatSkin_SkirtRig_4seg.glb WAIT=1 bash tools/game.sh spawn
```
The bed already defaults to this set (`DefaultBodyF/LegsF` in `ClothingTestBed.cs`).
### What worked
- **Placement needs NO z_nudge.** `final_top = bone_head_z(top_bone) + z_nudge`, so with
`z_nudge = 0` the bottom lands band-top 1.076 / hem 0.451 and the top lands hem 1.052 —
all inside their QC targets. The old configs' 0.037 / 0.0483 were compensating for the
older, badly-placed drapes. Don't copy them forward.
- **G2 PASS** on both. **G8 PASS** (38 entries / 38 assets). **G7 PASS** on both reimports.
- **The bottom reads correctly in game** across Idle / Walk / Dance: strands hang, part
around the moving leg, the waistband rides the hip, no hitching to one side. Leg visible
between strands reads as intentional — the design premise holds up in motion.
- `clothing/skirt_garment_weights.py` (it MOVED here from ariki-game) rebound 32 joints /
8 strands × 4 segments with **no dead strands** and a 1.96 heaviest:lightest ratio.
### Two things that need knowing
**1. `planar_deg` must be 0 for strand garments.** `reduce` applies a planar DISSOLVE
*unconditionally*, before the tri-budget check. The default 5 collapsed the bottom from
3012 → **884 verts** on flat strands, destroying the vertical rows the 4-segment ring needs
to bend (the rig session's ask 4). The 8000-tri budget was never the constraint (4136).
**2. G5 is NOT predictive for a skirt-boned garment — do not read its number as truth.**
At full density G5 measures the bottom at 259357 penetrating verts in Walk, *worse* than
v2's 61169. Two reasons that number is not the shipped behaviour, and the in-game result
disagrees with it:
- It measures `garment_pipeline.py`'s **2-segment** `strand_weights`, which
`skirt_garment_weights.py` then overwrites (their wiki: skirt weights have ONE owner).
- Blender **cannot run the spring simulation**, which is the thing that moves strands
out of a leg's way.
The renders confirm the penetration is geometrically real *in that pose with those
weights* — it is simply not the pose/weights the game uses. **The bed is the only valid
test for this garment class.** Note also the earlier 4648-vert reading was taken at 884
verts and is not comparable to v2's 2573-vert baseline.
### The one real defect: the top's neckline gapes in game
Not present in MD — the MD drape is a modest rounded scoop; in game it is a plunging V
that opens down the sternum, exposing the body's tank strap and skin. This is the same
**G6-class in-game defect** the earlier plan recorded for the old pari and explicitly noted
"did NOT reproduce in the Blender checkpoint". It has now reproduced on the new top, so it
is a property of the skinning/deformation, not of the authoring. G5 does not catch it (its
39-vert rest reading is a different measurement). **This is the next job.** Suspect the
free neckline edge taking body weights across the sternum via `weights: "dress"`.
---
## ⏸ RESUME HERE — state at 2026-07-31 end of session
**MD bridge is stopped.** Both garments are exported and their recipes reproduce them.
Nothing has been through the downstream Blender pipeline, so nothing is in the game and
there are no penetration numbers for either mesh.
| | file | state |
|---|---|---|
| bottom (`bottomTest1`) | `tools/tailor/lena_piupiu_v3.*` | 32 discrete flax strands. **Re-opened and fixed 2026-07-31** — see below. 3022 verts, hem 0.461 m (in tolerance), band top 1.086 m (6 mm over; take `align.z_nudge` 0.037 → 0.031). Front/back/side all clean and symmetric. |
| top (`topTest1`) | `tools/tailor/lena_pari_v4.*` | 1406 verts, hem 1.082 m. Five defects found and fixed. Two cosmetic issues remain. |
**Bottom — what the resumed pass found.** Two hypotheses were wrong and the third was
a bug I had introduced:
1. *Inside-out back panel* (the top's disease) — **not present.** Untextured, both front
and back read white. Worth knowing why: the skirt arranges on `Leg_Skirt_*` and the
top on `Body_*_Center_1`, and those families evidently differ in orientation.
2. *The inherited `50/0` arrangement split* — **not a bug here, and load-bearing.** Swept
all four combos with a validated control: `50/0` and `0/50` both drape (band top 1.093
/ 1.095) while `50/50` and `0/0` drop the skirt **on the floor** (0.10 / 0.14). For
`Leg_Skirt_*` the x values must DIFFER; for the top's `Body_*_Center_1` they must
MATCH. **Never harmonise the two recipes.** Both skills were corrected — the rule as I
first wrote it was wrong.
3. *The actual defect* — my comb outline was **asymmetric**. The leftmost tooth got a
half-gap and the rightmost did not (the loop skipped its band-bottom step point on the
first iteration), and the closing band edge was **slanted** rather than vertical. The
fingerprint said so — `[60.0, 60.075, 60.0, 60.075]` — and I dismissed the 0.075 as
rounding. Fixed: all four edges now measure 60.0, the side-seam notch is gone, and
placement improved as a side effect (band top 1.097 → 1.086).
Residual on the bottom: a small grey wedge where the band's unfinished top edge curls,
on both sides. It sits under the top garment's hem, so it is likely invisible in game —
confirm rather than assume.
**Top — the open question, and it is well-defined.** The remaining diagonal back crease
IS solved in principle: it is the back panel being inside out, and `MIRROR_BACK = True`
plus the `PAIRS` index remap eliminates it completely. It is not shipped because it
changes the fit and coverage regresses (510 under-covers, 560 collapses). **The v5 job is
a fitting pass, not a debugging one:** enable the mirror, then re-fit `W_BACK` / `TAPER` /
`TOP_BACK` from scratch somewhere between 510 and 560. Details in §1.
**Recommended next move — do this before more MD time.** Run both garments through the
downstream pipeline (§2.A, §2.B) and see what survives `fit` and decimation. Some of the
remaining cosmetic issues may not reach the game at all, and G5 will give the first real
number on whether the strand rebuild fixed the leg clipping.
**New skill content written this session:**
`.claude/skills/marvelous-designer/references/tooling.md` — which API call for which job,
verified signatures, the render/measure toolkit, the seam-pairing decision table, harness
patterns. `SKILL.md` was also corrected: its strengthen rule and its seam-pairing rule
were both stated unconditionally and are actually conditional.
---
You are picking up a garment job mid-flight. The **upstream (Marvelous Designer) half is
finished and exported**; the **downstream (Blender → ariki-game) half has not been run at
all**. Nothing from this session is in the game yet, and no penetration numbers exist for
the new meshes. Your job is mostly downstream, plus one blocked handoff to another agent.
---
## 0. Orientation — one thing everyone gets wrong
**The garment sources live in THIS repo, at `tools/tailor/`.** A concurrent session
concluded "there is no source file for this garment, only the exported glTF" and proposed
recreating it. That was wrong — they searched `ariki-game`. The game mesh
`Pattern_31891` is literally the front-panel group inside
`tools/tailor/lena_piupiu_v2_garment.obj`. Every garment has a `.zprj` (editable source)
plus a per-garment recipe script `tools/tailor/md_*.py`. Never recreate; re-export.
**Backup:** everything as it stood before this session is in
`tools/tailor/backup/2026-07-31-pre-v3/` (55 files, 86 MB, SHA1-verified, with
`MANIFEST.md` carrying restore commands). `tools/tailor/` is **untracked in git**, so that
folder is the only safety net. Don't delete it, and don't `git add` the 9.7 MB `.zprj`
files casually.
---
## 1. What this session produced
Two new garments, both draped on `Ariki_Female_QuatSkin.glb` and exported four ways
(`.zprj` / `_garment.fbx` / `_garment.obj` / `.zpac`) into `tools/tailor/`.
### Piupiu v3 — rebuilt as discrete flax strands
`lena_piupiu_v3.*`, recipe `tools/tailor/md_piupiu_v3.py`, render
`tools/tailor/screenshots/lena_piupiu_v3.png`.
v2 was a taut pencil wrap — waist cut 1000 mm against Lena's 1095 mm hips, i.e. tighter
than the hip it passes over, leaving 13 cm of ease. G3 measured 8 mm penetration **at
rest**; G5 measured 216231 verts up to 8.5 cm deep across Idle/Walk. v3 is 32 discrete
strands hanging from a rigid 60 mm waistband (Jeremy's call, 2026-07-31), built as two
comb-shaped panels with the strands cut into the outline and only the two band side edges
sewn. Rationale: a real piupiu *is* strands, gaps make "leg visible between strands"
correct rather than a defect, strands can't crease like a tube, and 32 strands = 4 per
bundle on the rig's 8-strand ring.
Measured off the exported OBJ:
| | v2 | v3 | target |
|---|---|---|---|
| verts (pre-decimate) | 3686 | **3012** | — |
| waistband top | — | **1.089 m** | 1.05 ±0.03 |
| hem | — | **0.468 m** | 0.45 ±0.04 |
| verts above bone ring (y 0.952) | 40% | **22.8%** | lower is better |
Band top is 9 mm over tolerance — absorb it downstream by taking
`align.z_nudge` from `0.037` to `0.028`, which also lands the hem at ~0.459.
### Pari v4 / topTest1 — flat front and back
`lena_pari_v4.*`, recipe `tools/tailor/md_pari_v4.py`, renders
`screenshots/lena_pari_v4.png` (front) + `lena_pari_v4_back.png` (back).
Sweep harness kept at `tools/tailor/md_pari_seamsweep.py`.
**ROOT CAUSE — a twisted side seam, not surplus cloth.** `md_pari.py` sews all four
seams with `AddSeamlinePairGroup(..., False, False)`. For the SIDE seams that is wrong:
it twists the panel, so part of the back turns inside out and rucks up, leaving the
lower back bare. The skill's rule reads "same line index on **mirrored** front/back
panels with `(False, False)`" — but `panel()` returns the *identical* point list for
both panels, only offset by dx, so they are **not mirrored**. Front-right and back-right
run in opposite directions once wrapped on the body, and joining them needs both edges
reversed: **`(True, True)`**. Proven by sweeping all four combos at fixed geometry:
`(0,0)`, `(0,1)` and `(1,0)` all produce the reverse-face flap; only `(1,1)` is clean.
Shoulder seams stay `(False, False)` — they were always right, which is why the front
never looked wrong. `md_piupiu_v3.py` dodged this by pairing right↔left.
**Fourth fix — back neckline height.** `SCOOP_BACK` was 45 mm, which put the back
neckline BELOW the body's baked-in tank neckline, so the tank showed as a cream band at
the back neck — it reads as the shirt not meeting the back and clipping through. Covering
that baked-in underwear is the entire reason the straps exist (per the skill). `SCOOP_BACK
= 15` closes it.
**Third fix — symmetric arrangement.** `md_pari.py` calls
`SetArrangementPosition(pf, 50, …)` but `SetArrangementPosition(pb, 0, …)`. That 50/0
pair is asymmetric, and it produced a folded flap on the **right shoulder only** (left
shoulder clean) — the cloth's reverse face poking through, which reads as the shirt
clipping. An asymmetric defect needs an asymmetric input and this was the only one. Both
panels now use `x = 50`. **`md_piupiu.py` carries the same 50/0 pair**, so check the
bottom for the same defect when you next open it — it looks inherited, not intended.
**Second fix — taper.** The panels were rectangles: cut for a 1083 mm bust while the hem
sits on a 675 mm waist, so ~400 mm of surplus could only fold. `TAPER = 95` mm off each
side edge at the hem gives 380 + 320 = 700 mm vs waist 675 + 25 ease. Use the SAME taper
on both panels — side-seam length is `sqrt(TAPER² + SIDE_Y²)` = 208 mm on both, so the
whole-edge pairing stays matched; different tapers skew it.
| | v3 shipped | v4 |
|---|---|---|
| verts | 1889 | **1406** |
| hem | — | **1.082 m** (target 1.05 ±0.03) |
| back | reverse-face flap, bare lower back | flat, fully covered, hem level |
**Do not chase "band top 1.31 m".** That target predates the straps — it was written for
the original strapless tube. With straps, max-z is the strap over the shoulder (~1.44 m;
`shoulder_z` is 1.397). The target is stale for this design, not a failure.
**Tried and reverted, with reasons, all recorded in the recipe header:** width rebalance
(570/510 kept as correct tailoring but it was NOT the cause; 600/480 dragged the front
motif onto the back), `SetPatternStrengthen` (flattens the back but rotates the garment,
because the sides are sewn only over y 0185 and the straps are 90 mm — so the failure
catalogue's soft-from-frame-0 rule applies only to garments with fully sewn sides),
shortening the back panel to `TOP_BACK 315` (good theory, changed nothing), and crossing
every seam (garment slides to the hips — cross-wired straps cancel).
**THE BACK CREASE IS SOLVED IN PRINCIPLE BUT NOT SHIPPED — read before touching it.**
The persistent diagonal crease across the back was the back panel being **inside out**.
Cloth renders WHITE on its front face and GREY on its back; untextured, the front view is
white but the back view is grey, because both panels are drafted with the same winding, so
when MD arranges one behind the body its front face points INTO the body.
`MIRROR_BACK = True` **plus** remapping the seam pair indices (the `PAIRS` block —
front `shoulder_l` ↔ back `shoulder_r`, front `side_r` ↔ back `side_l`, flags back to
`(False, False)`) **eliminates the crease completely** — the back comes out flat and
smooth. That is the proof of cause.
It is NOT shipped because it changes the effective fit and coverage regresses:
- `W_BACK 510` + mirror → flat back, but the panel under-covers: the body's tank shows at
the lower back and the front panel wraps round the sides.
- `W_BACK 560` + mirror → collapses into a diagonal band. Worse than either.
A correct v5 enables the mirror and then **re-fits `W_BACK` / `TAPER` / `TOP_BACK` from
scratch**, somewhere between 510 and 560. Shipped state is un-mirrored, because coverage
beats a crease. This is also why `SIDE_FLAGS` must stay `(True, True)` — it is compensating
for the un-mirrored panel.
**Also tested and rejected:** a concave armhole scye (`ARMHOLE_IN`, mid-armhole point
pulled inward, indices computed rather than hardcoded). Jeremy's hypothesis was that the
crease was surplus width bunching where the panels meet, and the arithmetic supported it —
bust and hem are matched (1080 vs 1083; 700 vs 675) but the panels stay near-full width
above the bust as the body narrows, ~968 mm of cloth at underarm height, with the side seam
only running y 0..185 so the edges above it are free. At `ARMHOLE_IN = 55` the crease
survived **unchanged**, so surplus upper width is not the cause. Left at 0.
**Remaining in the shipped state:** the diagonal back crease, and a small patch at the
lower centre back where the body's tank shows below the hem.
**Harness warning:** `tools/tailor/md_pari_armsweep.py` is BROKEN — its control case
(`arm00`, which should reproduce the working garment) drops the garment on the floor, so
all four of its results are void. `md_pari_seamsweep.py` worked. Don't trust armsweep
output until the harness is fixed; iterate in `md_pari_v4.py` itself, which is known good.
The pari's *other* known defect — the in-game neckline gape — **did not reproduce** in the
Blender checkpoint, so it is G6/in-game territory (attach, material, or LOD), not
authoring.
---
## 2. Work outstanding, in priority order
### A. Run piupiu v3 through the downstream pipeline and get a G5 number — the proof
Nothing about v3 is validated in-game. This is the payoff and it's fast (`census..export`
ran in 26 s in the pipeline's own acceptance test).
1. Write `clothing/configs/piupiu_v3.json`. **Do not edit `piupiu.json` or
`piupiu_sb.json`** — a concurrent session owns those. Copy
`clothing/configs/tests/piupiu_sb_test.json` (it already has an `expect` block and a
**scratch** export target) and change:
- `source` → `tools/tailor/lena_piupiu_v3_garment.fbx`
- `align.z_nudge` → `0.028` (see above)
- `expect.islands` → **rewrite from the new census, don't reuse v2's numbers.** v2's
block expects 34003800 verts; v3 is 3012.
2. **Island mapping is the live hazard here.** MD renumbers patterns every session — the
v3 groups are `Pattern_18856` / `Pattern_20333`, where v2's were `Pattern_31891` /
`Pattern_32004`. `clothing/README.md` calls a config mapping the wrong islands "the
silent killer", and G2 exists to catch it. Read `work/piupiu_v3/census.json` and map
from that, not from memory.
3. Run it, then read the G5 output and compare against v2's baseline
(`rest 1 vert / Idle 216231 verts / Walk 61169 verts`, max 8.5 cm).
4. **Set expectations honestly when you report:** clearance alone cannot fix this. The
hem target sits 0.495 m below the hip pivot, so 30° of hip flexion sweeps the leg
~25 cm forward — more than any believable piupiu silhouette clears. v3 should
eliminate the rest-pose failure and cut the motion numbers a lot; the runtime
spring-bone capsules do the last part. Don't claim more than the numbers show.
### B. Run pari v4 / topTest1 downstream
New config `clothing/configs/topTest1.json`; leave the shipped `pari.json` intact.
1363 verts. The seam fix means the back geometry changed substantially from v3, so
rewrite `expect.islands` from the new census rather than reusing v3's numbers — same
hazard as §2.A. The in-game neckline gape is G6 territory (see §1).
### C. ~~Unblock the skirt-rig session~~ — DONE on their side, your move now
**Step 1 is already delivered.** They shipped the 32-skirt-joint body:
```
ariki-game/assets/quaternius/derived-bodies/Ariki_Female_QuatSkin_SkirtRig_4seg.glb
97 joints (65 + 8 strands × 4 segments) · fracs 0.15/0.20/0.30/0.35
collision particles at y 0.884 / 0.793 / 0.656 / 0.497
```
It is a **side file on purpose** — a garment's skin must carry every bone it references, so
a body and its garments have to change segment count together, and the live 2-segment body
must keep working until a 32-joint garment exists. Point your config's `body` at `_4seg`;
they promote it over the canonical name once your export validates.
⚠️ **The tool paths in this section moved repos.** Clothing authoring now lives in THIS
repo — ariki-game receives delivered outfits and no longer holds authoring code:
| Was (ariki-game) | Now (this repo) |
|---|---|
| `tools/make_skirt_rig_body.py` | `clothing/skirt_rig_body.py` |
| `tools/skirt_garment_weights.py` | `clothing/skirt_garment_weights.py` |
Both resolve asset paths into the game checkout as a sibling; override with
`ARIKI_GAME_ROOT`. Segment count is now `SKIRT_SEGMENT_FRACS=0.15,0.2,0.3,0.35` (env) —
no source edit needed to build a different ring.
So the remaining sequence is entirely yours: **re-run the pipeline against `_4seg`** (the
glTF exporter emits every armature bone as a joint, so the skin picks up all 97
automatically — no pipeline change), **then run `clothing/skirt_garment_weights.py`**,
which reads segment count off the rig and needs no changes.
Also tell them: v3's 32 strands are uniform strips, which answers their ask 4 (azimuthal
density was varying 56:1) **by construction**, and the 60 mm band answers ask 5 (40% →
22.8% of verts above the ring).
### D. Pipeline bug worth fixing: G5 measures weights the game never uses
`clothing/garment_pipeline.py:876` `strand_weights` is hardcoded to two segments (root +
one tip). The game's actual weights come from `skirt_garment_weights.py`, run afterwards,
which overwrites them. So **G5's verdict and the test bed will disagree** until the
pipeline's own blend goes N-segment. Not urgent for shipping, very confusing if unknown.
Worse than a disagreeing gate, now that both tools live in this repo: **whichever runs last
wins and nothing enforces the order.** Re-running the pipeline after the weights tool
silently ships the 2-segment blend and reads as a rig regression. Agreed resolution —
**skirt weights have ONE owner, `skirt_garment_weights.py`.** For `weights: "skirt_bones"`
configs the pipeline should skip strand weighting entirely (or emit zeros) and G5 should
measure the file *after* the weights tool runs. Alternative, equally fine: port the
N-segment blend into the pipeline and delete the separate tool. Either way, one owner.
### E. Fold the findings into the docs
The MD skill's API-facts table and failure catalogue should gain the items in §3 —
especially the no-rear-view discovery, which is the reason a back defect shipped at all.
---
## 3. Verified traps (all cost time this session)
| Fact | Consequence |
|---|---|
| **`SetCamViewPoint` has NO rear view.** All 8 shot: 0 bottom, 1 & 3 front quadrants, 2 front, 4/6/7 sides, 5 top. | `ExportSnapshot3D` structurally cannot show the back. Every screenshot in this repo is a front view, which is how the pari's ballooning back shipped. Use `export_api.ExportTurntableImages(4)` — **index 2 is the back**. |
| `ExportTurntableImages` ignores its path arg in the `(int)` overload | It writes into MD's own output folder (`%LOCALAPPDATA%\CLO Virtual Fashion\Marvelous Designer Personal\<n>\output*.png`). The return value is the only way to find the files. The `(path, count, w, h, i)` overload returned `[]`. |
| `SetArrangementPosition` takes **4 ints** | A float raises `TypeError`. |
| Skirts arrange on **`Leg_Skirt_Front` / `Leg_Skirt_Back`** with y=92 | v2 used `Body_*_Waist` and G1 measured it +8/+14 cm high. Switching fixed placement to within 9 mm. |
| A light garment **does not slide to the waist** | A 60 mm band has none of v2's solid-panel mass, so it stays exactly where it's arranged. Arrangement height *is* the placement. First v3 drape sat at the chest. |
| **OBJ exports in mm; the FBX path reads as decimetres** | The census reads dm and `align.scale_z: 0.1` converts. The two exporters disagree — cost one wasted run. |
| `qc_placement.py` cannot measure a strand garment | Its pixel classifier needs a solid silhouette; on v3 it reported a 1.71 m span (nonsense). `md_piupiu_v3.py` now measures the exported OBJ geometry directly instead. |
| `fabric_api` has **no physics setters** | Stiffness comes from the `.zfab` preset. Stiff ladder: `V2_Woven_Canvas_1` < `V2_Woven_Denim_1` < `V2_Non-Fabric_Tyvek_1`. |
| `fit`'s `wrap_mode: "OUTSIDE"` only moves verts that are **inside** the target | Ease manufactured in MD survives the pipeline, and `hem_mm: 14` pushes too-close hem fabric further out. They're complementary, not fighting. |
| **`SetArrangementPosition` x must match across panels** | `md_pari.py` and `md_piupiu.py` both use front `x=50` / back `x=0`. That asymmetry folds one shoulder only, showing the cloth's reverse face — it reads as the garment clipping through. Use the same x on both panels. |
| **Untextured renders show face orientation** | Cloth is WHITE on its front face, GREY on its back. A garment panel showing grey from outside is inside out. This is the fastest way to tell a genuine fold from a winding problem — and a busy motif hides both completely. |
| **Seam reversal flags matter on non-mirrored panels** | `AddSeamlinePairGroup`'s two booleans reverse edge traversal. Identically-drafted (non-mirrored) front/back panels sewn same-index need **`(True, True)`** on the side seams; `(False, False)` twists the panel and turns part of it inside out. Diagnose by rendering UNTEXTURED — the cloth's reverse face is the tell. A busy motif hides it completely. |
| **The strengthen rule is not universal** | The skill says soft-from-frame-0 bunches, and strengthening *does* flatten the pari's back — but it also **rotates** the garment, because the pari's sides are sewn only over y 0185 and its straps are 90 mm, leaving it under-constrained. Strengthen applies to garments whose sides are fully sewn. Where it doesn't, remove surplus cloth instead. |
| Writing `tools/tailor/*.py` with Python's **locale codec corrupts them** | These files contain box-drawing and em-dash characters. Always `io.open(..., encoding='utf-8')`. A `open(p,'w').write(...)` cp1252 round-trip broke `md_piupiu_v3.py` and the bridge then failed to read it. |
---
## 4. Session mechanics
- MD needs a **human click** (Plugin → TinqsMDBridge) to start; you cannot start one.
MD's UI is frozen for the whole session ("Not Responding" is normal).
- The bridge was **stopped** at the end of this session, so MD is idle and usable. If you
need it again, ask Jeremy to click, then `python tools/md_bridge.py --ping`.
- `python tools/md_bridge.py --stop` when you hand back a result — Jeremy inspects in
MD's viewport and can't while the bridge holds the main thread.
- `Simulate()` needs a raised client `--timeout` (~1 min per 300 frames; use `--timeout 880`).
## 5. Don't break these
- `tools/tailor/backup/2026-07-31-pre-v3/` — the only copy of the pre-session state.
- `clothing/configs/piupiu.json`, `piupiu_sb.json` and the `Female_KapahakaSB_*` outputs —
the skirt-rig session owns them; they are the live comparison in the test bed and the
deprecated set gets deleted only once `Test1` validates. Write `Test1` to new configs and
a new `outfits/test1/` folder; leave these alone.
- v1/v2/v3 garment files — versioned, never overwritten. Keep it that way.
- `ariki-game` is read-only by convention for this repo's tooling; the `register` stage
emits a paste-in `Add(...)` line rather than editing `src/`.
+169
View File
@@ -0,0 +1,169 @@
# Plan v3: seated hearth dance (nd_02) — seated pose + salsa upper-body sway
**Status:** reviewed (independent Opus pass, GLM-4.6 API pass, Fable self-review),
awaiting Jeremy's go/route-confirmation · **Author + PC executor:** Fable 5 session 2026-07-21 (via
TinqsBridge) · **Mac-side steps:** later session
## Goal
A looping dance for a **hearth ceremony** — villagers seated around the hearth,
**seated the entire time** (lower body pinned to Jeremy's seated pose), **upper body
swaying through 4 Mixamo salsa clips**. Registry id: provisional **nd_02** (pack
`nd_02`, clip `ND02`, loop pair `nd_02_pp`/`ND02PP`); ceremony adoption is Jeremy's
call later (no hearth slot exists in the DanceType enum — closest candidate is
`fire_dance_01`, NOT assumed).
## §0 Inputs (confirmed by Jeremy 2026-07-21)
1. **Seated pose:** `A:\Reallusion Content\Reallusion Custom\Animation\Motion\External Motion\Male_Sitting_Pose.rlMotion` (39 KB — likely a single-frame pose; see Phase 1.2).
⚠ Provenance check at first eyeball: hearth circle implies **ground-seated**; if this
pose is chair-height the pelvis will float in-game.
2. **Sway sources:** the 4 Mixamo FBX in `C:\Users\Jeremy\Downloads\`:
`Salsa Dancing.fbx` (1.05 MB), `Salsa Dancing (1).fbx` (2.62 MB),
`Salsa Dancing (2).fbx` (1.84 MB), `Salsa Dancing (3).fbx` (0.55 MB).
3. **Avatar (for the seated export only):** any stock CC male.
4. **Length target:** 2030 s @ 60 fps overall; ~57 s per salsa segment.
## Route decision (changed in v2 after review)
**Primary is now Route B**: retarget the salsa FBX directly with the pipeline's own
`mixamo_retarget.py` (purpose-built, proven on `northern_soul`/`run_to_dive`) and
combine with the seated pose via the game-side **upper/lower mask composite**,
pre-baked to a GLB. iClone's role shrinks to its one irreplaceable contribution:
exporting Jeremy's seated pose as a clip (via the bridge).
Why (review consensus): Route A put two **unprobed** RLPy behaviors on its critical
path (`LoadMotion`-with-FBX — which even if it loads may skip Mixamo→CC
characterization and produce garbage — and `ExportFbxFile` option coverage), added a
lossy Mixamo→CC→game double-retarget plus the A-pose-bind risk, and then threw away
its own lower body at bake time. Route B is one proven retarget per source, iterates
via hot-reload JSON in the test bed (~0.5 s) instead of a full PC→Mac round-trip, and
gets beat-quantized transitions for free. Route A survives as fallback (§Route A) if
the salsa arm styling retargets badly onto the game skeleton.
**The lower-body guarantee moves into the composite bake itself** — the baked clip's
lower-mask tracks come from the seated clip and nowhere else. The separate
`lock_lower_body.py` from plan v1 is **dropped** (it only existed to repair Route A's
full-body take).
## Route B (primary)
### Phase 0 — salsa files into the repo (me, PC, now)
1. **Move** (not copy) the 4 FBX from Downloads into **`exchange/INBOX/`** (the repo's
existing external-input drop, alongside `lena_tpose_legs5_input.fbx`), renamed
`Salsa_Dancing_01.fbx``Salsa_Dancing_04.fbx`.
*Note:* plan v1 sent these to the Reallusion content folder on A:; review blocker —
the Mac retarget step can only see files that are **in the repo**. `.fbx` is
LFS-tracked here, so a normal commit + `tinqs push` carries them.
2. Commit (explicit paths only, co-author line
`Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>`), `tinqs push`.
### Phase 1 — seated clip out of iClone (me, PC, via bridge)
1. Launch check: `--ping`; if iClone is closed, start
`A:\Program Files (x86)\iClone 8\Bin64\iClone.exe`, poll ping (~6090 s).
Load a stock CC male (`RLPy.RFileIO.LoadFile`), apply
`LoadMotion(Male_Sitting_Pose.rlMotion, t=0, avatar)` — **check the returned
RStatus** (don't assume success), and verify a clip landed on the motion track.
2. **Ground-seat precheck (GLM review):** after applying the pose, read the pelvis/hip
world Y via the bridge and log it. Chair-height ≫ ground-height — if it reads like
a chair sit, stop and show Jeremy before wasting the export.
3. **Hold the pose ≥300 frames** (5 s @60): set the play range so the export is a real
take. Verified against code: `cc_retarget.py:104` rejects only `<5`-frame
`*TempMotion` takes (frame count, not motion content) — a 300-frame static hold
passes. Extend by re-placing the pose key at the range end (or Jeremy sets the clip
length in the timeline — one drag).
3. Export FBX → `exchange/incoming-fbx/seatbase1.fbx`. Probe
`RLPy.RFileIO.ExportFbxFile(*args)` for: Target Tool Preset **Blender**, 60 fps,
Range = All, **Include Motion ON**, Preserve Bone Names. This is the only
ExportFbxFile use left in the primary route — if the API can't reach the options,
**Jeremy clicks the export dialog once** and the route loses nothing.
4. Sanity: file is multi-MB-ish and ≥300 frames. Commit + `tinqs push` (etiquette as
Phase 0.2).
5. RTime/RFps handling: build times with RTime constructors from the stub; get numeric
fps via RFps methods, never repr.
### Phase 2 — Mac side: retarget everything (later session / handoff)
0. *Cheap check first (Path 1):* scan kevin/UAL packs for an existing ground-seated
idle — if one matches Jeremy's intended look better than the iClone export, offer
the swap at eyeball time; Jeremy's pose remains the default.
1. `mixamo_retarget.py` on the 4 salsa FBX →
`assets/quaternius/mixamo/salsa.glb` (clips `Salsa01..04` — batch mode per the
kevin precedent). **Read back the GLB's `animations[].name` immediately** and use
those exact strings in every later JSON/bake reference — don't assume the intended
names survived the batch tool (GLM review).
2. `cc_retarget.py` on `seatbase1.fbx``exchange/converted-glb/seatbase1.glb`, clip
`SeatBase1`. First-export QA: **A-pose arm check** in the showcase.
3. Copy `salsa.glb` into ariki-game (`assets/quaternius/mixamo/`), `seatbase1.glb`
`dancegen/`.
### Phase 3 — Mac side: composite, bake, loop, QC
1. **Iterate live first**: `nd_02.json` in `assets/dances/` with 4 chained moves, each
`{layers: [{mask:"upper", clip:"salsa/SalsaNN"}, {mask:"lower", clip:"seatbase1/SeatBase1"}]}`
— hot-reloads in the dance test bed (**ask Jeremy before launching the game**).
Tune segment order/durations/bpm here where a change costs 0.5 s.
2. **Pre-bake the approved composite to a GLB** (registry requires a baked pair):
generalize `tools/bake_run_punch.mjs` into a parameterized composite baker
(`tools/bake_composite.mjs`): inputs = ordered (upper-clip, duration) list + lower
clip; mask predicate **verbatim** from `bake_run_punch.mjs:103`
(`root || pelvis || /thigh|calf|foot|ball/i`); output `exchange/converted-glb/nd_02.glb`
clip `ND02`. **Verify gate built in:** assert every lower-mask bone track in the
output matches the seated clip's pose within an epsilon (per-channel max deviation,
e.g. 1e-3 — resampling/quantization makes strict equality false-fail; GLM review)
— exit nonzero otherwise (lesson from the boat prop: agent-authored gates must
assert what the eyeball would catch).
3. Loop pair: `pingpong_bake.py``nd_02_pp`/`ND02PP`. **Caveat (GLM review):**
ping-ponging a 4-segment composite plays A→B→C→D→D→C→B→A — the reversal is exposed
across *distinct* segments, not one homogeneous sway; it may read oddly even with a
clean seam. Mitigations, judged at the test-bed eyeball: bookend the bake with short
seated-base (arms-quiet) segments so `loop_fix.py`'s natural-loop trim becomes
viable, or reorder segments so the reversal lands on the most symmetric one. Both
variants must pass `loop_qc.py` (exit 0); nd_01 set the 0.00-seam bar.
4. Ship: GLBs + `.import` sidecars into ariki-game, final `nd_02.json` pointing at the
baked `nd_02/ND02` (+`_pp`), `game.sh import`, verify GLB `animations[].name ==
"ND02"`, registry row: origin "seated hearth sway — upper Mixamo salsa ×4, lower
iClone Male_Sitting_Pose (seatbase1)". ariki-game commits stay local.
## Route A (fallback only — if salsa retargets badly onto the game skeleton)
Assemble in iClone instead: import the salsa FBX **via the UI with the Mixamo
characterization profile** (the route that produced `Talking_mixamo_com.rlMotion`;
`LoadMotion(fbx)` may be probed as an optimization but is NOT assumed — loading
without characterization would splat raw `mixamorig:` tracks onto the CC rig), place
seated base + salsa segments + seated base on the timeline, Jeremy eyeballs, export
one take → `cc_retarget.py` → then a lower-body lock IS needed again: same §Phase 3.2
baker degenerates to it (upper = the take itself, lower = seatbase1) — no separate
tool. Known extra risks: transition hard-cuts via API placement, double-retarget
quality, A-pose bind.
## Acceptance
- `nd_02` and `nd_02_pp` pass `loop_qc.py` (exit 0).
- Bake verify gate proves lower-mask tracks constant == seated pose.
- Upper body visibly cycles all 4 salsa flavors without harsh cuts (test-bed eyeball).
- Ground-seat height looks right at the hearth (pose-provenance check §0.1).
- Registry row added; salsa sources + seatbase committed in this repo via LFS;
ariki-game not pushed.
## Review trail
- v1 reviewed 2026-07-21 by independent fresh-context Opus agent (GLM unavailable on
this PC): 1 blocker (Route B files never reach the Mac), 4 major (lock-script unspec'd;
frame-0 lock could freeze a standing pose; Route A's critical path rested on two
unprobed RLPy behaviors; Route A discards most of its own work), 4 minor (2-frame
guard on the seated export; loop-pair citation; co-author line; trim/pose-provenance).
All verified factual claims in v1 held.
- v2 by Fable: Route B promoted; lock tool folded into the composite baker's
verify gate; salsa destination changed from A:\Reallusion content to repo INBOX + LFS;
seated export spec'd ≥300 frames; kevin-pack Path-1 check added; co-author line added.
- v2 reviewed 2026-07-21 by **GLM-4.6** (direct API call — key from tinqs-docs/.env).
Accepted → v3: epsilon-tolerance verify gate (strict equality would false-fail on
resampling); pelvis-Y ground-seat precheck before export; read back salsa clip names
from the GLB instead of assuming; pingpong-reversal caveat for a 4-segment composite
+ bookend/reorder mitigations; check LoadMotion's returned RStatus. Rejected with
evidence: "LoadMotion needs LoadFile+ApplyMotion" (contradicts the verified stub
signature `RFileIO.LoadMotion(strFilePath, kTime, spObject)`, RLPy.py:13700 — still
probe-first per plan); "no Node toolchain exists" (`tools/bake_run_punch.mjs` is in
this repo and the gltf-transform route is documented in the animation-creation
skill); "static 300f take trips the no-motion guard" (cc_retarget.py:104 gates on
`<5` frames + `*TempMotion` name only); "Route A exports mixamorig bone names"
(characterized import retargets onto the CC rig before export).
+87
View File
@@ -0,0 +1,87 @@
# Handoff: UAL animation library → iClone (2026-07-28 morning)
## Goal
Get all ~134 Universal Animation Library (Kevin Iglesias) clips into iClone as reusable
motions on a character, so they can be layered/blended into new animations.
## ▶ FIRST THING TO DO (5 min, everything is staged)
Import this file into iClone via the **motion import** flow (select Lena or any avatar first):
```
C:\Users\Jeremy\tinqs\animation\UAL-Lib\Universal Animation Library 2[Source]\Unity\UAL2_takes_fixed.fbx
```
Dialog settings:
- **Character profile:** browse to
`A:\Program Files (x86)\iClone 8\Program\Assets\Share\CharaterizeProfiles\UE4_Skeleton.3dxProfile`
(folder name really is "CharaterizeProfiles" — Reallusion typo)
- **Motion T-Pose:** leave EMPTY. If iClone refuses without one, use
`...\Universal Animation Library 2[Source]\Female Mannequin\Unity\Mannequin_F.fbx`
(verified: identical 66-bone skeleton to UAL2)
- **Auto gen to perform list:** checked
- Keep root motion: default (RL_BoneRoot n/a here — source root is `root`)
- Select ONLY this one file (multi-selecting mixed-skeleton files causes the
"same bone structure" error)
Then check: character upright? → **right-click avatar → Perform** (or Animation Player →
Motion dropdown → Perform → Perform List Editor). Expect up to **134 takes** listed.
## Why this should work now (last night's chain of fixes)
1. **Original vendor FBX imports upright** (user-verified as prop; measured upright via
bridge). All Blender re-exports lie down — never route UAL through Blender for iClone.
2. **"No applicable character profile"** → fixed: iClone ships `UE4_Skeleton.3dxProfile`,
exact match for UAL bone names (pelvis/spine_0103/neck_01/clavicle_l/ball_l...).
3. **"Not compatible with profile T-pose"** → cause: `CC_Base_TPose.fbx` (a CC-skeleton
leftover from dead experiments) was in the Motion T-Pose field. Wrong skeleton. Leave
field empty / use Mannequin_F.fbx.
4. **"Failed to save motion file" ×134** → ROOT CAUSE FOUND: every take is named
`Armature|Bow_Shoot` — the `|` is illegal in iClone motion names & Windows filenames.
The error dialog listing UAL2.fbx 134 times proves iClone DOES enumerate all takes.
→ Fixed by byte-safe patch (`|``_`, same length, structure untouched):
**UAL2_takes_fixed.fbx** (134 takes verified, names now `Armature_Bow_Shoot` etc.)
## If the import succeeds
- Do the same for library 1: patch takes the same way
(`UAL-Lib\Universal Animation Library[Pro]\Unity\UAL1.fbx``UAL1_takes_fixed.fbx`).
One-liner pattern lives in this chat; simplest: ask Claude to "patch UAL1 take names
like UAL2".
- Save the converted motions into a custom motion library folder so they persist
(drag from Perform list / save as .rlMotion into
`A:\Reallusion Content\Reallusion Custom\Animation\Motion\`).
- Optional: `_RM` variants of both files carry root-motion versions of the same clips.
## If it fails again
Note the EXACT error text + which file was in each dialog slot, then resume with Claude.
Fallback order: (a) Motion T-Pose = Mannequin_F.fbx; (b) import a handful of takes at a
time; (c) split the patched FBX into single-take files WITHOUT Blender rig changes.
## Dead ends — do NOT retry (all disproven with measurements, see memory)
- Blender-retargeted CC_Base FBX into iClone (characterizer always lays it down;
rotations/axis flags/T-pose/root-motion settings all disproven).
- `RFileIO.ConvertFbxFileToRLMotion` on any Blender-built FBX (needs native fbxKey).
- `RFileIO.LoadFbxFile` via bridge for characters (pops a modal that freezes iClone
AND the bridge until manually dismissed).
- Direct bone-keying via bridge (48/55 bones OK, but legs+hip are HIK/IK-locked, no
API toggle → full-body clips come out leg-broken).
## Useful assets/tools produced
- `UAL2_takes_fixed.fbx` — the deliverable input file (134 legal take names).
- Bridge: `python tools/iclone_bridge.py --ping|--exec|--file` (iClone must be running).
- Bone-orientation measurement snippet (paste-ready): read `CC_Base_Head` minus
`CC_Base_Hip` world positions via bridge; up should be +Z.
- Scratchpad (this session) has: `ual_to_cc.py` (Blender UE→CC retargeter — works for
Blender/GLB targets, NOT for iClone import), calibration data for iClone's per-bone
Euler FK API (order XYZ right-to-left), `dummy_cc_base.fbx` (CC_Base skeleton export).
- Old CC experiments quarantined in
`A:\Reallusion Content\Reallusion Custom\Animation\Motion\Tests\old_cc_experiments\`
— safe to delete once the new route works.
## Context worth remembering
- UAL clips are on the UE4-mannequin skeleton; the whole library lives as 134 takes
inside ONE FBX (Unity folders). GLB equivalents (Unreal-Godot folders) already have
all 134 clips separated — the GAME pipeline can consume those directly without iClone.
- iClone is only needed for AUTHORING new motions from these clips; for pure playback
in ariki-game, the GLBs are already usable.