Files
AIGame/Tools/studio_accessory_pipeline.py

109 lines
5.2 KiB
Python

"""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()