79 lines
3.6 KiB
Python
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')
|