109 lines
5.2 KiB
Python
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()
|