"""Offline packaging + original-skeleton rebind of a Studio trial (generalized from greathorn_studio_finish.py). Studio mesh -> project Y-up FBX + XPbr -> weights from the ORIGINAL rig (bl_transfer_skin) -> Pipeline import -> StudioTrial.Prepare -> per-vertex sole constraints over the original clips -> local 1mm sole fit if needed -> Build/verify -> comparison review + capture -> optional Play check. Original prefab/controller/clips/rig hashes must stay unchanged. python Tools/studio_lowpoly_finish.py --actor RockBall --height 0.55 --play-check """ import argparse, hashlib, json, subprocess, sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] BLENDER = 'D:/Tools/Blender/blender-5.0.0-windows-x64/blender.exe' sys.path.insert(0, str(ROOT / 'Tools/model_tools')) from mt_run import run_blender_script sys.path.insert(0, str(ROOT / 'Tools')) from enemy_character_pipeline import unity, script, wait_editor def main(): ap = argparse.ArgumentParser() ap.add_argument('--actor', required=True) ap.add_argument('--height', type=float, required=True) ap.add_argument('--play-check', action='store_true') ap.add_argument('--recipe', type=Path, help='character recipe whose skinOverrides/skinRegions must be re-applied to the new mesh') a = ap.parse_args() out = ROOT / 'Tools/output/studio_trial' / a.actor receipt = json.loads((out / 'trial.json').read_text(encoding='utf-8')) assert receipt['status'] in ('mesh_ready', 'edit_validated', 'validated'), receipt['status'] kind, prefab, name = receipt['kind'], receipt['prefab'], a.actor + 'Studio' asset = f'Assets/Bundles/Characters/{kind}/{name}' entry = [a.actor, kind, prefab, a.height] def unchanged(): for relative, digest in receipt['originals'].items(): assert hashlib.sha256((ROOT / relative).read_bytes()).hexdigest() == digest, relative + ' changed' def blender(file, args, log): with (out / log).open('w', encoding='utf-8') as f: subprocess.run([BLENDER, '-b', '-t', '1', '--factory-startup', '--python', str(ROOT / 'Tools/model_tools' / file), '--', *map(str, args)], stdout=f, stderr=subprocess.STDOUT, timeout=600, check=True) unchanged() blender('bl_prepare_studio_trial.py', [out, name], 'static_export.log') result = run_blender_script(BLENDER, 'bl_convert.py', [str(out / f'static/{name}.fbx'), str(out / 'xpbr'), str(receipt.get('textureSize', 1024))]) (out / 'xpbr.json').write_text(json.dumps(result, indent=2), encoding='utf-8') print('XPbr packaging complete', flush=True) blender('bl_transfer_skin.py', [ROOT / receipt['rig'], out / f'xpbr/{name}.fbx', out / f'{name}_Rigged.fbx', '--low-up', 'Y', '--align-bounds', *(['--weight-overrides', ROOT / a.recipe] if a.recipe else [])], 'skin_transfer.log') print('Original rig weight transfer complete', flush=True) unity('set_autotick', enable=True) if unity('editor_status')['playMode'] != 'stopped': raise RuntimeError('Editor is in Play; offline trial is ready, import after it stops') for f in [f'{name}.fbx', f'{name}_base.png', f'{name}_mix.png']: unity('import_asset', source=str(out / 'xpbr' / f), path=asset + '/' + f, confirm=True) unity('import_asset', source=str(out / f'{name}_Rigged.fbx'), path=asset + f'/Rigged/{name}_Rigged.fbx', confirm=True) script('SetupCharacter', 'SetupCharacter.Run', [kind, name, a.height]) script('StudioTrial', 'StudioTrial.Prepare', entry) script('StudioTrial', 'StudioTrial.ExportFootConstraints', entry) blender('bl_fit_studio_soles.py', [out, name], 'sole_fit.log') unity('import_asset', source=str(out / f'{name}_Rigged.fbx'), path=asset + f'/Rigged/{name}_Rigged.fbx', confirm=True) script('StudioTrial', 'StudioTrial.Build', entry) script('StudioTrial', 'StudioTrial.BuildReview', entry) script('StudioTrial', 'StudioTrial.Capture', entry) receipt['status'] = 'edit_validated' if a.play_check: unity('editor_play') try: wait_editor('playing'); script('StudioTrial', 'StudioTrial.Play', entry); receipt['status'] = 'validated' finally: unity('editor_stop'); wait_editor('stopped') unchanged() receipt.update(soleFit=json.loads((out / 'sole_fit.json').read_text(encoding='utf-8')), riggedSha256=hashlib.sha256((out / f'{name}_Rigged.fbx').read_bytes()).hexdigest(), originalResourcesUnchanged=True) (out / 'trial.json').write_text(json.dumps(receipt, indent=2), encoding='utf-8') print('PASS studio trial', receipt['status'], flush=True) if __name__ == '__main__': main()