Getting to the way it's supposed to be!
This commit is contained in:
268
modules/ufbx/misc/transmute_fbx.py
Normal file
268
modules/ufbx/misc/transmute_fbx.py
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
from dataclasses import dataclass
|
||||
import struct
|
||||
from typing import Any, Tuple
|
||||
import zlib
|
||||
import sys
|
||||
import io
|
||||
import argparse
|
||||
|
||||
@dataclass
|
||||
class BinaryFormat:
|
||||
version: int
|
||||
big_endian: bool
|
||||
array_encoding: int = 1
|
||||
array_original: bool = False
|
||||
|
||||
@dataclass
|
||||
class Value:
|
||||
type: str
|
||||
value: Any
|
||||
original_data: Tuple[int, bytes] = (0, b"")
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
name: bytes
|
||||
values: list[Value]
|
||||
children: list["Node"]
|
||||
|
||||
@dataclass
|
||||
class FbxFile:
|
||||
root: Node
|
||||
version: int
|
||||
format: str
|
||||
footer: bytes = b""
|
||||
|
||||
def pack(stream, fmt, *args):
|
||||
stream.write(struct.pack(fmt, *args))
|
||||
|
||||
def unpack(stream, fmt):
|
||||
size = struct.calcsize(fmt)
|
||||
data = stream.read(size)
|
||||
return struct.unpack(fmt, data)
|
||||
|
||||
primitive_fmt = {
|
||||
b"C": "b", b"B": "b",
|
||||
b"Y": "h",
|
||||
b"I": "l", b"F": "f",
|
||||
b"L": "q", b"D": "d",
|
||||
}
|
||||
|
||||
def binary_parse_value(stream, bf):
|
||||
endian = "<>"[bf.big_endian]
|
||||
type = stream.read(1)
|
||||
fmt = primitive_fmt.get(type)
|
||||
if fmt:
|
||||
value, = unpack(stream, endian + fmt)
|
||||
return Value(type, value)
|
||||
if type in b"cbilfd":
|
||||
arr_fmt = endian + "L" * 3
|
||||
fmt = primitive_fmt[type.upper()]
|
||||
count, encoding, encoded_size = unpack(stream, arr_fmt)
|
||||
original_data = arr_data = stream.read(encoded_size)
|
||||
if encoding == 0: pass # Nop
|
||||
elif encoding == 1:
|
||||
arr_data = zlib.decompress(arr_data)
|
||||
else:
|
||||
raise ValueError(f"Unknown encoding: {encoding}")
|
||||
values = list(v[0] for v in struct.iter_unpack(endian + fmt, arr_data))
|
||||
assert len(values) == count
|
||||
return Value(type, values, original_data=(encoding, original_data))
|
||||
elif type in b"SR":
|
||||
length, = unpack(stream, endian + "L")
|
||||
return Value(type, stream.read(length))
|
||||
else:
|
||||
raise ValueError(f"Bad type: '{type}'")
|
||||
|
||||
def binary_parse_node(stream, bf):
|
||||
pos = stream.tell()
|
||||
endian = "<>"[bf.big_endian]
|
||||
head_fmt = endian + "LQ"[bf.version >= 7500] * 3 + "B"
|
||||
end_offset, num_values, values_len, name_len = unpack(stream, head_fmt)
|
||||
if end_offset == 0 and name_len == 0: return None
|
||||
name = stream.read(name_len)
|
||||
values_end = stream.tell() + values_len
|
||||
values = [binary_parse_value(stream, bf) for _ in range(num_values)]
|
||||
children = []
|
||||
if stream.tell() != values_end:
|
||||
assert stream.tell() < values_end
|
||||
stream.seek(pos + values_end)
|
||||
while stream.tell() < end_offset:
|
||||
node = binary_parse_node(stream, bf)
|
||||
if not node: break
|
||||
children.append(node)
|
||||
return Node(name, values, children)
|
||||
|
||||
def parse_fbx(stream):
|
||||
magic = stream.read(22)
|
||||
if magic == b"Kaydara FBX Binary \x00\x1a":
|
||||
big_endian = stream.read(1) != b"\x00"
|
||||
endian = "<>"[big_endian]
|
||||
version, = unpack(stream, endian + "L")
|
||||
bf = BinaryFormat(version, big_endian)
|
||||
children = []
|
||||
while True:
|
||||
node = binary_parse_node(stream, bf)
|
||||
if not node: break
|
||||
children.append(node)
|
||||
footer = stream.read(16)
|
||||
root = Node("", [], children)
|
||||
format = "binary-be" if big_endian else "binary"
|
||||
return FbxFile(root, version, format, footer)
|
||||
else:
|
||||
# TODO
|
||||
raise NotImplementedError()
|
||||
|
||||
def binary_dump_value(stream, value: Value, bf: BinaryFormat):
|
||||
endian = "<>"[bf.big_endian]
|
||||
fmt = primitive_fmt.get(value.type)
|
||||
stream.write(value.type)
|
||||
if fmt:
|
||||
pack(stream, endian + fmt, value.value)
|
||||
elif value.type in b"cbilfd":
|
||||
fmt = endian + primitive_fmt[value.type.upper()]
|
||||
|
||||
arr_fmt = endian + "L" * 3
|
||||
if bf.array_original:
|
||||
encoding, arr_data = value.original_data
|
||||
pack(stream, arr_fmt, len(value.value), encoding, len(arr_data))
|
||||
stream.write(arr_data)
|
||||
else:
|
||||
with io.BytesIO() as ds:
|
||||
for v in value.value:
|
||||
pack(ds, fmt, v)
|
||||
arr_data = ds.getvalue()
|
||||
|
||||
count = len(value.value)
|
||||
encoding = bf.array_encoding
|
||||
if encoding == 1:
|
||||
arr_data = zlib.compress(arr_data)
|
||||
encoded_size = len(arr_data)
|
||||
|
||||
pack(stream, arr_fmt, count, encoding, encoded_size)
|
||||
stream.write(arr_data)
|
||||
|
||||
elif value.type in b"SR":
|
||||
pack(stream, endian + "L", len(value.value))
|
||||
stream.write(value.value)
|
||||
else:
|
||||
raise ValueError(f"Bad type: '{value.type}'")
|
||||
|
||||
def binary_dump_node(stream, node: Node, bf: BinaryFormat):
|
||||
endian = "<>"[bf.big_endian]
|
||||
head_size = 25 if bf.version >= 7500 else 13
|
||||
head_null = b"\x00" * head_size
|
||||
off_start = stream.tell()
|
||||
stream.write(head_null)
|
||||
stream.write(node.name)
|
||||
off_value_start = stream.tell()
|
||||
for value in node.values:
|
||||
binary_dump_value(stream, value, bf)
|
||||
values_size = stream.tell() - off_value_start
|
||||
for child in node.children:
|
||||
binary_dump_node(stream, child, bf)
|
||||
if node.children or node.name in { b"References", b"AnimationStack", b"AnimationLayer" }:
|
||||
stream.write(head_null)
|
||||
off_end = stream.tell()
|
||||
head_fmt = endian + "LQ"[bf.version >= 7500] * 3 + "B"
|
||||
stream.seek(off_start)
|
||||
pack(stream, head_fmt, off_end, len(node.values), values_size, len(node.name))
|
||||
stream.seek(off_end)
|
||||
|
||||
def binary_dump_root(stream, root: Node, bf: BinaryFormat, footer: bytes):
|
||||
head_size = 25 if bf.version >= 7500 else 13
|
||||
head_null = b"\x00" * head_size
|
||||
endian = "<>"[bf.big_endian]
|
||||
|
||||
stream.write(b"Kaydara FBX Binary \x00\x1a")
|
||||
pack(stream, "B", bf.big_endian)
|
||||
pack(stream, endian + "L", bf.version)
|
||||
|
||||
for node in root.children:
|
||||
binary_dump_node(stream, node, bf)
|
||||
stream.write(head_null)
|
||||
|
||||
stream.write(footer)
|
||||
stream.write(b"\x00" * 4)
|
||||
|
||||
ofs = stream.tell()
|
||||
pad = ((ofs + 15) & ~15) - ofs
|
||||
if pad == 0:
|
||||
pad = 16
|
||||
|
||||
stream.write(b"\0" * pad)
|
||||
pack(stream, endian + "I", bf.version)
|
||||
stream.write(b"\0" * 120)
|
||||
stream.write(b"\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b")
|
||||
|
||||
def ascii_dump_value(stream, value: Value, indent: str):
|
||||
if value.type in b"CBYILFD":
|
||||
stream.write(str(value.value))
|
||||
elif value.type in b"SR":
|
||||
s = str(value.value)[2:-1]
|
||||
stream.write(f"\"{s}\"")
|
||||
elif value.type in b"cbilfd":
|
||||
stream.write(f"* {len(value.value)} {{")
|
||||
first = True
|
||||
for v in value.value:
|
||||
stream.write(" " if first else ", ")
|
||||
stream.write(str(v))
|
||||
first = False
|
||||
stream.write(" }")
|
||||
else:
|
||||
raise ValueError(f"Bad value type: '{value.type}'")
|
||||
|
||||
def ascii_dump_node(stream, node: Node, indent: str):
|
||||
name = node.name.decode("utf-8")
|
||||
stream.write(f"{indent}{name}:")
|
||||
first = True
|
||||
for value in node.values:
|
||||
stream.write(" " if first else ", ")
|
||||
first = False
|
||||
ascii_dump_value(stream, value, indent + " ")
|
||||
if node.children:
|
||||
stream.write(" {\n")
|
||||
for node in node.children:
|
||||
ascii_dump_node(stream, node, indent + " ")
|
||||
stream.write(indent + "}\n")
|
||||
else:
|
||||
stream.write("\n")
|
||||
|
||||
def ascii_dump_root(stream, root: Node, version: int):
|
||||
v0 = version // 1000 % 10
|
||||
v1 = version // 100 % 10
|
||||
v2 = version // 10 % 10
|
||||
stream.write(f"; FBX {v0}.{v1}.{v2} project file\n")
|
||||
stream.write("----------------------------------------------------\n")
|
||||
for child in root.children:
|
||||
ascii_dump_node(stream, child, "")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(usage="transmute_fbx.py src -o dst -v 7400 -f binary-be")
|
||||
parser.add_argument("src", help="Source file to read")
|
||||
parser.add_argument("--output", "-o", required=True, help="Output filename")
|
||||
parser.add_argument("--version", "-v", help="File version")
|
||||
parser.add_argument("--format", "-f", help="File format")
|
||||
argv = parser.parse_args()
|
||||
|
||||
with open(argv.src, "rb") as f:
|
||||
fbx = parse_fbx(f)
|
||||
|
||||
format = argv.format
|
||||
if not format:
|
||||
format = fbx.format
|
||||
version = argv.version
|
||||
if not version:
|
||||
version = fbx.version
|
||||
|
||||
with open(argv.output, "wt" if format == "ascii" else "wb") as f:
|
||||
if format == "ascii":
|
||||
ascii_dump_root(f, fbx.root, version)
|
||||
else:
|
||||
if format == "binary-be":
|
||||
bf = BinaryFormat(version, True, 0)
|
||||
elif format == "binary":
|
||||
bf = BinaryFormat(version, False, 1)
|
||||
else:
|
||||
raise ValueError(f"Unknown format: {format}")
|
||||
binary_dump_root(f, fbx.root, bf, fbx.footer)
|
||||
Reference in New Issue
Block a user