diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index eff4e13aa764f798e4f35958b81bc281486d4bce..dde8d39a9872d8c1b78b12f42799a05fc43a7978 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -19,12 +19,12 @@ # <pep8 compliant> bl_info = { - "name": "Autodesk FBX format", - "author": "Campbell Barton, Bastien Montagne", - "version": (3, 1, 0), - "blender": (2, 70, 0), + "name": "FBX format", + "author": "Campbell Barton, Bastien Montagne, Jens Restemeier", + "version": (3, 2, 0), + "blender": (2, 72, 0), "location": "File > Import-Export", - "description": "Export FBX meshes, UV's, vertex colors, materials, " + "description": "FBX IO meshes, UV's, vertex colors, materials, " "textures, cameras, lamps and actions", "warning": "", "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" @@ -59,7 +59,7 @@ from bpy_extras.io_utils import (ImportHelper, class ImportFBX(bpy.types.Operator, ImportHelper): - """Load a FBX geometry file""" + """Load a FBX file""" bl_idname = "import_scene.fbx" bl_label = "Import FBX" bl_options = {'UNDO', 'PRESET'} @@ -101,6 +101,13 @@ class ImportFBX(bpy.types.Operator, ImportHelper): min=0.001, max=1000.0, default=1.0, ) + bake_space_transform = BoolProperty( + name="Apply Transform", + description=("Bake space transform into object data, avoids getting unwanted rotations to objects when " + "target space is not aligned with Blender's space " + "(WARNING! experimental option, might give odd/wrong results)"), + default=False, + ) use_image_search = BoolProperty( name="Image Search", @@ -135,6 +142,41 @@ class ImportFBX(bpy.types.Operator, ImportHelper): options={'HIDDEN'}, ) + ignore_leaf_bones = BoolProperty( + name="Ignore leaf bones", + description="Ignore the last bone at the end of a chain that is used to mark the length of the previous bone", + default=False, + options={'HIDDEN'}, + ) + automatic_bone_orientation = BoolProperty( + name="Automatic Bone Orientation", + description="Try to align the major bone axis with the bone children", + default=False, + options={'HIDDEN'}, + ) + primary_bone_axis = EnumProperty( + name="Primary Bone Axis", + items=(('X', "X Axis", ""), + ('Y', "Y Axis", ""), + ('Z', "Z Axis", ""), + ('-X', "-X Axis", ""), + ('-Y', "-Y Axis", ""), + ('-Z', "-Z Axis", ""), + ), + default='Y', + ) + secondary_bone_axis = EnumProperty( + name="Secondary Bone Axis", + items=(('X', "X Axis", ""), + ('Y', "Y Axis", ""), + ('Z', "Z Axis", ""), + ('-X', "-X Axis", ""), + ('-Y', "-Y Axis", ""), + ('-Z', "-Z Axis", ""), + ), + default='X', + ) + def draw(self, context): layout = self.layout @@ -143,7 +185,8 @@ class ImportFBX(bpy.types.Operator, ImportHelper): sub.enabled = self.use_manual_orientation sub.prop(self, "axis_forward") sub.prop(self, "axis_up") - sub.prop(self, "global_scale") + layout.prop(self, "global_scale") + layout.prop(self, "bake_space_transform") layout.prop(self, "use_image_search") # layout.prop(self, "use_alpha_decals") @@ -154,6 +197,14 @@ class ImportFBX(bpy.types.Operator, ImportHelper): sub.enabled = self.use_custom_props sub.prop(self, "use_custom_props_enum_as_string") + layout.prop(self, "ignore_leaf_bones") + + layout.prop(self, "automatic_bone_orientation"), + sub = layout.column() + sub.enabled = not self.automatic_bone_orientation + sub.prop(self, "primary_bone_axis") + sub.prop(self, "secondary_bone_axis") + def execute(self, context): keywords = self.as_keywords(ignore=("filter_glob", "directory")) keywords["use_cycles"] = (context.scene.render.engine == 'CYCLES') @@ -163,7 +214,7 @@ class ImportFBX(bpy.types.Operator, ImportHelper): class ExportFBX(bpy.types.Operator, ExportHelper): - """Selection to an ASCII Autodesk FBX""" + """Write a FBX file""" bl_idname = "export_scene.fbx" bl_label = "Export FBX" bl_options = {'UNDO', 'PRESET'} @@ -273,6 +324,35 @@ class ExportFBX(bpy.types.Operator, ExportHelper): description="Export custom properties", default=False, ) + add_leaf_bones = BoolProperty( + name="Add leaf bones", + description=("Append a last bone to the end of each chain to specify bone length. It is useful to, " + "enable this when exporting into another modelling application and to disable this when" + "exporting into a game engine or real-time viewer."), + default=True # False for commit! + ) + primary_bone_axis = EnumProperty( + name="Primary Bone Axis", + items=(('X', "X Axis", ""), + ('Y', "Y Axis", ""), + ('Z', "Z Axis", ""), + ('-X', "-X Axis", ""), + ('-Y', "-Y Axis", ""), + ('-Z', "-Z Axis", ""), + ), + default='Y', + ) + secondary_bone_axis = EnumProperty( + name="Secondary Bone Axis", + items=(('X', "X Axis", ""), + ('Y', "Y Axis", ""), + ('Z', "Z Axis", ""), + ('-X', "-X Axis", ""), + ('-Y', "-Y Axis", ""), + ('-Z', "-Z Axis", ""), + ), + default='X', + ) use_armature_deform_only = BoolProperty( name="Only Deform Bones", description="Only write deforming bones (and non-deforming ones when they have deforming children)", @@ -387,6 +467,9 @@ class ExportFBX(bpy.types.Operator, ExportHelper): layout.prop(self, "use_armature_deform_only") if is_74bin: layout.prop(self, "use_custom_props") + layout.prop(self, "add_leaf_bones") + layout.prop(self, "primary_bone_axis") + layout.prop(self, "secondary_bone_axis") layout.prop(self, "bake_anim") col = layout.column() col.enabled = self.bake_anim @@ -442,11 +525,11 @@ class ExportFBX(bpy.types.Operator, ExportHelper): def menu_func_import(self, context): - self.layout.operator(ImportFBX.bl_idname, text="Autodesk FBX (.fbx)") + self.layout.operator(ImportFBX.bl_idname, text="FBX (.fbx)") def menu_func_export(self, context): - self.layout.operator(ExportFBX.bl_idname, text="Autodesk FBX (.fbx)") + self.layout.operator(ExportFBX.bl_idname, text="FBX (.fbx)") def register(): diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index ff223fa8aaf7cc559bf54b4cb83bb0350d3daf10..a5cfdd25089212b807cf6bc0492eb9ee032d0c29 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -1371,6 +1371,8 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): mat_world_arm = arm_obj.fbx_object_matrix(scene_data, global_space=True) bones = tuple(bo_obj for bo_obj in arm_obj.bones if bo_obj in scene_data.objects) + bone_radius_scale = scene_data.settings.global_scale * 33.0 + # Bones "data". for bo_obj in bones: bo = bo_obj.bdata @@ -1382,13 +1384,18 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): tmpl = elem_props_template_init(scene_data.templates, b"Bone") props = elem_properties(fbx_bo) - elem_props_template_set(tmpl, props, "p_double", b"Size", (bo.tail_local - bo.head_local).length) + elem_props_template_set(tmpl, props, "p_double", b"Size", bo.head_radius * bone_radius_scale) elem_props_template_finalize(tmpl, props) # Custom properties. if scene_data.settings.use_custom_props: fbx_data_element_custom_properties(props, bo) + # Store Blender bone length - XXX Not much useful actually :/ + # (LimbLength can't be used because it is a scale factor 0-1 for the parent-child distance: + # http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/cpp_ref/class_fbx_skeleton.html#a9bbe2a70f4ed82cd162620259e649f0f ) + # elem_props_set(props, "p_double", "BlenderBoneLength".encode(), (bo.tail_local - bo.head_local).length, custom=True) + # Skin deformers and BindPoses. # Note: we might also use Deformers for our "parent to vertex" stuff??? deformer = scene_data.data_deformers_skin.get(arm_obj, None) @@ -1450,6 +1457,56 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm)) +def fbx_data_leaf_bone_elements(root, scene_data): + # Write a dummy leaf bone that is used by applications to show the length of the last bone in a chain + for (node_name, _par_uuid, node_uuid, attr_uuid, matrix, hide, size) in scene_data.data_leaf_bones: + # Bone 'data'... + fbx_bo = elem_data_single_int64(root, b"NodeAttribute", attr_uuid) + fbx_bo.add_string(fbx_name_class(node_name.encode(), b"NodeAttribute")) + fbx_bo.add_string(b"LimbNode") + elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton") + + tmpl = elem_props_template_init(scene_data.templates, b"Bone") + props = elem_properties(fbx_bo) + elem_props_template_set(tmpl, props, "p_double", b"Size", size) + elem_props_template_finalize(tmpl, props) + + # And bone object. + model = elem_data_single_int64(root, b"Model", node_uuid) + model.add_string(fbx_name_class(node_name.encode(), b"Model")) + model.add_string(b"LimbNode") + + elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION) + + # Object transform info. + loc, rot, scale = matrix.decompose() + rot = rot.to_euler('XYZ') + rot = tuple(convert_rad_to_deg_iter(rot)) + + tmpl = elem_props_template_init(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) + elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not hide)) + + # Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to + # invalid -1 value... + elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0) + + elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1) # RSrs + + # 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") + + elem_props_template_finalize(tmpl, props) + + def fbx_data_object_elements(root, ob_obj, scene_data): """ Write the Object (Model) data blocks. @@ -1709,6 +1766,37 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes, objects.update(bones) +def fbx_generate_leaf_bones(settings, data_bones): + # find which bons have no children + child_count = {bo: 0 for bo, _bo_key in data_bones.items()} + for bo, _bo_key in data_bones.items(): + if bo.parent and bo.parent.is_bone: + child_count[bo.parent] += 1 + + bone_radius_scale = settings.global_scale * 33.0 + + # generate bone data + leaf_parents = [bo for bo, count in child_count.items() if count == 0] + leaf_bones = [] + for parent in leaf_parents: + node_name = parent.name + "_end" + parent_uuid = parent.fbx_uuid + node_uuid = get_fbx_uuid_from_key(node_name + "_node") + attr_uuid = get_fbx_uuid_from_key(node_name + "_nodeattr") + + hide = parent.hide + size = parent.bdata.head_radius * bone_radius_scale + bone_length = (parent.bdata.tail_local - parent.bdata.head_local).length + matrix = Matrix.Translation((0, bone_length, 0)) + if settings.bone_correction_matrix_inv: + matrix = settings.bone_correction_matrix_inv * matrix + if settings.bone_correction_matrix: + matrix = matrix * settings.bone_correction_matrix + leaf_bones.append((node_name, parent_uuid, node_uuid, attr_uuid, matrix, hide, size)) + + return leaf_bones + + def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None, force_keep=False): """ Generate animation data (a single AnimStack) from objects, for a given frame range. @@ -2050,6 +2138,11 @@ def fbx_data_from_scene(scene, settings): fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes, data_bones, data_deformers_skin, arm_parents) + # Generate leaf bones + data_leaf_bones = None + if settings.add_leaf_bones: + data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones) + # Some world settings are embedded in FBX materials... if scene.world: data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),)) @@ -2125,7 +2218,7 @@ def fbx_data_from_scene(scene, settings): None, None, None, settings, scene, objects, None, 0.0, 0.0, data_empties, data_lamps, data_cameras, data_meshes, None, - data_bones, data_deformers_skin, data_deformers_shape, + data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape, data_world, data_materials, data_textures, data_videos, ) animations, frame_start, frame_end = fbx_animations(tmp_scdata) @@ -2248,6 +2341,11 @@ def fbx_data_from_scene(scene, settings): mesh_key, _me, _free = data_meshes[ob_obj] connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None)) + # Leaf Bones + for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones: + connections.append((b"OO", node_uuid, parent_uuid, None)) + connections.append((b"OO", attr_uuid, node_uuid, None)) + # 'Shape' deformers (shape keys, only for meshes currently)... for me_key, shapes_key, shapes in data_deformers_shape.values(): # shape -> geometry @@ -2332,7 +2430,7 @@ def fbx_data_from_scene(scene, settings): 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_skin, data_deformers_shape, + data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape, data_world, data_materials, data_textures, data_videos, ) @@ -2546,6 +2644,9 @@ def fbx_objects_elements(root, scene_data): continue fbx_data_armature_elements(objects, ob_obj, scene_data) + if scene_data.data_leaf_bones: + fbx_data_leaf_bone_elements(objects, scene_data) + for mat in scene_data.data_materials: fbx_data_material_elements(objects, mat, scene_data) @@ -2608,6 +2709,9 @@ def save_single(operator, scene, filepath="", bake_anim_use_all_actions=True, bake_anim_step=1.0, bake_anim_simplify_factor=1.0, + add_leaf_bones=False, + primary_bone_axis='Y', + secondary_bone_axis='X', use_metadata=True, path_mode='AUTO', use_mesh_edges=True, @@ -2627,6 +2731,11 @@ def save_single(operator, scene, filepath="", if 'OTHER' in object_types: object_types |= BLENDER_OTHER_OBJECT_TYPES + # Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property + # (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length. + # However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix... + if scene.unit_settings.system != 'NONE': + global_matrix = global_matrix * Matrix.Scale(1.0 / scene.unit_settings.scale_length, 4) global_scale = global_matrix.median_scale global_matrix_inv = global_matrix.inverted() # For transforming mesh normals. @@ -2636,6 +2745,19 @@ def save_single(operator, scene, filepath="", if embed_textures and path_mode != 'COPY': embed_textures = False + # Calcuate bone correction matrix + bone_correction_matrix = None # Default is None = no change + bone_correction_matrix_inv = None + if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'): + from bpy_extras.io_utils import axis_conversion + bone_correction_matrix = axis_conversion(from_forward=secondary_bone_axis, + from_up=primary_bone_axis, + to_forward='X', + to_up='Y', + ).to_4x4() + bone_correction_matrix_inv = bone_correction_matrix.inverted() + + media_settings = FBXExportSettingsMedia( path_mode, os.path.dirname(bpy.data.filepath), # base_src @@ -2650,7 +2772,8 @@ def save_single(operator, scene, filepath="", operator.report, (axis_up, axis_forward), 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, + mesh_smooth_type, use_mesh_edges, use_tspace, + use_armature_deform_only, add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv, bake_anim, bake_anim_use_nla_strips, bake_anim_use_all_actions, bake_anim_step, bake_anim_simplify_factor, False, media_settings, use_custom_props, ) @@ -2730,6 +2853,9 @@ def defaults_unity3d(): "bake_anim_step": 1.0, "bake_anim_use_nla_strips": True, "bake_anim_use_all_actions": True, + "add_leaf_bones": False, # Avoid memory/performance cost for something only useful for modelling + "primary_bone_axis": 'Y', # Doesn't really matter for Unity, so leave unchanged + "secondary_bone_axis": 'X', "path_mode": 'AUTO', "embed_textures": False, diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index 6264f90fde3538c4a1c134f4cd00a7eb12264019..dc3551f33b7da65c9356a651f7c2de6c4d6fb580 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -972,9 +972,12 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): # Bones, lamps and cameras need to be rotated (in local space!). if self._tag == 'BO': - # XXX This should work smoothly, but actually is only OK for 'rest' pose, actual pose/animations - # give insane results... :( - matrix = matrix * MAT_CONVERT_BONE + # If we have a bone parent we need to undo the parent correction. + if not is_global and scene_data.settings.bone_correction_matrix_inv and parent and parent.is_bone: + matrix = scene_data.settings.bone_correction_matrix_inv * matrix + # Apply the bone correction. + if scene_data.settings.bone_correction_matrix: + matrix = matrix * scene_data.settings.bone_correction_matrix elif self.bdata.type == 'LAMP': matrix = matrix * MAT_CONVERT_LAMP elif self.bdata.type == 'CAMERA': @@ -1100,7 +1103,8 @@ FBXExportSettings = namedtuple("FBXExportSettings", ( "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", + "mesh_smooth_type", "use_mesh_edges", "use_tspace", + "use_armature_deform_only", "add_leaf_bones", "bone_correction_matrix", "bone_correction_matrix_inv", "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_props", )) @@ -1116,15 +1120,17 @@ FBXExportData = namedtuple("FBXExportData", ( "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_skin", "data_deformers_shape", + "data_bones", "data_leaf_bones", "data_deformers_skin", "data_deformers_shape", "data_world", "data_materials", "data_textures", "data_videos", )) # Helper container gathering all importer settings. FBXImportSettings = namedtuple("FBXImportSettings", ( "report", "to_axes", "global_matrix", "global_scale", + "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed", "use_cycles", "use_image_search", "use_alpha_decals", "decal_offset", "use_custom_props", "use_custom_props_enum_as_string", - "object_tdata_cache", "cycles_material_wrap_map", "image_cache", + "cycles_material_wrap_map", "image_cache", + "ignore_leaf_bones", "automatic_bone_orientation", "bone_correction_matrix" )) diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index d809fd68deab8b03459db0b11c92eac3d1356e50..8a1028c95395c78783204e2f72928d1979a35a27 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -420,232 +420,6 @@ def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat): sca, sca_ofs, sca_piv) -def blen_read_object(fbx_tmpl, fbx_obj, object_data, settings): - elem_name_utf8 = elem_name_ensure_class(fbx_obj) - - # Object data must be created already - obj = bpy.data.objects.new(name=elem_name_utf8, object_data=object_data) - object_tdata_cache = settings.object_tdata_cache - - fbx_props = (elem_find_first(fbx_obj, b'Properties70'), - elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) - assert(fbx_props[0] is not None) - - # ---- - # Misc Attributes - - obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8)) - obj.hide = not bool(elem_props_get_visibility(fbx_props, b'Visibility', 1.0)) - - # ---- - # Transformation - - from mathutils import Matrix - from math import pi - - # rotation corrections - if obj.type == 'CAMERA': - rot_alt_mat = MAT_CONVERT_CAMERA - elif obj.type == 'LAMP': - rot_alt_mat = MAT_CONVERT_LAMP - else: - rot_alt_mat = Matrix() - - transform_data = object_tdata_cache.get(obj) - if transform_data is None: - transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat) - object_tdata_cache[obj] = transform_data - obj.matrix_basis = blen_read_object_transform_do(transform_data) - - if settings.use_custom_props: - blen_read_custom_properties(fbx_obj, obj, settings) - - return obj - - -# -------- -# Armature - -def blen_read_armatures_add_bone(bl_obj, bl_arm, bones, b_uuid, matrices, fbx_tmpl_model): - from mathutils import Matrix, Vector - - b_item, bsize, p_uuid, clusters = bones[b_uuid] - fbx_bdata, bl_bname = b_item - if bl_bname is not None: - return bl_arm.edit_bones[bl_bname] # Have already been created... - - p_ebo = None - if p_uuid is not None: - # Recurse over parents! - p_ebo = blen_read_armatures_add_bone(bl_obj, bl_arm, bones, p_uuid, matrices, fbx_tmpl_model) - - if clusters: - # Note in some cases, one bone can have several clusters (kind of LoD?), in Blender we'll always - # use only the first, for now. - fbx_cdata, meshes, objects = clusters[0] - objects = {blen_o for fbx_o, blen_o in objects} - - # We assume matrices in cluster are rest pose of bones (they are in Global space!). - # TransformLink is matrix of bone, in global space. - # TransformAssociateModel is matrix of armature, in global space (at bind time). - elm = elem_find_first(fbx_cdata, b'Transform', default=None) - mmat_bone = array_to_matrix4(elm.props[0]) if elm is not None else None - elm = elem_find_first(fbx_cdata, b'TransformLink', default=None) - bmat_glob = array_to_matrix4(elm.props[0]) if elm is not None else Matrix() - elm = elem_find_first(fbx_cdata, b'TransformAssociateModel', default=None) - amat_glob = array_to_matrix4(elm.props[0]) if elm is not None else Matrix() - - mmat_glob = bmat_glob * mmat_bone - - # We seek for matrix of bone in armature space... - bmat_arm = amat_glob.inverted_safe() * bmat_glob - - # Bone correction, works here... - bmat_loc = (p_ebo.matrix.inverted_safe() * bmat_arm) if p_ebo else bmat_arm - bmat_loc = bmat_loc * MAT_CONVERT_BONE - bmat_arm = (p_ebo.matrix * bmat_loc) if p_ebo else bmat_loc - else: - # Armature bound to no mesh... - fbx_cdata, meshes, objects = (None, (), ()) - mmat_bone = None - amat_glob = bl_obj.matrix_world - - fbx_props = (elem_find_first(fbx_bdata, b'Properties70'), - elem_find_first(fbx_tmpl_model, b'Properties70', fbx_elem_nil)) - assert(fbx_props[0] is not None) - - # Bone correction, works here... - transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_bdata, MAT_CONVERT_BONE) - bmat_loc = blen_read_object_transform_do(transform_data) - # Bring back matrix in armature space. - bmat_arm = (p_ebo.matrix * bmat_loc) if p_ebo else bmat_loc - - # ---- - # Now, create the (edit)bone. - bone_name = elem_name_ensure_class(fbx_bdata, b'Model') - - ebo = bl_arm.edit_bones.new(name=bone_name) - bone_name = ebo.name # Might differ from FBX bone name! - b_item[1] = bone_name # since ebo is only valid in Edit mode... :/ - - # So that our bone gets its final length, but still Y-aligned in armature space. - # XXX We now know bsize is not len of bone... but still forbid zero len! - ebo.tail = Vector((0.0, 1.0, 0.0)) * max(bsize, 1e-3) - # And rotate/move it to its final "rest pose". - ebo.matrix = bmat_arm.normalized() - - # Connection to parent. - if p_ebo is not None: - ebo.parent = p_ebo - if similar_values_iter(p_ebo.tail, ebo.head): - ebo.use_connect = True - - if fbx_cdata is not None: - # ---- - # Add a new vgroup to the meshes (their objects, actually!). - # Quite obviously, only one mesh is expected... - indices = elem_prop_first(elem_find_first(fbx_cdata, b'Indexes', default=None), default=()) - weights = elem_prop_first(elem_find_first(fbx_cdata, b'Weights', default=None), default=()) - add_vgroup_to_objects(indices, weights, bone_name, objects) - - # ---- - # If we get a valid mesh matrix (in bone space), store armature and mesh global matrices, we need to set temporarily - # both objects to those matrices when actually binding them via the modifier. - # Note we assume all bones were bound with the same mesh/armature (global) matrix, we do not support otherwise - # in Blender anyway! - if mmat_bone is not None: - for obj in objects: - if obj in matrices: - continue - matrices[obj] = (amat_glob, mmat_glob) - - return ebo - - -def blen_read_armatures(fbx_tmpl, armatures, fbx_bones_to_fake_object, scene, arm_parents, settings): - from mathutils import Matrix - - global_matrix = settings.global_matrix - assert(global_matrix is not None) - object_tdata_cache = settings.object_tdata_cache - - for a_item, bones in armatures: - fbx_adata, bl_adata = a_item - matrices = {} - - # ---- - # Armature data. - elem_name_utf8 = elem_name_ensure_class(fbx_adata, b'Model') - bl_arm = bpy.data.armatures.new(name=elem_name_utf8) - - # Need to create the object right now, since we can only add bones in Edit mode... :/ - assert(a_item[1] is None) - - if fbx_adata.props[2] in {b'LimbNode', b'Root'}: - # rootbone-as-armature case... - bl_adata = blen_read_object(fbx_tmpl, fbx_adata, bl_arm, settings) - fbx_bones_to_fake_object[fbx_adata.props[0]] = bl_adata - # reset transform. - bl_adata.matrix_basis = Matrix() - else: - bl_adata = a_item[1] = blen_read_object(fbx_tmpl, fbx_adata, bl_arm, settings) - - # Instantiate in scene. - obj_base = scene.objects.link(bl_adata) - obj_base.select = True - - # Switch to Edit mode. - scene.objects.active = bl_adata - is_hidden = bl_adata.hide - bl_adata.hide = False # Can't switch to Edit mode hidden objects... - bpy.ops.object.mode_set(mode='EDIT') - - for b_uuid in bones: - blen_read_armatures_add_bone(bl_adata, bl_arm, bones, b_uuid, matrices, fbx_tmpl) - - bpy.ops.object.mode_set(mode='OBJECT') - bl_adata.hide = is_hidden - - # Bind armature to objects. - arm_mat_back = bl_adata.matrix_basis.copy() - for ob_me, (amat, mmat) in matrices.items(): - # bring global armature & mesh matrices into *Blender* global space. - amat = global_matrix * amat - mmat = global_matrix * mmat - - bl_adata.matrix_basis = amat - me_mat_back = ob_me.matrix_basis.copy() - ob_me.matrix_basis = mmat - - mod = ob_me.modifiers.new(elem_name_utf8, 'ARMATURE') - mod.object = bl_adata - - ob_me.parent = bl_adata - ob_me.matrix_basis = me_mat_back - # Store the pair for later space corrections (bring back mesh in parent space). - arm_parents.add((bl_adata, ob_me)) - bl_adata.matrix_basis = arm_mat_back - - # Set Pose transformations... - for b_item, _b_size, _p_uuid, _clusters in bones.values(): - fbx_bdata, bl_bname = b_item - fbx_props = (elem_find_first(fbx_bdata, b'Properties70'), - elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) - assert(fbx_props[0] is not None) - - pbo = b_item[1] = bl_adata.pose.bones[bl_bname] - transform_data = object_tdata_cache.get(pbo) - if transform_data is None: - # Bone correction, gives a mess as result. :( - transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_bdata, MAT_CONVERT_BONE) - object_tdata_cache[pbo] = transform_data - mat = blen_read_object_transform_do(transform_data) - if pbo.parent: - # Bring back matrix in armature space. - mat = pbo.parent.matrix * mat - pbo.matrix = mat - - # --------- # Animation def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps): @@ -690,52 +464,53 @@ def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_of yield (curr_blenkframe, curr_values) -def blen_read_animations_action_item(action, item, cnodes, force_global, fps, settings): +def blen_read_animations_action_item(action, item, cnodes, fps): """ - 'Bake' loc/rot/scale into the action, taking into account global_matrix if no parent is present. + 'Bake' loc/rot/scale into the action, taking any pre_ and post_ matrix into account to transform from fbx into blender space. """ from bpy.types import Object, PoseBone, ShapeKey from mathutils import Euler, Matrix from itertools import chain - global_matrix = settings.global_matrix - object_tdata_cache = settings.object_tdata_cache - blen_curves = [] fbx_curves = [] + for curves, fbxprop in cnodes.values(): + for (fbx_acdata, _blen_data), channel in curves.values(): + fbx_curves.append((fbxprop, channel, fbx_acdata)) + + # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0). + if len(fbx_curves) == 0: + return + + blen_curves = [] props = [] if isinstance(item, ShapeKey): props = [(item.path_from_id("value"), 1, "Key")] else: # Object or PoseBone: - if item not in object_tdata_cache: - print("ERROR! object '%s' has no transform data, while being animated!" % item.name) - return + if item.is_bone: + bl_obj = item.bl_obj.pose.bones[item.bl_bone] + else: + bl_obj = item.bl_obj # We want to create actions for objects, but for bones we 'reuse' armatures' actions! - grpname = None - if item.id_data != item: - grpname = item.name + grpname = item.bl_obj.name # Since we might get other channels animated in the end, due to all FBX transform magic, # we need to add curves for whole loc/rot/scale in any case. - props = [(item.path_from_id("location"), 3, grpname or "Location"), + props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"), None, - (item.path_from_id("scale"), 3, grpname or "Scale")] - rot_mode = item.rotation_mode + (bl_obj.path_from_id("scale"), 3, grpname or "Scale")] + rot_mode = bl_obj.rotation_mode if rot_mode == 'QUATERNION': - props[1] = (item.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation") + props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation") elif rot_mode == 'AXIS_ANGLE': - props[1] = (item.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation") + props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation") else: # Euler - props[1] = (item.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation") + props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation") blen_curves = [action.fcurves.new(prop, channel, grpname) for prop, nbr_channels, grpname in props for channel in range(nbr_channels)] - for curves, fbxprop in cnodes.values(): - for (fbx_acdata, _blen_data), channel in curves.values(): - fbx_curves.append((fbxprop, channel, fbx_acdata)) - if isinstance(item, ShapeKey): # We assume for now blen init point is frame 1.0, while FBX ktime init point is 0. for frame, values in blen_read_animations_curves_iter(fbx_curves, 1.0, 0, fps): @@ -749,16 +524,16 @@ def blen_read_animations_action_item(action, item, cnodes, force_global, fps, se fc.keyframe_points.insert(frame, v, {'NEEDED', 'FAST'}).interpolation = 'LINEAR' else: # Object or PoseBone: - transform_data = object_tdata_cache[item] - rot_prev = item.rotation_euler.copy() + if item.is_bone: + bl_obj = item.bl_obj.pose.bones[item.bl_bone] + else: + bl_obj = item.bl_obj + + transform_data = item.fbx_transform_data + rot_prev = bl_obj.rotation_euler.copy() # Pre-compute inverted local rest matrix of the bone, if relevant. - if isinstance(item, PoseBone): - # First, get local (i.e. parentspace) rest pose matrix - restmat = item.bone.matrix_local - if item.parent: - restmat = item.parent.bone.matrix_local.inverted_safe() * restmat - restmat_inv = restmat.inverted_safe() + restmat_inv = item.get_bind_matrix().inverted() if item.is_bone else None # We assume for now blen init point is frame 1.0, while FBX ktime init point is 0. for frame, values in blen_read_animations_curves_iter(fbx_curves, 1.0, 0, fps): @@ -770,12 +545,21 @@ def blen_read_animations_action_item(action, item, cnodes, force_global, fps, se elif fbxprop == b'Lcl Scaling': transform_data.sca[channel] = v mat = blen_read_object_transform_do(transform_data) - # Don't forget global matrix - but never for bones! - if isinstance(item, Object): - if (not item.parent or force_global) and global_matrix is not None: - mat = global_matrix * mat - else: # PoseBone, Urg! - # And now, remove that rest pose matrix from current mat (also in parent space). + + # compensate for changes in the local matrix during processing + if item.anim_compensation_matrix: + mat = mat * item.anim_compensation_matrix + + # apply pre- and post matrix + # post-matrix will contain any correction for lights, camera and bone orientation + # pre-matrix will contain any correction for a parent's correction matrix or the global matrix + if item.pre_matrix: + mat = item.pre_matrix * mat + if item.post_matrix: + mat = mat * item.post_matrix + + # And now, remove that rest pose matrix from current mat (also in parent space). + if restmat_inv: mat = restmat_inv * mat # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes! @@ -796,26 +580,32 @@ def blen_read_animations_action_item(action, item, cnodes, force_global, fps, se fc.update() -def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, force_global_objects, settings): +def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene): """ Recreate an action per stack/layer/object combinations. Note actions are not linked to objects, this is up to the user! """ + from bpy.types import ShapeKey + actions = {} for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items(): stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack') for al_uuid, ((fbx_aldata, _blen_data), items) in alayers.items(): layer_name = elem_name_ensure_class(fbx_aldata, b'AnimLayer') for item, cnodes in items.items(): - id_data = item.id_data + if isinstance(item, ShapeKey): + id_data = item.id_data + else: + id_data = item.bl_obj + if id_data is None: + continue key = (as_uuid, al_uuid, id_data) action = actions.get(key) if action is None: action_name = "|".join((id_data.name, stack_name, layer_name)) actions[key] = action = bpy.data.actions.new(action_name) action.use_fake_user = True - blen_read_animations_action_item(action, item, cnodes, - item in force_global_objects, scene.render.fps, settings) + blen_read_animations_action_item(action, item, cnodes, scene.render.fps) # ---- @@ -834,15 +624,21 @@ def blen_read_geom_array_mapped_vert( fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, stride, item_size, descr, + xform=None ): # TODO, generic mapping apply function if fbx_layer_mapping == b'ByVertice': if fbx_layer_ref == b'Direct': assert(fbx_layer_index is None) # TODO, more generic support for mapping types - for i, blen_data_item in enumerate(blen_data): - setattr(blen_data_item, blend_attr, - fbx_layer_data[(i * stride): (i * stride) + item_size]) + if xform is None: + for i, blen_data_item in enumerate(blen_data): + setattr(blen_data_item, blend_attr, + fbx_layer_data[(i * stride): (i * stride) + item_size]) + else: + for i, blen_data_item in enumerate(blen_data): + setattr(blen_data_item, blend_attr, + xform(fbx_layer_data[(i * stride): (i * stride) + item_size])) return True else: print("warning layer %r ref type unsupported: %r" % (descr, fbx_layer_ref)) @@ -1091,7 +887,7 @@ def blen_read_geom_layer_smooth(fbx_obj, mesh): return False -def blen_read_geom_layer_normal(fbx_obj, mesh): +def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None): fbx_layer = elem_find_first(fbx_obj, b'LayerElementNormal') if fbx_layer is None: @@ -1112,10 +908,25 @@ def blen_read_geom_layer_normal(fbx_obj, mesh): fbx_layer_data, None, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, + xform ) def blen_read_geom(fbx_tmpl, fbx_obj, settings): + from mathutils import Matrix, Vector + from itertools import chain + import array + + # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the + # global matrix, so we need to apply the global matrix to the vertices to get the correct result. + geom_mat_co = settings.global_matrix if settings.bake_space_transform else None + # We need to apply the inverse transpose of the global matrix when transforming normals. + geom_mat_no = Matrix(settings.global_matrix_inv_transposed) if settings.bake_space_transform else None + if geom_mat_no is not None: + # Remove translation & scaling! + geom_mat_no.translation = Vector() + geom_mat_no.normalize() + # TODO, use 'fbx_tmpl' elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Geometry') @@ -1123,6 +934,12 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex')) fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges')) + if geom_mat_co is not None: + def _vcos_transformed_gen(raw_cos, m=None): + # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now. + return chain(*(m * Vector(v) for v in zip(*(iter(raw_cos),) * 3))) + fbx_verts = array.array(fbx_verts.typecode, _vcos_transformed_gen(fbx_verts, geom_mat_co)) + if fbx_verts is None: fbx_verts = () if fbx_polys is None: @@ -1188,7 +1005,12 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): # must be after edge, face loading. ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh) - ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh) + if geom_mat_no is None: + ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh) + else: + def nortrans(v): + return geom_mat_no * Vector(v) + ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans) mesh.validate() @@ -1205,7 +1027,7 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings): return mesh -def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene, settings): +def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene): from mathutils import Vector elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry') @@ -1227,7 +1049,7 @@ def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene, settings): for me, objects in meshes: vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos)) - objects = list({blen_o for fbx_o, blen_o in objects}) + objects = list({node.bl_obj for node in objects}) assert(objects) if me.shape_keys is None: @@ -1416,6 +1238,597 @@ def blen_read_light(fbx_tmpl, fbx_obj, global_scale): return lamp +# ### Import Utility class +class FbxImportHelperNode: + """ + Temporary helper node to store a hierarchy of fbxNode objects before building + Objects, Armatures and Bones. It tries to keep the correction data in one place so it can be applied consistently to the imported data. + """ + + __slots__ = ('_parent', 'anim_compensation_matrix', 'armature_setup', 'bind_matrix', 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', + 'children', 'clusters', 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type', 'has_bone_children', 'ignore', 'is_armature', + 'is_bone', 'is_root', 'matrix', 'meshes', 'post_matrix', 'pre_matrix') + + def __init__(self, fbx_elem, bl_data, fbx_transform_data, is_bone): + self.fbx_name = elem_name_ensure_class(fbx_elem, b'Model') if fbx_elem else 'Unknown' + self.fbx_type = fbx_elem.props[2] if fbx_elem else None + self.fbx_elem = fbx_elem + self.bl_obj = None + self.bl_data = bl_data + self.bl_bone = None # Name of bone if this is a bone (this may be different to fbx_name if there was a name conflict in Blender!) + self.fbx_transform_data = fbx_transform_data + self.is_root = False + self.is_bone = is_bone + self.is_armature = False + self.has_bone_children = False # True if the hierarchy below this node contains bones, important to support mixed hierarchies. + self.ignore = False # True for leaf-bones added to the end of some bone chains to set the lengths. + self.pre_matrix = None # correction matrix that needs to be applied before the FBX transform + self.bind_matrix = None # for bones this is the matrix used to bind to the skin + self.matrix = blen_read_object_transform_do(fbx_transform_data) if fbx_transform_data else None + self.post_matrix = None # correction matrix that needs to be applied after the FBX transform + self.bone_child_matrix = None # Objects attached to a bone end not the beginning, this matrix corrects for that + self.anim_compensation_matrix = None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this. + + self.meshes = None # List of meshes influenced by this bone. + self.clusters = [] # Deformer Cluster nodes + self.armature_setup = None # mesh and armature matrix when the mesh was bound + + self._parent = None + self.children = [] + + @property + def parent(self): + return self._parent + + @parent.setter + def parent(self, value): + if self._parent is not None: + self._parent.children.remove(self) + self._parent = value + if self._parent is not None: + self._parent.children.append(self) + + def print_info(self, indent=0): + print(" " * indent + (self.fbx_name if self.fbx_name else "(Null)") + + ("[root]" if self.is_root else "") + + ("[ignore]" if self.ignore else "") + + ("[armature]" if self.is_armature else "") + + ("[bone]" if self.is_bone else "") + + ("[HBC]" if self.has_bone_children else "") + ) + for c in self.children: + c.print_info(indent + 1) + + def mark_leaf_bones(self): + if self.is_bone and len(self.children) == 1: + child = self.children[0] + if child.is_bone and len(child.children) == 0: + child.ignore = True # Ignore leaf bone at end of chain + for child in self.children: + child.mark_leaf_bones() + + def do_bake_transform(self, settings): + return (settings.bake_space_transform and self.fbx_type in (b'Mesh', b'Null') and + not self.is_armature and not self.is_bone) + + def find_correction_matrix(self, settings, parent_correction_inv=None): + from bpy_extras.io_utils import axis_conversion + from mathutils import Matrix, Vector + + if self.parent and (self.parent.is_root or self.parent.do_bake_transform(settings)): + self.pre_matrix = settings.global_matrix + + if parent_correction_inv: + self.pre_matrix = parent_correction_inv * (self.pre_matrix if self.pre_matrix else Matrix()) + + correction_matrix = None + + if self.is_bone: + if settings.automatic_bone_orientation: + # find best orientation to align bone with + bone_children = [child for child in self.children if child.is_bone] + if len(bone_children) == 0: + # no children, inherit the correction from parent (if possible) + if self.parent and self.parent.is_bone: + correction_matrix = parent_correction_inv.inverted() if parent_correction_inv else None + else: + # else find how best to rotate the bone to align the Y axis with the children + best_axis = (1, 0, 0) + if len(bone_children) == 1: + vec = bone_children[0].bind_matrix.to_translation() + best_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1)) + if abs(vec[0]) > abs(vec[1]): + if abs(vec[0]) > abs(vec[2]): + best_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0)) + elif abs(vec[1]) > abs(vec[2]): + best_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0)) + else: + # get the child directions once because they may be checked several times + child_locs = [loc.normalized() for loc in [bind_matrix.to_translation() for bind_matrix in [child.bind_matrix for child in bone_children]] if loc.magnitude > 0.0] + + # I'm not sure which one I like better... + if False: + best_angle = -1.0 + for i in range(6): + a = i // 2 + s = -1 if i % 2 == 1 else 1 + test_axis = Vector((s if a == 0 else 0, s if a == 1 else 0, s if a == 2 else 0)) + + # find max angle to children + max_angle = 1.0 + for loc in child_locs: + max_angle = min(max_angle, test_axis.dot(loc)) + + # is it better than the last one? + if best_angle < max_angle: + best_angle = max_angle + best_axis = test_axis + else: + best_angle = -1.0 + for vec in child_locs: + test_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1)) + if abs(vec[0]) > abs(vec[1]): + if abs(vec[0]) > abs(vec[2]): + test_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0)) + elif abs(vec[1]) > abs(vec[2]): + test_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0)) + + # find max angle to children + max_angle = 1.0 + for loc in child_locs: + max_angle = min(max_angle, test_axis.dot(loc)) + + # is it better than the last one? + if best_angle < max_angle: + best_angle = max_angle + best_axis = test_axis + + # convert best_axis to axis string + to_up = 'Z' if best_axis[2] >= 0 else '-Z' + if abs(best_axis[0]) > abs(best_axis[1]): + if abs(best_axis[0]) > abs(best_axis[2]): + to_up = 'X' if best_axis[0] >= 0 else '-X' + elif abs(best_axis[1]) > abs(best_axis[2]): + to_up = 'Y' if best_axis[1] >= 0 else '-Y' + to_forward = 'X' if to_up not in {'X', '-X'} else 'Y' + + # Build correction matrix + if (to_up, to_forward) != ('Y', 'X'): + correction_matrix = axis_conversion(from_forward='X', + from_up='Y', + to_forward=to_forward, + to_up=to_up, + ).to_4x4() + else: + correction_matrix = settings.bone_correction_matrix + else: + # camera and light can be hard wired + if self.fbx_type == b'Camera': + correction_matrix = MAT_CONVERT_CAMERA + elif self.fbx_type == b'Light': + correction_matrix = MAT_CONVERT_LAMP + + self.post_matrix = correction_matrix + + if self.do_bake_transform(settings): + self.post_matrix = settings.global_matrix_inv * (self.post_matrix if self.post_matrix else Matrix()) + + # process children + correction_matrix_inv = correction_matrix.inverted() if correction_matrix else None + for child in self.children: + child.find_correction_matrix(settings, correction_matrix_inv) + + def find_armatures(self): + needs_armature = False + for child in self.children: + if child.is_bone: + needs_armature = True + break + if needs_armature: + if self.fbx_type == b'Null': + # if empty then convert into armature + self.is_armature = True + else: + # otherwise insert a new node + armature = FbxImportHelperNode(None, None, None, False) + armature.fbx_name = "Armature" + armature.is_armature = True + + for child in self.children[:]: + if child.is_bone: + child.parent = armature + + armature.parent = self + + for child in self.children: + if child.is_armature: + continue + if child.is_bone: + continue + child.find_armatures() + + def find_bone_children(self): + has_bone_children = False + for child in self.children: + has_bone_children |= child.find_bone_children() + self.has_bone_children = has_bone_children + return self.is_bone or has_bone_children + + def find_fake_bones(self, in_armature=False): + if in_armature and not self.is_bone and self.has_bone_children: + self.is_bone = True + # if we are not a null node we need an intermediate node for the data + if self.fbx_type != b'Null': + node = FbxImportHelperNode(self.fbx_elem, self.bl_data, None, False) + self.fbx_elem = None + self.bl_data = None + + # transfer children + for child in self.children: + if child.is_bone or child.has_bone_children: + continue + child.parent = node + + # attach to parent + node.parent = self + + if self.is_armature: + in_armature = True + for child in self.children: + child.find_fake_bones(in_armature) + + def get_world_matrix(self): + from mathutils import Matrix + + matrix = self.parent.get_world_matrix() if self.parent else Matrix() + if self.matrix: + matrix = matrix * self.matrix + return matrix + + def get_matrix(self): + from mathutils import Matrix + + matrix = self.matrix if self.matrix else Matrix() + if self.pre_matrix: + matrix = self.pre_matrix * matrix + if self.post_matrix: + matrix = matrix * self.post_matrix + return matrix + + def get_bind_matrix(self): + from mathutils import Matrix + + matrix = self.bind_matrix if self.bind_matrix else Matrix() + if self.pre_matrix: + matrix = self.pre_matrix * matrix + if self.post_matrix: + matrix = matrix * self.post_matrix + return matrix + + def make_bind_pose_local(self, parent_matrix=None): + from mathutils import Matrix + + if parent_matrix is None: + parent_matrix = Matrix() + + if self.bind_matrix: + bind_matrix = parent_matrix.inverted() * self.bind_matrix + else: + bind_matrix = self.matrix.copy() if self.matrix else None + + self.bind_matrix = bind_matrix + if bind_matrix: + parent_matrix = parent_matrix * bind_matrix + + for child in self.children: + child.make_bind_pose_local(parent_matrix) + + def collect_skeleton_meshes(self, meshes): + for _, m in self.clusters: + meshes.update(m) + for child in self.children: + child.collect_skeleton_meshes(meshes) + + def collect_armature_meshes(self): + if self.is_armature: + armature_matrix_inv = self.get_world_matrix().inverted() + + meshes = set() + for child in self.children: + child.collect_skeleton_meshes(meshes) + for m in meshes: + old_matrix = m.matrix + m.matrix = armature_matrix_inv * m.get_world_matrix() + m.anim_compensation_matrix = old_matrix.inverted() * m.matrix + m.parent = self + self.meshes = meshes + else: + for child in self.children: + child.collect_armature_meshes() + + def build_skeleton(self, arm, parent_matrix, parent_bone_size=1): + from mathutils import Vector, Matrix + + # ---- + # Now, create the (edit)bone. + bone = arm.bl_data.edit_bones.new(name=self.fbx_name) + bone.select = True + self.bl_obj = arm.bl_obj + self.bl_data = arm.bl_data + self.bl_bone = bone.name # Could be different from the FBX name! + + # get average distance to children + bone_size = 0.0 + bone_count = 0 + for child in self.children: + if child.is_bone: + bone_size += child.bind_matrix.to_translation().magnitude + bone_count += 1 + if bone_count > 0: + bone_size /= bone_count + else: + bone_size = parent_bone_size + + # So that our bone gets its final length, but still Y-aligned in armature space. + # 0-length bones are automatically collapsed into their parent when you leave edit mode, so this enforces a minimum length + bone_tail = Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size) + bone.tail = bone_tail + + # And rotate/move it to its final "rest pose". + bone_matrix = parent_matrix * self.get_bind_matrix().normalized() + + bone.matrix = bone_matrix + + # correction for children attached to a bone. Fbx expects to attach to the head of a bone, while blender attaches to the tail. + self.bone_child_matrix = Matrix.Translation(-bone_tail) + + for child in self.children: + if child.ignore: + continue + if child.is_bone: + child_bone = child.build_skeleton(arm, bone_matrix, bone_size) + # Connection to parent. + child_bone.parent = bone + if similar_values_iter(bone.tail, child_bone.head): + child_bone.use_connect = True + + return bone + + def build_node(self, fbx_tmpl, settings): + # create when linking since we need object data + elem_name_utf8 = self.fbx_name + + # Object data must be created already + self.bl_obj = obj = bpy.data.objects.new(name=elem_name_utf8, object_data=self.bl_data) + + fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'), + elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) + assert(fbx_props[0] is not None) + + # ---- + # Misc Attributes + + obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8)) + obj.hide = not bool(elem_props_get_visibility(fbx_props, b'Visibility', 1.0)) + + obj.matrix_basis = self.get_matrix() + + if settings.use_custom_props: + blen_read_custom_properties(fbx_props[0], obj, settings) + + return obj + + def build_skeleton_children(self, fbx_tmpl, settings, scene): + if self.is_bone: + for child in self.children: + if child.ignore: + continue + child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene) + if child_obj: + child_obj.parent = self.bl_obj # get the armature the bone belongs to + child_obj.parent_bone = self.bl_bone + child_obj.parent_type = 'BONE' + + # Blender attaches to the end of a bone, while FBX attaches to the start. bone_child_matrix corrects for that. + if child.pre_matrix: + child.pre_matrix = self.bone_child_matrix * child.pre_matrix + else: + child.pre_matrix = self.bone_child_matrix + + child_obj.matrix_basis = child.get_matrix() + else: + # child is not a bone + obj = self.build_node(fbx_tmpl, settings) + + for child in self.children: + if child.ignore: + continue + child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene) + if child_obj: + child_obj.parent = obj + + # instance in scene + obj_base = scene.objects.link(obj) + obj_base.select = True + + return obj + + def set_pose_matrix(self, arm): + pose_bone = arm.bl_obj.pose.bones[self.bl_bone] + pose_bone.matrix_basis = self.get_bind_matrix().inverted() * self.get_matrix() + + for child in self.children: + if child.ignore: + continue + if child.is_bone: + child.set_pose_matrix(arm) + + def merge_weights(self, combined_weights, fbx_cluster): + indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=()) + weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=()) + + for index, weight in zip(indices, weights): + w = combined_weights.get(index) + if w is None: + combined_weights[index] = [weight] + else: + w.append(weight) + + def set_bone_weights(self): + ignored_children = [child for child in self.children if child.is_bone and child.ignore and len(child.clusters) > 0] + + if len(ignored_children) > 0: + # If we have an ignored child bone we need to merge their weights into the current bone weights. + # (This can happen both intentionally and accidentally when skinning a model. Either way, they + # need to be moved into a parent bone or they cause animation glitches.) + + for fbx_cluster, meshes in self.clusters: + combined_weights = {} + self.merge_weights(combined_weights, fbx_cluster) + + for child in ignored_children: + for child_cluster, child_meshes in child.clusters: + if not meshes.isdisjoint(child_meshes): + self.merge_weights(combined_weights, child_cluster) + + # combine child weights + indices = [] + weights = [] + for i, w in combined_weights.items(): + indices.append(i) + if len(w) > 1: + weights.append(sum(w) / len(w)) + else: + weights.append(w[0]) + + add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes]) + + # clusters that drive meshes not included in a parent don't need to be merged + all_meshes = set().union(*[meshes for _, meshes in self.clusters]) + for child in ignored_children: + for child_cluster, child_meshes in child.clusters: + if all_meshes.isdisjoint(child_meshes): + indices = elem_prop_first(elem_find_first(child_cluster, b'Indexes', default=None), default=()) + weights = elem_prop_first(elem_find_first(child_cluster, b'Weights', default=None), default=()) + add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in child_meshes]) + else: + # set the vertex weights on meshes + for fbx_cluster, meshes in self.clusters: + indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=()) + weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=()) + add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes]) + + for child in self.children: + if child.ignore: + continue + if child.is_bone: + child.set_bone_weights() + + def build_hierarchy(self, fbx_tmpl, settings, scene): + from mathutils import Matrix + + if self.is_armature: + # create when linking since we need object data + elem_name_utf8 = self.fbx_name + + self.bl_data = arm_data = bpy.data.armatures.new(name=elem_name_utf8) + + # Object data must be created already + self.bl_obj = arm = bpy.data.objects.new(name=elem_name_utf8, object_data=arm_data) + + arm.matrix_basis = self.get_matrix() + + if self.fbx_elem: + fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'), + elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) + assert(fbx_props[0] is not None) + + if settings.use_custom_props: + blen_read_custom_properties(fbx_props[0], arm, settings) + + # instance in scene + obj_base = scene.objects.link(arm) + obj_base.select = True + + # Add bones: + + # Switch to Edit mode. + scene.objects.active = arm + is_hidden = arm.hide + arm.hide = False # Can't switch to Edit mode hidden objects... + bpy.ops.object.mode_set(mode='EDIT') + + for child in self.children: + if child.ignore: + continue + if child.is_bone: + child_obj = child.build_skeleton(self, Matrix()) + + bpy.ops.object.mode_set(mode='OBJECT') + + arm.hide = is_hidden + + # Set pose matrix + for child in self.children: + if child.ignore: + continue + if child.is_bone: + child.set_pose_matrix(self) + + # Add bone children: + for child in self.children: + if child.ignore: + continue + child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene) + if child_obj: + child_obj.parent = arm + + # Add armature modifiers to the meshes + if self.meshes: + arm_mat_back = arm.matrix_basis.copy() + for mesh in self.meshes: + (amat, mmat) = mesh.armature_setup + + # bring global armature & mesh matrices into *Blender* global space. + amat = settings.global_matrix * amat + mmat = settings.global_matrix * mmat + + arm.matrix_basis = amat + me_mat_back = mesh.bl_obj.matrix_basis.copy() + mesh.bl_obj.matrix_basis = mmat + + mod = mesh.bl_obj.modifiers.new(elem_name_utf8, 'ARMATURE') + mod.object = arm + + mesh.bl_obj.matrix_basis = me_mat_back + arm.matrix_basis = arm_mat_back + + # Add bone weights to the deformers + for child in self.children: + if child.ignore: + continue + if child.is_bone: + child.set_bone_weights() + + return arm + elif self.fbx_elem: + obj = self.build_node(fbx_tmpl, settings) + + # walk through children + for child in self.children: + child_obj = child.build_hierarchy(fbx_tmpl, settings, scene) + child_obj.parent = obj + + # instance in scene + obj_base = scene.objects.link(obj) + obj_base.select = True + + return obj + else: + for child in self.children: + child_obj = child.build_hierarchy(fbx_tmpl, settings, scene) + + def is_ascii(filepath, size): with open(filepath, 'r', encoding="utf-8") as f: try: @@ -1432,12 +1845,17 @@ def load(operator, context, filepath="", axis_forward='-Z', axis_up='Y', global_scale=1.0, + bake_space_transform=False, use_cycles=True, use_image_search=False, use_alpha_decals=False, decal_offset=0.0, use_custom_props=True, - use_custom_props_enum_as_string=True): + use_custom_props_enum_as_string=True, + ignore_leaf_bones=False, + automatic_bone_orientation=False, + primary_bone_axis='Y', + secondary_bone_axis='X'): global fbx_elem_nil fbx_elem_nil = FBXElem('', (), (), ()) @@ -1479,7 +1897,6 @@ def load(operator, context, filepath="", basedir = os.path.dirname(filepath) - object_tdata_cache = {} cycles_material_wrap_map = {} image_cache = {} if not use_cycles: @@ -1503,6 +1920,8 @@ def load(operator, context, filepath="", operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath) return {'CANCELLED'} + # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter... + global_scale *= elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 100.0) / 100.0 # Compute global matrix and scale. if not use_manual_orientation: axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1), @@ -1513,11 +1932,24 @@ def load(operator, context, filepath="", elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1)) axis_key = (axis_up, axis_forward, axis_coord) axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y')) - # FBX base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter... - global_scale = elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 100.0) / 100.0 global_matrix = (Matrix.Scale(global_scale, 4) * axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4()) + # To cancel out unwanted rotation/scale on nodes. + global_matrix_inv = global_matrix.inverted() + # For transforming mesh normals. + global_matrix_inv_transposed = global_matrix_inv.transposed() + + # Compute bone correction matrix + bone_correction_matrix = None # None means no correction/identity + if not automatic_bone_orientation: + if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'): + bone_correction_matrix = axis_conversion(from_forward='X', + from_up='Y', + to_forward=secondary_bone_axis, + to_up=primary_bone_axis, + ).to_4x4() + # Compute framerate settings. custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0) time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode') @@ -1530,10 +1962,12 @@ def load(operator, context, filepath="", # store global settings that need to be accessed during conversion settings = FBXImportSettings( operator.report, (axis_up, axis_forward), global_matrix, global_scale, + bake_space_transform, global_matrix_inv, global_matrix_inv_transposed, use_cycles, use_image_search, use_alpha_decals, decal_offset, use_custom_props, use_custom_props_enum_as_string, - object_tdata_cache, cycles_material_wrap_map, image_cache, + cycles_material_wrap_map, image_cache, + ignore_leaf_bones, automatic_bone_orientation, bone_correction_matrix, ) # #### And now, the "real" data. @@ -1688,174 +2122,170 @@ def load(operator, context, filepath="", def connection_filter_reverse(fbx_uuid, fbx_id): return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse) - # Armatures pre-processing! - fbx_objects_ignore = set() - fbx_objects_parent_ignore = set() - # Arg! In some case, root bone is used as armature as well, in Blender we have to 'insert' - # an armature object between them, so to handle possible parents of root bones we need a mapping - # from root bone uuid to Blender's object... - fbx_bones_to_fake_object = dict() - armatures = [] + # -- temporary helper hierarchy to build armatures and objects from + fbx_helper_nodes = {} # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes. def _(): - nonlocal fbx_objects_ignore, fbx_objects_parent_ignore + from mathutils import Matrix + + # We build an intermediate hierarchy + # - used to calculate and store bone orientation correction matrices. The same matrices will be reused for animation. + # - find/insert armature nodes + # - filter leaf bones + + # create scene root + fbx_helper_nodes[0] = root_helper = FbxImportHelperNode(None, None, None, False) + root_helper.is_root = True + + # add fbx nodes + fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode')) for a_uuid, a_item in fbx_table_nodes.items(): - root_bone = False - fbx_adata, bl_adata = a_item = fbx_table_nodes.get(a_uuid, (None, None)) - if fbx_adata is None or fbx_adata.id != b'Model': + fbx_obj, bl_data = a_item = fbx_table_nodes.get(a_uuid, (None, None)) # why this double lookup? + if fbx_obj is None or fbx_obj.id != b'Model': continue - elif fbx_adata.props[2] != b'Null': - if fbx_adata.props[2] not in {b'LimbNode', b'Root'}: - continue - # In some cases, armatures have no root 'Null' object, we have to consider all root bones - # as armatures in this case. :/ - root_bone = True - for p_uuid, p_ctype in fbx_connection_map.get(a_uuid, ()): - if p_ctype.props[0] != b'OO': - continue - fbx_pdata, bl_pdata = p_item = fbx_table_nodes.get(p_uuid, (None, None)) - if (fbx_pdata and fbx_pdata.id == b'Model' and - fbx_pdata.props[2] in {b'LimbNode', b'Root', b'Null'}): - # Not a root bone... - root_bone = False - if not root_bone: + + fbx_props = (elem_find_first(fbx_obj, b'Properties70'), + elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) + assert(fbx_props[0] is not None) + + transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_obj, Matrix()) + is_bone = fbx_obj.props[2] in {b'LimbNode', b'Root'} + fbx_helper_nodes[a_uuid] = FbxImportHelperNode(fbx_obj, bl_data, transform_data, is_bone) + + # add parent-child relations and add blender data to the node + for fbx_link in fbx_connections.elems: + if fbx_link.props[0] != b'OO': + continue + if fbx_link.props_type[1:3] == b'LL': + c_src, c_dst = fbx_link.props[1:3] + parent = fbx_helper_nodes.get(c_dst) + if parent is None: continue - fbx_bones_to_fake_object[a_uuid] = None - - bones = {} - todo_uuids = set() if root_bone else {a_uuid} - init_uuids = {a_uuid} if root_bone else set() - done_uuids = set() - while todo_uuids or init_uuids: - if init_uuids: - p_uuid = None - uuids = [(uuid, None) for uuid in init_uuids] - init_uuids = None - else: - p_uuid = todo_uuids.pop() - uuids = fbx_connection_map_reverse.get(p_uuid, ()) - # bone -> cluster -> skin -> mesh. - # XXX Note: only LimbNode for now (there are also Limb's :/ ). - for b_uuid, b_ctype in uuids: - if b_ctype and b_ctype.props[0] != b'OO': + + child = fbx_helper_nodes.get(c_src) + if child is None: + # add blender data (meshes, lights, cameras, etc.) to a helper node + fbx_sdata, bl_data = p_item = fbx_table_nodes.get(c_src, (None, None)) + if fbx_sdata is None: continue - fbx_bdata, bl_bdata = b_item = fbx_table_nodes.get(b_uuid, (None, None)) - if (fbx_bdata is None or fbx_bdata.id != b'Model' or - fbx_bdata.props[2] not in {b'LimbNode', b'Root'}): + if fbx_sdata.id in {b'Material', b'Texture', b'Video'}: continue + parent.bl_data = bl_data + else: + # set parent + child.parent = parent - # Find bone's size. - size = 1.0 - for t_uuid, t_ctype in fbx_connection_map_reverse.get(b_uuid, ()): - if t_ctype.props[0] != b'OO': - continue - fbx_tdata, _bl_tdata = fbx_table_nodes.get(t_uuid, (None, None)) - if fbx_tdata is None or fbx_tdata.id != b'NodeAttribute' or fbx_tdata.props[2] != b'LimbNode': - continue - fbx_props = (elem_find_first(fbx_tdata, b'Properties70'),) - if fbx_props[0] is not None: # Some bones have no Properties70 at all... - size = elem_props_get_number(fbx_props, b'Size', default=size) - break # Only one bone data per bone! - - clusters = [] - for c_uuid, c_ctype in fbx_connection_map.get(b_uuid, ()): - if c_ctype.props[0] != b'OO': - continue - fbx_cdata, _bl_cdata = fbx_table_nodes.get(c_uuid, (None, None)) - if fbx_cdata is None or fbx_cdata.id != b'Deformer' or fbx_cdata.props[2] != b'Cluster': - continue - meshes = set() - objects = [] - for s_uuid, s_ctype in fbx_connection_map.get(c_uuid, ()): - if s_ctype.props[0] != b'OO': - continue - fbx_sdata, _bl_sdata = fbx_table_nodes.get(s_uuid, (None, None)) - if fbx_sdata is None or fbx_sdata.id != b'Deformer' or fbx_sdata.props[2] != b'Skin': - continue - for m_uuid, m_ctype in fbx_connection_map.get(s_uuid, ()): - if m_ctype.props[0] != b'OO': - continue - fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None)) - if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh': - continue - # Blenmeshes are assumed already created at that time! - assert(isinstance(bl_mdata, bpy.types.Mesh)) - # And we have to find all objects using this mesh! - for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()): - if o_ctype.props[0] != b'OO': - continue - fbx_odata, bl_odata = o_item = fbx_table_nodes.get(o_uuid, (None, None)) - if fbx_odata is None or fbx_odata.id != b'Model' or fbx_odata.props[2] != b'Mesh': - continue - # bl_odata is still None, objects have not yet been created... - objects.append(o_item) - meshes.add(bl_mdata) - # Skin deformers are only here to connect clusters to meshes, for us, nothing else to do. - clusters.append((fbx_cdata, meshes, objects)) - # For now, we assume there is only one cluster & skin per bone (at least for a given armature)! - # XXX This is not true, some apps export several clusters (kind of LoD), we only use first one! - # assert(len(clusters) <= 1) - bones[b_uuid] = (b_item, size, p_uuid if (p_uuid != a_uuid or root_bone) else None, clusters) - fbx_objects_parent_ignore.add(b_uuid) - done_uuids.add(p_uuid) - todo_uuids.add(b_uuid) - if bones: - # in case we have no Null parent, rootbone will be a_item too... - armatures.append((a_item, bones)) - fbx_objects_ignore.add(a_uuid) - fbx_objects_ignore |= fbx_objects_parent_ignore - # We need to handle parenting at object-level for rootbones-as-armature case :/ - fbx_objects_parent_ignore -= set(fbx_bones_to_fake_object.keys()) - _(); del _ + # find armatures (either an empty below a bone or a new node inserted at the bone + root_helper.find_armatures() - def _(): - fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode')) + # mark nodes that have bone children + root_helper.find_bone_children() - # Link objects, keep first, this also creates objects - for fbx_uuid, fbx_item in fbx_table_nodes.items(): - if fbx_uuid in fbx_objects_ignore: - # armatures and bones, handled separately. + # mark nodes that need a bone to attach child-bones to + root_helper.find_fake_bones() + + # mark leaf nodes that are only required to mark the end of their parent bone + if settings.ignore_leaf_bones: + root_helper.mark_leaf_bones() + + # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well, and you can have several clusters per bone! + # Maybe some conversion can be applied to put them all into the same frame of reference? + + # get the bind pose from pose elements + for a_uuid, a_item in fbx_table_nodes.items(): + fbx_obj, bl_data = a_item = fbx_table_nodes.get(a_uuid, (None, None)) # why this double lookup? + if fbx_obj is None: continue - fbx_obj, blen_data = fbx_item - if fbx_obj.id != b'Model' or fbx_obj.props[2] in {b'Root', b'LimbNode'}: + if fbx_obj.id != b'Pose': continue + if fbx_obj.props[2] != b'BindPose': + continue + for fbx_pose_node in fbx_obj.elems: + if fbx_pose_node.id != b'PoseNode': + continue + node_elem = elem_find_first(fbx_pose_node, b'Node') + node = elem_uuid(node_elem) + matrix_elem = elem_find_first(fbx_pose_node, b'Matrix') + matrix = array_to_matrix4(matrix_elem.props[0]) if matrix_elem else None + bone = fbx_helper_nodes.get(node) + if bone and matrix: + # Store the matrix in the helper node. + # There may be several bind pose matrices for the same node, but in my tests they seem to be identical. + bone.bind_matrix = matrix # global space + + # get clusters and bind pose + for helper_uuid, helper_node in fbx_helper_nodes.items(): + if not helper_node.is_bone: + continue + for cluster_uuid, cluster_link in fbx_connection_map.get(helper_uuid, ()): + if cluster_link.props[0] != b'OO': + continue + fbx_cluster, _ = fbx_table_nodes.get(cluster_uuid, (None, None)) + if fbx_cluster is None or fbx_cluster.id != b'Deformer' or fbx_cluster.props[2] != b'Cluster': + continue - # Create empty object or search for object data - if fbx_obj.props[2] == b'Null': - fbx_lnk_item = None - ok = True - else: - ok = False - for (fbx_lnk, - fbx_lnk_item, - fbx_lnk_type) in connection_filter_reverse(fbx_uuid, None): + # Get the bind pose from the cluster: + transform_elem = elem_find_first(fbx_cluster, b'Transform', default=None) + transform = array_to_matrix4(transform_elem.props[0]) if transform_elem else Matrix() - if fbx_lnk_type.props[0] != b'OO': - continue - if not isinstance(fbx_lnk_item, bpy.types.ID): - continue - if isinstance(fbx_lnk_item, (bpy.types.Material, bpy.types.Image)): + transform_link_elem = elem_find_first(fbx_cluster, b'TransformLink', default=None) + transform_link = array_to_matrix4(transform_link_elem.props[0]) if transform_link_elem else None + + transform_associate_model_elem = elem_find_first(fbx_cluster, b'TransformAssociateModel', default=None) + transform_associate_model = array_to_matrix4(transform_associate_model_elem.props[0]) if transform_associate_model_elem else Matrix() + + mesh_matrix = transform + armature_matrix = transform_associate_model + + if transform_link: + mesh_matrix = transform_link * mesh_matrix + helper_node.bind_matrix = transform_link # overwrite the bind matrix + + # Get the meshes driven by this cluster: (Shouldn't that be only one?) + meshes = set() + for skin_uuid, skin_link in fbx_connection_map.get(cluster_uuid): + if skin_link.props[0] != b'OO': continue - # Need to check why this happens, Bird_Leg.fbx - # This is basic object parenting, also used by "bones". - if isinstance(fbx_lnk_item, (bpy.types.Object)): + fbx_skin, _ = fbx_table_nodes.get(skin_uuid, (None, None)) + if fbx_skin is None or fbx_skin.id != b'Deformer' or fbx_skin.props[2] != b'Skin': continue - ok = True - break - if ok: - # create when linking since we need object data - obj = blen_read_object(fbx_tmpl, fbx_obj, fbx_lnk_item, settings) - assert(fbx_item[1] is None) - fbx_item[1] = obj - - # instance in scene - obj_base = scene.objects.link(obj) - obj_base.select = True - _(); del _ + for mesh_uuid, mesh_link in fbx_connection_map.get(skin_uuid): + if mesh_link.props[0] != b'OO': + continue + fbx_mesh, _ = fbx_table_nodes.get(mesh_uuid, (None, None)) + if fbx_mesh is None or fbx_mesh.id != b'Geometry' or fbx_mesh.props[2] != b'Mesh': + continue + for object_uuid, object_link in fbx_connection_map.get(mesh_uuid): + if object_link.props[0] != b'OO': + continue + mesh_node = fbx_helper_nodes[object_uuid] + if mesh_node: + # ---- + # If we get a valid mesh matrix (in bone space), store armature and mesh global matrices, we need to set temporarily + # both objects to those matrices when actually binding them via the modifier. + # Note we assume all bones were bound with the same mesh/armature (global) matrix, we do not support otherwise + # in Blender anyway! + mesh_node.armature_setup = (mesh_matrix, armature_matrix) + meshes.add(mesh_node) + + helper_node.clusters.append((fbx_cluster, meshes)) - # Now that we have objects... + # convert bind poses from global space into local space + root_helper.make_bind_pose_local() - # I) We can handle shapes. + # collect armature meshes + root_helper.collect_armature_meshes() + + # find the correction matrices to align FBX objects with their Blender equivalent + root_helper.find_correction_matrix(settings) + + # build the Object/Armature/Bone hierarchy + root_helper.build_hierarchy(fbx_tmpl, settings, scene) + + # root_helper.print_info(0) + _(); del _ + + # We can handle shapes. blend_shape_channels = {} # We do not need Shapes themselves, but keyblocks, for anim. def _(): @@ -1894,87 +2324,17 @@ def load(operator, context, filepath="", for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()): if o_ctype.props[0] != b'OO': continue - fbx_odata, bl_odata = o_item = fbx_table_nodes.get(o_uuid, (None, None)) - if fbx_odata is None or fbx_odata.id != b'Model' or fbx_odata.props[2] != b'Mesh': - continue - # bl_odata is still None, objects have not yet been created... - objects.append(o_item) + node = fbx_helper_nodes[o_uuid] + if node: + objects.append(node) meshes.append((bl_mdata, objects)) # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do. # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation. - keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene, settings) + keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene) blend_shape_channels[bc_uuid] = keyblocks _(); del _ - # II) We can finish armatures processing. - arm_parents = set() - force_global_objects = set() - - def _(): - fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode')) - - blen_read_armatures(fbx_tmpl, armatures, fbx_bones_to_fake_object, scene, arm_parents, settings) - _(); del _ - - def _(): - from bpy.types import PoseBone - - # Parent objects, after we created them... - for fbx_uuid, fbx_item in fbx_table_nodes.items(): - if fbx_uuid in fbx_objects_parent_ignore: - # Ignore bones, but not armatures here! - continue - fbx_obj, blen_data = fbx_item - if fbx_obj.id != b'Model': - continue - # Handle rootbone-as-armature case :/ - t_data = fbx_bones_to_fake_object.get(fbx_uuid) - if t_data is not None: - blen_data = t_data - elif blen_data is None: - continue # no object loaded.. ignore - - for (fbx_lnk, - fbx_lnk_item, - fbx_lnk_type) in connection_filter_forward(fbx_uuid, b'Model'): - - if isinstance(fbx_lnk_item, PoseBone): - blen_data.parent = fbx_lnk_item.id_data # get the armature the bone belongs to - blen_data.parent_bone = fbx_lnk_item.name - blen_data.parent_type = 'BONE' - else: - blen_data.parent = fbx_lnk_item - _(); del _ - - def _(): - if global_matrix is not None: - # Apply global matrix last (after parenting) - for fbx_uuid, fbx_item in fbx_table_nodes.items(): - if fbx_uuid in fbx_objects_parent_ignore: - # Ignore bones, but not armatures here! - continue - fbx_obj, blen_data = fbx_item - if fbx_obj.id != b'Model': - continue - # Handle rootbone-as-armature case :/ - t_data = fbx_bones_to_fake_object.get(fbx_uuid) - if t_data is not None: - blen_data = t_data - elif blen_data is None: - continue # no object loaded.. ignore - - if blen_data.parent is None: - blen_data.matrix_basis = global_matrix * blen_data.matrix_basis - - for (ob_arm, ob_me) in arm_parents: - # Rigged meshes are in global space in FBX... - ob_me.matrix_basis = global_matrix * ob_me.matrix_basis - # And reverse-apply armature transform, so that it gets valid parented (local) position! - ob_me.matrix_parent_inverse = ob_arm.matrix_basis.inverted_safe() - force_global_objects.add(ob_me) - _(); del _ - # Animation! def _(): fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack')) @@ -2020,7 +2380,7 @@ def load(operator, context, filepath="", lnk_prop = n_ctype.props[3] if lnk_prop in {b'Lcl Translation', b'Lcl Rotation', b'Lcl Scaling'}: # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712. - ob = fbx_table_nodes.get(n_uuid, (None, None))[1] + ob = fbx_helper_nodes.get(n_uuid, None) if ob is None: continue items.append((ob, lnk_prop)) @@ -2062,7 +2422,7 @@ def load(operator, context, filepath="", curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel) # And now that we have sorted all this, apply animations! - blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, force_global_objects, settings) + blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene) _(); del _