Files
Blockline/tools/prepare_operator_sources.py

79 lines
3.6 KiB
Python

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