"""Static T2 accessory: Studio topology + Studio UV, bake, UV-preserving finish. Run in LLM3DGraphics's venv. This script only prepares external files; Unity imports and prefab authoring are performed separately through Pipeline. """ import sys sys.dont_write_bytecode = True import argparse import hashlib import json import shutil import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] WORKBENCH = Path('D:/UD/AI/XModel/LLM3DGraphics') BLENDER = 'D:/Tools/Blender/blender-5.0.0-windows-x64/blender.exe' sys.path.insert(0, str(ROOT / 'Tools')) from hunyuan_3d import auth sys.path.insert(0, str(WORKBENCH)) from backend import hunyuan_studio as studio from backend.retopology import _run_blender_step, compress_bake_maps, limit_studio_faces sys.path.insert(0, str(ROOT / 'Tools/model_tools')) from mt_run import run_blender_script from png_compress import compress_dir_pngs def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() def update(**values): print(json.dumps(values, ensure_ascii=True), flush=True) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--actor', default='TideWhale') parser.add_argument('--source', type=Path, default=Path('Tools/output/t2_3d/TideWhale/tidewhale_t2.glb')) parser.add_argument('--tris', type=int, default=400) parser.add_argument('--size', type=int, default=512) args = parser.parse_args() if not args.actor.isalnum() or args.tris < 100 or args.size not in (512, 1024): raise ValueError('Invalid accessory parameters') source = (ROOT / args.source).resolve() if not source.is_relative_to(ROOT) or not source.is_file(): raise ValueError('Missing source inside project') out = ROOT / 'Tools/output/t2_studio' / args.actor out.mkdir(parents=True, exist_ok=True) receipt = out / 'production.json' current = json.loads(receipt.read_text('utf-8')) if receipt.exists() else {} digest = sha(source) if current and (current['sourceSha256'] != digest or current['targetTriangles'] != args.tris or current['textureSize'] != args.size): raise ValueError('Source or settings changed; use a separate production directory') current.update(actor=args.actor, source=source.relative_to(ROOT).as_posix(), sourceSha256=digest, targetTriangles=args.tris, textureSize=args.size, status='studio') def save(): receipt.write_text(json.dumps(current, indent=2) + '\n', encoding='utf-8') save() shutil.copy2(source, out / 'model.glb') studio.is_logged_in = lambda client_id=None: auth.is_logged_in() studio.load_storage_state = lambda client_id=None: (json.loads(auth.storage_state_path().read_text('utf-8')), None) uv = out / 'model_lowpoly_uv.glb' if uv.exists(): studio.validate_uv_glb(uv) else: meta_path = out / 'hunyuan_studio.json' meta = json.loads(meta_path.read_text('utf-8')) if meta_path.exists() else {} studio.process_existing(source, out, update, target_faces=args.tris, resume_low_id=meta.get('lowpoly_works_id'), resume_uv_id=meta.get('uv_works_id')) current.update(status='baking', studio=json.loads((out / 'hunyuan_studio.json').read_text('utf-8'))) save() aligned = out / 'model_lowpoly_uv_aligned.glb' if not aligned.exists(): with (out / 'alignment.log').open('w', encoding='utf-8') as log: subprocess.run([BLENDER, '-b', '-t', '1', '--factory-startup', '--python', str(ROOT / 'Tools/model_tools/bl_align_studio_uv.py'), '--', str(out)], stdout=log, stderr=subprocess.STDOUT, timeout=120, check=True) if not current.get('bakeAligned'): _run_blender_step(out, BLENDER, 'blender_lowpoly_bake.py', (out / 'model.glb', out / 'model_baked.glb', 1, args.size, aligned), 'bake.log', marker='Lowpoly bake saved to') compress_bake_maps(out) current['bakeAligned'] = True save() shutil.copy2(out / 'model_baked.glb', out / 'model_ex.glb') mesh, reduction = limit_studio_faces(out, args.tris, BLENDER, update) current.update(triangles=len(mesh.faces), vertices=len(mesh.vertices), decimation=reduction, bakeAlignment=json.loads((out / 'bake_alignment.json').read_text('utf-8'))) name = args.actor + 'T2' with (out / 'static_export.log').open('w', encoding='utf-8') as log: subprocess.run([BLENDER, '-b', '-t', '1', '--factory-startup', '--python', str(ROOT / 'Tools/model_tools/bl_prepare_studio_trial.py'), '--', str(out), name], stdout=log, stderr=subprocess.STDOUT, check=True, timeout=120) xpbr = run_blender_script(BLENDER, 'bl_convert.py', [str(out / 'static' / (name + '.fbx')), str(out / 'xpbr'), str(args.size)], blender_args=('-t', '1')) compress_dir_pngs(str(out / 'xpbr'), suffixes=('_base.png', '_mix.png')) current.update(status='ready_for_unity', xpbr=xpbr, staticExport=json.loads((out / 'static_export.json').read_text('utf-8')), outputs={p.relative_to(ROOT).as_posix(): sha(p) for p in sorted((out / 'xpbr').iterdir()) if p.is_file()}) save() update(status=current['status'], triangles=current['triangles'], output=str(out / 'xpbr')) if __name__ == '__main__': main()