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:
@@ -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) | ~5–15k tokens per clip; ~20–40k per small batch | convenience wrapper that runs `loop_qc.py` and interprets numbers |
|
||||
| Claude running a full convert batch interactively | session model | ~50–150k 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.5–1d |
|
||||
| **P5 Docs + pilot** | `pipeline/README.md`; skills point at it; pilot batch: 4× `HumanM@Mining*.fbx` (bstyle) | P2–P4 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.
|
||||
@@ -0,0 +1,161 @@
|
||||
# TASK: Repair Lena's HIGH-DENSITY sculpt into a nude body (C–D 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 C–D 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.030–0.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 C–D cup.** Apex projection off the repaired chest wall between **0.036 and
|
||||
0.044 units** (6.5–8.0 cm real), targeting projection ≈ 0.9–1.0× the base RADIUS. Apex
|
||||
height z = 0.688 ± 0.010; apexes at x = ±0.033–0.038. Base diameter 0.075–0.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.485–0.495,
|
||||
0.583–0.593, 0.632–0.642, 0.680–0.690, 0.729–0.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.50–0.56, |x|<0.05,
|
||||
front) vs mid-thigh skin (z 0.28–0.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 C–D 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 (1−u²)^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.
|
||||
@@ -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 **68–83** per row vs 0–13 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.18–0.22, i.e. still the 1–3 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 259–357 penetrating verts in Walk, *worse* than
|
||||
v2's 61–169. 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 46–48-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 1–3 cm of ease. G3 measured 8 mm penetration **at
|
||||
rest**; G5 measured 216–231 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 0–185 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 3400–3800 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 216–231 verts / Walk 61–169 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 5–6: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 0–185 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/`.
|
||||
@@ -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:** 20–30 s @ 60 fps overall; ~5–7 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 (~60–90 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).
|
||||
@@ -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_01–03/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.
|
||||
Reference in New Issue
Block a user