Files
AIGame/Tools/studio_lowpoly_finish.py
T

78 lines
4.6 KiB
Python

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