diff options
author | Bastien Montagne <montagne29@wanadoo.fr> | 2014-03-12 23:22:51 +0400 |
---|---|---|
committer | Bastien Montagne <montagne29@wanadoo.fr> | 2014-03-12 23:22:51 +0400 |
commit | 3879603620c4241bec612665dd97ab4b598559c6 (patch) | |
tree | e639cf6d345b602fa02b4d2977bb9bd56efa222b | |
parent | a7da6cfa5b2190e7a41c73e33396969d04f48c2f (diff) |
Initial commit of new FBX 7.4 binary exporter
What to expect:
* Static export of empties, meshes, cameras and lamps, as well as materials and (image!) textures should work OK.
There are a few advanced topics still TODO regarding meshes and mat/tex, though.
* Custom properties from objects/meshes/lamps/cameras/armatures/bones/materials/textures are exported too (only simple ones, ints/floats/strings).
* Armature export: this needs testing by people having *native* FBX aplications, linking between bones and meshes seems to work, but I have doubts about bones orientations.
* Animation: still a complete TODO.
Note that old FBX ASCII 6.1 exporter is still available (top dropdown in exporter's UI).
Many thanks to Campbell, which did the ground work of decyphering FBX binary format and wrote basic code to read/write it.
-rw-r--r-- | io_scene_fbx/__init__.py | 47 | ||||
-rw-r--r-- | io_scene_fbx/data_types.py | 74 | ||||
-rw-r--r-- | io_scene_fbx/encode_bin.py | 320 | ||||
-rw-r--r-- | io_scene_fbx/export_fbx.py | 1 | ||||
-rw-r--r-- | io_scene_fbx/export_fbx_bin.py | 2253 | ||||
-rw-r--r-- | io_scene_fbx/import_fbx.py | 12 | ||||
-rwxr-xr-x | io_scene_fbx/json2fbx.py | 176 | ||||
-rw-r--r-- | io_scene_fbx/parse_fbx.py | 35 |
8 files changed, 2881 insertions, 37 deletions
diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index 1d7ba1c5..8adb5ebf 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -16,12 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# <pep8-80 compliant> +# <pep8 compliant> bl_info = { "name": "Autodesk FBX format", - "author": "Campbell Barton", - "blender": (2, 59, 0), + "author": "Campbell Barton, Bastien Montagne", + "blender": (2, 70, 0), "location": "File > Import-Export", "description": "Export FBX meshes, UV's, vertex colors, materials, " "textures, cameras, lamps and actions", @@ -37,6 +37,8 @@ if "bpy" in locals(): import imp if "import_fbx" in locals(): imp.reload(import_fbx) + if "export_fbx_bin" in locals(): + imp.reload(export_fbx_bin) if "export_fbx" in locals(): imp.reload(export_fbx) @@ -148,6 +150,15 @@ class ExportFBX(bpy.types.Operator, ExportHelper): # List of operator properties, the attributes will be assigned # to the class instance from the operator settings before calling. + version = EnumProperty( + items=(('BIN7400', "FBX 7.4 binary", "Newer 7.4 binary version, still in development (no animation yet)"), + ('ASCII6100', "FBX 6.1 ASCII", "Legacy 6.1 ascii version"), + ), + name="Exporter Version", + description="Choose which version of the exporter to use", + default='BIN7400', + ) + use_selection = BoolProperty( name="Selected Objects", description="Export selected objects on visible layers", @@ -211,7 +222,18 @@ class ExportFBX(bpy.types.Operator, ExportHelper): ) use_mesh_edges = BoolProperty( - name="Include Edges", + name="Include Loose Edges", + default=False, + ) + use_tspace = BoolProperty( + name="Include Tangent Space", + description=("Add binormal and tangent vectors, together with normal they form the tangent space " + "(will only work correctly with tris/quads only meshes!)"), + default=False, + ) + use_custom_properties = BoolProperty( + name="Custom Properties", + description="Export custom properties", default=False, ) use_armature_deform_only = BoolProperty( @@ -251,6 +273,11 @@ class ExportFBX(bpy.types.Operator, ExportHelper): default=6.0, # default: 10^-4 frames. ) path_mode = path_reference_mode + embed_textures = BoolProperty( + name="Embed Textures", + description="Embed textures in FBX binary file (only for \"Copy\" path mode!)", + default=False, + ) batch_mode = EnumProperty( name="Batch Mode", items=(('OFF', "Off", "Active scene to file"), @@ -284,17 +311,19 @@ class ExportFBX(bpy.types.Operator, ExportHelper): to_up=self.axis_up, ).to_4x4()) - keywords = self.as_keywords(ignore=("axis_forward", - "axis_up", - "global_scale", + keywords = self.as_keywords(ignore=("global_scale", "check_existing", "filter_glob", )) keywords["global_matrix"] = global_matrix - from . import export_fbx - return export_fbx.save(self, context, **keywords) + if self.version == 'BIN7400': + from . import export_fbx_bin + return export_fbx_bin.save(self, context, **keywords) + else: + from . import export_fbx + return export_fbx.save(self, context, **keywords) def menu_func_import(self, context): diff --git a/io_scene_fbx/data_types.py b/io_scene_fbx/data_types.py new file mode 100644 index 00000000..3a505be8 --- /dev/null +++ b/io_scene_fbx/data_types.py @@ -0,0 +1,74 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Script copyright (C) 2006-2012, assimp team +# Script copyright (C) 2013 Blender Foundation + +BOOL = b'C'[0] +INT16 = b'Y'[0] +INT32 = b'I'[0] +INT64 = b'L'[0] +FLOAT32 = b'F'[0] +FLOAT64 = b'D'[0] +BYTES = b'R'[0] +STRING = b'S'[0] +INT32_ARRAY = b'i'[0] +INT64_ARRAY = b'l'[0] +FLOAT32_ARRAY = b'f'[0] +FLOAT64_ARRAY = b'd'[0] +BOOL_ARRAY = b'b'[0] +BYTE_ARRAY = b'c'[0] + +# array types - actual length may vary (depending on underlying C implementation)! +import array + +# For now, bytes and bool are assumed always 1byte. +ARRAY_BOOL = 'b' +ARRAY_BYTE = 'B' + +ARRAY_INT32 = None +ARRAY_INT64 = None +for _t in 'ilq': + size = array.array(_t).itemsize + if size == 4: + ARRAY_INT32 = _t + elif size == 8: + ARRAY_INT64 = _t + if ARRAY_INT32 and ARRAY_INT64: + break +if not ARRAY_INT32: + raise Exception("Impossible to get a 4-bytes integer type for array!") +if not ARRAY_INT64: + raise Exception("Impossible to get an 8-bytes integer type for array!") + +ARRAY_FLOAT32 = None +ARRAY_FLOAT64 = None +for _t in 'fd': + size = array.array(_t).itemsize + if size == 4: + ARRAY_FLOAT32 = _t + elif size == 8: + ARRAY_FLOAT64 = _t + if ARRAY_FLOAT32 and ARRAY_FLOAT64: + break +if not ARRAY_FLOAT32: + raise Exception("Impossible to get a 4-bytes float type for array!") +if not ARRAY_FLOAT64: + raise Exception("Impossible to get an 8-bytes float type for array!") diff --git a/io_scene_fbx/encode_bin.py b/io_scene_fbx/encode_bin.py new file mode 100644 index 00000000..5cd3b744 --- /dev/null +++ b/io_scene_fbx/encode_bin.py @@ -0,0 +1,320 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Script copyright (C) 2013 Campbell Barton + +try: + from . import data_types +except: + import data_types + +from struct import pack +import array +import zlib + +_BLOCK_SENTINEL_LENGTH = 13 +_BLOCK_SENTINEL_DATA = (b'\0' * _BLOCK_SENTINEL_LENGTH) +_IS_BIG_ENDIAN = (__import__("sys").byteorder != 'little') +_HEAD_MAGIC = b'Kaydara FBX Binary\x20\x20\x00\x1a\x00' + +# fbx has very strict CRC rules, all based on file timestamp +# until we figure these out, write files at a fixed time. (workaround!) + +# Assumes: CreationTime +_TIME_ID = b'1970-01-01 10:00:00:000' +_FILE_ID = b'\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1' +_FOOT_ID = b'\xfa\xbc\xab\x09\xd0\xc8\xd4\x66\xb1\x76\xfb\x83\x1c\xf7\x26\x7e' + + +class FBXElem: + __slots__ = ( + "id", + "props", + "props_type", + "elems", + + "_props_length", # combine length of props + "_end_offset", # byte offset from the start of the file. + ) + + def __init__(self, id): + assert(len(id) < 256) # length must fit in a uint8 + self.id = id + self.props = [] + self.props_type = bytearray() + self.elems = [] + self._end_offset = -1 + self._props_length = -1 + + def add_bool(self, data): + assert(isinstance(data, bool)) + data = pack('?', data) + + self.props_type.append(data_types.BOOL) + self.props.append(data) + + def add_int16(self, data): + assert(isinstance(data, int)) + data = pack('<h', data) + + self.props_type.append(data_types.INT16) + self.props.append(data) + + def add_int32(self, data): + assert(isinstance(data, int)) + data = pack('<i', data) + + self.props_type.append(data_types.INT32) + self.props.append(data) + + def add_int64(self, data): + assert(isinstance(data, int)) + data = pack('<q', data) + + self.props_type.append(data_types.INT64) + self.props.append(data) + + def add_float32(self, data): + assert(isinstance(data, float)) + data = pack('<f', data) + + self.props_type.append(data_types.FLOAT32) + self.props.append(data) + + def add_float64(self, data): + assert(isinstance(data, float)) + data = pack('<d', data) + + self.props_type.append(data_types.FLOAT64) + self.props.append(data) + + def add_bytes(self, data): + assert(isinstance(data, bytes)) + data = pack('<I', len(data)) + data + + self.props_type.append(data_types.BYTES) + self.props.append(data) + + def add_string(self, data): + assert(isinstance(data, bytes)) + data = pack('<I', len(data)) + data + + self.props_type.append(data_types.STRING) + self.props.append(data) + + def add_string_unicode(self, data): + assert(isinstance(data, str)) + data = data.encode('utf8') + data = pack('<I', len(data)) + data + + self.props_type.append(data_types.STRING) + self.props.append(data) + + def _add_array_helper(self, data, array_type, prop_type): + assert(isinstance(data, array.array)) + assert(data.typecode == array_type) + + length = len(data) + + if _IS_BIG_ENDIAN: + data = data[:] + data.byteswap() + data = data.tobytes() + + # mimic behavior of fbxconverter (also common sense) + # we could make this configurable. + encoding = 0 if len(data) <= 128 else 1 + if encoding == 0: + pass + elif encoding == 1: + data = zlib.compress(data, 1) + + comp_len = len(data) + + data = pack('<3I', length, encoding, comp_len) + data + + self.props_type.append(prop_type) + self.props.append(data) + + def add_int32_array(self, data): + if not isinstance(data, array.array): + data = array.array(data_types.ARRAY_INT32, data) + self._add_array_helper(data, data_types.ARRAY_INT32, data_types.INT32_ARRAY) + + def add_int64_array(self, data): + if not isinstance(data, array.array): + data = array.array(data_types.ARRAY_INT64, data) + self._add_array_helper(data, data_types.ARRAY_INT64, data_types.INT64_ARRAY) + + def add_float32_array(self, data): + if not isinstance(data, array.array): + data = array.array(data_types.ARRAY_FLOAT32, data) + self._add_array_helper(data, data_types.ARRAY_FLOAT32, data_types.FLOAT32_ARRAY) + + def add_float64_array(self, data): + if not isinstance(data, array.array): + data = array.array(data_types.ARRAY_FLOAT64, data) + self._add_array_helper(data, data_types.ARRAY_FLOAT64, data_types.FLOAT64_ARRAY) + + def add_bool_array(self, data): + if not isinstance(data, array.array): + data = array.array(data_types.ARRAY_BOOL, data) + self._add_array_helper(data, data_types.ARRAY_BOOL, data_types.BOOL_ARRAY) + + def add_byte_array(self, data): + if not isinstance(data, array.array): + data = array.array(data_types.ARRAY_BYTE, data) + self._add_array_helper(data, data_types.ARRAY_BYTE, data_types.BYTE_ARRAY) + + # ------------------------- + # internal helper functions + + def _calc_offsets(self, offset, is_last): + """ + Call before writing, calculates fixed offsets. + """ + assert(self._end_offset == -1) + assert(self._props_length == -1) + + # print("Offset", offset) + offset += 12 # 3 uints + offset += 1 + len(self.id) # len + idname + + props_length = 0 + for data in self.props: + # 1 byte for the prop type + props_length += 1 + len(data) + self._props_length = props_length + offset += props_length + + offset = self._calc_offsets_children(offset, is_last) + + self._end_offset = offset + return offset + + def _calc_offsets_children(self, offset, is_last): + if self.elems: + elem_last = self.elems[-1] + for elem in self.elems: + offset = elem._calc_offsets(offset, (elem is elem_last)) + offset += _BLOCK_SENTINEL_LENGTH + elif not self.props: + if not is_last: + offset += _BLOCK_SENTINEL_LENGTH + + return offset + + def _write(self, write, tell, is_last): + assert(self._end_offset != -1) + assert(self._props_length != -1) + + # print(self.id, self._end_offset, len(self.props), self._props_length) + write(pack('<3I', self._end_offset, len(self.props), self._props_length)) + + write(bytes((len(self.id),))) + write(self.id) + + for i, data in enumerate(self.props): + write(bytes((self.props_type[i],))) + write(data) + + self._write_children(write, tell, is_last) + + if tell() != self._end_offset: + raise IOError("scope length not reached, " + "something is wrong (%d)" % (end_offset - tell())) + + def _write_children(self, write, tell, is_last): + if self.elems: + elem_last = self.elems[-1] + for elem in self.elems: + assert(elem.id != b'') + elem._write(write, tell, (elem is elem_last)) + write(_BLOCK_SENTINEL_DATA) + elif not self.props: + if not is_last: + write(_BLOCK_SENTINEL_DATA) + + +def _write_timedate_hack(elem_root): + # perform 2 changes + # - set the FileID + # - set the CreationTime + + ok = 0 + for elem in elem_root.elems: + if elem.id == b'FileId': + assert(elem.props_type[0] == b'R'[0]) + assert(len(elem.props_type) == 1) + elem.props.clear() + elem.props_type.clear() + + elem.add_bytes(_FILE_ID) + ok += 1 + elif elem.id == b'CreationTime': + assert(elem.props_type[0] == b'S'[0]) + assert(len(elem.props_type) == 1) + elem.props.clear() + elem.props_type.clear() + + elem.add_string(_TIME_ID) + ok += 1 + + if ok == 2: + break + + if ok != 2: + print("Missing fields!") + + +def write(fn, elem_root, version): + assert(elem_root.id == b'') + + with open(fn, 'wb') as f: + write = f.write + tell = f.tell + + write(_HEAD_MAGIC) + write(pack('<I', version)) + + # hack since we don't decode time. + # ideally we would _not_ modify this data. + _write_timedate_hack(elem_root) + + elem_root._calc_offsets_children(tell(), False) + elem_root._write_children(write, tell, False) + + write(_FOOT_ID) + write(b'\x00' * 4) + + # padding for alignment (values between 1 & 16 observed) + # if already aligned to 16, add a full 16 bytes padding. + ofs = tell() + pad = ((ofs + 15) & ~15) - ofs + if pad == 0: + pad = 16 + + write(b'\0' * pad) + + write(pack('<I', version)) + + # unknown magic (always the same) + write(b'\0' * 120) + write(b'\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b') diff --git a/io_scene_fbx/export_fbx.py b/io_scene_fbx/export_fbx.py index 94ffe377..bea02a82 100644 --- a/io_scene_fbx/export_fbx.py +++ b/io_scene_fbx/export_fbx.py @@ -248,6 +248,7 @@ def save_single(operator, scene, filepath="", path_mode='AUTO', use_mesh_edges=True, use_default_take=True, + **kwargs ): import bpy_extras.io_utils diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py new file mode 100644 index 00000000..ee3ea497 --- /dev/null +++ b/io_scene_fbx/export_fbx_bin.py @@ -0,0 +1,2253 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Script copyright (C) Campbell Barton, Bastien Montagne + + +import array +import datetime +import math +import os +import time + +import collections +from collections import namedtuple, OrderedDict +import itertools +from itertools import zip_longest, chain + +import bpy +import bpy_extras +from mathutils import Vector, Matrix + +from . import encode_bin, data_types + + +# "Constants" +FBX_VERSION = 7400 +FBX_HEADER_VERSION = 1003 +FBX_SCENEINFO_VERSION = 100 +FBX_TEMPLATES_VERSION = 100 + +FBX_MODELS_VERSION = 232 + +FBX_GEOMETRY_VERSION = 124 +FBX_GEOMETRY_NORMAL_VERSION = 102 +FBX_GEOMETRY_BINORMAL_VERSION = 102 +FBX_GEOMETRY_TANGENT_VERSION = 102 +FBX_GEOMETRY_SMOOTHING_VERSION = 102 +FBX_GEOMETRY_VCOLOR_VERSION = 101 +FBX_GEOMETRY_UV_VERSION = 101 +FBX_GEOMETRY_MATERIAL_VERSION = 101 +FBX_GEOMETRY_LAYER_VERSION = 100 +FBX_POSE_BIND_VERSION = 100 +FBX_DEFORMER_SKIN_VERSION = 101 +FBX_DEFORMER_CLUSTER_VERSION = 100 +FBX_MATERIAL_VERSION = 102 +FBX_TEXTURE_VERSION = 202 + +FBX_NAME_CLASS_SEP = b"\x00\x01" + +FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...) + + +MAT_CONVERT_LAMP = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y. +MAT_CONVERT_CAMERA = Matrix.Rotation(math.pi / 2.0, 4, 'Y') # Blender is -Z, FBX is +X. +MAT_CONVERT_BONE = Matrix() #Matrix.Rotation(math.pi / -2.0, 4, 'X') # Blender is +Y, FBX is +Z. + + +# Lamps. +FBX_LIGHT_TYPES = { + 'POINT': 0, # Point. + 'SUN': 1, # Directional. + 'SPOT': 2, # Spot. + 'HEMI': 1, # Directional. + 'AREA': 3, # Area. +} +FBX_LIGHT_DECAY_TYPES = { + 'CONSTANT': 0, # None. + 'INVERSE_LINEAR': 1, # Linear. + 'INVERSE_SQUARE': 2, # Quadratic. + 'CUSTOM_CURVE': 2, # Quadratic. + 'LINEAR_QUADRATIC_WEIGHTED': 2, # Quadratic. +} + + +##### Misc utilities ##### + +# Note: this could be in a utility (math.units e.g.)... + +UNITS = { + "meter": 1.0, # Ref unit! + "kilometer": 0.001, + "millimeter": 1000.0, + "foot": 1.0 / 0.3048, + "inch": 1.0 / 0.0254, + "turn": 1.0, # Ref unit! + "degree": 360.0, + "radian": math.pi * 2.0, + "second": 1.0, # Ref unit! + "ktime": FBX_KTIME, +} + +def units_convert(val, u_from, u_to): + """Convert value.""" + conv = UNITS[u_to] / UNITS[u_from] + try: + return (v * conv for v in val) + except: + return val * conv + + +def matrix_to_array(mat): + """Concatenate matrix's columns into a single, flat tuple""" + # blender matrix is row major, fbx is col major so transpose on write + return tuple(f for v in mat.transposed() for f in v) + + +RIGHT_HAND_AXES = { + # Up, Front -> FBX values (tuples of (axis, sign), Up, Front, Coord). + # Note: Since we always stay in right-handed system, third coord sign is always positive! + ('X', 'Y'): ((0, 1), (1, 1), (2, 1)), + ('X', '-Y'): ((0, 1), (1, -1), (2, 1)), + ('X', 'Z'): ((0, 1), (2, 1), (1, 1)), + ('X', '-Z'): ((0, 1), (2, -1), (1, 1)), + ('-X', 'Y'): ((0, -1), (1, 1), (2, 1)), + ('-X', '-Y'): ((0, -1), (1, -1), (2, 1)), + ('-X', 'Z'): ((0, -1), (2, 1), (1, 1)), + ('-X', '-Z'): ((0, -1), (2, -1), (1, 1)), + ('Y', 'X'): ((1, 1), (0, 1), (2, 1)), + ('Y', '-X'): ((1, 1), (0, -1), (2, 1)), + ('Y', 'Z'): ((1, 1), (2, 1), (0, 1)), + ('Y', '-Z'): ((1, 1), (2, -1), (0, 1)), + ('-Y', 'X'): ((1, -1), (0, 1), (2, 1)), + ('-Y', '-X'): ((1, -1), (0, -1), (2, 1)), + ('-Y', 'Z'): ((1, -1), (2, 1), (0, 1)), + ('-Y', '-Z'): ((1, -1), (2, -1), (0, 1)), + ('Z', 'X'): ((2, 1), (0, 1), (1, 1)), + ('Z', '-X'): ((2, 1), (0, -1), (1, 1)), + ('Z', 'Y'): ((2, 1), (1, 1), (0, 1)), # Blender system! + ('Z', '-Y'): ((2, 1), (1, -1), (0, 1)), + ('-Z', 'X'): ((2, -1), (0, 1), (1, 1)), + ('-Z', '-X'): ((2, -1), (0, -1), (1, 1)), + ('-Z', 'Y'): ((2, -1), (1, 1), (0, 1)), + ('-Z', '-Y'): ((2, -1), (1, -1), (0, 1)), +} + + +##### UIDs code. ##### + +# ID class (mere int). +class UID(int): + pass + + +# UIDs storage. +_keys_to_uids = {} +_uids_to_keys = {} + + +def _key_to_uid(uids, key): + # TODO: Check this is robust enough for our needs! + # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids! + # As int64 is signed in FBX, we keep uids below 2**63... + if isinstance(key, int) and 0 <= key < 2**63: + # We can use value directly as id! + uid = key + else: + uid = hash(key) + if uid < 0: + uid = -uid + if uid >= 2**63: + uid //= 2 + # Make sure our uid *is* unique. + if uid in uids: + inc = 1 if uid < 2**62 else -1 + while uid in uids: + uid += inc + if 0 > uid >= 2**63: + # Note that this is more that unlikely, but does not harm anyway... + raise ValueError("Unable to generate an UID for key {}".format(key)) + return UID(uid) + + +def get_fbxuid_from_key(key): + """ + Return an UID for given key, which is assumed hasable. + """ + uid = _keys_to_uids.get(key, None) + if uid is None: + uid = _key_to_uid(_uids_to_keys, key) + _keys_to_uids[key] = uid + _uids_to_keys[uid] = key + return uid + + +# XXX Not sure we'll actually need this one? +def get_key_from_fbxuid(uid): + """ + Return the key which generated this uid. + """ + assert(uid.__class__ == UID) + return _uids_to_keys.get(uid, None) + + +# Blender-specific key generators +def get_blenderID_key(bid): + return "B" + bid.rna_type.name + "::" + bid.name + + +def get_blender_bone_key(armature, bone): + """Return bone's keys (Model and NodeAttribute).""" + key = "|".join((get_blenderID_key(armature), get_blenderID_key(bone))) + return key, key + "_Data" + + +def get_blender_armature_bindpose_key(armature, mesh): + """Return armature's bindpose key.""" + return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "BindPose")) + + +def get_blender_armature_skin_key(armature, mesh): + """Return armature's skin key.""" + return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "DeformerSkin")) + + +def get_blender_bone_cluster_key(armature, mesh, bone): + """Return bone's cluster key.""" + return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), + get_blenderID_key(bone), "SubDeformerCluster")) + + +##### Element generators. ##### + +# Note: elem may be None, in this case the element is not added to any parent. +def elem_empty(elem, name): + sub_elem = encode_bin.FBXElem(name) + if elem is not None: + elem.elems.append(sub_elem) + return sub_elem + + +def elem_properties(elem): + return elem_empty(elem, b"Properties70") + + +def _elem_data_single(elem, name, value, func_name): + sub_elem = elem_empty(elem, name) + getattr(sub_elem, func_name)(value) + return sub_elem + + +def _elem_data_vec(elem, name, value, func_name): + sub_elem = elem_empty(elem, name) + func = getattr(sub_elem, func_name) + for v in value: + func(v) + return sub_elem + + +def elem_data_single_bool(elem, name, value): + return _elem_data_single(elem, name, value, "add_bool") + + +def elem_data_single_int16(elem, name, value): + return _elem_data_single(elem, name, value, "add_int16") + + +def elem_data_single_int32(elem, name, value): + return _elem_data_single(elem, name, value, "add_int32") + + +def elem_data_single_int64(elem, name, value): + return _elem_data_single(elem, name, value, "add_int64") + + +def elem_data_single_float32(elem, name, value): + return _elem_data_single(elem, name, value, "add_float32") + + +def elem_data_single_float64(elem, name, value): + return _elem_data_single(elem, name, value, "add_float64") + + +def elem_data_single_bytes(elem, name, value): + return _elem_data_single(elem, name, value, "add_bytes") + + +def elem_data_single_string(elem, name, value): + return _elem_data_single(elem, name, value, "add_string") + + +def elem_data_single_string_unicode(elem, name, value): + return _elem_data_single(elem, name, value, "add_string_unicode") + + +def elem_data_single_bool_array(elem, name, value): + return _elem_data_single(elem, name, value, "add_bool_array") + + +def elem_data_single_int32_array(elem, name, value): + return _elem_data_single(elem, name, value, "add_int32_array") + + +def elem_data_single_int64_array(elem, name, value): + return _elem_data_single(elem, name, value, "add_int64_array") + + +def elem_data_single_float32_array(elem, name, value): + return _elem_data_single(elem, name, value, "add_float32_array") + + +def elem_data_single_float64_array(elem, name, value): + return _elem_data_single(elem, name, value, "add_float64_array") + + +def elem_data_single_byte_array(elem, name, value): + return _elem_data_single(elem, name, value, "add_byte_array") + + +def elem_data_vec_float64(elem, name, value): + return _elem_data_vec(elem, name, value, "add_float64") + +##### Generators for standard FBXProperties70 properties. ##### + +# Properties definitions, format: (b"type_1", b"type_2", b"type_3", "name_set_value_1", "name_set_value_2", ...) +# XXX Looks like there can be various variations of formats here... Will have to be checked ultimately! +# Among other things, what are those "A"/"A+"/"AU" codes? +FBX_PROPERTIES_DEFINITIONS = { + "p_bool": (b"bool", b"", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!! + "p_integer": (b"int", b"Integer", b"", "add_int32"), + "p_enum": (b"enum", b"", b"", "add_int32"), + "p_number": (b"double", b"Number", b"", "add_float64"), + "p_visibility": (b"Visibility", b"", b"A+", "add_float64"), + "p_fov": (b"FieldOfView", b"", b"A+", "add_float64"), + "p_fov_x": (b"FieldOfViewX", b"", b"A+", "add_float64"), + "p_fov_y": (b"FieldOfViewY", b"", b"A+", "add_float64"), + "p_vector_3d": (b"Vector3D", b"Vector", b"", "add_float64", "add_float64", "add_float64"), + "p_lcl_translation": (b"Lcl Translation", b"", b"A+", "add_float64", "add_float64", "add_float64"), + "p_lcl_rotation": (b"Lcl Rotation", b"", b"A+", "add_float64", "add_float64", "add_float64"), + "p_lcl_scaling": (b"Lcl Scaling", b"", b"A+", "add_float64", "add_float64", "add_float64"), + "p_color_rgb": (b"ColorRGB", b"Color", b"", "add_float64", "add_float64", "add_float64"), + "p_string": (b"KString", b"", b"", "add_string_unicode"), + "p_string_url": (b"KString", b"Url", b"", "add_string_unicode"), + "p_timestamp": (b"KTime", b"Time", b"", "add_int64"), + "p_datetime": (b"DateTime", b"", b"", "add_string_unicode"), + "p_object": (b"object", b"", b""), # XXX Check this! No value for this prop??? + "p_compound": (b"Compound", b"", b""), # XXX Check this! No value for this prop??? +} + + +def _elem_props_set(elem, ptype, name, value): + p = elem_data_single_string(elem, b"P", name) + for t in ptype[:3]: + p.add_string(t) + if len(ptype) == 4: + getattr(p, ptype[3])(value) + elif len(ptype) > 4: + # We assume value is iterable, else it's a bug! + for callback, val in zip(ptype[3:], value): + getattr(p, callback)(val) + + +def elem_props_set(elem, ptype, name, value=None): + ptype = FBX_PROPERTIES_DEFINITIONS[ptype] + _elem_props_set(elem, ptype, name, value) + + +def elem_props_compound(elem, cmpd_name): + def _setter(ptype, name, value): + name = cmpd_name + b"|" + name + elem_props_set(elem, ptype, name, value) + + elem_props_set(elem, "p_compound", cmpd_name) + return _setter + + +def elem_props_template_set(template, elem, ptype_name, name, value): + """ + Only add a prop if the same value is not already defined in given template. + Note it is important to not give iterators as value, here! + """ + ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name] + tmpl_val, tmpl_ptype = template.properties.get(name, (None, None)) + if tmpl_ptype is not None: + if ((len(ptype) == 4 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or + (len(ptype) > 4 and (tuple(tmpl_val), tmpl_ptype) == (tuple(value), ptype_name))): + return # Already in template and same value. + _elem_props_set(elem, ptype, name, value) + + +##### Generators for connection elements. ##### + +def elem_connection(elem, c_type, uid_src, uid_dst, prop_dst=None): + e = elem_data_single_string(elem, b"C", c_type) + e.add_int64(uid_src) + e.add_int64(uid_dst) + if prop_dst is not None: + e.add_string(prop_dst) + + +##### Templates ##### +# TODO: check all those "default" values, they should match Blender's default as much as possible, I guess? + +FBXTemplate = namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users")) + + +def fbx_templates_generate(root, fbx_templates): + # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties + # for Lights, Cameras, LibNodes, etc.). + templates = OrderedDict() + for type_name, prop_type_name, properties, nbr_users in fbx_templates.values(): + if type_name not in templates: + templates[type_name] = [OrderedDict(((prop_type_name, properties),)), nbr_users] + else: + templates[type_name][0][prop_type_name] = properties + templates[type_name][1] += nbr_users + + for type_name, (subprops, nbr_users) in templates.items(): + template = elem_data_single_string(root, b"ObjectType", type_name) + elem_data_single_int32(template, b"Count", nbr_users) + + for prop_type_name, properties in subprops.items(): + if prop_type_name and properties: + elem = elem_data_single_string(template, b"PropertyTemplate", prop_type_name) + props = elem_properties(elem) + for name, (value, ptype) in properties.items(): + elem_props_set(props, ptype, name, value) + + +def fbx_template_def_globalsettings(scene, settings, override_defaults=None, nbr_users=0): + props = {} + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"GlobalSettings", b"", props, nbr_users) + + +def fbx_template_def_model(scene, settings, override_defaults=None, nbr_users=0): + gscale = settings.global_scale + props = { + b"QuaternionInterpolate": (False, "p_bool"), + b"RotationOffset": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"RotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"ScalingOffset": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"ScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"TranslationActive": (False, "p_bool"), + b"TranslationMin": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"TranslationMax": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"TranslationMinX": (False, "p_bool"), + b"TranslationMinY": (False, "p_bool"), + b"TranslationMinZ": (False, "p_bool"), + b"TranslationMaxX": (False, "p_bool"), + b"TranslationMaxY": (False, "p_bool"), + b"TranslationMaxZ": (False, "p_bool"), + b"RotationOrder": (0, "p_enum"), # we always use 'XYZ' order. + b"RotationSpaceForLimitOnly": (False, "p_bool"), + b"RotationStiffnessX": (0.0, "p_number"), + b"RotationStiffnessY": (0.0, "p_number"), + b"RotationStiffnessZ": (0.0, "p_number"), + b"AxisLen": (10.0, "p_number"), + b"PreRotation": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"PostRotation": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"RotationActive": (False, "p_bool"), + b"RotationMin": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"RotationMax": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"RotationMinX": (False, "p_bool"), + b"RotationMinY": (False, "p_bool"), + b"RotationMinZ": (False, "p_bool"), + b"RotationMaxX": (False, "p_bool"), + b"RotationMaxY": (False, "p_bool"), + b"RotationMaxZ": (False, "p_bool"), + b"InheritType": (1, "p_enum"), # RSrs + b"ScalingActive": (False, "p_bool"), + b"ScalingMin": (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d"), + b"ScalingMax": (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d"), + b"ScalingMinX": (False, "p_bool"), + b"ScalingMinY": (False, "p_bool"), + b"ScalingMinZ": (False, "p_bool"), + b"ScalingMaxX": (False, "p_bool"), + b"ScalingMaxY": (False, "p_bool"), + b"ScalingMaxZ": (False, "p_bool"), + b"GeometricTranslation": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"GeometricRotation": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"GeometricScaling": (Vector((1.0, 1.0, 1.0)) * gscale, "p_vector_3d"), + b"MinDampRangeX": (0.0, "p_number"), + b"MinDampRangeY": (0.0, "p_number"), + b"MinDampRangeZ": (0.0, "p_number"), + b"MaxDampRangeX": (0.0, "p_number"), + b"MaxDampRangeY": (0.0, "p_number"), + b"MaxDampRangeZ": (0.0, "p_number"), + b"MinDampStrengthX": (0.0, "p_number"), + b"MinDampStrengthY": (0.0, "p_number"), + b"MinDampStrengthZ": (0.0, "p_number"), + b"MaxDampStrengthX": (0.0, "p_number"), + b"MaxDampStrengthY": (0.0, "p_number"), + b"MaxDampStrengthZ": (0.0, "p_number"), + b"PreferedAngleX": (0.0, "p_number"), + b"PreferedAngleY": (0.0, "p_number"), + b"PreferedAngleZ": (0.0, "p_number"), + b"LookAtProperty": (None, "p_object"), + b"UpVectorProperty": (None, "p_object"), + b"Show": (True, "p_bool"), + b"NegativePercentShapeSupport": (True, "p_bool"), + b"DefaultAttributeIndex": (0, "p_integer"), + b"Freeze": (False, "p_bool"), + b"LODBox": (False, "p_bool"), + b"Lcl Translation": ((0.0, 0.0, 0.0), "p_lcl_translation"), + b"Lcl Rotation": ((0.0, 0.0, 0.0), "p_lcl_rotation"), + b"Lcl Scaling": (Vector((1.0, 1.0, 1.0)) * gscale, "p_lcl_scaling"), + b"Visibility": (1.0, "p_visibility"), + } + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Model", b"FbxNode", props, nbr_users) + + +def fbx_template_def_light(scene, settings, override_defaults=None, nbr_users=0): + gscale = settings.global_scale + props = { + b"LightType": (0, "p_enum"), # Point light. + b"CastLight": (True, "p_bool"), + b"Color": ((1.0, 1.0, 1.0), "p_color_rgb"), + b"Intensity": (100.0, "p_number"), # Times 100 compared to Blender values... + b"DecayType": (2, "p_enum"), # Quadratic. + b"DecayStart": (30.0 * gscale, "p_number"), + b"CastShadows": (True, "p_bool"), + b"ShadowColor": ((0.0, 0.0, 0.0), "p_color_rgb"), + b"AreaLightShape": (0, "p_enum"), # Rectangle. + } + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"NodeAttribute", b"FbxLight", props, nbr_users) + + +def fbx_template_def_camera(scene, settings, override_defaults=None, nbr_users=0): + props = {} + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"NodeAttribute", b"FbxCamera", props, nbr_users) + + +def fbx_template_def_bone(scene, settings, override_defaults=None, nbr_users=0): + props = {} + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"NodeAttribute", b"LimbNode", props, nbr_users) + + +def fbx_template_def_geometry(scene, settings, override_defaults=None, nbr_users=0): + props = { + b"Color": ((0.8, 0.8, 0.8), "p_color_rgb"), + b"BBoxMin": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"BBoxMax": ((0.0, 0.0, 0.0), "p_vector_3d"), + } + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Geometry", b"FbxMesh", props, nbr_users) + + +def fbx_template_def_material(scene, settings, override_defaults=None, nbr_users=0): + # WIP... + props = { + b"ShadingModel": ("phong", "p_string"), + b"MultiLayer": (False, "p_bool"), + # Lambert-specific. + b"EmissiveColor": ((0.8, 0.8, 0.8), "p_color_rgb"), # Same as diffuse. + b"EmissiveFactor": (0.0, "p_number"), + b"AmbientColor": ((0.0, 0.0, 0.0), "p_color_rgb"), + b"AmbientFactor": (1.0, "p_number"), + b"DiffuseColor": ((0.8, 0.8, 0.8), "p_color_rgb"), + b"DiffuseFactor": (0.8, "p_number"), + b"TransparentColor": ((0.8, 0.8, 0.8), "p_color_rgb"), # Same as diffuse. + b"TransparencyFactor": (0.0, "p_number"), + b"NormalMap": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"BumpFactor": (1.0, "p_number"), + b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"), + b"DisplacementFactor": (0.0, "p_number"), + # Phong-specific. + b"SpecularColor": ((1.0, 1.0, 1.0), "p_color_rgb"), + b"SpecularFactor": (0.5 / 2.0, "p_number"), + # Not sure about the name, importer use this (but ShininessExponent for tex prop name!) + # And in fbx exported by sdk, you have one in template, the other in actual material!!! :/ + # For now, using both. + b"Shininess": ((50.0 - 1.0) / 5.10, "p_number"), + b"ShininessExponent": ((50.0 - 1.0) / 5.10, "p_number"), + b"ReflectionColor": ((1.0, 1.0, 1.0), "p_color_rgb"), + b"ReflectionFactor": (0.0, "p_number"), + } + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Material", b"FbxSurfacePhong", props, nbr_users) + + +def fbx_template_def_texture_file(scene, settings, override_defaults=None, nbr_users=0): + # WIP... + # XXX Not sure about all names! + props = { + b"TextureTypeUse": (0, "p_enum"), # Standard. + b"AlphaSource": (2, "p_enum"), # Black (i.e. texture's alpha), XXX name guessed!. + b"Texture alpha": (1.0, "p_number"), + b"PremultiplyAlpha": (False, "p_bool"), + b"CurrentTextureBlendMode": (0, "p_enum"), # Translucent, assuming this means "Alpha over"! + b"CurrentMappingType": (1, "p_enum"), # Planar. + b"WrapModeU": (0, "p_enum"), # Repeat. + b"WrapModeV": (0, "p_enum"), # Repeat. + b"UVSwap": (False, "p_bool"), + b"Translation": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"Rotation": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"Scaling": ((1.0, 1.0, 1.0), "p_vector_3d"), + b"TextureRotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"TextureScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d"), + # Not sure about those two... At least, UseMaterial should always be ON imho. + b"UseMaterial": (True, "p_bool"), + b"UseMipMap": (False, "p_bool"), + } + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Texture", b"FbxFileTexture", props, nbr_users) + + +def fbx_template_def_video(scene, settings, override_defaults=None, nbr_users=0): + # WIP... + props = { + # All pictures. + b"Width": (0, "p_integer"), + b"Height": (0, "p_integer"), + b"Path": ("", "p_string_url"), + b"AccessMode": (0, "p_enum"), # Disk (0=Disk, 1=Mem, 2=DiskAsync). + # All videos. + b"StartFrame": (0, "p_integer"), + b"StopFrame": (0, "p_integer"), + b"Offset": (0, "p_timestamp"), + b"PlaySpeed": (1.0, "p_number"), + b"FreeRunning": (False, "p_bool"), + b"Loop": (False, "p_bool"), + b"InterlaceMode": (0, "p_enum"), # None, i.e. progressive. + # Image sequences. + b"ImageSequence": (False, "p_bool"), + b"ImageSequenceOffset": (0, "p_integer"), + b"FrameRate": (scene.render.fps / scene.render.fps_base, "p_number"), + b"LastFrame": (0, "p_integer"), + } + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Video", b"FbxVideo", props, nbr_users) + + +def fbx_template_def_pose(scene, settings, override_defaults=None, nbr_users=0): + props = {} + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Pose", b"", props, nbr_users) + + +def fbx_template_def_deformer(scene, settings, override_defaults=None, nbr_users=0): + props = {} + if override_defaults is not None: + props.update(override_defaults) + return FBXTemplate(b"Deformer", b"", props, nbr_users) + + +##### FBX objects generators. ##### + +def object_matrix(scene_data, obj, armature=None, global_space=False): + """ + Generate object transform matrix. + If global_space is False, returned matrix is in parent space if parent exists and is exported, else in world space. + If global_space is True, returned matrix is always in world space. + If obj is a bone, and global_space is True, armature must be provided (it's the bone's armature object!). + Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX). + """ + is_bone = isinstance(obj, bpy.types.Bone) + # Objects which are not bones and do not have any parent are *always* in global space! + is_global = global_space or not (is_bone or (obj.parent and obj.parent in scene_data.objects)) + + #assert((is_bone and is_global and armature is None) == False, + #"You must provide an armature object to get bones transform matrix in global space!") + + matrix = obj.matrix_local + + # Lamps, cameras and bones need to be rotated (in local space!). + if is_bone: + matrix = matrix * MAT_CONVERT_BONE + elif obj.type == 'LAMP': + matrix = matrix * MAT_CONVERT_LAMP + elif obj.type == 'CAMERA': + matrix = matrix * MAT_CONVERT_CAMERA + + # Up till here, our matrix is in local space, time to bring it in its final desired space. + if is_bone: + # Bones are in armature (object) space currently, either bring them to global space or real + # local space (relative to parent bone). + if is_global: + matrix = scene_data.settings.global_matrix * armature.matrix_world * matrix + elif obj.parent: # Parent bone, get matrix relative to it. + par_matrix = obj.parent.matrix_local * MAT_CONVERT_BONE + matrix = par_matrix.inverted() * matrix + elif is_global: + if obj.parent: + matrix = obj.parent.matrix_world * matrix + matrix = scene_data.settings.global_matrix * matrix + + return matrix + + +def object_tx(scene_data, obj): + """ + Generate object transform data (always in local space when possible). + """ + matrix = object_matrix(scene_data, obj) + loc, rot, scale = matrix.decompose() + matrix_rot = rot.to_matrix() + rot = rot.to_euler() # quat -> euler, we always use 'XYZ' order. + + return loc, rot, scale, matrix, matrix_rot + + +def fbx_name_class(name, cls): + return FBX_NAME_CLASS_SEP.join((name, cls)) + + +def fbx_data_element_custom_properties(tmpl, props, bid): + """ + Store custom properties of blender ID bid (any mapping-like object, in fact) into FBX properties props. + """ + for k, v in bid.items(): + if isinstance(v, str): + elem_props_template_set(tmpl, props, "p_string", k.encode(), v) + elif isinstance(v, int): + elem_props_template_set(tmpl, props, "p_integer", k.encode(), v) + if isinstance(v, float): + elem_props_template_set(tmpl, props, "p_number", k.encode(), v) + + +def fbx_data_lamp_elements(root, lamp, scene_data): + """ + Write the Lamp data block. + """ + gscale = scene_data.settings.global_scale + + lamp_key = scene_data.data_lamps[lamp] + do_light = True + decay_type = FBX_LIGHT_DECAY_TYPES['CONSTANT'] + do_shadow = False + shadow_color = Vector((0.0, 0.0, 0.0)) + if lamp.type not in {'HEMI'}: + if lamp.type not in {'SUN'}: + decay_type = FBX_LIGHT_DECAY_TYPES[lamp.falloff_type] + do_light = (not lamp.use_only_shadow) and (lamp.use_specular or lamp.use_diffuse) + do_shadow = lamp.shadow_method not in {'NOSHADOW'} + shadow_color = lamp.shadow_color + + light = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(lamp_key)) + light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute")) + light.add_string(b"Light") + + elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION) # Sic... + + tmpl = scene_data.templates[b"Light"] + props = elem_properties(light) + elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type]) + elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light) + elem_props_template_set(tmpl, props, "p_color_rgb", b"Color", lamp.color) + elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0) + elem_props_template_set(tmpl, props, "p_enum", b"DecayType", decay_type) + elem_props_template_set(tmpl, props, "p_number", b"DecayStart", lamp.distance * gscale) + elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow) + elem_props_template_set(tmpl, props, "p_color_rgb", b"ShadowColor", shadow_color) + if lamp.type in {'SPOT'}: + elem_props_template_set(tmpl, props, "p_number", b"OuterAngle", math.degrees(lamp.spot_size)) + elem_props_template_set(tmpl, props, "p_number", b"InnerAngle", + math.degrees(lamp.spot_size * (1.0 - lamp.spot_blend))) + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, lamp) + + +def fbx_data_camera_elements(root, cam_obj, scene_data): + """ + Write the Camera data blocks. + """ + gscale = scene_data.settings.global_scale + + cam_data = cam_obj.data + cam_key = scene_data.data_cameras[cam_obj] + + # Real data now, good old camera! + # Object transform info. + loc, rot, scale, matrix, matrix_rot = object_tx(scene_data, cam_obj) + up = matrix_rot * Vector((0.0, 1.0, 0.0)) + to = matrix_rot * Vector((0.0, 0.0, -1.0)) + # Render settings. + # TODO We could export much more... + render = scene_data.scene.render + width = render.resolution_x + height = render.resolution_y + aspect = width / height + # Film width & height from mm to inches + filmwidth = units_convert(cam_data.sensor_width, "millimeter", "inch") + filmheight = units_convert(cam_data.sensor_height, "millimeter", "inch") + filmaspect = filmwidth / filmheight + # Film offset + offsetx = filmwidth * cam_data.shift_x + offsety = filmaspect * filmheight * cam_data.shift_y + + cam = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(cam_key)) + cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute")) + cam.add_string(b"Camera") + + tmpl = scene_data.templates[b"Camera"] + props = elem_properties(cam) + elem_props_template_set(tmpl, props, "p_vector_3d", b"Position", loc) + elem_props_template_set(tmpl, props, "p_vector_3d", b"UpVector", up) + elem_props_template_set(tmpl, props, "p_vector_3d", b"InterestPosition", to) + # Should we use world value? + elem_props_template_set(tmpl, props, "p_color_rgb", b"BackgroundColor", (0.0, 0.0, 0.0)) + elem_props_template_set(tmpl, props, "p_bool", b"DisplayTurnTableIcon", True) + + elem_props_template_set(tmpl, props, "p_number", b"FilmWidth", filmwidth) + elem_props_template_set(tmpl, props, "p_number", b"FilmHeight", filmheight) + elem_props_template_set(tmpl, props, "p_number", b"FilmAspectRatio", filmaspect) + elem_props_template_set(tmpl, props, "p_number", b"FilmOffsetX", offsetx) + elem_props_template_set(tmpl, props, "p_number", b"FilmOffsetY", offsety) + + elem_props_template_set(tmpl, props, "p_enum", b"ApertureMode", 3) # FocalLength. + elem_props_template_set(tmpl, props, "p_enum", b"GateFit", 2) # FitHorizontal. + elem_props_template_set(tmpl, props, "p_fov", b"FieldOfView", math.degrees(cam_data.angle_x)) + elem_props_template_set(tmpl, props, "p_fov_x", b"FieldOfViewX", math.degrees(cam_data.angle_x)) + elem_props_template_set(tmpl, props, "p_fov_y", b"FieldOfViewY", math.degrees(cam_data.angle_y)) + # No need to convert to inches here... + elem_props_template_set(tmpl, props, "p_number", b"FocalLength", cam_data.lens) + elem_props_template_set(tmpl, props, "p_number", b"SafeAreaAspectRatio", aspect) + + elem_props_template_set(tmpl, props, "p_number", b"NearPlane", cam_data.clip_start * gscale) + elem_props_template_set(tmpl, props, "p_number", b"FarPlane", cam_data.clip_end * gscale) + elem_props_template_set(tmpl, props, "p_enum", b"BackPlaneDistanceMode", 1) # RelativeToCamera. + elem_props_template_set(tmpl, props, "p_number", b"BackPlaneDistance", cam_data.clip_end * gscale) + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, cam_data) + + elem_data_single_string(cam, b"TypeFlags", b"Camera") + elem_data_single_int32(cam, b"GeometryVersion", 124) # Sic... + elem_data_vec_float64(cam, b"Position", loc) + elem_data_vec_float64(cam, b"Up", up) + elem_data_vec_float64(cam, b"LookAt", to) + elem_data_single_int32(cam, b"ShowInfoOnMoving", 1) + elem_data_single_int32(cam, b"ShowAudio", 0) + elem_data_vec_float64(cam, b"AudioColor", (0.0, 1.0, 0.0)) + elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0) + + +def fbx_data_mesh_elements(root, me, scene_data): + """ + Write the Mesh (Geometry) data block. + """ + # No gscale/gmat here, all data are supposed to be in object space. + smooth_type = scene_data.settings.mesh_smooth_type + + me_key = scene_data.data_meshes[me] + geom = elem_data_single_int64(root, b"Geometry", get_fbxuid_from_key(me_key)) + geom.add_string(fbx_name_class(me.name.encode(), b"Geometry")) + geom.add_string(b"Mesh") + + tmpl = scene_data.templates[b"Geometry"] + props = elem_properties(geom) + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, me) + + elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION) + + # Vertex cos. + t_co = array.array(data_types.ARRAY_FLOAT64, [0.0] * len(me.vertices) * 3) + me.vertices.foreach_get("co", t_co) + elem_data_single_float64_array(geom, b"Vertices", t_co) + del t_co + + # Polygon indices. + # + # We do loose edges as two-vertices faces, if enabled... + # + # Note we have to process Edges in the same time, as they are based on poly's loops... + loop_nbr = len(me.loops) + t_pvi = array.array(data_types.ARRAY_INT32, (0,) * loop_nbr) + t_ls = [None] * len(me.polygons) + + me.loops.foreach_get("vertex_index", t_pvi) + me.polygons.foreach_get("loop_start", t_ls) + + # Add "fake" faces for loose edges. + if scene_data.settings.use_mesh_edges: + t_le = tuple(e.vertices for e in me.edges if e.is_loose) + t_pvi.extend(chain(*t_le)) + t_ls.extend(range(loop_nbr, loop_nbr + len(t_le), 2)) + del t_le + + # Edges... + # Note: Edges are represented as a loop here: each edge uses a single index, which refers to the polygon array. + # The edge is made by the vertex indexed py this polygon's point and the next one on the same polygon. + # Advantage: Only one index per edge. + # Drawback: Only polygon's edges can be represented (that's why we have to add fake two-verts polygons + # for loose edges). + # We also have to store a mapping from real edges to their indices in this array, for edge-mapped data + # (like e.g. crease). + t_eli = array.array(data_types.ARRAY_INT32) + edges_map = {} + edges_nbr = 0 + if t_ls and t_pvi: + t_ls = set(t_ls) + todo_edges = [None] * len(me.edges) * 2 + me.edges.foreach_get("vertices", todo_edges) + todo_edges = set((v1, v2) if v1 < v2 else (v2, v1) for v1, v2 in zip(*(iter(todo_edges),) * 2)) + + li = 0 + vi = vi_start = t_pvi[0] + for li_next, vi_next in enumerate(t_pvi[1:] + t_pvi[:1], start=1): + if li_next in t_ls: # End of a poly's loop. + vi2 = vi_start + vi_start = vi_next + else: + vi2 = vi_next + + e_key = (vi, vi2) if vi < vi2 else (vi2, vi) + if e_key in todo_edges: + t_eli.append(li) + todo_edges.remove(e_key) + edges_map[e_key] = edges_nbr + edges_nbr += 1 + + vi = vi_next + li = li_next + # End of edges! + + # We have to ^-1 last index of each loop. + for ls in t_ls: + t_pvi[ls - 1] ^= -1 + + # And finally we can write data! + elem_data_single_int32_array(geom, b"PolygonVertexIndex", t_pvi) + elem_data_single_int32_array(geom, b"Edges", t_eli) + del t_pvi + del t_ls + del t_eli + + # And now, layers! + + # Smoothing. + if smooth_type in {'FACE', 'EDGE'}: + t_ps = None + _map = b"" + if smooth_type == 'FACE': + t_ps = array.array(data_types.ARRAY_INT32, [0] * len(me.polygons)) + me.polygons.foreach_get("use_smooth", t_ps) + _map = b"ByPolygon" + else: # EDGE + # Write Edge Smoothing. + t_ps = array.array(data_types.ARRAY_INT32, (0,) * edges_nbr) + for e in me.edges: + if e.key not in edges_map: + continue # Only loose edges, in theory! + t_ps[edges_map[e.key]] = not e.use_edge_sharp + _map = b"ByEdge" + lay_smooth = elem_data_single_int32(geom, b"LayerElementSmoothing", 0) + elem_data_single_int32(lay_smooth, b"Version", FBX_GEOMETRY_SMOOTHING_VERSION) + elem_data_single_string(lay_smooth, b"Name", b"") + elem_data_single_string(lay_smooth, b"MappingInformationType", _map) + elem_data_single_string(lay_smooth, b"ReferenceInformationType", b"Direct") + elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps); # Sight, int32 for bool... + del t_ps + + # TODO: Edge crease (LayerElementCrease). + + # And we are done with edges! + del edges_map + + # Loop normals. + # NOTE: this is not supported by importer currently. + # XXX Official docs says normals should use IndexToDirect, + # but this does not seem well supported by apps currently... + me.calc_normals_split() + if 0: + def _nortuples_gen(raw_nors): + return zip(*(iter(raw_nors),) * 3) + + t_ln = array.array(data_types.ARRAY_FLOAT64, [0.0] * len(me.loops) * 3) + me.loops.foreach_get("normal", t_ln) + lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) + elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION) + elem_data_single_string(lay_nor, b"Name", b"") + elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect") + + ln2idx = tuple(set(_nortuples_gen(t_ln))) + elem_data_single_float64_array(lay_nor, b"Normals", chain(*ln2idx)) + # Normal weights, no idea what it is. + elem_data_single_float64_array(lay_nor, b"NormalsW", (0.0,) * len(ln2idx)) + + ln2idx = {nor: idx for idx, nor in enumerate(ln2idx)} + elem_data_single_int32_array(lay_nor, b"NormalIndex", (ln2idx[n] for n in _nortuples_gen(t_ln))) + + del ln2idx + del t_ln + del _nortuples_gen + else: + t_ln = array.array(data_types.ARRAY_FLOAT64, [0.0] * len(me.loops) * 3) + me.loops.foreach_get("normal", t_ln) + lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0) + elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION) + elem_data_single_string(lay_nor, b"Name", b"") + elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct") + elem_data_single_float64_array(lay_nor, b"Normals", t_ln) + # Normal weights, no idea what it is. + elem_data_single_float64_array(lay_nor, b"NormalsW", (0.0,) * len(t_ln)) + del t_ln + + # tspace + tspacenumber = 0 + if scene_data.settings.use_tspace: + tspacenumber = len(me.uv_layers) + if tspacenumber: + t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,) * len(me.loops) * 3) + for idx, uvlayer in enumerate(me.uv_layers): + name = uvlayer.name + me.calc_tangents(name) + # Loop bitangents (aka binormals). + # NOTE: this is not supported by importer currently. + me.loops.foreach_get("bitangent", t_ln) + lay_nor = elem_data_single_int32(geom, b"LayerElementBinormal", idx) + elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_BINORMAL_VERSION) + elem_data_single_string_unicode(lay_nor, b"Name", name) + elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct") + elem_data_single_float64_array(lay_nor, b"Binormals", t_ln) + # Binormal weights, no idea what it is. + elem_data_single_float64_array(lay_nor, b"BinormalsW", (0.0,) * len(t_ln)) + + # Loop tangents. + # NOTE: this is not supported by importer currently. + me.loops.foreach_get("tangent", t_ln) + lay_nor = elem_data_single_int32(geom, b"LayerElementTangent", idx) + elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_TANGENT_VERSION) + elem_data_single_string_unicode(lay_nor, b"Name", name) + elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct") + elem_data_single_float64_array(lay_nor, b"Tangents", t_ln) + # Tangent weights, no idea what it is. + elem_data_single_float64_array(lay_nor, b"TangentsW", (0.0,) * len(t_ln)) + + del t_ln + me.free_tangents() + + me.free_normals_split() + + # Write VertexColor Layers + # note, no programs seem to use this info :/ + vcolnumber = len(me.vertex_colors) + if vcolnumber: + def _coltuples_gen(raw_cols): + def _infinite_gen(val): + while 1: yield val + return zip(*(iter(raw_cols),) * 3 + (_infinite_gen(1.0),)) # We need a fake alpha... + + t_lc = array.array(data_types.ARRAY_FLOAT64, [0.0] * len(me.loops) * 3) + for colindex, collayer in enumerate(me.vertex_colors): + collayer.data.foreach_get("color", t_lc) + lay_vcol = elem_data_single_int32(geom, b"LayerElementColor", colindex) + elem_data_single_int32(lay_vcol, b"Version", FBX_GEOMETRY_VCOLOR_VERSION) + elem_data_single_string_unicode(lay_vcol, b"Name", collayer.name) + elem_data_single_string(lay_vcol, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_vcol, b"ReferenceInformationType", b"IndexToDirect") + + col2idx = tuple(set(_coltuples_gen(t_lc))) + elem_data_single_float64_array(lay_vcol, b"Colors", chain(*col2idx)) # Flatten again... + + col2idx = {col: idx for idx, col in enumerate(col2idx)} + elem_data_single_int32_array(lay_vcol, b"ColorIndex", (col2idx[c] for c in _coltuples_gen(t_lc))); + del col2idx + del t_lc + del _coltuples_gen + + # Write UV layers. + # Note: LayerElementTexture is deprecated since FBX 2011 - luckily! + # Textures are now only related to materials, in FBX! + uvnumber = len(me.uv_layers) + if uvnumber: + def _uvtuples_gen(raw_uvs): + return zip(*(iter(raw_uvs),) * 2) + + t_luv = array.array(data_types.ARRAY_FLOAT64, [0.0] * len(me.loops) * 2) + for uvindex, uvlayer in enumerate(me.uv_layers): + uvlayer.data.foreach_get("uv", t_luv) + lay_uv = elem_data_single_int32(geom, b"LayerElementUV", uvindex) + elem_data_single_int32(lay_uv, b"Version", FBX_GEOMETRY_UV_VERSION) + elem_data_single_string_unicode(lay_uv, b"Name", uvlayer.name) + elem_data_single_string(lay_uv, b"MappingInformationType", b"ByPolygonVertex") + elem_data_single_string(lay_uv, b"ReferenceInformationType", b"IndexToDirect") + + uv2idx = tuple(set(_uvtuples_gen(t_luv))) + elem_data_single_float64_array(lay_uv, b"UV", chain(*uv2idx)) # Flatten again... + + uv2idx = {uv: idx for idx, uv in enumerate(uv2idx)} + elem_data_single_int32_array(lay_uv, b"UVIndex", (uv2idx[uv] for uv in _uvtuples_gen(t_luv))); + del uv2idx + del t_luv + del _uvtuples_gen + + # Face's materials. + me_fbxmats_idx = None + if me in scene_data.mesh_mat_indices: + me_fbxmats_idx = scene_data.mesh_mat_indices[me] + me_blmats = me.materials + if me_fbxmats_idx and me_blmats: + lay_mat = elem_data_single_int32(geom, b"LayerElementMaterial", 0) + elem_data_single_int32(lay_mat, b"Version", FBX_GEOMETRY_MATERIAL_VERSION) + elem_data_single_string(lay_mat, b"Name", b"") + nbr_mats = len(me_fbxmats_idx) + if nbr_mats > 1: + t_pm = array.array(data_types.ARRAY_INT32, [0] * len(me.polygons)) + me.polygons.foreach_get("material_index", t_pm) + + # We have to validate mat indices, and map them to FBX indices. + blmats_to_fbxmats_idxs = [me_fbxmats_idx[m] for m in me_blmats] + mat_idx_limit = len(blmats_to_fbxmats_idxs) + def_mat = blmats_to_fbxmats_idxs[0] + _gen = (blmats_to_fbxmats_idxs[m] if m < mat_idx_limit else def_mat for m in range(len(me_blmats))) + t_pm = array.array(data_types.ARRAY_INT32, _gen) + + elem_data_single_string(lay_mat, b"MappingInformationType", b"ByPolygon") + elem_data_single_string(lay_mat, b"ReferenceInformationType", b"Direct") + elem_data_single_int32_array(lay_mat, b"Materials", t_pm) + del t_pm + else: + elem_data_single_string(lay_mat, b"MappingInformationType", b"AllSame") + elem_data_single_string(lay_mat, b"ReferenceInformationType", b"IndexToDirect") + elem_data_single_int32_array(lay_mat, b"Materials", [0]) + + # And the "layer TOC"... + + layer = elem_data_single_int32(geom, b"Layer", 0) + elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION) + lay_nor = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_nor, b"Type", b"LayerElementNormal") + elem_data_single_int32(lay_nor, b"TypedIndex", 0) + if smooth_type in {'FACE', 'EDGE'}: + lay_smooth = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_smooth, b"Type", b"LayerElementSmoothing") + elem_data_single_int32(lay_smooth, b"TypedIndex", 0) + if vcolnumber: + lay_vcol = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor") + elem_data_single_int32(lay_vcol, b"TypedIndex", 0) + if uvnumber: + lay_uv = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_uv, b"Type", b"LayerElementUV") + elem_data_single_int32(lay_uv, b"TypedIndex", 0) + if me_fbxmats_idx is not None: + lay_mat = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_mat, b"Type", b"LayerElementMaterial") + elem_data_single_int32(lay_mat, b"TypedIndex", 0) + + # Add other uv and/or vcol layers... + for vcolidx, uvidx, tspaceidx in zip_longest(range(1, vcolnumber), range(1, uvnumber), range(1, tspacenumber), + fillvalue=0): + layer = elem_data_single_int32(geom, b"Layer", max(vcolidx, uvidx)) + elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION) + if vcolidx: + lay_vcol = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor") + elem_data_single_int32(lay_vcol, b"TypedIndex", vcolidx) + if uvidx: + lay_uv = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_uv, b"Type", b"LayerElementUV") + elem_data_single_int32(lay_uv, b"TypedIndex", uvidx) + if tspaceidx: + lay_binor = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal") + elem_data_single_int32(lay_binor, b"TypedIndex", tspaceidx) + lay_tan = elem_empty(layer, b"LayerElement") + elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent") + elem_data_single_int32(lay_tan, b"TypedIndex", tspaceidx) + + +def fbx_data_material_elements(root, mat, scene_data): + """ + Write the Material data block. + """ + ambient_color = (0.0, 0.0, 0.0) + if scene_data.data_world: + ambient_color = next(iter(scene_data.data_world.keys())).ambient_color + + mat_key, _objs = scene_data.data_materials[mat] + # Approximation... + mat_type = b"phong" if mat.specular_shader in {'COOKTORR', 'PHONG', 'BLINN'} else b"lambert" + + fbx_mat = elem_data_single_int64(root, b"Material", get_fbxuid_from_key(mat_key)) + fbx_mat.add_string(fbx_name_class(mat.name.encode(), b"Material")) + fbx_mat.add_string(b"") + + elem_data_single_int32(fbx_mat, b"Version", FBX_MATERIAL_VERSION) + # those are not yet properties, it seems... + elem_data_single_string(fbx_mat, b"ShadingModel", mat_type) + elem_data_single_int32(fbx_mat, b"MultiLayer", 0) # Should be bool... + + tmpl = scene_data.templates[b"Material"] + props = elem_properties(fbx_mat) + elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", mat_type.decode()) + elem_props_template_set(tmpl, props, "p_color_rgb", b"EmissiveColor", mat.diffuse_color) + elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", mat.emit) + elem_props_template_set(tmpl, props, "p_color_rgb", b"AmbientColor", ambient_color) + elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", mat.ambient) + elem_props_template_set(tmpl, props, "p_color_rgb", b"DiffuseColor", mat.diffuse_color) + elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", mat.diffuse_intensity) + elem_props_template_set(tmpl, props, "p_color_rgb", b"TransparentColor", mat.diffuse_color) + elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor", mat.alpha if mat.use_transparency else 1.0) + # Those are for later! + """ + b"NormalMap": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"), + b"BumpFactor": (1.0, "p_number"), + b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"), + b"DisplacementFactor": (0.0, "p_number"), + """ + if mat_type == b"phong": + elem_props_template_set(tmpl, props, "p_color_rgb", b"SpecularColor", mat.specular_color) + elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", mat.specular_intensity / 2.0) + # See Material template about those two! + elem_props_template_set(tmpl, props, "p_number", b"Shininess", (mat.specular_hardness - 1.0) / 5.10) + elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", (mat.specular_hardness - 1.0) / 5.10) + elem_props_template_set(tmpl, props, "p_color_rgb", b"ReflectionColor", mat.mirror_color) + elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor", + mat.raytrace_mirror.reflect_factor if mat.raytrace_mirror.use else 0.0) + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, mat) + + +def _gen_vid_path(img, scene_data): + msetts = scene_data.settings.media_settings + fname_rel = bpy_extras.io_utils.path_reference(img.filepath, msetts.base_src, msetts.base_dst, msetts.path_mode, + msetts.subdir, msetts.copy_set, img.library) + fname_abs = os.path.normpath(os.path.abspath(os.path.join(msetts.base_dst, fname_rel))) + return fname_abs, fname_rel + + +def fbx_data_texture_file_elements(root, tex, scene_data): + """ + Write the (file) Texture data block. + """ + # XXX All this is very fuzzy to me currently... + # Textures do not seem to use properties as much as they could. + # For now assuming most logical and simple stuff. + + tex_key, _mats = scene_data.data_textures[tex] + img = tex.texture.image + fname_abs, fname_rel = _gen_vid_path(img, scene_data) + + fbx_tex = elem_data_single_int64(root, b"Texture", get_fbxuid_from_key(tex_key)) + fbx_tex.add_string(fbx_name_class(tex.name.encode(), b"Texture")) + fbx_tex.add_string(b"") + + elem_data_single_string(fbx_tex, b"Type", b"TextureVideoClip") + elem_data_single_int32(fbx_tex, b"Version", FBX_TEXTURE_VERSION) + elem_data_single_string(fbx_tex, b"TextureName", fbx_name_class(tex.name.encode(), b"Texture")) + elem_data_single_string(fbx_tex, b"Media", fbx_name_class(img.name.encode(), b"Video")) + elem_data_single_string_unicode(fbx_tex, b"FileName", fname_abs) + elem_data_single_string_unicode(fbx_tex, b"RelativeFilename", fname_rel) + + alpha_source = 0 # None + if img.use_alpha: + if tex.texture.use_calculate_alpha: + alpha_source = 1 # RGBIntensity as alpha. + else: + alpha_source = 2 # Black, i.e. alpha channel. + # BlendMode not useful for now, only affects layered textures afaics. + mapping = 0 # None. + if tex.texture_coords in {'ORCO'}: # XXX Others? + if tex.mapping in {'FLAT'}: + mapping = 1 # Planar + elif tex.mapping in {'CUBE'}: + mapping = 4 # Box + elif tex.mapping in {'TUBE'}: + mapping = 3 # Cylindrical + elif tex.mapping in {'SPHERE'}: + mapping = 2 # Spherical + elif tex.texture_coords in {'UV'}: + # XXX *HOW* do we link to correct UVLayer??? + mapping = 6 # UV + wrap_mode = 1 # Clamp + if tex.texture.extension in {'REPEAT'}: + wrap_mode = 0 # Repeat + + tmpl = scene_data.templates[b"TextureFile"] + props = elem_properties(fbx_tex) + elem_props_template_set(tmpl, props, "p_enum", b"AlphaSource", alpha_source) + elem_props_template_set(tmpl, props, "p_bool", b"PremultiplyAlpha", + img.alpha_mode in {'STRAIGHT'}) # Or is it PREMUL? + elem_props_template_set(tmpl, props, "p_enum", b"CurrentMappingType", mapping) + elem_props_template_set(tmpl, props, "p_enum", b"WrapModeU", wrap_mode) + elem_props_template_set(tmpl, props, "p_enum", b"WrapModeV", wrap_mode) + elem_props_template_set(tmpl, props, "p_vector_3d", b"Translation", tex.offset) + elem_props_template_set(tmpl, props, "p_vector_3d", b"Scaling", tex.scale) + elem_props_template_set(tmpl, props, "p_bool", b"UseMipMap", tex.texture.use_mipmap) + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, tex.texture) + + +def fbx_data_video_elements(root, vid, scene_data): + """ + Write the actual image data block. + """ + vid_key, _texs = scene_data.data_videos[vid] + fname_abs, fname_rel = _gen_vid_path(vid, scene_data) + + fbx_vid = elem_data_single_int64(root, b"Video", get_fbxuid_from_key(vid_key)) + fbx_vid.add_string(fbx_name_class(vid.name.encode(), b"Video")) + fbx_vid.add_string(b"Clip") + + elem_data_single_string(fbx_vid, b"Type", b"Clip") + # XXX No Version??? + elem_data_single_string_unicode(fbx_vid, b"FileName", fname_abs) + elem_data_single_string_unicode(fbx_vid, b"RelativeFilename", fname_rel) + + if scene_data.settings.media_settings.embed_textures: + try: + with open(vid.filepath, 'br') as f: + elem_data_single_byte_array(fbx_vid, b"Content", f.read()) + except Exception as e: + print("WARNING: embeding file {} failed ({})".format(vid.filepath, e)) + elem_data_single_byte_array(fbx_vid, b"Content", b"") + else: + elem_data_single_byte_array(fbx_vid, b"Content", b"") + + +def fbx_data_armature_elements(root, armature, scene_data): + """ + Write: + * Bones "data" (NodeAttribute::LimbNode, contains pretty much nothing!). + * Deformers (i.e. Skin), bind between an armature and a mesh. + ** SubDeformers (i.e. Cluster), one per bone/vgroup pair. + * BindPose. + Note armature itself has no data, it is a mere "Null" Model... + """ + + # Bones "data". + tmpl = scene_data.templates[b"Bone"] + for bo in armature.data.bones: + _bo_key, bo_data_key, _arm = scene_data.data_bones[bo] + fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(bo_data_key)) + fbx_bo.add_string(fbx_name_class(bo.name.encode(), b"NodeAttribute")) + fbx_bo.add_string(b"LimbNode") + elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton") + + props = elem_properties(fbx_bo) + elem_props_template_set(tmpl, props, "p_number", b"Size", (bo.tail_local - bo.head_local).length) + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, bo) + + + # Deformers and BindPoses. + # Note: we might also use Deformers for our "parent to vertex" stuff??? + deformer = scene_data.data_deformers.get(armature, None) + if deformer is not None: + for me, (skin_key, obj, clusters) in deformer.items(): + # BindPose. + # We assume bind pose for our bones are their "Editmode" pose... + # All matrices are expected in global (world) space. + bindpose_key = get_blender_armature_bindpose_key(armature, me) + fbx_pose = elem_data_single_int64(root, b"Pose", get_fbxuid_from_key(bindpose_key)) + fbx_pose.add_string(fbx_name_class(me.name.encode(), b"Pose")) + fbx_pose.add_string(b"BindPose") + + elem_data_single_string(fbx_pose, b"Type", b"BindPose") + elem_data_single_int32(fbx_pose, b"Version", FBX_POSE_BIND_VERSION) + elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + len(armature.data.bones)) + + # First node is mesh/object. + mat_world_obj = object_matrix(scene_data, obj, global_space=True) + fbx_posenode = elem_empty(fbx_pose, b"PoseNode") + elem_data_single_int64(fbx_posenode, b"Node", get_fbxuid_from_key(scene_data.objects[obj])) + elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(mat_world_obj)) + # And all bones of armature! + mat_world_bones = {} + for bo in armature.data.bones: + bomat = object_matrix(scene_data, bo, armature, global_space=True) + mat_world_bones[bo] = bomat + fbx_posenode = elem_empty(fbx_pose, b"PoseNode") + elem_data_single_int64(fbx_posenode, b"Node", get_fbxuid_from_key(scene_data.objects[bo])) + elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(bomat)) + + # Deformer. + fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbxuid_from_key(skin_key)) + fbx_skin.add_string(fbx_name_class(armature.name.encode(), b"Deformer")) + fbx_skin.add_string(b"Skin") + + elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION) + elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0) # Only vague idea what it is... + + for bo, clstr_key in clusters.items(): + # Find which vertices are affected by this bone/vgroup pair, and matching weights. + indices = []; + weights = []; + vg_idx = obj.vertex_groups[bo.name].index + for idx, v in enumerate(me.vertices): + vert_vg = [vg for vg in v.groups if vg.group == vg_idx] + if not vert_vg: + continue + indices.append(idx) + weights.append(vert_vg[0].weight) + + # Create the cluster. + fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbxuid_from_key(clstr_key)) + fbx_clstr.add_string(fbx_name_class(bo.name.encode(), b"SubDeformer")) + fbx_clstr.add_string(b"Cluster") + + elem_data_single_int32(fbx_clstr, b"Version", FBX_DEFORMER_CLUSTER_VERSION) + # No idea what that user data might be... + fbx_userdata = elem_data_single_string(fbx_clstr, b"UserData", b"") + fbx_userdata.add_string(b"") + if indices: + elem_data_single_int32_array(fbx_clstr, b"Indexes", indices) + elem_data_single_float64_array(fbx_clstr, b"Weights", weights) + # Transform and TransformLink matrices... + # They seem to be mostly the same as BindPose ones??? + # WARNING! Even though official FBX API presents Transform in global space, + # **it is stored in bone space in FBX data!** See + # http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/why-the-values-return-by-fbxcluster-gettransformmatrix-x-not-same-with-the-value-in-ascii-fbx-file/ + elem_data_single_float64_array(fbx_clstr, b"Transform", + matrix_to_array(mat_world_bones[bo].inverted() * mat_world_obj)) + elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix_to_array(mat_world_bones[bo])) + + +def fbx_data_object_elements(root, obj, scene_data): + """ + Write the Object (Model) data blocks. + Note we handle "Model" part of bones as well here! + """ + obj_type = b"Null" # default, sort of empty... + if isinstance(obj, bpy.types.Bone): + obj_type = b"LimbNode" + elif (obj.type == 'MESH'): + obj_type = b"Mesh" + elif (obj.type == 'LAMP'): + obj_type = b"Light" + elif (obj.type == 'CAMERA'): + obj_type = b"Camera" + obj_key = scene_data.objects[obj] + model = elem_data_single_int64(root, b"Model", get_fbxuid_from_key(obj_key)) + model.add_string(fbx_name_class(obj.name.encode(), b"Model")) + model.add_string(obj_type) + + elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION) + + # Object transform info. + loc, rot, scale, matrix, matrix_rot = object_tx(scene_data, obj) + rot = tuple(units_convert(rot, "radian", "degree")) + + tmpl = scene_data.templates[b"Model"] + # For now add only loc/rot/scale... + props = elem_properties(model) + elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc) + elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot) + elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale) + + # TODO: "constraints" (limit loc/rot/scale, and target-to-object). + + # Custom properties. + if scene_data.settings.use_custom_properties: + fbx_data_element_custom_properties(tmpl, props, obj) + + # Those settings would obviously need to be edited in a complete version of the exporter, may depends on + # object type, etc. + elem_data_single_int32(model, b"MultiLayer", 0) + elem_data_single_int32(model, b"MultiTake", 0) + elem_data_single_bool(model, b"Shading", True) + elem_data_single_string(model, b"Culling", b"CullingOff") + + if isinstance(obj, bpy.types.Object) and obj.type == 'CAMERA': + # Why, oh why are FBX cameras such a mess??? + # And WHY add camera data HERE??? Not even sure this is needed... + render = scene_data.scene.render + width = render.resolution_x * 1.0 + height = render.resolution_y * 1.0 + elem_props_template_set(tmpl, props, "p_enum", b"ResolutionMode", 0) # Don't know what it means + elem_props_template_set(tmpl, props, "p_number", b"AspectW", width) + elem_props_template_set(tmpl, props, "p_number", b"AspectH", height) + elem_props_template_set(tmpl, props, "p_bool", b"ViewFrustum", True) + elem_props_template_set(tmpl, props, "p_enum", b"BackgroundMode", 0) # Don't know what it means + elem_props_template_set(tmpl, props, "p_bool", b"ForegroundTransparent", True) + + +##### Top-level FBX data container. ##### + +# Helper container gathering some data we need multiple times: +# * templates. +# * objects. +# * connections. +# * takes. +FBXData = namedtuple("FBXData", ( + "templates", "templates_users", "connections", + "settings", "scene", "objects", + "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices", + "data_bones", "data_deformers", + "data_world", "data_materials", "data_textures", "data_videos", +)) + + +def fbx_mat_properties_from_texture(tex): + """ + Returns a set of FBX metarial properties that are affected by the given texture. + Quite obviously, this is a fuzzy and far-from-perfect mapping! Amounts of influence are completely lost, e.g. + Note tex is actually expected to be a texture slot. + """ + # Tex influence does not exists in FBX, so assume influence < 0.5 = no influence... :/ + INFLUENCE_THRESHOLD = 0.5 + + # Mapping Blender -> FBX (blend_use_name, blend_fact_name, fbx_name). + blend_to_fbx = ( + # Lambert & Phong... + ("diffuse", "diffuse", b"DiffuseFactor"), + ("color_diffuse", "diffuse_color", b"DiffuseColor"), + ("alpha", "alpha", b"TransparencyFactor"), + ("diffuse", "diffuse", b"TransparentColor"), # Uses diffuse color in Blender! + ("emit", "emit", b"EmissiveFactor"), + ("diffuse", "diffuse", b"EmissiveColor"), # Uses diffuse color in Blender! + ("ambient", "ambient", b"AmbientFactor"), + #("", "", b"AmbientColor"), # World stuff in Blender, for now ignore... + # Those are for later! + #("", "", b"NormalMap"), + #("", "", b"Bump"), + #("", "", b"BumpFactor"), + #("", "", b"DisplacementColor"), + #("", "", b"DisplacementFactor"), + # Phong only. + ("specular", "specular", b"SpecularFactor"), + ("color_spec", "specular_color", b"SpecularColor"), + # See Material template about those two! + ("hardness", "hardness", b"Shininess"), + ("hardness", "hardness", b"ShininessExponent"), + ("mirror", "mirror", b"ReflectionColor"), + ("raymir", "raymir", b"ReflectionFactor"), + ) + + tex_fbx_props = set() + for use_map_name, name_factor, fbx_prop_name in blend_to_fbx: + if getattr(tex, "use_map_" + use_map_name) and getattr(tex, name_factor + "_factor") >= INFLUENCE_THRESHOLD: + tex_fbx_props.add(fbx_prop_name) + + return tex_fbx_props + + +def fbx_skeleton_from_armature(scene, settings, armature, objects, data_bones, data_deformers, arm_parents): + """ + Create skeleton from armature/bones (NodeAttribute/LimbNode and Model/LimbNode), and for each deformed mesh, + create Pose/BindPose(with sub PoseNode) and Deformer/Skin(with Deformer/SubDeformer/Cluster). + Also supports "parent to bone" (simple parent to Model/LimbNode). + arm_parents is a set of tuples (armature, object) for all successful armature bindings. + """ + arm = armature.data + bones = {} + for bo in arm.bones: + key, data_key = get_blender_bone_key(armature, bo) + objects[bo] = key + data_bones[bo] = (key, data_key, armature) + bones[bo.name] = bo + + for obj in objects.keys(): + if not isinstance(obj, bpy.types.Object): + continue + if obj.type not in {'MESH'}: + continue + if obj.parent != armature: + continue + + # Always handled by an Armature modifier... + found = False + for mod in obj.modifiers: + if mod.type not in {'ARMATURE'}: + continue + # We only support vertex groups binding method, not bone envelopes one! + if mod.object == armature and mod.use_vertex_groups: + found = True + break + + if not found: + continue + + # Now we have a mesh using this armature. First, find out which bones are concerned! + # XXX Assuming here non-used bones can have no cluster, this has to be checked! + used_bones = tuple(bones[vg.name] for vg in obj.vertex_groups if vg.name in bones) + if not used_bones: + continue + + # Note: bindpose have no relations at all (no connections), so no need for any preprocess for them. + + # Create skin & clusters relations (note skins are connected to geometry, *not* model!). + me = obj.data + clusters = {bo: get_blender_bone_cluster_key(armature, me, bo) for bo in used_bones} + data_deformers.setdefault(armature, {})[me] = (get_blender_armature_skin_key(armature, me), obj, clusters) + + # We don't want a regular parent relationship for those in FBX... + arm_parents.add((armature, obj)) + + +def fbx_data_from_scene(scene, settings): + """ + Do some pre-processing over scene's data... + """ + objtypes = settings.object_types + + ##### Gathering data... + + # This is rather simple for now, maybe we could end generating templates with most-used values + # instead of default ones? + objects = {obj: get_blenderID_key(obj) for obj in scene.objects if obj.type in objtypes} + data_lamps = {obj.data: get_blenderID_key(obj.data) for obj in objects if obj.type == 'LAMP'} + # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)... + data_cameras = {obj: get_blenderID_key(obj.data) for obj in objects if obj.type == 'CAMERA'} + data_meshes = {obj.data: get_blenderID_key(obj.data) for obj in objects if obj.type == 'MESH'} + + # Armatures! + data_bones = {} + data_deformers = {} + arm_parents = set() + for obj in tuple(objects.keys()): + if obj.type not in {'ARMATURE'}: + continue + fbx_skeleton_from_armature(scene, settings, obj, objects, data_bones, data_deformers, arm_parents) + + # Some world settings are embedded in FBX materials... + if scene.world: + data_world = {scene.world: get_blenderID_key(scene.world)} + else: + data_world = {} + + # TODO: Check all the mat stuff works even when mats are linked to Objects + # (we can then have the same mesh used with different materials...). + # *Should* work, as FBX always links its materials to Models (i.e. objects). + # XXX However, material indices would probably break... + data_materials = {} + for obj in objects: + # Only meshes for now! + if not isinstance(obj, bpy.types.Object) or obj.type not in {'MESH'}: + continue + for mat_s in obj.material_slots: + mat = mat_s.material + # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc. + # However, I doubt anything else than Lambert/Phong is really portable! + # TODO: Support nodes (*BIG* todo!). + if mat.type in {'SURFACE'} and mat.diffuse_shader in {'LAMBERT'} and not mat.use_nodes: + if mat in data_materials: + data_materials[mat][1].append(obj) + else: + data_materials[mat] = (get_blenderID_key(mat), [obj]) + + # Note FBX textures also hold their mapping info. + # TODO: Support layers? + data_textures = {} + # FbxVideo also used to store static images... + data_videos = {} + # For now, do not use world textures, don't think they can be linked to anything FBX wise... + for mat in data_materials.keys(): + for tex in mat.texture_slots: + if tex is None: + continue + # For now, only consider image textures. + # Note FBX does has support for procedural, but this is not portable at all (opaque blob), + # so not useful for us. + # TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside. + #if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}: + if tex.texture.type not in {'IMAGE'}: + continue + img = tex.texture.image + if img is None: + continue + # Find out whether we can actually use this texture for this material, in FBX context. + tex_fbx_props = fbx_mat_properties_from_texture(tex) + if not tex_fbx_props: + continue + if tex in data_textures: + data_textures[tex][1][mat] = tex_fbx_props + else: + data_textures[tex] = (get_blenderID_key(tex), {mat: tex_fbx_props}) + if img in data_videos: + data_videos[img][1].append(tex) + else: + data_videos[img] = (get_blenderID_key(img), [tex]) + + ##### Creation of templates... + + templates = OrderedDict() + templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1) + + if data_lamps: + templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps)) + + if data_cameras: + templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras)) + + if data_bones: + templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones)) + + if data_meshes: + templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=len(data_meshes)) + + if objects: + templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects)) + + if arm_parents: + # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures + templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents)) + + if data_deformers: + nbr = len(data_deformers) + sum(len(clusters) for def_me in data_deformers.values() for a, b, clusters in def_me.values()) + templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr) + + # No world support in FBX... + """ + if data_world: + templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world)) + """ + + if data_materials: + templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials)) + + if data_textures: + templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures)) + + if data_videos: + templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos)) + + templates_users = sum(tmpl.nbr_users for tmpl in templates.values()) + + ##### Creation of connections... + + connections = [] + + # Objects (with classical parenting). + for obj, obj_key in objects.items(): + # Bones are handled later. + if isinstance(obj, bpy.types.Object): + par = obj.parent + par_key = 0 # Convention, "root" node (never explicitly written). + if par and par in objects: + par_type = obj.parent_type + if par_type in {'OBJECT', 'BONE'}: + # Meshes parented to armature also have 'OBJECT' par_type, in FBX this is handled separately, + # we do not want an extra object parenting! + if (par, obj) not in arm_parents: + par_key = objects[par] + else: + print("Sorry, “{}” parenting type is not supported".format(par_type)) + connections.append((b"OO", get_fbxuid_from_key(obj_key), get_fbxuid_from_key(par_key), None)) + + # Armature & Bone chains. + for bo, (bo_key, _bo_data_key, arm) in data_bones.items(): + par = bo.parent + if not par: # Root bone. + par = arm + if par not in objects: + continue + connections.append((b"OO", get_fbxuid_from_key(bo_key), get_fbxuid_from_key(objects[par]), None)) + + # Cameras + for obj_cam, cam_key in data_cameras.items(): + cam_obj_key = objects[obj_cam] + connections.append((b"OO", get_fbxuid_from_key(cam_key), get_fbxuid_from_key(cam_obj_key), None)) + + # Object data. + for obj, obj_key in objects.items(): + if isinstance(obj, bpy.types.Bone): + _bo_key, bo_data_key, _arm = data_bones[obj] + assert(_bo_key == obj_key) + connections.append((b"OO", get_fbxuid_from_key(bo_data_key), get_fbxuid_from_key(obj_key), None)) + elif obj.type == 'LAMP': + lamp_key = data_lamps[obj.data] + connections.append((b"OO", get_fbxuid_from_key(lamp_key), get_fbxuid_from_key(obj_key), None)) + elif obj.type == 'MESH': + mesh_key = data_meshes[obj.data] + connections.append((b"OO", get_fbxuid_from_key(mesh_key), get_fbxuid_from_key(obj_key), None)) + + # Deformers (armature-to-geometry, only for meshes currently)... + for arm, deformed_meshes in data_deformers.items(): + for me, (skin_key, _obj, clusters) in deformed_meshes.items(): + # skin -> geometry + connections.append((b"OO", get_fbxuid_from_key(skin_key), get_fbxuid_from_key(data_meshes[me]), None)) + for bo, clstr_key in clusters.items(): + # cluster -> skin + connections.append((b"OO", get_fbxuid_from_key(clstr_key), get_fbxuid_from_key(skin_key), None)) + # bone -> cluster + connections.append((b"OO", get_fbxuid_from_key(objects[bo]), get_fbxuid_from_key(clstr_key), None)) + + # Materials + mesh_mat_indices = {} + _objs_indices = {} + for mat, (mat_key, objs) in data_materials.items(): + for obj in objs: + obj_key = objects[obj] + connections.append((b"OO", get_fbxuid_from_key(mat_key), get_fbxuid_from_key(obj_key), None)) + # Get index of this mat for this object. + # Mat indices for mesh faces are determined by their order in 'mat to ob' connections. + # Only mats for meshes currently... + me = obj.data + idx = _objs_indices[obj] = _objs_indices.get(obj, -1) + 1 + mesh_mat_indices.setdefault(me, {})[mat] = idx + del _objs_indices + + # Textures + for tex, (tex_key, mats) in data_textures.items(): + for mat, fbx_mat_props in mats.items(): + mat_key, _objs = data_materials[mat] + for fbx_prop in fbx_mat_props: + # texture -> material properties + connections.append((b"OP", get_fbxuid_from_key(tex_key), get_fbxuid_from_key(mat_key), fbx_prop)) + + # Images + for vid, (vid_key, texs) in data_videos.items(): + for tex in texs: + tex_key, _texs = data_textures[tex] + connections.append((b"OO", get_fbxuid_from_key(vid_key), get_fbxuid_from_key(tex_key), None)) + + ##### And pack all this! + + return FBXData( + templates, templates_users, connections, + settings, scene, objects, + data_lamps, data_cameras, data_meshes, mesh_mat_indices, + data_bones, data_deformers, + data_world, data_materials, data_textures, data_videos, + ) + + +##### Top-level FBX elements generators. ##### + +def fbx_header_elements(root, scene_data, time=None): + """ + Write boiling code of FBX root. + time is expected to be a datetime.datetime object, or None (using now() in this case). + """ + ##### Start of FBXHeaderExtension element. + header_ext = elem_empty(root, b"FBXHeaderExtension") + + elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION) + + elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION) + + # No encryption! + elem_data_single_int32(header_ext, b"EncryptionType", 0) + + if time is None: + time = datetime.datetime.now() + elem = elem_empty(header_ext, b"CreationTimeStamp") + elem_data_single_int32(elem, b"Version", 1000) + elem_data_single_int32(elem, b"Year", time.year) + elem_data_single_int32(elem, b"Month", time.month) + elem_data_single_int32(elem, b"Day", time.day) + elem_data_single_int32(elem, b"Hour", time.hour) + elem_data_single_int32(elem, b"Minute", time.minute) + elem_data_single_int32(elem, b"Second", time.second) + elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000) + + elem_data_single_string_unicode(header_ext, b"Creator", "Blender version %s" % bpy.app.version_string) + + # 'SceneInfo' seems mandatory to get a valid FBX file... + # TODO use real values! + # XXX Should we use scene.name.encode() here? + scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo")) + scene_info.add_string(b"UserData") + elem_data_single_string(scene_info, b"Type", b"UserData") + elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION) + meta_data = elem_empty(scene_info, b"MetaData") + elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION) + elem_data_single_string(meta_data, b"Title", b"") + elem_data_single_string(meta_data, b"Subject", b"") + elem_data_single_string(meta_data, b"Author", b"") + elem_data_single_string(meta_data, b"Keywords", b"") + elem_data_single_string(meta_data, b"Revision", b"") + elem_data_single_string(meta_data, b"Comment", b"") + + props = elem_properties(scene_info) + elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx") + elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx") + original = elem_props_compound(props, b"Original") + original("p_string", b"ApplicationVendor", "Blender Foundation") + original("p_string", b"ApplicationName", "Blender") + original("p_string", b"ApplicationVersion", "2.70") + original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000") + original("p_string", b"FileName", "/foobar.fbx") + lastsaved = elem_props_compound(props, b"LastSaved") + lastsaved("p_string", b"ApplicationVendor", "Blender Foundation") + lastsaved("p_string", b"ApplicationName", "Blender") + lastsaved("p_string", b"ApplicationVersion", "2.70") + lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000") + + ##### End of FBXHeaderExtension element. + + # FileID is replaced by dummy value currently... + elem_data_single_bytes(root, b"FileId", b"FooBar") + + # CreationTime is replaced by dummy value currently, but anyway... + elem_data_single_string_unicode(root, b"CreationTime", + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}" + "".format(time.year, time.month, time.day, time.hour, time.minute, time.second, + time.microsecond * 1000)) + + elem_data_single_string_unicode(root, b"Creator", "Blender version %s" % bpy.app.version_string) + + ##### Start of GlobalSettings element. + global_settings = elem_empty(root, b"GlobalSettings") + + elem_data_single_int32(global_settings, b"Version", 1000) + + props = elem_properties(global_settings) + up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes] + elem_props_set(props, "p_integer", b"UpAxis", up_axis[0]) + elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1]) + elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0]) + elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1]) + elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0]) + elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1]) + elem_props_set(props, "p_number", b"UnitScaleFactor", 1.0) + elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0)) + elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective") + # XXX Those time stuff is taken from a file, have no (complete) idea what it means! + elem_props_set(props, "p_enum", b"TimeMode", 11) + elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0) + elem_props_set(props, "p_timestamp", b"TimeSpanStop", 46186158000) # XXX One second! + + ##### End of GlobalSettings element. + + +def fbx_documents_elements(root, scene_data): + """ + Write 'Document' part of FBX root. + Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc! + time is expected to be a datetime.datetime object, or None (using now() in this case). + """ + name = scene_data.scene.name + + ##### Start of Documents element. + docs = elem_empty(root, b"Documents") + + elem_data_single_int32(docs, b"Count", 1) + + doc_uid = get_fbxuid_from_key("__FBX_Document__" + name) + doc = elem_data_single_int64(docs, b"Document", doc_uid) + doc.add_string(b"") + doc.add_string_unicode(name) + + props = elem_properties(doc) + elem_props_set(props, "p_object", b"SourceObject") + elem_props_set(props, "p_string", b"ActiveAnimStackName", "") + + # XXX Some kind of ID? Offset? + # Anyway, as long as we have only one doc, probably not an issue. + elem_data_single_int64(doc, b"RootNode", 0) + + +def fbx_references_elements(root, scene_data): + """ + Have no idea what references are in FBX currently... Just writing empty element. + """ + docs = elem_empty(root, b"References") + + +def fbx_definitions_elements(root, scene_data): + """ + Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one). + """ + definitions = elem_empty(root, b"Definitions") + + elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION) + elem_data_single_int32(definitions, b"Count", scene_data.templates_users) + + fbx_templates_generate(definitions, scene_data.templates) + + +def fbx_objects_elements(root, scene_data): + """ + Data (objects, geometry, material, textures, armatures, etc. + """ + objects = elem_empty(root, b"Objects") + + for lamp in scene_data.data_lamps.keys(): + fbx_data_lamp_elements(objects, lamp, scene_data) + + for cam in scene_data.data_cameras.keys(): + fbx_data_camera_elements(objects, cam, scene_data) + + for mesh in scene_data.data_meshes: + fbx_data_mesh_elements(objects, mesh, scene_data) + + for obj in scene_data.objects.keys(): + fbx_data_object_elements(objects, obj, scene_data) + + for obj in scene_data.objects.keys(): + if not isinstance(obj, bpy.types.Object) or obj.type not in {'ARMATURE'}: + continue + fbx_data_armature_elements(objects, obj, scene_data) + + for mat in scene_data.data_materials.keys(): + fbx_data_material_elements(objects, mat, scene_data) + + for tex in scene_data.data_textures.keys(): + fbx_data_texture_file_elements(objects, tex, scene_data) + + for vid in scene_data.data_videos.keys(): + fbx_data_video_elements(objects, vid, scene_data) + + +def fbx_connections_elements(root, scene_data): + """ + Relations between Objects (which material uses which texture, and so on). + """ + connections = elem_empty(root, b"Connections") + + for c in scene_data.connections: + elem_connection(connections, *c) + + +def fbx_takes_elements(root, scene_data): + """ + Animations. Have yet to check how this work... + """ + takes = elem_empty(root, b"Takes") + elem_data_single_string(takes, b"Current", b"") + + +##### "Main" functions. ##### +FBXSettingsMedia = namedtuple("FBXSettingsMedia", ( + "path_mode", "base_src", "base_dst", "subdir", + "embed_textures", "copy_set", +)) +FBXSettings = namedtuple("FBXSettings", ( + "to_axes", "global_matrix", "global_scale", "context_objects", "object_types", "use_mesh_modifiers", + "mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only", + "use_anim", "use_anim_optimize", "anim_optimize_precision", "use_anim_action_all", "use_default_take", + "use_metadata", "media_settings", "use_custom_properties", +)) + +# This func can be called with just the filepath +def save_single(operator, scene, filepath="", + global_matrix=Matrix(), + axis_up="Z", + axis_forward="Y", + context_objects=None, + object_types=None, + use_mesh_modifiers=True, + mesh_smooth_type='FACE', + use_armature_deform_only=False, + use_anim=True, + use_anim_optimize=True, + anim_optimize_precision=6, + use_anim_action_all=False, + use_metadata=True, + path_mode='AUTO', + use_mesh_edges=True, + use_tspace=True, + use_default_take=True, + embed_textures=False, + use_custom_properties=False, + **kwargs + ): + + if object_types is None: + # XXX Temp, during dev... + #object_types = {'EMPTY', 'CAMERA', 'LAMP', 'MESH'} + object_types = {'EMPTY', 'CAMERA', 'LAMP', 'ARMATURE', 'MESH'} + + global_scale = global_matrix.median_scale + + # Only embed textures in COPY mode! + if embed_textures and path_mode != 'COPY': + embed_textures = False + + media_settings = FBXSettingsMedia( + path_mode, + os.path.dirname(bpy.data.filepath), # base_src + os.path.dirname(filepath), # base_dst + # Local dir where to put images (medias), using FBX conventions. + os.path.splitext(os.path.basename(filepath))[0] + ".fbm", # subdir + embed_textures, + set(), # copy_set + ) + + settings = FBXSettings( + (axis_up, axis_forward), global_matrix, global_scale, context_objects, object_types, use_mesh_modifiers, + mesh_smooth_type, use_mesh_edges, use_tspace, use_armature_deform_only, + use_anim, use_anim_optimize, anim_optimize_precision, use_anim_action_all, use_default_take, + use_metadata, media_settings, use_custom_properties, + ) + + import bpy_extras.io_utils + + print('\nFBX export starting... %r' % filepath) + start_time = time.process_time() + + # Generate some data about exported scene... + scene_data = fbx_data_from_scene(scene, settings) + + root = elem_empty(None, b"") # Root element has no id, as it is not saved per se! + + # Mostly FBXHeaderExtension and GlobalSettings. + fbx_header_elements(root, scene_data) + + # Documents and References are pretty much void currently. + fbx_documents_elements(root, scene_data) + fbx_references_elements(root, scene_data) + + # Templates definitions. + fbx_definitions_elements(root, scene_data) + + # Actual data. + fbx_objects_elements(root, scene_data) + + # How data are inter-connected. + fbx_connections_elements(root, scene_data) + + # Animation. + fbx_takes_elements(root, scene_data) + + # And we are down, we can write the whole thing! + encode_bin.write(filepath, root, FBX_VERSION) + + # copy all collected files, if we did not embed them. + if not media_settings.embed_textures: + bpy_extras.io_utils.path_reference_copy(media_settings.copy_set) + + print('export finished in %.4f sec.' % (time.process_time() - start_time)) + return {'FINISHED'} + + +# defaults for applications, currently only unity but could add others. +def defaults_unity3d(): + return { + "global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'), + "use_selection": False, + "object_types": {'ARMATURE', 'EMPTY', 'MESH'}, + "use_mesh_modifiers": True, + "use_armature_deform_only": True, + "use_anim": True, + "use_anim_optimize": False, + "use_anim_action_all": True, + "batch_mode": 'OFF', + "use_default_take": True, + } + + +def save(operator, context, + filepath="", + use_selection=False, + batch_mode='OFF', + use_batch_own_dir=False, + **kwargs + ): + """ + This is a wrapper around save_single, which handles multi-scenes (or groups) cases, when batch-exporting a whole + .blend file. + """ + + ret = None + + org_mode = None + if context.active_object and context.active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll(): + org_mode = context.active_object.mode + bpy.ops.object.mode_set(mode='OBJECT') + + if batch_mode == 'OFF': + kwargs_mod = kwargs.copy() + if use_selection: + kwargs_mod["context_objects"] = context.selected_objects + else: + kwargs_mod["context_objects"] = context.scene.objects + + ret = save_single(operator, context.scene, filepath, **kwargs_mod) + else: + fbxpath = filepath + + prefix = os.path.basename(fbxpath) + if prefix: + fbxpath = os.path.dirname(fbxpath) + + if batch_mode == 'GROUP': + data_seq = bpy.data.groups + else: + data_seq = bpy.data.scenes + + # call this function within a loop with BATCH_ENABLE == False + # no scene switching done at the moment. + # orig_sce = context.scene + + new_fbxpath = fbxpath # own dir option modifies, we need to keep an original + for data in data_seq: # scene or group + newname = "_".join((prefix, bpy.path.clean_name(data.name))) + + if use_batch_own_dir: + new_fbxpath = os.path.join(fbxpath, newname) + # path may already exist + # TODO - might exist but be a file. unlikely but should probably account for it. + + if not os.path.exists(new_fbxpath): + os.makedirs(new_fbxpath) + + filepath = os.path.join(new_fbxpath, newname + '.fbx') + + print('\nBatch exporting %s as...\n\t%r' % (data, filepath)) + + if batch_mode == 'GROUP': # group + # group, so objects update properly, add a dummy scene. + scene = bpy.data.scenes.new(name="FBX_Temp") + scene.layers = [True] * 20 + # bpy.data.scenes.active = scene # XXX, cant switch + for ob_base in data.objects: + scene.objects.link(ob_base) + + scene.update() + # TODO - BUMMER! Armatures not in the group wont animate the mesh + else: + scene = data + + kwargs_batch = kwargs.copy() + kwargs_batch["context_objects"] = data.objects + + save_single(operator, scene, filepath, **kwargs_batch) + + if batch_mode == 'GROUP': + # remove temp group scene + bpy.data.scenes.remove(scene) + + # no active scene changing! + # bpy.data.scenes.active = orig_sce + + ret = {'FINISHED'} # so the script wont run after we have batch exported. + + if context.active_object and org_mode and bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode=org_mode) + + return ret diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 697d7503..0cec24a7 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -841,7 +841,17 @@ def blen_read_light(fbx_tmpl, fbx_obj, global_scale): lamp = bpy.data.lamps.new(name=elem_name_utf8, type=light_type) if light_type == 'SPOT': - lamp.spot_size = math.radians(elem_props_get_number(fbx_props, b'Cone angle', 45.0)) + spot_size = elem_props_get_number(fbx_props, b'OuterAngle', None) + if spot_size is None: + # Deprecated. + spot_size = elem_props_get_number(fbx_props, b'Cone angle', 45.0) + lamp.spot_size = math.radians(spot_size) + + spot_blend = elem_props_get_number(fbx_props, b'InnerAngle', None) + if spot_blend is None: + # Deprecated. + spot_blend = elem_props_get_number(fbx_props, b'HotSpot', 45.0) + lamp.spot_blend = 1.0 - (spot_blend / spot_size) # TODO, cycles lamp.color = elem_props_get_color_rgb(fbx_props, b'Color', (1.0, 1.0, 1.0)) diff --git a/io_scene_fbx/json2fbx.py b/io_scene_fbx/json2fbx.py new file mode 100755 index 00000000..808564ad --- /dev/null +++ b/io_scene_fbx/json2fbx.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Script copyright (C) 2014 Blender Foundation + +""" +Usage +===== + + json2fbx [FILES]... + +This script will write a binary FBX file for each JSON argument given. + + +Input +====== + +The JSON data is formatted into a list of nested lists of 4 items: + + ``[id, [data, ...], "data_types", [subtree, ...]]`` + +Where each list may be empty, and the items in +the subtree are formatted the same way. + +data_types is a string, aligned with data that spesifies a type +for each property. + +The types are as follows: + +* 'Y': - INT16 +* 'C': - BOOL +* 'I': - INT32 +* 'F': - FLOAT32 +* 'D': - FLOAT64 +* 'L': - INT64 +* 'R': - BYTES +* 'S': - STRING +* 'f': - FLOAT32_ARRAY +* 'i': - INT32_ARRAY +* 'd': - FLOAT64_ARRAY +* 'l': - INT64_ARRAY +* 'b': - BOOL ARRAY +* 'c': - BYTE ARRAY + +Note that key:value pairs aren't used since the id's are not +ensured to be unique. +""" + + +import encode_bin + + +def elem_empty(elem, name): + sub_elem = encode_bin.FBXElem(name) + if elem is not None: + elem.elems.append(sub_elem) + return sub_elem + + +def parse_json_rec(fbx_root, json_node): + name, data, data_types, children = json_node + ver = 0 + + assert(len(data_types) == len(data)) + + e = elem_empty(fbx_root, name.encode()) + for d, dt in zip(data, data_types): + if dt == "C": + e.add_bool(d) + elif dt == "Y": + e.add_int16(d) + elif dt == "I": + e.add_int32(d) + elif dt == "L": + e.add_int64(d) + elif dt == "F": + e.add_float32(d) + elif dt == "D": + e.add_float64(d) + elif dt == "R": + d = eval('b"""' + d + '"""') + e.add_bytes(d) + elif dt == "S": + d = d.encode() + d.replace(b"::", b"\x00\x01") + e.add_string(d) + elif dt == "i": + e.add_int32_array(d) + elif dt == "l": + e.add_int64_array(d) + elif dt == "f": + e.add_float32_array(d) + elif dt == "d": + e.add_float64_array(d) + elif dt == "b": + e.add_byte_array(d) + elif dt == "c": + e.add_bool_array(d) + + if name == "FBXVersion": + assert(data_types == "I") + ver = int(data[0]) + + for child in children: + _ver = parse_json_rec(e, child) + if _ver: + ver = _ver + + return ver + + +def parse_json(json_root): + root = elem_empty(None, b"") + ver = 0 + + for n in json_root: + _ver = parse_json_rec(root, n) + if _ver: + ver = _ver + + return root, ver + + +def json2fbx(fn): + import os, json + + fn_fbx = "%s.fbx" % os.path.splitext(fn)[0] + print("Writing: %r " % fn_fbx, end="") + json_root = [] + with open(fn) as f_json: + json_root = json.load(f_json) + fbx_root, fbx_version = parse_json(json_root) + print("(Version %d) ..." % fbx_version) + encode_bin.write(fn_fbx, fbx_root, fbx_version) + + +# ---------------------------------------------------------------------------- +# Command Line + +def main(): + import sys + + if "--help" in sys.argv: + print(__doc__) + return + + for arg in sys.argv[1:]: + try: + json2fbx(arg) + except: + print("Failed to convert %r, error:" % arg) + + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/io_scene_fbx/parse_fbx.py b/io_scene_fbx/parse_fbx.py index f634e52e..87f8624f 100644 --- a/io_scene_fbx/parse_fbx.py +++ b/io_scene_fbx/parse_fbx.py @@ -32,6 +32,8 @@ from struct import unpack import array import zlib +from . import data_types + # at the end of each nested block, there is a NUL record to indicate # that the sub-scope exists (i.e. to distinguish between P: and P : {}) # this NUL record is 13 bytes long. @@ -87,12 +89,12 @@ read_data_dict = { b'L'[0]: lambda read: unpack(b'<q', read(8))[0], # 64 bit int b'R'[0]: lambda read: read(read_uint(read)), # binary data b'S'[0]: lambda read: read(read_uint(read)), # string data - b'f'[0]: lambda read: unpack_array(read, 'f', 4, False), # array (float) - b'i'[0]: lambda read: unpack_array(read, 'i', 4, True), # array (int) - b'd'[0]: lambda read: unpack_array(read, 'd', 8, False), # array (double) - b'l'[0]: lambda read: unpack_array(read, 'q', 8, True), # array (long) - b'b'[0]: lambda read: unpack_array(read, 'b', 1, False), # array (bool) - b'c'[0]: lambda read: unpack_array(read, 'B', 1, False), # array (ubyte) + b'f'[0]: lambda read: unpack_array(read, data_types.ARRAY_FLOAT32, 4, False), # array (float) + b'i'[0]: lambda read: unpack_array(read, data_types.ARRAY_INT32, 4, True), # array (int) + b'd'[0]: lambda read: unpack_array(read, data_types.ARRAY_FLOAT64, 8, False), # array (double) + b'l'[0]: lambda read: unpack_array(read, data_types.ARRAY_INT64, 8, True), # array (long) + b'b'[0]: lambda read: unpack_array(read, data_types.ARRAY_BOOL, 1, False), # array (bool) + b'c'[0]: lambda read: unpack_array(read, data_types.ARRAY_BYTE, 1, False), # array (ubyte) } @@ -166,24 +168,3 @@ def parse(fn, use_namedtuple=True): args = (b'', [], bytearray(0), root_elems) return FBXElem(*args) if use_namedtuple else args, fbx_version - -# Inline module, only for external use -# pyfbx.data_types -data_types = type(array)("data_types") -data_types.__dict__.update( -dict( -INT16 = b'Y'[0], -BOOL = b'C'[0], -INT32 = b'I'[0], -FLOAT32 = b'F'[0], -FLOAT64 = b'D'[0], -INT64 = b'L'[0], -BYTES = b'R'[0], -STRING = b'S'[0], -FLOAT32_ARRAY = b'f'[0], -INT32_ARRAY = b'i'[0], -FLOAT64_ARRAY = b'd'[0], -INT64_ARRAY = b'l'[0], -BOOL_ARRAY = b'b'[0], -BYTE_ARRAY = b'c'[0], -)) |