"""Run with Blender --background --factory-startup --python this_file. Lossless FBX repack: externalize embedded images without re-exporting geometry, weights, animations or the bind pose. Original user ZIP archives are untouched. """ from pathlib import Path import hashlib import json import os import sys import bpy sys.path.insert(0, str(Path(bpy.utils.resource_path('LOCAL')) / 'scripts' / 'addons_core')) from io_scene_fbx import parse_fbx, encode_bin ROOT = Path(__file__).resolve().parents[1] TEXTURES = ROOT / 'assets/textures/operators' TEXTURES.mkdir(parents=True, exist_ok=True) METHODS = dict(zip('BCZYILFDRSilfdbc', ( 'bool', 'char', 'int8', 'int16', 'int32', 'int64', 'float32', 'float64', 'bytes', 'string', 'int32_array', 'int64_array', 'float32_array', 'float64_array', 'bool_array', 'byte_array'))) def encode(element): result = encode_bin.FBXElem(element.id) for code, value in zip(element.props_type, element.props): getattr(result, 'add_' + METHODS[chr(code)])(value) result.elems = [encode(child) for child in element.elems] return result report = [] for package, filename in [('steve', 'Steve'), ('gas_mask', 'Gas_Mask'), ('pro_rifle', 'Ch49_nonPBR'), ('basic_shooter', 'Ch49_nonPBR'), ('slim_shooter', 'Ch49_nonPBR')]: source = ROOT / 'assets/animations' / package / (filename + '.fbx') tree, version = parse_fbx.parse(str(source)) objects = next(node for node in tree.elems if node.id == b'Objects') mapping = {} for obj in objects.elems: if obj.id != b'Video': continue content = next((el.props[0] for el in obj.elems if el.id == b'Content' and el.props), None) name = next((el.props[0] for el in obj.elems if el.id == b'Filename'), b'') if not content: continue leaf = Path(name.decode().replace('\\', '/')).name target = TEXTURES / leaf if target.exists() and target.read_bytes() != content: target = TEXTURES / (package + '_' + leaf) if not target.exists(): target.write_bytes(content) mapping[name] = os.path.relpath(target, source.parent).replace('\\', '/').encode() if not mapping: print('Already external:', source.name) continue original_size = source.stat().st_size original_hash = hashlib.sha256(source.read_bytes()).hexdigest() for obj in objects.elems: if obj.id not in (b'Video', b'Texture'): continue name = next((el.props[0] for el in obj.elems if el.id in (b'Filename', b'FileName')), b'') assert name in mapping, ('Missing embedded texture', name) obj.elems[:] = [el for el in obj.elems if el.id != b'Content'] for child in obj.elems: if child.id in (b'Filename', b'FileName', b'RelativeFilename'): child.props[0] = mapping[name] temporary = source.with_suffix('.repacked') encode_bin.write(str(temporary), encode(tree), version) # Parse the rewritten file before replacing only our project copy. verified, verified_version = parse_fbx.parse(str(temporary)) assert verified_version == version temporary.replace(source) report.append({'file': source.relative_to(ROOT).as_posix(), 'original_bytes': original_size, 'bytes': source.stat().st_size, 'original_sha256': original_hash, 'textures': [v.decode() for v in mapping.values()]}) print('EXTERNALIZED', package, original_size, '->', source.stat().st_size) if report: (TEXTURES / 'sources.json').write_text(json.dumps(report, indent=2), encoding='utf-8')