diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index fa681f8bbcf94c88d5f87c4b9ae497374aa402a7..d26d26657cba53c7d2d93f97220cbcf82e914892 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -270,9 +270,9 @@ from collections import namedtuple FBXTransformData = namedtuple("FBXTransformData", ( - "loc", - "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", - "sca", "sca_ofs", "sca_piv", + "loc", "geom_loc", + "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot", + "sca", "sca_ofs", "sca_piv", "geom_sca", )) @@ -334,14 +334,57 @@ def blen_read_custom_properties(fbx_obj, blen_obj, settings): def blen_read_object_transform_do(transform_data): + # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple: + # + # WorldTransform = ParentWorldTransform * T * Roff * Rp * Rpre * R * Rpost * Rp-1 * Soff * Sp * S * Sp-1 + # + # Where all those terms are 4 x 4 matrices that contain: + # WorldTransform: Transformation matrix of the node in global space. + # ParentWorldTransform: Transformation matrix of the parent node in global space. + # T: Translation + # Roff: Rotation offset + # Rp: Rotation pivot + # Rpre: Pre-rotation + # R: Rotation + # Rpost: Post-rotation + # Rp-1: Inverse of the rotation pivot + # Soff: Scaling offset + # Sp: Scaling pivot + # S: Scaling + # Sp-1: Inverse of the scaling pivot + # + # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to + # support 3DSMax way: + # + # WorldTransform = ParentWorldTransform * T * R * S * OT * OR * OS + # + # Where all those terms are 4 x 4 matrices that contain: + # WorldTransform: Transformation matrix of the node in global space + # ParentWorldTransform: Transformation matrix of the parent node in global space + # T: Translation + # R: Rotation + # S: Scaling + # OT: Geometric transform translation + # OR: Geometric transform rotation + # OS: Geometric transform translation + # + # Notes: + # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS + # of WorldTransform's parent node. + # + # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/ + # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429 + # translation lcl_translation = Matrix.Translation(transform_data.loc) + geom_loc = Matrix.Translation(transform_data.geom_loc) # rotation to_rot = lambda rot, rot_ord: Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4() lcl_rot = to_rot(transform_data.rot, transform_data.rot_ord) * transform_data.rot_alt_mat pre_rot = to_rot(transform_data.pre_rot, transform_data.rot_ord) pst_rot = to_rot(transform_data.pst_rot, transform_data.rot_ord) + geom_rot = to_rot(transform_data.geom_rot, transform_data.rot_ord) rot_ofs = Matrix.Translation(transform_data.rot_ofs) rot_piv = Matrix.Translation(transform_data.rot_piv) @@ -351,8 +394,10 @@ def blen_read_object_transform_do(transform_data): # scale lcl_scale = Matrix() lcl_scale[0][0], lcl_scale[1][1], lcl_scale[2][2] = transform_data.sca + geom_scale = Matrix(); + geom_scale[0][0], geom_scale[1][1], geom_scale[2][2] = transform_data.geom_sca - return ( + base_mat = ( lcl_translation * rot_ofs * rot_piv * @@ -365,6 +410,9 @@ def blen_read_object_transform_do(transform_data): lcl_scale * sca_piv.inverted_safe() ) + geom_mat = geom_loc * geom_rot * geom_scale + # We return mat without 'geometric transforms' too, because it is to be used for children, sigh... + return (base_mat * geom_mat, base_mat, geom_mat) # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become @@ -390,6 +438,10 @@ def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_p rot = list(elem_props_get_vector_3d(fbx_props, b'Lcl Rotation', const_vector_zero_3d)) sca = list(elem_props_get_vector_3d(fbx_props, b'Lcl Scaling', const_vector_one_3d)) + geom_loc = list(elem_props_get_vector_3d(fbx_props, b'GeometricTranslation', const_vector_zero_3d)) + geom_rot = list(elem_props_get_vector_3d(fbx_props, b'GeometricRotation', const_vector_zero_3d)) + geom_sca = list(elem_props_get_vector_3d(fbx_props, b'GeometricScaling', const_vector_one_3d)) + rot_ofs = elem_props_get_vector_3d(fbx_props, b'RotationOffset', const_vector_zero_3d) rot_piv = elem_props_get_vector_3d(fbx_props, b'RotationPivot', const_vector_zero_3d) sca_ofs = elem_props_get_vector_3d(fbx_props, b'ScalingOffset', const_vector_zero_3d) @@ -418,9 +470,9 @@ def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_p pst_rot = const_vector_zero_3d rot_ord = 'XYZ' - return FBXTransformData(loc, - rot, rot_ofs, rot_piv, pre_rot, pst_rot, rot_ord, rot_alt_mat, - sca, sca_ofs, sca_piv) + return FBXTransformData(loc, geom_loc, + rot, rot_ofs, rot_piv, pre_rot, pst_rot, rot_ord, rot_alt_mat, geom_rot, + sca, sca_ofs, sca_piv, geom_sca) # --------- @@ -547,7 +599,7 @@ def blen_read_animations_action_item(action, item, cnodes, fps): transform_data.rot[channel] = v elif fbxprop == b'Lcl Scaling': transform_data.sca[channel] = v - mat = blen_read_object_transform_do(transform_data) + mat, _, _ = blen_read_object_transform_do(transform_data) # compensate for changes in the local matrix during processing if item.anim_compensation_matrix: @@ -1259,9 +1311,10 @@ class FbxImportHelperNode: 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') + __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', 'matrix_as_parent', 'matrix_geom', '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' @@ -1278,7 +1331,10 @@ class FbxImportHelperNode: 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 + if fbx_transform_data: + self.matrix, self.matrix_as_parent, self.matrix_geom = blen_read_object_transform_do(fbx_transform_data) + else: + self.matrix, self.matrix_as_parent, self.matrix_geom = (None, None, 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. @@ -1495,8 +1551,14 @@ class FbxImportHelperNode: for child in self.children: child.find_fake_bones(in_armature) + def get_world_matrix_as_parent(self): + matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix() + if self.matrix_as_parent: + matrix = matrix * self.matrix_as_parent + return matrix + def get_world_matrix(self): - matrix = self.parent.get_world_matrix() if self.parent else Matrix() + matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix() if self.matrix: matrix = matrix * self.matrix return matrix @@ -1794,24 +1856,29 @@ class FbxImportHelperNode: # 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 + (mmat, amat) = mesh.armature_setup + me_obj = mesh.bl_obj # bring global armature & mesh matrices into *Blender* global space. - amat = settings.global_matrix * amat + # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle. + # Among other things, why in hell isn't it taken into account by bindpose & co??? + # Probably because org app (max) handles it completely aside from any parenting stuff, + # which we obviously cannot do in Blender. :/ + amat = settings.global_matrix * (amat if amat is not None else self.bind_matrix) + if self.matrix_geom: + amat = amat * self.matrix_geom mmat = settings.global_matrix * mmat + if mesh.matrix_geom: + mmat = mmat * mesh.matrix_geom - arm.matrix_basis = amat - me_mat_back = mesh.bl_obj.matrix_basis.copy() - mesh.bl_obj.matrix_basis = mmat + # Now that we have armature and mesh in there (global) bind 'state' (matrix), + # we can compute inverse parenting matrix of the mesh. + me_obj.matrix_parent_inverse = amat.inverted_safe() * mmat * me_obj.matrix_basis.inverted_safe() 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: @@ -2242,7 +2309,7 @@ def load(operator, context, filepath="", tx_bone = array_to_matrix4(tx_bone_elem.props[0]) if tx_bone_elem else None tx_arm_elem = elem_find_first(fbx_cluster, b'TransformAssociateModel', default=None) - tx_arm = array_to_matrix4(tx_arm_elem.props[0]) if tx_arm_elem else Matrix() + tx_arm = array_to_matrix4(tx_arm_elem.props[0]) if tx_arm_elem else None mesh_matrix = tx_mesh armature_matrix = tx_arm @@ -2271,10 +2338,11 @@ def load(operator, context, filepath="", 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! + # If we get a valid mesh matrix (in bone space), store armature and + # mesh global matrices, we need them to compute mesh's matrix_parent_inverse + # 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)