diff options
-rw-r--r-- | io_scene_fbx/__init__.py | 17 | ||||
-rw-r--r-- | io_scene_fbx/export_fbx_bin.py | 64 | ||||
-rw-r--r-- | io_scene_fbx/fbx_utils.py | 86 | ||||
-rw-r--r-- | io_scene_fbx/import_fbx.py | 11 |
4 files changed, 120 insertions, 58 deletions
diff --git a/io_scene_fbx/__init__.py b/io_scene_fbx/__init__.py index 9ccc5b7f..7e77ec69 100644 --- a/io_scene_fbx/__init__.py +++ b/io_scene_fbx/__init__.py @@ -342,6 +342,17 @@ class ExportFBX(bpy.types.Operator, ExportHelper, IOFBXOrientationHelper): description="Export custom properties", default=False, ) + + use_armature_as_root = BoolProperty( + name="Export Armature As Root", + description="Export armature object itself as an 'empty' FBX object, parent of all bones", + default=True, + ) + use_armature_deform_only = BoolProperty( + name="Only Deform Bones", + description="Only write deforming bones (and non-deforming ones when they have deforming children)", + default=False, + ) add_leaf_bones = BoolProperty( name="Add Leaf Bones", description="Append a final bone to the end of each chain to specify last bone length " @@ -370,11 +381,6 @@ class ExportFBX(bpy.types.Operator, ExportHelper, IOFBXOrientationHelper): ), 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)", - default=False, - ) # Anim - 7.4 bake_anim = BoolProperty( name="Baked Animation", @@ -513,6 +519,7 @@ class ExportFBX(bpy.types.Operator, ExportHelper, IOFBXOrientationHelper): #~ sub.enabled = self.mesh_smooth_type in {'OFF'} sub.prop(self, "use_tspace") elif self.ui_tab == 'ARMATURE': + layout.prop(self, "use_armature_as_root") layout.prop(self, "use_armature_deform_only") layout.prop(self, "add_leaf_bones") layout.prop(self, "primary_bone_axis") diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index d3aeee76..7247caea 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -719,7 +719,8 @@ def fbx_data_bindpose_element(root, me_obj, me, scene_data, arm_obj=None, mat_wo elem_data_single_string(fbx_pose, b"Type", b"BindPose") elem_data_single_int32(fbx_pose, b"Version", FBX_POSE_BIND_VERSION) - elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + (1 if (arm_obj != me_obj) else 0) + len(bones)) + elem_data_single_int32(fbx_pose, b"NbPoseNodes", + 1 + (1 if (arm_obj != me_obj and mat_world_arm) else 0) + len(bones)) # First node is mesh/object. mat_world_obj = me_obj.fbx_object_matrix(scene_data, global_space=True) @@ -727,7 +728,7 @@ def fbx_data_bindpose_element(root, me_obj, me, scene_data, arm_obj=None, mat_wo elem_data_single_int64(fbx_posenode, b"Node", me_obj.fbx_uuid) elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_obj)) # Second node is armature object itself. - if arm_obj != me_obj: + if arm_obj != me_obj and mat_world_arm: fbx_posenode = elem_empty(fbx_pose, b"PoseNode") elem_data_single_int64(fbx_posenode, b"Node", arm_obj.fbx_uuid) elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_arm)) @@ -1405,10 +1406,10 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): * Deformers (i.e. Skin), bind between an armature and a mesh. ** SubDeformers (i.e. Cluster), one per bone/vgroup pair. * BindPose. - Note armature itself has no data, it is a mere "Null" Model... + Note armature itself has no data, it is a mere "Null" Model (when exported)... """ - 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) + mat_world_arm = arm_obj.fbx_object_matrix(scene_data, global_space=True) if not arm_obj.skip_export else None + bones = tuple(bo_obj for bo_obj in arm_obj.bones if bo_obj in scene_data.objects and not bo_obj.skip_export) bone_radius_scale = 33.0 @@ -1494,7 +1495,8 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): elem_data_single_float64_array(fbx_clstr, b"Transform", matrix4_to_array(mat_world_bones[bo_obj].inverted_safe() * mat_world_obj)) elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix4_to_array(mat_world_bones[bo_obj])) - elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm)) + if mat_world_arm: + elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm)) def fbx_data_leaf_bone_elements(root, scene_data): @@ -1758,8 +1760,11 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes, Also supports "parent to bone" (simple parent to Model/LimbNode). arm_parents is a set of tuples (armature, object) for all successful armature bindings. """ - # We need some data for our armature 'object' too!!! - data_empties[arm_obj] = get_blender_empty_key(arm_obj.bdata) + if (settings.use_armature_as_root): + # We need some data for our armature 'object' too!!! + data_empties[arm_obj] = get_blender_empty_key(arm_obj.bdata) + else: + arm_obj.skip_export = True arm_data = arm_obj.bdata.data bones = OrderedDict() @@ -1817,11 +1822,14 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes, def fbx_generate_leaf_bones(settings, data_bones): - # find which bons have no children + # find which bones 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 + if bo.skip_export: + continue + bo_par = bo.fbx_parent + if bo_par and bo_par.is_bone: + child_count[bo_par] += 1 bone_radius_scale = settings.global_scale * 33.0 @@ -2345,7 +2353,8 @@ def fbx_data_from_scene(scene, settings): templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras)) if data_bones: - templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones)) + nbr_users = sum(1 for db in data_bones if not db.skip_export) + templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=nbr_users) if data_meshes: nbr = len({me_key for me_key, _me, _free in data_meshes.values()}) @@ -2354,7 +2363,8 @@ def fbx_data_from_scene(scene, settings): templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr) if objects: - templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects)) + nbr_users = sum(1 for ob in objects if not ob.skip_export) + templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=nbr_users) if arm_parents: # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures @@ -2417,22 +2427,25 @@ def fbx_data_from_scene(scene, settings): for ob_obj in objects: # Bones are handled later. if not ob_obj.is_bone: - par_obj = ob_obj.parent + par_obj = ob_obj.fbx_parent # Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0). - if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents: + if par_obj and ob_obj.has_valid_fbx_parent(objects) and (par_obj, ob_obj) not in arm_parents: connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None)) else: connections.append((b"OO", ob_obj.fbx_uuid, 0, None)) # Armature & Bone chains. for bo_obj in data_bones.keys(): - par_obj = bo_obj.parent - if par_obj not in objects: - continue - connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None)) + par_obj = bo_obj.fbx_parent + if par_obj is None: + connections.append((b"OO", bo_obj.fbx_uuid, 0, None)) + elif par_obj in objects: + connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None)) # Object data. for ob_obj in objects: + if ob_obj.skip_export: + continue if ob_obj.is_bone: bo_data_key = data_bones[ob_obj] connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None)) @@ -2767,7 +2780,7 @@ def fbx_objects_elements(root, scene_data): perfmon.step("FBX export fetch objects (%d)..." % len(scene_data.objects)) for ob_obj in scene_data.objects: - if ob_obj.is_dupli: + if ob_obj.is_dupli or ob_obj.skip_export: continue fbx_data_object_elements(objects, ob_obj, scene_data) ob_obj.dupli_list_create(scene_data.scene, 'RENDER') @@ -2840,6 +2853,7 @@ def fbx_takes_elements(root, scene_data): # ##### "Main" functions. ##### # This func can be called with just the filepath +# Warning! Arg order is not guaranteed, only use keyargs! def save_single(operator, scene, filepath="", global_matrix=Matrix(), apply_unit_scale=False, @@ -2849,7 +2863,11 @@ def save_single(operator, scene, filepath="", object_types=None, use_mesh_modifiers=True, mesh_smooth_type='FACE', + use_armature_as_root=True, use_armature_deform_only=False, + add_leaf_bones=False, + primary_bone_axis='Y', + secondary_bone_axis='X', bake_anim=True, bake_anim_use_all_bones=True, bake_anim_use_nla_strips=True, @@ -2857,9 +2875,6 @@ def save_single(operator, scene, filepath="", bake_anim_step=1.0, bake_anim_simplify_factor=1.0, bake_anim_force_startend_keying=True, - add_leaf_bones=False, - primary_bone_axis='Y', - secondary_bone_axis='X', use_metadata=True, path_mode='AUTO', use_mesh_edges=True, @@ -2919,7 +2934,8 @@ def save_single(operator, scene, filepath="", 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, add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv, + use_armature_as_root, use_armature_deform_only, add_leaf_bones, + bone_correction_matrix, bone_correction_matrix_inv, bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions, bake_anim_step, bake_anim_simplify_factor, bake_anim_force_startend_keying, False, media_settings, use_custom_props, diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index f0501792..35a1b6f9 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -880,7 +880,7 @@ class MetaObjectWrapper(type): if instance is not None: # 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). + # other data is supposed valid during whole cache life, so we can skip resetting it). instance._dupli_matrix = dup_mat return instance @@ -902,7 +902,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): we need to use a key to identify each. """ __slots__ = ( - 'name', 'key', 'bdata', 'parented_to_armature', + 'name', 'key', 'bdata', 'parented_to_armature', 'skip_export', 'is_root', '_tag', '_ref', '_dupli_matrix' ) @@ -939,6 +939,8 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): self.bdata = bdata self._ref = armature self.parented_to_armature = False + self.skip_export = False + self.is_root = False def __eq__(self, other): return isinstance(other, self.__class__) and self.key == other.key @@ -974,16 +976,29 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref) parent = property(get_parent) + @property + def fbx_parent(self): + par = self.parent + while par and par.skip_export: + par = par.parent + return par + 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_safe() * 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_safe() if par else Matrix() - return par_mat_inv * self._ref.pose.bones[self.bdata.name].matrix + par = self.parent + if par and par.skip_export: + par = self.fbx_parent + par_mat_inv = par.matrix_global.inverted_safe() if par else Matrix() + return par_mat_inv * self.matrix_global + else: + if self._tag == 'OB': + return self.bdata.matrix_local.copy() + elif self._tag == 'DP': + return self._ref.matrix_world.inverted_safe() * 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_safe() 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): @@ -996,13 +1011,19 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): 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_safe() if par else Matrix() - return par_mat_inv * self.bdata.matrix_local + par = self.parent + if par and par.skip_export: + par = self.fbx_parent + par_mat_inv = par.matrix_rest_global.inverted_safe() if par else Matrix() + return par_mat_inv * self.matrix_rest_global else: - return self.matrix_local.copy() + 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_safe() 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): @@ -1013,8 +1034,8 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): matrix_rest_global = property(get_matrix_rest_global) # #### Transform and helpers - def has_valid_parent(self, objects): - par = self.parent + def has_valid_fbx_parent(self, objects): + par = self.fbx_parent if par in objects: if self._tag == 'OB': par_type = self.bdata.parent_type @@ -1029,7 +1050,9 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): def use_bake_space_transform(self, scene_data): # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like... # TODO: Check whether this can work for bones too... - return (scene_data.settings.bake_space_transform and self._tag in {'OB', 'DP'} and + return (scene_data.settings.bake_space_transform and + self._tag in {'OB', 'DP'} and + not self.skip_export and self.bdata.type in BLENDER_OBJECT_TYPES_MESHLIKE | {'EMPTY'}) def fbx_object_matrix(self, scene_data, rest=False, local_space=False, global_space=False): @@ -1046,7 +1069,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): # 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)))) + (global_space or not (self._tag == 'DP' or self.has_valid_fbx_parent(scene_data.objects)))) # Objects (meshes!) parented to armature are not parented to anything in FBX, hence we need them # in global space, which is their 'virtual' local space... @@ -1054,7 +1077,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): # Since we have to apply corrections to some types of object, we always need local Blender space here... matrix = self.matrix_rest_local if rest else self.matrix_local - parent = self.parent + parent = self.fbx_parent # Bones, lamps and cameras need to be rotated (in local space!). if self._tag == 'BO': @@ -1069,8 +1092,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): elif self.bdata.type == 'CAMERA': matrix = matrix * MAT_CONVERT_CAMERA - if self._tag in {'DP', 'OB'} and parent: - if parent._tag == 'BO': + if self._tag in {'DP', 'OB'} and parent and parent.is_bone: # In bone parent case, we get transformation in **bone tip** space (sigh). # Have to bring it back into bone root, which is FBX expected value. matrix = Matrix.Translation((0, (parent.bdata.tail - parent.bdata.head).length, 0)) * matrix @@ -1080,7 +1102,7 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): if is_global: # Move matrix to global Blender space. matrix = (parent.matrix_rest_global if rest else parent.matrix_global) * matrix - elif parent.use_bake_space_transform(scene_data): + elif parent and 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_rest_local if rest else parent.matrix_local) * matrix @@ -1095,6 +1117,17 @@ class ObjectWrapper(metaclass=MetaObjectWrapper): 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 + elif 0: #parent: + par = parent + while par and par.skip_export: + print(matrix) + # Since this parent will not be exported, we have to apply its transform to its children... + par_mat = par.fbx_object_matrix(scene_data, rest=rest) + print(par_mat) + matrix = par_mat * matrix + print(matrix) + print("\n\n") + par = par.parent return matrix @@ -1192,7 +1225,8 @@ FBXExportSettings = namedtuple("FBXExportSettings", ( "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", "add_leaf_bones", "bone_correction_matrix", "bone_correction_matrix_inv", + "use_armature_as_root", "use_armature_deform_only", "add_leaf_bones", + "bone_correction_matrix", "bone_correction_matrix_inv", "bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions", "bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying", "use_metadata", "media_settings", "use_custom_props", diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index b8429071..244bc746 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -1661,7 +1661,7 @@ class FbxImportHelperNode: child.armature = armature child.find_armature_bones(armature) - def find_armatures(self): + def find_armatures(self, settings): needs_armature = False for child in self.children: if child.is_bone: @@ -1678,6 +1678,11 @@ class FbxImportHelperNode: armature.fbx_name = "Armature" armature.is_armature = True + # if our fake armature is a non-parented object, we need to set a transform + # that will result in NOP once applied all corrections... + if self.is_root: + armature.matrix = settings.global_matrix_inv + for child in self.children: if child.is_bone: child.parent = armature @@ -1689,7 +1694,7 @@ class FbxImportHelperNode: for child in self.children: if child.is_armature or child.is_bone: continue - child.find_armatures() + child.find_armatures(settings) def find_bone_children(self): has_bone_children = False @@ -2563,7 +2568,7 @@ def load(operator, context, filepath="", child.parent = parent # find armatures (either an empty below a bone or a new node inserted at the bone - root_helper.find_armatures() + root_helper.find_armatures(settings) # mark nodes that have bone children root_helper.find_bone_children() |