feat: clothing lane, character sources, and DCC bridges

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:55:43 -07:00
parent 3363209cac
commit 3ba86b2ea8
558 changed files with 68622 additions and 8 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
reproduce_test1.py — rebuild the Test1 outfit end to end, in the ONE correct order.
python clothing/reproduce_test1.py # both garments
python clothing/reproduce_test1.py --only bottom
python clothing/reproduce_test1.py --dry-run
WHY THIS SCRIPT EXISTS
`garment.py` cannot express this build on its own. The skirt's weights are owned by
`skirt_garment_weights.py`, which must run **after `export` and before `import`** —
`garment_pipeline.py`'s own 2-segment `strand_weights` is overwritten by it, and
whichever runs LAST wins. Nothing enforces that, so re-running the pipeline the
obvious way (`--from census --to import`) silently ships the 2-segment blend and
reads as a rig regression. This script is the enforcement.
It reads the tuning from the config's `post_export` block, so the config stays the
single source of truth and nobody has to remember four env vars.
ORDER (per garment):
census -> prepare -> fit -> reduce -> skin -> export
[bottom only] skirt_garment_weights.py <-- the step that gets forgotten
register -> import
The texture is a build input too: bottom_test1.png is generated and must stay V-only
(see make_bottom_test1.py). Pass --texture to regenerate it first.
AFTERWARDS, to look at it (BODY_OVERRIDE is mandatory — the 4-segment skirt ring
exists only on that body copy; on the standard Lena the garment's skirt bones do not
exist and the hem collapses):
SESSION_ID=<you> 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
"""
import argparse
import json
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PY = sys.executable
GARMENT = os.path.join(ROOT, "clothing", "garment.py")
CONFIGS = {"bottom": "clothing/configs/bottomTest1.json",
"top": "clothing/configs/topTest1.json"}
# The bottom must be built first only for tidiness; they are independent.
ORDER = ["bottom", "top"]
def run(cmd, env=None, dry=False):
shown = " ".join(cmd)
if env:
shown = " ".join("%s=%s" % kv for kv in sorted(env.items())) + " " + shown
print("\n$ " + shown, flush=True)
if dry:
return 0
e = dict(os.environ)
if env:
e.update(env)
return subprocess.call(cmd, cwd=ROOT, env=e)
def die(msg):
print("[reproduce] FATAL: " + msg, file=sys.stderr)
raise SystemExit(1)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--only", choices=ORDER, help="build just one garment")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--texture", action="store_true",
help="regenerate bottom_test1.png first (must stay V-only)")
a = ap.parse_args()
names = [a.only] if a.only else ORDER
if a.texture:
gen = os.path.join(ROOT, "tools", "tailor", "textures", "make_bottom_test1.py")
if run([PY, gen], dry=a.dry_run) != 0:
die("texture generation failed")
for name in names:
cfg_rel = CONFIGS[name]
cfg = json.load(open(os.path.join(ROOT, cfg_rel)))
# 1. Blender half, up to and including export.
if run([PY, GARMENT, cfg_rel, "--from", "census", "--to", "export",
"--no-gate-stop"], dry=a.dry_run) != 0:
die("%s: pipeline census..export failed" % name)
# 2. THE STEP THAT GETS FORGOTTEN. Config-driven so the tuning cannot drift.
pe = cfg.get("post_export")
if pe:
tool = os.path.join(ROOT, pe["tool"].replace("/", os.sep))
exp = cfg["export"]
target = "%s/%s_%s_%s.gltf" % (
exp["out_dir"].rstrip("/"), exp["gender"], exp["set"],
list(cfg["parts"].values())[0]["slot"])
if not a.dry_run and not os.path.exists(target):
die("%s: expected export at %s" % (name, target))
if run([PY, tool, target, pe["body"]],
env=pe.get("env"), dry=a.dry_run) != 0:
die("%s: %s failed" % (name, pe["tool"]))
print("[reproduce] %s: post_export weights applied — check the log above for "
"'%s'" % (name, pe.get("expect", "no dead strands")))
# 3. Game half. Import LAST so Godot caches the corrected weights.
if run([PY, GARMENT, cfg_rel, "--only", "register,import",
"--no-gate-stop"], dry=a.dry_run) != 0:
die("%s: register/import failed" % name)
print("\n[reproduce] done. Spawn the bed with BODY_OVERRIDE=…_SkirtRig_4seg.glb "
"(see this file's docstring) — the standard Lena body has no skirt ring.")
if __name__ == "__main__":
main()