diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 7bc11eba14deefbbfcc0cfd9f311378046d6b4ec..44a7eff69d9cc1f621ab1bd4a57537cf1efd16cc 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -27,582 +27,61 @@ import math import os import time -import collections -from collections import namedtuple, OrderedDict -from collections.abc import Iterable -import itertools +from collections import OrderedDict from itertools import zip_longest, chain import bpy import bpy_extras -from bpy.types import Object, Bone, PoseBone, DupliObject 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 -# Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps -# currently, apart from some AD products. -FBX_GEOMETRY_NORMAL_VERSION = 101 -FBX_GEOMETRY_BINORMAL_VERSION = 101 -FBX_GEOMETRY_TANGENT_VERSION = 101 -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_ANIM_KEY_VERSION = 4008 - -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.Rotation(math.pi / -2.0, 4, 'X') # Blender is +Y, FBX is +Z. -MAT_CONVERT_BONE = Matrix() - - -BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'} -BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES - - -# 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] - return val * conv - - -def units_convert_iter(it, u_from, u_to): - """Convert value.""" - conv = UNITS[u_to] / UNITS[u_from] - return (v * conv for v in it) - - -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) - - -def similar_values(v1, v2, e=1e-6): - """Return True if v1 and v2 are nearly the same.""" - if v1 == v2: - return True - return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e) - - -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)), -} - - -FBX_FRAMERATES = ( - (-1.0, 14), # Custom framerate. - (120.0, 1), - (100.0, 2), - (60.0, 3), - (50.0, 4), - (48.0, 5), - (30.0, 6), # BW NTSC. - (30.0 / 1.001, 9), # Color NTSC. - (25.0, 10), - (24.0, 11), - (24.0 / 1.001, 13), - (96.0, 15), - (72.0, 16), - (60.0 / 1.001, 17), +from .export_fbx_bin_utils import ( + # Constants. + FBX_VERSION, FBX_HEADER_VERSION, FBX_SCENEINFO_VERSION, FBX_TEMPLATES_VERSION, + FBX_MODELS_VERSION, + FBX_GEOMETRY_VERSION, FBX_GEOMETRY_NORMAL_VERSION, FBX_GEOMETRY_BINORMAL_VERSION, FBX_GEOMETRY_TANGENT_VERSION, + FBX_GEOMETRY_SMOOTHING_VERSION, FBX_GEOMETRY_VCOLOR_VERSION, FBX_GEOMETRY_UV_VERSION, + FBX_GEOMETRY_MATERIAL_VERSION, FBX_GEOMETRY_LAYER_VERSION, + FBX_POSE_BIND_VERSION, FBX_DEFORMER_SKIN_VERSION, FBX_DEFORMER_CLUSTER_VERSION, + FBX_MATERIAL_VERSION, FBX_TEXTURE_VERSION, + FBX_ANIM_KEY_VERSION, + FBX_KTIME, + BLENDER_OTHER_OBJECT_TYPES, BLENDER_OBJECT_TYPES_MESHLIKE, + FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES, + RIGHT_HAND_AXES, FBX_FRAMERATES, + # Miscellaneous utils. + units_convert, units_convert_iter, matrix_to_array, similar_values, + # UUID from key. + get_fbx_uuid_from_key, + # Key generators. + get_blenderID_key, get_blenderID_name, + get_blender_empty_key, get_blender_bone_key, + get_blender_armature_bindpose_key, get_blender_armature_skin_key, get_blender_bone_cluster_key, + get_blender_anim_id_base, get_blender_anim_stack_key, get_blender_anim_layer_key, + get_blender_anim_curve_node_key, get_blender_anim_curve_key, + # FBX element data. + elem_empty, + elem_data_single_bool, elem_data_single_int16, elem_data_single_int32, elem_data_single_int64, + elem_data_single_float32, elem_data_single_float64, + elem_data_single_bytes, elem_data_single_string, elem_data_single_string_unicode, + elem_data_single_bool_array, elem_data_single_int32_array, elem_data_single_int64_array, + elem_data_single_float32_array, elem_data_single_float64_array, + elem_data_single_byte_array, elem_data_vec_float64, + # FBX element properties. + elem_properties, elem_props_set, elem_props_compound, + # FBX element properties handling templates. + elem_props_template_init, elem_props_template_set, elem_props_template_finalize, + # Templates. + FBXTemplate, fbx_templates_generate, + # Objects. + ObjectWrapper, fbx_name_class, + # Top level. + FBXSettingsMedia, FBXSettings, FBXData, ) - -##### 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 - # Try to make our uid shorter! - if uid > int(1e9): - t_uid = uid % int(1e9) - if t_uid not in uids: - uid = t_uid - # 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): - if isinstance(bid, Iterable): - return "|".join("B" + e.rna_type.name + "#" + e.name for e in bid) - else: - return "B" + bid.rna_type.name + "#" + bid.name - - -def get_blenderID_name(bid): - if isinstance(bid, Iterable): - return "|".join(e.name for e in bid) - else: - return bid.name - - -def get_blender_empty_key(obj): - """Return bone's keys (Model and NodeAttribute).""" - return "|".join((get_blenderID_key(obj), "Empty")) - - -def get_blender_dupli_key(dup): - """Return dupli's key (Model only).""" - return "|".join((get_blenderID_key(dup.id_data), get_blenderID_key(dup.object), "Dupli", - "".join(str(i) for i in dup.persistent_id))) - - -def get_blender_bone_key(armature, bone): - """Return bone's keys (Model and NodeAttribute).""" - return "|".join((get_blenderID_key((armature, bone)), "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")) - - -def get_blender_anim_id_base(scene, ref_id): - if ref_id is not None: - return get_blenderID_key(scene) + "|" + get_blenderID_key(ref_id) - else: - return get_blenderID_key(scene) - - -def get_blender_anim_stack_key(scene, ref_id): - """Return single anim stack key.""" - return get_blender_anim_id_base(scene, ref_id) + "|AnimStack" - - -def get_blender_anim_layer_key(scene, ref_id): - """Return ID's anim layer key.""" - return get_blender_anim_id_base(scene, ref_id) + "|AnimLayer" - - -def get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_prop_name): - """Return (stack/layer, ID, fbxprop) curve node key.""" - return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, "AnimCurveNode")) - - -def get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_prop_name, fbx_prop_item_name): - """Return (stack/layer, ID, fbxprop, item) curve key.""" - return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, - fbx_prop_item_name, "AnimCurve")) - - -##### 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"label(???)", "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! -# Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense, -# these are just Vector3D ultimately... *sigh* (again). -FBX_PROPERTIES_DEFINITIONS = { - # Generic types. - "p_bool": (b"bool", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!! - "p_integer": (b"int", b"Integer", "add_int32"), - "p_ulonglong": (b"ULongLong", b"", "add_int64"), - "p_double": (b"double", b"Number", "add_float64"), # Non-animatable? - "p_number": (b"Number", b"", "add_float64"), # Animatable-only? - "p_enum": (b"enum", b"", "add_int32"), - "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable? - "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only? - "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable? - "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only? - "p_string": (b"KString", b"", "add_string_unicode"), - "p_string_url": (b"KString", b"Url", "add_string_unicode"), - "p_timestamp": (b"KTime", b"Time", "add_int64"), - "p_datetime": (b"DateTime", b"", "add_string_unicode"), - # Special types. - "p_object": (b"object", b""), # XXX Check this! No value for this prop??? Would really like to know how it works! - "p_compound": (b"Compound", b""), - # Specific types (sic). - ## Objects (Models). - "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"), - "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"), - "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"), - "p_visibility": (b"Visibility", b"", "add_float64"), - "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"), - ## Cameras!!! - "p_roll": (b"Roll", b"", "add_float64"), - "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"), - "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"), - "p_fov": (b"FieldOfView", b"", "add_float64"), - "p_fov_x": (b"FieldOfViewX", b"", "add_float64"), - "p_fov_y": (b"FieldOfViewY", b"", "add_float64"), -} - - -def _elem_props_set(elem, ptype, name, value, flags): - p = elem_data_single_string(elem, b"P", name) - for t in ptype[:2]: - p.add_string(t) - p.add_string(flags) - if len(ptype) == 3: - getattr(p, ptype[2])(value) - elif len(ptype) > 3: - # We assume value is iterable, else it's a bug! - for callback, val in zip(ptype[2:], value): - getattr(p, callback)(val) - - -def _elem_props_flags(animatable, custom): - if animatable and custom: - return b"AU" - elif animatable: - return b"A" - elif custom: - return b"U" - return b"" - - -def elem_props_set(elem, ptype, name, value=None, animatable=False, custom=False): - ptype = FBX_PROPERTIES_DEFINITIONS[ptype] - _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, custom)) - - -def elem_props_compound(elem, cmpd_name, custom=False): - def _setter(ptype, name, value, animatable=False, custom=False): - name = cmpd_name + b"|" + name - elem_props_set(elem, ptype, name, value, animatable=animatable, custom=custom) - - elem_props_set(elem, "p_compound", cmpd_name, custom=custom) - return _setter - - -def elem_props_template_init(templates, template_type): - """ - Init a writing template of given type, for *one* element's properties. - """ - ret = None - if template_type in templates: - tmpl = templates[template_type] - written = tmpl.written[0] - props = tmpl.properties - ret = OrderedDict((name, [val, ptype, anim, written]) for name, (val, ptype, anim) in props.items()) - return ret or OrderedDict() - - -def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False): - """ - 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] - if len(ptype) > 3: - value = tuple(value) - tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False)) - # Note animatable flag from template takes precedence over given one, if applicable. - if tmpl_ptype is not None: - if (tmpl_written and - ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or - (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (value, ptype_name)))): - return # Already in template and same value. - _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, False)) - template[name][3] = True - else: - _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False)) - - -def elem_props_template_finalize(template, elem): - """ - Finalize one element's template/props. - Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.), - but values for only *one* subtype can be written as template. So we have to be sure we write those for ths other - subtypes in each and every elements, if they are not overriden by that element. - Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :( - """ - for name, (value, ptype_name, animatable, written) in template.items(): - if written: - continue - ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name] - _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False)) - - -##### 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", "written")) - - -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.). - ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()} - - templates = OrderedDict() - for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values(): - if type_name not in templates: - templates[type_name] = [OrderedDict(((prop_type_name, (properties, nbr_users)),)), nbr_users] - else: - templates[type_name][0][prop_type_name] = (properties, nbr_users) - 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) - - if len(subprops) == 1: - prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items())) - subprops = (prop_type_name, properties) - ref_templates[(type_name, prop_type_name)].written[0] = True - else: - # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :| - max_users = max_props = -1 - written_prop_type_name = None - for prop_type_name, (properties, nbr_sub_type_users) in subprops.items(): - if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props): - max_users = nbr_sub_type_users - max_props = len(properties) - written_prop_type_name = prop_type_name - subprops = (written_prop_type_name, properties) - ref_templates[(type_name, written_prop_type_name)].written[0] = True - - prop_type_name, properties = subprops - 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, animatable) in properties.items(): - elem_props_set(props, ptype, name, value, animatable=animatable) - - def fbx_template_def_globalsettings(scene, settings, override_defaults=None, nbr_users=0): props = OrderedDict() if override_defaults is not None: @@ -1008,301 +487,17 @@ def fbx_template_def_animcurve(scene, settings, override_defaults=None, nbr_user return FBXTemplate(b"AnimationCurve", b"", props, nbr_users, [False]) -##### FBX objects generators. ##### +##### Generators for connection elements. ##### -# FBX Model-like data (i.e. Blender objects, dupliobjects and bones) are wrapped in ObjectWrapper. -# This allows us to have a (nearly) same code FBX-wise for all those types. -# The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...)) -# to actual Blender data it contains. -# Note it caches its instances, so that you may call several times ObjectWrapper(your_object) -# with a minimal cost (just re-computing the key). - -class MetaObjectWrapper(type): - def __call__(cls, bdata, armature=None): - if bdata is None: - return None - dup_mat = None - if isinstance(bdata, Object): - key = get_blenderID_key(bdata) - elif isinstance(bdata, DupliObject): - key = "|".join((get_blenderID_key((bdata.id_data, bdata.object)), cls._get_dup_num_id(bdata))) - dup_mat = bdata.matrix.copy() - else: # isinstance(bdata, (Bone, PoseBone)): - if isinstance(bdata, PoseBone): - bdata = armature.data.bones[bdata.name] - key = get_blenderID_key((armature, bdata)) - - cache = getattr(cls, "_cache", None) - if cache is None: - cache = cls._cache = {} - if key in cache: - instance = cache[key] - # Duplis hack: since duplis are not persistent in Blender (we have to re-create them to get updated - # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all - # other data is supposed valid during whole cache live, so we can skip resetting it). - instance._dupli_matrix = dup_mat - return instance - - instance = cls.__new__(cls, bdata, armature) - instance.__init__(bdata, armature) - instance.key = key - instance._dupli_matrix = dup_mat - cache[key] = instance - return instance - - -class ObjectWrapper(metaclass=MetaObjectWrapper): - """ - This class provides a same common interface for all (FBX-wise) object-like elements: - * Blender Object - * Blender Bone and PoseBone - * Blender DupliObject - Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis), - we need to use a key to identify each. - """ - __slots__ = ('name', 'key', 'bdata', '_tag', '_ref', '_dupli_matrix') - - @classmethod - def cache_clear(cls): - if hasattr(cls, "_cache"): - del cls._cache - - @staticmethod - def _get_dup_num_id(bdata): - return ".".join(str(i) for i in bdata.persistent_id if i != 2147483647) - - def __init__(self, bdata, armature=None): - """ - bdata might be an Object, DupliObject, Bone or PoseBone. - If Bone or PoseBone, armature Object must be provided. - """ - if isinstance(bdata, Object): - self._tag = 'OB' - self.name = get_blenderID_name(bdata) - self.bdata = bdata - self._ref = None - elif isinstance(bdata, DupliObject): - self._tag = 'DP' - self.name = "|".join((get_blenderID_name((bdata.id_data, bdata.object)), - "Dupli", self._get_dup_num_id(bdata))) - self.bdata = bdata.object - self._ref = bdata.id_data - else: # isinstance(bdata, (Bone, PoseBone)): - if isinstance(bdata, PoseBone): - bdata = armature.data.bones[bdata.name] - self._tag = 'BO' - self.name = get_blenderID_name((armature, bdata)) - self.bdata = bdata - self._ref = armature - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.key == other.key - - def __hash__(self): - return hash(self.key) - - #### Common to all _tag values. - def get_fbx_uuid(self): - return get_fbxuid_from_key(self.key) - fbx_uuid = property(get_fbx_uuid) - - def get_parent(self): - if self._tag == 'OB': - return ObjectWrapper(self.bdata.parent) - elif self._tag == 'DP': - return ObjectWrapper(self.bdata.parent or self._ref) - else: # self._tag == 'BO' - return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref) - parent = property(get_parent) - - def get_matrix_local(self): - if self._tag == 'OB': - return self.bdata.matrix_local.copy() - elif self._tag == 'DP': - return self._ref.matrix_world.inverted() * self._dupli_matrix - else: # 'BO', current pose - # PoseBone.matrix is in armature space, bring in back in real local one! - par = self.bdata.parent - par_mat_inv = self._ref.pose.bones[par.name].matrix.inverted() if par else Matrix() - return par_mat_inv * self._ref.pose.bones[self.bdata.name].matrix - matrix_local = property(get_matrix_local) - - def get_matrix_global(self): - if self._tag == 'OB': - return self.bdata.matrix_world.copy() - elif self._tag == 'DP': - return self._dupli_matrix - else: # 'BO', current pose - return self._ref.matrix_world * self._ref.pose.bones[self.bdata.name].matrix - matrix_global = property(get_matrix_global) - - def get_matrix_rest_local(self): - if self._tag == 'BO': - # Bone.matrix_local is in armature space, bring in back in real local one! - par = self.bdata.parent - par_mat_inv = par.matrix_local.inverted() if par else Matrix() - return par_mat_inv * self.bdata.matrix_local - else: - return self.matrix_local - matrix_rest_local = property(get_matrix_rest_local) +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) - def get_matrix_rest_global(self): - if self._tag == 'BO': - return self._ref.matrix_world * self.bdata.matrix_local - else: - return self.matrix_global - matrix_rest_global = property(get_matrix_rest_global) - - #### Transform and helpers - def has_valid_parent(self, objects): - par = self.parent - if par in objects: - if self._tag == 'OB': - par_type = self.bdata.parent_type - if par_type in {'OBJECT', 'BONE'}: - return True - else: - print("Sorry, “{}” parenting type is not supported".format(par_type)) - return False - return True - return False - - def use_bake_space_transform(self, scene_data): - # NOTE: Only applies to object types supporting this!!! Currently, only meshes... - # Also, do not apply it to children objects. - # TODO: Check whether this can work for bones too... - return (scene_data.settings.bake_space_transform and self._tag == 'OB' and - self.bdata.type in BLENDER_OBJECT_TYPES_MESHLIKE and not self.has_valid_parent(scene_data.objects)) - - def fbx_object_matrix(self, scene_data, rest=False, local_space=False, global_space=False): - """ - Generate object transform matrix (*always* in matching *FBX* space!). - If local_space is True, returned matrix is *always* in local space. - Else if global_space is True, returned matrix is always in world space. - If both local_space and global_space are False, returned matrix is in parent space if parent is valid, - else in world space. - Note local_space has precedence over global_space. - If rest is True and object is a Bone, returns matching rest pose transform instead of current pose one. - Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX). - """ - # Objects which are not bones and do not have any parent are *always* in global space - # (unless local_space is True!). - is_global = (not local_space and - (global_space or not (self._tag in {'DP', 'BO'} or self.has_valid_parent(scene_data.objects)))) - - if self._tag == 'BO': - if rest: - matrix = self.matrix_rest_global if is_global else self.matrix_rest_local - else: # Current pose. - matrix = self.matrix_global if is_global else self.matrix_local - else: - # Since we have to apply corrections to some types of object, we always need local Blender space here... - matrix = self.matrix_local - parent = self.parent - - # Lamps and cameras need to be rotated (in local space!). - if self.bdata.type == 'LAMP': - matrix = matrix * MAT_CONVERT_LAMP - elif self.bdata.type == 'CAMERA': - matrix = matrix * MAT_CONVERT_CAMERA - - # Our matrix is in local space, time to bring it in its final desired space. - if parent: - if is_global: - # Move matrix to global Blender space. - matrix = parent.matrix_global * matrix - elif parent.use_bake_space_transform(scene_data): - # Blender's and FBX's local space of parent may differ if we use bake_space_transform... - # Apply parent's *Blender* local space... - matrix = parent.matrix_local * matrix - # ...and move it back into parent's *FBX* local space. - par_mat = parent.fbx_object_matrix(scene_data, local_space=True) - matrix = par_mat.inverted() * matrix - - if self.use_bake_space_transform(scene_data): - # If we bake the transforms we need to post-multiply inverse global transform. - # This means that the global transform will not apply to children of this transform. - matrix = matrix * scene_data.settings.global_matrix_inv - if is_global: - # In any case, pre-multiply the global matrix to get it in FBX global space! - matrix = scene_data.settings.global_matrix * matrix - - return matrix - - def fbx_object_tx(self, scene_data, rest=False, rot_euler_compat=None): - """ - Generate object transform data (always in local space when possible). - """ - matrix = self.fbx_object_matrix(scene_data, rest=rest) - loc, rot, scale = matrix.decompose() - matrix_rot = rot.to_matrix() - # quat -> euler, we always use 'XYZ' order, use ref rotation if given. - if rot_euler_compat is not None: - rot = rot.to_euler('XYZ', rot_euler_compat) - else: - rot = rot.to_euler('XYZ') - return loc, rot, scale, matrix, matrix_rot - - #### _tag dependent... - def get_is_object(self): - return self._tag == 'OB' - is_object = property(get_is_object) - - def get_is_dupli(self): - return self._tag == 'DP' - is_dupli = property(get_is_dupli) - - def get_is_bone(self): - return self._tag == 'BO' - is_bone = property(get_is_bone) - - def get_type(self): - if self._tag in {'OB', 'DP'}: - return self.bdata.type - return ... - type = property(get_type) - - def get_armature(self): - if self._tag == 'BO': - return ObjectWrapper(self._ref) - return None - armature = property(get_armature) - - def get_bones(self): - if self._tag == 'OB' and self.bdata.type == 'ARMATURE': - return (ObjectWrapper(bo, self.bdata) for bo in self.bdata.data.bones) - return () - bones = property(get_bones) - - def get_material_slots(self): - if self._tag in {'OB', 'DP'}: - return self.bdata.material_slots - return () - material_slots = property(get_material_slots) - - #### Duplis... - def dupli_list_create(self, scene, settings='PREVIEW'): - if self._tag == 'OB': - # Sigh, why raise exception here? :/ - try: - self.bdata.dupli_list_create(scene, settings) - except: - pass - - def dupli_list_clear(self): - if self._tag == 'OB': - self.bdata.dupli_list_clear() - - def get_dupli_list(self): - if self._tag == 'OB': - return (ObjectWrapper(dup) for dup in self.bdata.dupli_list) - return () - dupli_list = property(get_dupli_list) - - -def fbx_name_class(name, cls): - return FBX_NAME_CLASS_SEP.join((name, cls)) +##### FBX objects generators. ##### def fbx_data_element_custom_properties(props, bid): """ @@ -1323,7 +518,7 @@ def fbx_data_empty_elements(root, empty, scene_data): """ empty_key = scene_data.data_empties[empty] - null = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(empty_key)) + null = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(empty_key)) null.add_string(fbx_name_class(empty.name.encode(), b"NodeAttribute")) null.add_string(b"Null") @@ -1354,7 +549,7 @@ def fbx_data_lamp_elements(root, lamp, scene_data): 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 = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(lamp_key)) light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute")) light.add_string(b"Light") @@ -1410,7 +605,7 @@ def fbx_data_camera_elements(root, cam_obj, scene_data): 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 = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(cam_key)) cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute")) cam.add_string(b"Camera") @@ -1491,7 +686,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): geom_mat_no.translation = Vector() geom_mat_no.normalize() - geom = elem_data_single_int64(root, b"Geometry", get_fbxuid_from_key(me_key)) + geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(me_key)) geom.add_string(fbx_name_class(me.name.encode(), b"Geometry")) geom.add_string(b"Mesh") @@ -1856,7 +1051,7 @@ def fbx_data_material_elements(root, mat, scene_data): # 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 = elem_data_single_int64(root, b"Material", get_fbx_uuid_from_key(mat_key)) fbx_mat.add_string(fbx_name_class(mat.name.encode(), b"Material")) fbx_mat.add_string(b"") @@ -1925,7 +1120,7 @@ def fbx_data_texture_file_elements(root, tex, scene_data): 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 = elem_data_single_int64(root, b"Texture", get_fbx_uuid_from_key(tex_key)) fbx_tex.add_string(fbx_name_class(tex.name.encode(), b"Texture")) fbx_tex.add_string(b"") @@ -1985,7 +1180,7 @@ def fbx_data_video_elements(root, vid, scene_data): 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 = elem_data_single_int64(root, b"Video", get_fbx_uuid_from_key(vid_key)) fbx_vid.add_string(fbx_name_class(vid.name.encode(), b"Video")) fbx_vid.add_string(b"Clip") @@ -2020,7 +1215,7 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): for bo_obj in arm_obj.bones: bo = bo_obj.bdata bo_data_key = scene_data.data_bones[bo_obj] - fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbxuid_from_key(bo_data_key)) + fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_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") @@ -2043,7 +1238,7 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): # 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(arm_obj.bdata, me) - fbx_pose = elem_data_single_int64(root, b"Pose", get_fbxuid_from_key(bindpose_key)) + fbx_pose = elem_data_single_int64(root, b"Pose", get_fbx_uuid_from_key(bindpose_key)) fbx_pose.add_string(fbx_name_class(me.name.encode(), b"Pose")) fbx_pose.add_string(b"BindPose") @@ -2066,7 +1261,7 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): 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 = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key)) fbx_skin.add_string(fbx_name_class(arm_obj.name.encode(), b"Deformer")) fbx_skin.add_string(b"Skin") @@ -2095,7 +1290,7 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): indices, weights = ((), ()) if vg_idx is None or not vgroups[vg_idx] else zip(*vgroups[vg_idx].items()) # Create the cluster. - fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbxuid_from_key(clstr_key)) + fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(clstr_key)) fbx_clstr.add_string(fbx_name_class(bo.name.encode(), b"SubDeformer")) fbx_clstr.add_string(b"Cluster") @@ -2191,7 +1386,7 @@ def fbx_data_animation_elements(root, scene_data): # Animation stacks. for astack_key, alayers, alayer_key, name, f_start, f_end in animations: - astack = elem_data_single_int64(root, b"AnimationStack", get_fbxuid_from_key(astack_key)) + astack = elem_data_single_int64(root, b"AnimationStack", get_fbx_uuid_from_key(astack_key)) astack.add_string(fbx_name_class(name, b"AnimStack")) astack.add_string(b"") @@ -2208,19 +1403,19 @@ def fbx_data_animation_elements(root, scene_data): elem_props_template_finalize(astack_tmpl, astack_props) # For now, only one layer for all animations. - alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbxuid_from_key(alayer_key)) + alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key)) alayer.add_string(fbx_name_class(name, b"AnimLayer")) alayer.add_string(b"") for ob_obj, (alayer_key, acurvenodes) in alayers.items(): # Animation layer. - # alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbxuid_from_key(alayer_key)) + # alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key)) # alayer.add_string(fbx_name_class(ob_obj.name.encode(), b"AnimLayer")) # alayer.add_string(b"") for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items(): # Animation curve node. - acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbxuid_from_key(acurvenode_key)) + acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbx_uuid_from_key(acurvenode_key)) acurvenode.add_string(fbx_name_class(acurvenode_name.encode(), b"AnimCurveNode")) acurvenode.add_string(b"") @@ -2233,7 +1428,7 @@ def fbx_data_animation_elements(root, scene_data): # Only create Animation curve if needed! if keys: - acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbxuid_from_key(acurve_key)) + acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key)) acurve.add_string(fbx_name_class(b"", b"AnimCurve")) acurve.add_string(b"") @@ -2264,20 +1459,6 @@ def fbx_data_animation_elements(root, scene_data): ##### 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", "animations", "frame_start", "frame_end", - "data_empties", "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. @@ -2879,20 +2060,20 @@ def fbx_data_from_scene(scene, settings): for ob_obj in objects: if ob_obj.is_bone: bo_data_key = data_bones[ob_obj] - connections.append((b"OO", get_fbxuid_from_key(bo_data_key), ob_obj.fbx_uuid, None)) + connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None)) else: if ob_obj.type == 'LAMP': lamp_key = data_lamps[ob_obj.bdata.data] - connections.append((b"OO", get_fbxuid_from_key(lamp_key), ob_obj.fbx_uuid, None)) + connections.append((b"OO", get_fbx_uuid_from_key(lamp_key), ob_obj.fbx_uuid, None)) elif ob_obj.type == 'CAMERA': cam_key = data_cameras[ob_obj] - connections.append((b"OO", get_fbxuid_from_key(cam_key), ob_obj.fbx_uuid, None)) + connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None)) elif ob_obj.type == 'EMPTY': empty_key = data_empties[ob_obj] - connections.append((b"OO", get_fbxuid_from_key(empty_key), ob_obj.fbx_uuid, None)) + connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None)) elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE: mesh_key, _me, _free = data_meshes[ob_obj.bdata] - connections.append((b"OO", get_fbxuid_from_key(mesh_key), ob_obj.fbx_uuid, None)) + connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None)) # Deformers (armature-to-geometry, only for meshes currently)... for arm, deformed_meshes in data_deformers.items(): @@ -2900,19 +2081,19 @@ def fbx_data_from_scene(scene, settings): # skin -> geometry mesh_key, _me, _free = data_meshes[ob_obj.bdata] assert(me == _me) - connections.append((b"OO", get_fbxuid_from_key(skin_key), get_fbxuid_from_key(mesh_key), None)) + connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None)) for bo_obj, clstr_key in clusters.items(): # cluster -> skin - connections.append((b"OO", get_fbxuid_from_key(clstr_key), get_fbxuid_from_key(skin_key), None)) + connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None)) # bone -> cluster - connections.append((b"OO", bo_obj.fbx_uuid, get_fbxuid_from_key(clstr_key), None)) + connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None)) # Materials mesh_mat_indices = OrderedDict() _objs_indices = {} for mat, (mat_key, ob_objs) in data_materials.items(): for ob_obj in ob_objs: - connections.append((b"OO", get_fbxuid_from_key(mat_key), ob_obj.fbx_uuid, None)) + connections.append((b"OO", get_fbx_uuid_from_key(mat_key), ob_obj.fbx_uuid, None)) if ob_obj.is_object: # Get index of this mat for this object. # Mat indices for mesh faces are determined by their order in 'mat to ob' connections. @@ -2930,36 +2111,36 @@ def fbx_data_from_scene(scene, settings): mat_key, _ob_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)) + connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_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)) + connections.append((b"OO", get_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None)) #Animations for astack_key, astack, alayer_key, _name, _fstart, _fend in animations: # Animstack itself is linked nowhere! - astack_id = get_fbxuid_from_key(astack_key) + astack_id = get_fbx_uuid_from_key(astack_key) # For now, only one layer! - alayer_id = get_fbxuid_from_key(alayer_key) + alayer_id = get_fbx_uuid_from_key(alayer_key) connections.append((b"OO", alayer_id, astack_id, None)) for ob_obj, (alayer_key, acurvenodes) in astack.items(): ob_id = ob_obj.fbx_uuid # Animlayer -> animstack. - # alayer_id = get_fbxuid_from_key(alayer_key) + # alayer_id = get_fbx_uuid_from_key(alayer_key) # connections.append((b"OO", alayer_id, astack_id, None)) for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items(): # Animcurvenode -> animalayer. - acurvenode_id = get_fbxuid_from_key(acurvenode_key) + acurvenode_id = get_fbx_uuid_from_key(acurvenode_key) connections.append((b"OO", acurvenode_id, alayer_id, None)) # Animcurvenode -> object property. connections.append((b"OP", acurvenode_id, ob_id, fbx_prop.encode())) for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items(): if acurve: # Animcurve -> Animcurvenode. - connections.append((b"OP", get_fbxuid_from_key(acurve_key), acurvenode_id, fbx_item.encode())) + connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode())) ##### And pack all this! @@ -3106,7 +2287,7 @@ def fbx_documents_elements(root, scene_data): elem_data_single_int32(docs, b"Count", 1) - doc_uid = get_fbxuid_from_key("__FBX_Document__" + name) + doc_uid = get_fbx_uuid_from_key("__FBX_Document__" + name) doc = elem_data_single_int64(docs, b"Document", doc_uid) doc.add_string_unicode(name) doc.add_string_unicode(name) @@ -3221,19 +2402,6 @@ def fbx_takes_elements(root, scene_data): ##### "Main" functions. ##### -FBXSettingsMedia = namedtuple("FBXSettingsMedia", ( - "path_mode", "base_src", "base_dst", "subdir", - "embed_textures", "copy_set", -)) -FBXSettings = namedtuple("FBXSettings", ( - "report", "to_axes", "global_matrix", "global_scale", - "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed", - "context_objects", "object_types", "use_mesh_modifiers", - "mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only", - "bake_anim", "bake_anim_use_nla_strips", "bake_anim_use_all_actions", "bake_anim_step", "bake_anim_simplify_factor", - "use_metadata", "media_settings", "use_custom_properties", -)) - # This func can be called with just the filepath def save_single(operator, scene, filepath="", diff --git a/io_scene_fbx/export_fbx_bin_utils.py b/io_scene_fbx/export_fbx_bin_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..641bf0b7f9ed8167e1cd1374fb0a0373f381b298 --- /dev/null +++ b/io_scene_fbx/export_fbx_bin_utils.py @@ -0,0 +1,911 @@ +# ##### 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 math + +from collections import namedtuple, OrderedDict +from collections.abc import Iterable +from itertools import zip_longest, chain + +import bpy +import bpy_extras +from bpy.types import Object, Bone, PoseBone, DupliObject +from mathutils import 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 +# Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps +# currently, apart from some AD products. +FBX_GEOMETRY_NORMAL_VERSION = 101 +FBX_GEOMETRY_BINORMAL_VERSION = 101 +FBX_GEOMETRY_TANGENT_VERSION = 101 +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_ANIM_KEY_VERSION = 4008 + +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.Rotation(math.pi / -2.0, 4, 'X') # Blender is +Y, FBX is +Z. +MAT_CONVERT_BONE = Matrix() + + +BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'} +BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES + + +# 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. +} + + +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)), +} + + +FBX_FRAMERATES = ( + (-1.0, 14), # Custom framerate. + (120.0, 1), + (100.0, 2), + (60.0, 3), + (50.0, 4), + (48.0, 5), + (30.0, 6), # BW NTSC. + (30.0 / 1.001, 9), # Color NTSC. + (25.0, 10), + (24.0, 11), + (24.0 / 1.001, 13), + (96.0, 15), + (72.0, 16), + (60.0 / 1.001, 17), +) + + +##### 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] + return val * conv + + +def units_convert_iter(it, u_from, u_to): + """Convert value.""" + conv = UNITS[u_to] / UNITS[u_from] + return (v * conv for v in it) + + +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) + + +def similar_values(v1, v2, e=1e-6): + """Return True if v1 and v2 are nearly the same.""" + if v1 == v2: + return True + return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e) + + +##### UIDs code. ##### + +# ID class (mere int). +class UUID(int): + pass + + +# UIDs storage. +_keys_to_uuids = {} +_uuids_to_keys = {} + + +def _key_to_uuid(uuids, 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! + uuid = key + else: + uuid = hash(key) + if uuid < 0: + uuid = -uuid + if uuid >= 2**63: + uuid //= 2 + # Try to make our uid shorter! + if uuid > int(1e9): + t_uuid = uuid % int(1e9) + if t_uuid not in uuids: + uuid = t_uuid + # Make sure our uuid *is* unique. + if uuid in uuids: + inc = 1 if uuid < 2**62 else -1 + while uuid in uuids: + uuid += inc + if 0 > uuid >= 2**63: + # Note that this is more that unlikely, but does not harm anyway... + raise ValueError("Unable to generate an UUID for key {}".format(key)) + return UUID(uuid) + + +def get_fbx_uuid_from_key(key): + """ + Return an UUID for given key, which is assumed hasable. + """ + uuid = _keys_to_uuids.get(key, None) + if uuid is None: + uuid = _key_to_uuid(_uuids_to_keys, key) + _keys_to_uuids[key] = uuid + _uuids_to_keys[uuid] = key + return uuid + + +# XXX Not sure we'll actually need this one? +def get_key_from_fbx_uuid(uuid): + """ + Return the key which generated this uid. + """ + assert(uuid.__class__ == UUID) + return _uuids_to_keys.get(uuid, None) + + +# Blender-specific key generators +def get_blenderID_key(bid): + if isinstance(bid, Iterable): + return "|".join("B" + e.rna_type.name + "#" + e.name for e in bid) + else: + return "B" + bid.rna_type.name + "#" + bid.name + + +def get_blenderID_name(bid): + if isinstance(bid, Iterable): + return "|".join(e.name for e in bid) + else: + return bid.name + + +def get_blender_empty_key(obj): + """Return bone's keys (Model and NodeAttribute).""" + return "|".join((get_blenderID_key(obj), "Empty")) + + +def get_blender_bone_key(armature, bone): + """Return bone's keys (Model and NodeAttribute).""" + return "|".join((get_blenderID_key((armature, bone)), "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")) + + +def get_blender_anim_id_base(scene, ref_id): + if ref_id is not None: + return get_blenderID_key(scene) + "|" + get_blenderID_key(ref_id) + else: + return get_blenderID_key(scene) + + +def get_blender_anim_stack_key(scene, ref_id): + """Return single anim stack key.""" + return get_blender_anim_id_base(scene, ref_id) + "|AnimStack" + + +def get_blender_anim_layer_key(scene, ref_id): + """Return ID's anim layer key.""" + return get_blender_anim_id_base(scene, ref_id) + "|AnimLayer" + + +def get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_prop_name): + """Return (stack/layer, ID, fbxprop) curve node key.""" + return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, "AnimCurveNode")) + + +def get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_prop_name, fbx_prop_item_name): + """Return (stack/layer, ID, fbxprop, item) curve key.""" + return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, + fbx_prop_item_name, "AnimCurve")) + + +##### 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_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. ##### + +def elem_properties(elem): + return elem_empty(elem, b"Properties70") + + +# Properties definitions, format: (b"type_1", b"label(???)", "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! +# Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense, +# these are just Vector3D ultimately... *sigh* (again). +FBX_PROPERTIES_DEFINITIONS = { + # Generic types. + "p_bool": (b"bool", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!! + "p_integer": (b"int", b"Integer", "add_int32"), + "p_ulonglong": (b"ULongLong", b"", "add_int64"), + "p_double": (b"double", b"Number", "add_float64"), # Non-animatable? + "p_number": (b"Number", b"", "add_float64"), # Animatable-only? + "p_enum": (b"enum", b"", "add_int32"), + "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable? + "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only? + "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable? + "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only? + "p_string": (b"KString", b"", "add_string_unicode"), + "p_string_url": (b"KString", b"Url", "add_string_unicode"), + "p_timestamp": (b"KTime", b"Time", "add_int64"), + "p_datetime": (b"DateTime", b"", "add_string_unicode"), + # Special types. + "p_object": (b"object", b""), # XXX Check this! No value for this prop??? Would really like to know how it works! + "p_compound": (b"Compound", b""), + # Specific types (sic). + ## Objects (Models). + "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"), + "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"), + "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"), + "p_visibility": (b"Visibility", b"", "add_float64"), + "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"), + ## Cameras!!! + "p_roll": (b"Roll", b"", "add_float64"), + "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"), + "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"), + "p_fov": (b"FieldOfView", b"", "add_float64"), + "p_fov_x": (b"FieldOfViewX", b"", "add_float64"), + "p_fov_y": (b"FieldOfViewY", b"", "add_float64"), +} + + +def _elem_props_set(elem, ptype, name, value, flags): + p = elem_data_single_string(elem, b"P", name) + for t in ptype[:2]: + p.add_string(t) + p.add_string(flags) + if len(ptype) == 3: + getattr(p, ptype[2])(value) + elif len(ptype) > 3: + # We assume value is iterable, else it's a bug! + for callback, val in zip(ptype[2:], value): + getattr(p, callback)(val) + + +def _elem_props_flags(animatable, custom): + if animatable and custom: + return b"AU" + elif animatable: + return b"A" + elif custom: + return b"U" + return b"" + + +def elem_props_set(elem, ptype, name, value=None, animatable=False, custom=False): + ptype = FBX_PROPERTIES_DEFINITIONS[ptype] + _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, custom)) + + +def elem_props_compound(elem, cmpd_name, custom=False): + def _setter(ptype, name, value, animatable=False, custom=False): + name = cmpd_name + b"|" + name + elem_props_set(elem, ptype, name, value, animatable=animatable, custom=custom) + + elem_props_set(elem, "p_compound", cmpd_name, custom=custom) + return _setter + + +def elem_props_template_init(templates, template_type): + """ + Init a writing template of given type, for *one* element's properties. + """ + ret = None + if template_type in templates: + tmpl = templates[template_type] + written = tmpl.written[0] + props = tmpl.properties + ret = OrderedDict((name, [val, ptype, anim, written]) for name, (val, ptype, anim) in props.items()) + return ret or OrderedDict() + + +def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False): + """ + 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] + if len(ptype) > 3: + value = tuple(value) + tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False)) + # Note animatable flag from template takes precedence over given one, if applicable. + if tmpl_ptype is not None: + if (tmpl_written and + ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or + (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (value, ptype_name)))): + return # Already in template and same value. + _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, False)) + template[name][3] = True + else: + _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False)) + + +def elem_props_template_finalize(template, elem): + """ + Finalize one element's template/props. + Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.), + but values for only *one* subtype can be written as template. So we have to be sure we write those for ths other + subtypes in each and every elements, if they are not overriden by that element. + Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :( + """ + for name, (value, ptype_name, animatable, written) in template.items(): + if written: + continue + ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name] + _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False)) + + +##### 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", "written")) + + +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.). + ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()} + + templates = OrderedDict() + for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values(): + if type_name not in templates: + templates[type_name] = [OrderedDict(((prop_type_name, (properties, nbr_users)),)), nbr_users] + else: + templates[type_name][0][prop_type_name] = (properties, nbr_users) + 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) + + if len(subprops) == 1: + prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items())) + subprops = (prop_type_name, properties) + ref_templates[(type_name, prop_type_name)].written[0] = True + else: + # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :| + max_users = max_props = -1 + written_prop_type_name = None + for prop_type_name, (properties, nbr_sub_type_users) in subprops.items(): + if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props): + max_users = nbr_sub_type_users + max_props = len(properties) + written_prop_type_name = prop_type_name + subprops = (written_prop_type_name, properties) + ref_templates[(type_name, written_prop_type_name)].written[0] = True + + prop_type_name, properties = subprops + 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, animatable) in properties.items(): + elem_props_set(props, ptype, name, value, animatable=animatable) + + +##### FBX objects generators. ##### + +# FBX Model-like data (i.e. Blender objects, dupliobjects and bones) are wrapped in ObjectWrapper. +# This allows us to have a (nearly) same code FBX-wise for all those types. +# The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...)) +# to actual Blender data it contains. +# Note it caches its instances, so that you may call several times ObjectWrapper(your_object) +# with a minimal cost (just re-computing the key). + +class MetaObjectWrapper(type): + def __call__(cls, bdata, armature=None): + if bdata is None: + return None + dup_mat = None + if isinstance(bdata, Object): + key = get_blenderID_key(bdata) + elif isinstance(bdata, DupliObject): + key = "|".join((get_blenderID_key((bdata.id_data, bdata.object)), cls._get_dup_num_id(bdata))) + dup_mat = bdata.matrix.copy() + else: # isinstance(bdata, (Bone, PoseBone)): + if isinstance(bdata, PoseBone): + bdata = armature.data.bones[bdata.name] + key = get_blenderID_key((armature, bdata)) + + cache = getattr(cls, "_cache", None) + if cache is None: + cache = cls._cache = {} + if key in cache: + instance = cache[key] + # Duplis hack: since duplis are not persistent in Blender (we have to re-create them to get updated + # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all + # other data is supposed valid during whole cache live, so we can skip resetting it). + instance._dupli_matrix = dup_mat + return instance + + instance = cls.__new__(cls, bdata, armature) + instance.__init__(bdata, armature) + instance.key = key + instance._dupli_matrix = dup_mat + cache[key] = instance + return instance + + +class ObjectWrapper(metaclass=MetaObjectWrapper): + """ + This class provides a same common interface for all (FBX-wise) object-like elements: + * Blender Object + * Blender Bone and PoseBone + * Blender DupliObject + Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis), + we need to use a key to identify each. + """ + __slots__ = ('name', 'key', 'bdata', '_tag', '_ref', '_dupli_matrix') + + @classmethod + def cache_clear(cls): + if hasattr(cls, "_cache"): + del cls._cache + + @staticmethod + def _get_dup_num_id(bdata): + return ".".join(str(i) for i in bdata.persistent_id if i != 2147483647) + + def __init__(self, bdata, armature=None): + """ + bdata might be an Object, DupliObject, Bone or PoseBone. + If Bone or PoseBone, armature Object must be provided. + """ + if isinstance(bdata, Object): + self._tag = 'OB' + self.name = get_blenderID_name(bdata) + self.bdata = bdata + self._ref = None + elif isinstance(bdata, DupliObject): + self._tag = 'DP' + self.name = "|".join((get_blenderID_name((bdata.id_data, bdata.object)), + "Dupli", self._get_dup_num_id(bdata))) + self.bdata = bdata.object + self._ref = bdata.id_data + else: # isinstance(bdata, (Bone, PoseBone)): + if isinstance(bdata, PoseBone): + bdata = armature.data.bones[bdata.name] + self._tag = 'BO' + self.name = get_blenderID_name((armature, bdata)) + self.bdata = bdata + self._ref = armature + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.key == other.key + + def __hash__(self): + return hash(self.key) + + #### Common to all _tag values. + def get_fbx_uuid(self): + return get_fbx_uuid_from_key(self.key) + fbx_uuid = property(get_fbx_uuid) + + def get_parent(self): + if self._tag == 'OB': + return ObjectWrapper(self.bdata.parent) + elif self._tag == 'DP': + return ObjectWrapper(self.bdata.parent or self._ref) + else: # self._tag == 'BO' + return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref) + parent = property(get_parent) + + def get_matrix_local(self): + if self._tag == 'OB': + return self.bdata.matrix_local.copy() + elif self._tag == 'DP': + return self._ref.matrix_world.inverted() * self._dupli_matrix + else: # 'BO', current pose + # PoseBone.matrix is in armature space, bring in back in real local one! + par = self.bdata.parent + par_mat_inv = self._ref.pose.bones[par.name].matrix.inverted() if par else Matrix() + return par_mat_inv * self._ref.pose.bones[self.bdata.name].matrix + matrix_local = property(get_matrix_local) + + def get_matrix_global(self): + if self._tag == 'OB': + return self.bdata.matrix_world.copy() + elif self._tag == 'DP': + return self._dupli_matrix + else: # 'BO', current pose + return self._ref.matrix_world * self._ref.pose.bones[self.bdata.name].matrix + matrix_global = property(get_matrix_global) + + def get_matrix_rest_local(self): + if self._tag == 'BO': + # Bone.matrix_local is in armature space, bring in back in real local one! + par = self.bdata.parent + par_mat_inv = par.matrix_local.inverted() if par else Matrix() + return par_mat_inv * self.bdata.matrix_local + else: + return self.matrix_local + matrix_rest_local = property(get_matrix_rest_local) + + def get_matrix_rest_global(self): + if self._tag == 'BO': + return self._ref.matrix_world * self.bdata.matrix_local + else: + return self.matrix_global + matrix_rest_global = property(get_matrix_rest_global) + + #### Transform and helpers + def has_valid_parent(self, objects): + par = self.parent + if par in objects: + if self._tag == 'OB': + par_type = self.bdata.parent_type + if par_type in {'OBJECT', 'BONE'}: + return True + else: + print("Sorry, “{}” parenting type is not supported".format(par_type)) + return False + return True + return False + + def use_bake_space_transform(self, scene_data): + # NOTE: Only applies to object types supporting this!!! Currently, only meshes... + # Also, do not apply it to children objects. + # TODO: Check whether this can work for bones too... + return (scene_data.settings.bake_space_transform and self._tag == 'OB' and + self.bdata.type in BLENDER_OBJECT_TYPES_MESHLIKE and not self.has_valid_parent(scene_data.objects)) + + def fbx_object_matrix(self, scene_data, rest=False, local_space=False, global_space=False): + """ + Generate object transform matrix (*always* in matching *FBX* space!). + If local_space is True, returned matrix is *always* in local space. + Else if global_space is True, returned matrix is always in world space. + If both local_space and global_space are False, returned matrix is in parent space if parent is valid, + else in world space. + Note local_space has precedence over global_space. + If rest is True and object is a Bone, returns matching rest pose transform instead of current pose one. + Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX). + """ + # Objects which are not bones and do not have any parent are *always* in global space + # (unless local_space is True!). + is_global = (not local_space and + (global_space or not (self._tag in {'DP', 'BO'} or self.has_valid_parent(scene_data.objects)))) + + if self._tag == 'BO': + if rest: + matrix = self.matrix_rest_global if is_global else self.matrix_rest_local + else: # Current pose. + matrix = self.matrix_global if is_global else self.matrix_local + else: + # Since we have to apply corrections to some types of object, we always need local Blender space here... + matrix = self.matrix_local + parent = self.parent + + # Lamps and cameras need to be rotated (in local space!). + if self.bdata.type == 'LAMP': + matrix = matrix * MAT_CONVERT_LAMP + elif self.bdata.type == 'CAMERA': + matrix = matrix * MAT_CONVERT_CAMERA + + # Our matrix is in local space, time to bring it in its final desired space. + if parent: + if is_global: + # Move matrix to global Blender space. + matrix = parent.matrix_global * matrix + elif parent.use_bake_space_transform(scene_data): + # Blender's and FBX's local space of parent may differ if we use bake_space_transform... + # Apply parent's *Blender* local space... + matrix = parent.matrix_local * matrix + # ...and move it back into parent's *FBX* local space. + par_mat = parent.fbx_object_matrix(scene_data, local_space=True) + matrix = par_mat.inverted() * matrix + + if self.use_bake_space_transform(scene_data): + # If we bake the transforms we need to post-multiply inverse global transform. + # This means that the global transform will not apply to children of this transform. + matrix = matrix * scene_data.settings.global_matrix_inv + if is_global: + # In any case, pre-multiply the global matrix to get it in FBX global space! + matrix = scene_data.settings.global_matrix * matrix + + return matrix + + def fbx_object_tx(self, scene_data, rest=False, rot_euler_compat=None): + """ + Generate object transform data (always in local space when possible). + """ + matrix = self.fbx_object_matrix(scene_data, rest=rest) + loc, rot, scale = matrix.decompose() + matrix_rot = rot.to_matrix() + # quat -> euler, we always use 'XYZ' order, use ref rotation if given. + if rot_euler_compat is not None: + rot = rot.to_euler('XYZ', rot_euler_compat) + else: + rot = rot.to_euler('XYZ') + return loc, rot, scale, matrix, matrix_rot + + #### _tag dependent... + def get_is_object(self): + return self._tag == 'OB' + is_object = property(get_is_object) + + def get_is_dupli(self): + return self._tag == 'DP' + is_dupli = property(get_is_dupli) + + def get_is_bone(self): + return self._tag == 'BO' + is_bone = property(get_is_bone) + + def get_type(self): + if self._tag in {'OB', 'DP'}: + return self.bdata.type + return ... + type = property(get_type) + + def get_armature(self): + if self._tag == 'BO': + return ObjectWrapper(self._ref) + return None + armature = property(get_armature) + + def get_bones(self): + if self._tag == 'OB' and self.bdata.type == 'ARMATURE': + return (ObjectWrapper(bo, self.bdata) for bo in self.bdata.data.bones) + return () + bones = property(get_bones) + + def get_material_slots(self): + if self._tag in {'OB', 'DP'}: + return self.bdata.material_slots + return () + material_slots = property(get_material_slots) + + #### Duplis... + def dupli_list_create(self, scene, settings='PREVIEW'): + if self._tag == 'OB': + # Sigh, why raise exception here? :/ + try: + self.bdata.dupli_list_create(scene, settings) + except: + pass + + def dupli_list_clear(self): + if self._tag == 'OB': + self.bdata.dupli_list_clear() + + def get_dupli_list(self): + if self._tag == 'OB': + return (ObjectWrapper(dup) for dup in self.bdata.dupli_list) + return () + dupli_list = property(get_dupli_list) + + +def fbx_name_class(name, cls): + return FBX_NAME_CLASS_SEP.join((name, cls)) + + +##### Top-level FBX data container. ##### + +# Helper sub-container gathering all exporter settings related to media (texture files). +FBXSettingsMedia = namedtuple("FBXSettingsMedia", ( + "path_mode", "base_src", "base_dst", "subdir", + "embed_textures", "copy_set", +)) + +# Helper container gathering all exporter settings. +FBXSettings = namedtuple("FBXSettings", ( + "report", "to_axes", "global_matrix", "global_scale", + "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed", + "context_objects", "object_types", "use_mesh_modifiers", + "mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only", + "bake_anim", "bake_anim_use_nla_strips", "bake_anim_use_all_actions", "bake_anim_step", "bake_anim_simplify_factor", + "use_metadata", "media_settings", "use_custom_properties", +)) + +# Helper container gathering some data we need multiple times: +# * templates. +# * settings, scene. +# * objects. +# * object data. +# * skinning data (binding armature/mesh). +# * animations. +FBXData = namedtuple("FBXData", ( + "templates", "templates_users", "connections", + "settings", "scene", "objects", "animations", "frame_start", "frame_end", + "data_empties", "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices", + "data_bones", "data_deformers", + "data_world", "data_materials", "data_textures", "data_videos", +))