def __init__(self): self.transform_anim = TransformAnimation() # to get access to the nif bone in object mode self.name_to_block = {} self.pose_store = {} self.bind_store = {} self.skinned = False self.n_armature = None
class KfImport(NifCommon): def __init__(self, operator, context): NifCommon.__init__(self, operator, context) # Helper systems self.tranform_anim = TransformAnimation() def execute(self): """Main import function.""" try: dirname = os.path.dirname(NifOp.props.filepath) kf_files = [ os.path.join(dirname, file.name) for file in NifOp.props.files if file.name.lower().endswith(".kf") ] b_armature = math.get_armature() if not b_armature: raise NifError( "No armature was found in scene, can not import KF animation!" ) # the axes used for bone correction depend on the armature in our scene math.set_bone_orientation(b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up) # get nif space bind pose of armature here for all anims bind_data = armature.get_bind_data(b_armature) for kf_file in kf_files: kfdata = KFFile.load_kf(kf_file) # use pyffi toaster to scale the tree toaster = pyffi.spells.nif.NifToaster() toaster.scale = NifOp.props.scale_correction pyffi.spells.nif.fix.SpellScale(data=kfdata, toaster=toaster).recurse() # calculate and set frames per second self.tranform_anim.set_frames_per_second(kfdata.roots) for kf_root in kfdata.roots: self.tranform_anim.import_kf_root(kf_root, b_armature, bind_data) except NifError: return {'CANCELLED'} NifLog.info("Finished successfully") return {'FINISHED'}
class KfImport(NifCommon): def __init__(self, operator, context): NifCommon.__init__(self, operator, context) # Helper systems self.transform_anim = TransformAnimation() def execute(self): """Main import function.""" try: dirname = os.path.dirname(NifOp.props.filepath) kf_files = [os.path.join(dirname, file.name) for file in NifOp.props.files if file.name.lower().endswith(".kf")] # if an armature is present, prepare the bones for all actions b_armature = math.get_armature() if b_armature: # the axes used for bone correction depend on the armature in our scene math.set_bone_orientation(b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up) # get nif space bind pose of armature here for all anims self.transform_anim.get_bind_data(b_armature) for kf_file in kf_files: kfdata = KFFile.load_kf(kf_file) self.apply_scale(kfdata, NifOp.props.scale_correction) # calculate and set frames per second self.transform_anim.set_frames_per_second(kfdata.roots) for kf_root in kfdata.roots: self.transform_anim.import_kf_root(kf_root, b_armature) except NifError: return {'CANCELLED'} NifLog.info("Finished successfully") return {'FINISHED'}
def __init__(self): self.transform_anim = TransformAnimation() # this is used to hold lists of bones for each armature during mark_armatures_bones self.dict_armatures = {} # to get access to the nif bone in object mode self.name_to_block = {}
class Armature: def __init__(self): self.transform_anim = TransformAnimation() # this is used to hold lists of bones for each armature during mark_armatures_bones self.dict_armatures = {} # to get access to the nif bone in object mode self.name_to_block = {} def store_pose_matrix(self, n_node, armature_space_pose_store, n_root): # check that n_block is indeed a bone if not self.is_bone(n_node): return None armature_space_pose_store[n_node] = n_node.get_transform(n_root) # move down the hierarchy for n_child in n_node.children: self.store_pose_matrix(n_child, armature_space_pose_store, n_root) def import_pose(self, n_armature): """Ported and adapted from pyffi send_bones_to_bind_position""" armature_space_bind_store = {} armature_space_pose_store = {} # check all bones and bone datas to see if a bind position exists bonelist = [] # store the original pose matrix for all nodes for n_child in n_armature.children: self.store_pose_matrix(n_child, armature_space_pose_store, n_armature) # prioritize geometries that have most nodes in their skin instance for geom in sorted(n_armature.get_skinned_geometries(), key=lambda g: g.skin_instance.num_bones, reverse=True): skininst = geom.skin_instance skindata = skininst.data for bonenode, bonedata in zip(skininst.bones, skindata.bone_list): # bonenode can be None; see pyffi issue #3114079 if not bonenode: continue # make sure all bone data of shared bones coincides for othergeom, otherbonenode, otherbonedata in bonelist: if bonenode is otherbonenode: diff = ((otherbonedata.get_transform().get_inverse(fast=False) * othergeom.get_transform(n_armature)) - (bonedata.get_transform().get_inverse(fast=False) * geom.get_transform(n_armature))) if diff.sup_norm() > 1e-3: NifLog.debug( f"Geometries {geom.name} and {othergeom.name} do not share the same bind position." f"bone {bonenode.name} will be sent to a position matching only one of these") # break the loop break else: # the loop did not break, so the bone was not yet added # add it now NifLog.debug(f"Found bind position data for {bonenode.name}") bonelist.append((geom, bonenode, bonedata)) # get the bind pose from the skin data # NiSkinData stores the inverse bind (=rest) pose for each bone, in armature space for geom, bonenode, bonedata in bonelist: n_bind = (bonedata.get_transform().get_inverse(fast=False) * geom.get_transform(n_armature)) armature_space_bind_store[bonenode] = n_bind NifLog.debug("Storing non-skeletal bone poses") self.fix_pose(n_armature, n_armature, armature_space_bind_store, armature_space_pose_store) return armature_space_bind_store, armature_space_pose_store def fix_pose(self, n_armature, n_node, armature_space_bind_store, armature_space_pose_store): """reposition non-skeletal bones to maintain their local orientation to their skeletal parents""" for n_child_node in n_node.children: # only process nodes if not isinstance(n_child_node, NifFormat.NiNode): continue if n_child_node not in armature_space_bind_store and n_child_node in armature_space_pose_store: NifLog.debug(f"Calculating bind pose for non-skeletal bone {n_child_node.name}") # get matrices for n_node (the parent) - fallback to getter if it is not in the store n_armature_pose = armature_space_pose_store.get(n_node, n_node.get_transform(n_armature)) # get bind of parent node or pose if it has no bind pose n_armature_bind = armature_space_bind_store.get(n_node, n_armature_pose) # the child must have a pose, no need for a fallback n_child_armature_pose = armature_space_pose_store[n_child_node] # get the relative transform of n_child_node from pose * inverted parent pose n_child_local_pose = n_child_armature_pose * n_armature_pose.get_inverse(fast=False) # get object space transform by multiplying with bind of parent bone armature_space_bind_store[n_child_node] = n_child_local_pose * n_armature_bind self.fix_pose(n_armature, n_child_node, armature_space_bind_store, armature_space_pose_store) def import_armature(self, n_armature): """Scans an armature hierarchy, and returns a whole armature. This is done outside the normal node tree scan to allow for positioning of the bones before skins are attached.""" armature_name = block_store.import_name(n_armature) b_armature_data = bpy.data.armatures.new(armature_name) b_armature_data.display_type = 'STICK' # use heuristics to determine a suitable orientation forward, up = self.guess_orientation(n_armature) # pass them to the matrix utility math.set_bone_orientation(forward, up) # store axis orientation for export b_armature_data.niftools.axis_forward = forward b_armature_data.niftools.axis_up = up b_armature_obj = Object.create_b_obj(n_armature, b_armature_data) b_armature_obj.show_in_front = True armature_space_bind_store, armature_space_pose_store = self.import_pose(n_armature) # make armature editable and create bones bpy.ops.object.mode_set(mode='EDIT', toggle=False) for n_child in n_armature.children: self.import_bone_bind(n_child, armature_space_bind_store, b_armature_data, n_armature) self.fix_bone_lengths(b_armature_data) bpy.ops.object.mode_set(mode='OBJECT', toggle=False) # The armature has been created in editmode, # now we are ready to set the bone keyframes and store the bones' long names. if NifOp.props.animation: self.transform_anim.create_action(b_armature_obj, armature_name + "-Anim") for bone_name, b_bone in b_armature_obj.data.bones.items(): n_block = self.name_to_block[bone_name] # the property is only available from object mode! block_store.store_longname(b_bone, n_block.name.decode()) if NifOp.props.animation: self.transform_anim.import_transforms(n_block, b_armature_obj, bone_name) # import pose for b_name, n_block in self.name_to_block.items(): n_pose = armature_space_pose_store[n_block] b_pose_bone = b_armature_obj.pose.bones[b_name] n_bind = mathutils.Matrix(n_pose.as_list()).transposed() b_pose_bone.matrix = math.nif_bind_to_blender_bind(n_bind) # force update is required to ensure the transforms are set properly in blender bpy.context.view_layer.update() return b_armature_obj def import_bone_bind(self, n_block, n_bind_store, b_armature_data, n_armature, b_parent_bone=None): """Adds a bone to the armature in edit mode.""" # check that n_block is indeed a bone if not self.is_bone(n_block): return None # bone name bone_name = block_store.import_name(n_block) # create a new bone b_edit_bone = b_armature_data.edit_bones.new(bone_name) # store nif block for access from object mode self.name_to_block[b_edit_bone.name] = n_block # get the nif bone's armature space matrix (under the hood all bone space matrixes are multiplied together) n_bind = mathutils.Matrix(n_bind_store.get(n_block, NifFormat.Matrix44()).as_list()).transposed() # get transformation in blender's coordinate space b_bind = math.nif_bind_to_blender_bind(n_bind) # the following is a workaround because blender can no longer set matrices to bones directly tail, roll = bpy.types.Bone.AxisRollFromMatrix(b_bind.to_3x3()) b_edit_bone.head = b_bind.to_translation() b_edit_bone.tail = tail + b_edit_bone.head b_edit_bone.roll = roll # link to parent if b_parent_bone: b_edit_bone.parent = b_parent_bone # import and parent bone children for n_child in n_block.children: self.import_bone_bind(n_child, n_bind_store, b_armature_data, n_armature, b_edit_bone) def guess_orientation(self, n_armature): """Analyze all bones' translations to see what the nif considers the 'forward' axis""" axis_indices = [] ids = ["X", "Y", "Z", "-X", "-Y", "-Z"] for n_child in n_armature.children: self.get_forward_axis(n_child, axis_indices) # the forward index is the most common one from the list forward_ind = max(set(axis_indices), key=axis_indices.count) # move the up index one coordinate to the right, account for end of list up_ind = (forward_ind + 1) % len(ids) # return string identifiers return ids[forward_ind], ids[up_ind] def get_forward_axis(self, n_bone, axis_indices): """Helper function to get the forward axis of a bone""" # check that n_block is indeed a bone if not self.is_bone(n_bone): return None trans = n_bone.translation.as_tuple() trans_abs = tuple(abs(v) for v in trans) # do argmax max_coord_ind = max(zip(trans_abs, range(len(trans_abs))))[1] # now check the sign actual_value = trans[max_coord_ind] # handle sign accordingly so negative indices map to the negative identifiers in list if actual_value < 0: max_coord_ind += 3 axis_indices.append(max_coord_ind) # move down the hierarchy for n_child in n_bone.children: self.get_forward_axis(n_child, axis_indices) @staticmethod def fix_bone_lengths(b_armature_data): """Sets all edit_bones to a suitable length.""" for b_edit_bone in b_armature_data.edit_bones: # don't change root bones if b_edit_bone.parent: # take the desired length from the mean of all children's heads if b_edit_bone.children: child_heads = mathutils.Vector() for b_child in b_edit_bone.children: child_heads += b_child.head bone_length = (b_edit_bone.head - child_heads / len(b_edit_bone.children)).length if bone_length < 0.01: bone_length = 0.25 # end of a chain else: bone_length = b_edit_bone.parent.length b_edit_bone.length = bone_length def mark_armatures_bones(self, ni_block): """Mark armatures and bones by peeking into NiSkinInstance blocks.""" # case where we import skeleton only, # or importing an Oblivion or Fallout 3 skeleton: # do all NiNode's as bones if NifOp.props.skeleton == "SKELETON_ONLY" or \ (NifData.data.version in (0x14000005, 0x14020007) and os.path.basename(NifOp.props.filepath).lower() in ('skeleton.nif', 'skeletonbeast.nif')): if not isinstance(ni_block, NifFormat.NiNode): raise io_scene_niftools.utils.logging.NifError("Cannot import skeleton: root is not a NiNode") # for morrowind, take the Bip01 node to be the skeleton root if NifData.data.version == 0x04000002: skelroot = ni_block.find(block_name='Bip01', block_type=NifFormat.NiNode) if not skelroot: skelroot = ni_block else: skelroot = ni_block if skelroot not in self.dict_armatures: self.dict_armatures[skelroot] = [] NifLog.info(f"Selecting node '{skelroot.name}' as skeleton root") # add bones self.populate_bone_tree(skelroot) return # done! # attaching to selected armature -> first identify armature and bones elif NifOp.props.skeleton == "GEOMETRY_ONLY" and not self.dict_armatures: b_armature_obj = bpy.context.selected_objects[0] skelroot = ni_block.find(block_name=b_armature_obj.name) if not skelroot: skelroot = ni_block # raise nif_utils.NifError(f"nif has no armature '{b_armature_obj.name}'") NifLog.debug(f"Identified '{skelroot.name}' as armature") self.dict_armatures[skelroot] = [] for bone_name in b_armature_obj.data.bones.keys(): # blender bone naming -> nif bone naming nif_bone_name = block_store_export.get_bone_name_for_nif(bone_name) # find a block with bone name bone_block = skelroot.find(block_name=nif_bone_name) # add it to the name list if there is a bone with that name if bone_block: NifLog.info(f"Identified nif block '{nif_bone_name}' with bone '{bone_name}' in selected armature") self.dict_armatures[skelroot].append(bone_block) self.complete_bone_tree(bone_block, skelroot) # search for all NiTriShape or NiTriStrips blocks... if isinstance(ni_block, NifFormat.NiTriBasedGeom): # yes, we found one, get its skin instance if ni_block.is_skin(): NifLog.debug(f"Skin found on block '{ni_block.name}'") # it has a skin instance, so get the skeleton root # which is an armature only if it's not a skinning influence # so mark the node to be imported as an armature skininst = ni_block.skin_instance skelroot = skininst.skeleton_root if NifOp.props.skeleton == "EVERYTHING": if skelroot not in self.dict_armatures: self.dict_armatures[skelroot] = [] NifLog.debug(f"'{skelroot.name}' is an armature") elif NifOp.props.skeleton == "GEOMETRY_ONLY": if skelroot not in self.dict_armatures: raise io_scene_niftools.utils.logging.NifError(f"Nif structure incompatible with '{b_armature_obj.name}' as armature: node '{ni_block.name}' has '{skelroot.name}' as armature") for boneBlock in skininst.bones: # boneBlock can be None; see pyffi issue #3114079 if not boneBlock: continue if boneBlock not in self.dict_armatures[skelroot]: self.dict_armatures[skelroot].append(boneBlock) NifLog.debug(f"'{boneBlock.name}' is a bone of armature '{skelroot.name}'") # now we "attach" the bone to the armature: # we make sure all NiNodes from this bone all the way # down to the armature NiNode are marked as bones self.complete_bone_tree(boneBlock, skelroot) # mark all nodes as bones self.populate_bone_tree(skelroot) # continue down the tree for child in ni_block.get_refs(): if not isinstance(child, NifFormat.NiAVObject): continue # skip blocks that don't have transforms self.mark_armatures_bones(child) def populate_bone_tree(self, skelroot): """Add all of skelroot's bones to its dict_armatures list.""" for bone in skelroot.tree(): if bone is skelroot: continue if not isinstance(bone, NifFormat.NiNode): continue if isinstance(bone, NifFormat.NiLODNode): # LOD nodes are never bones continue if bone not in self.dict_armatures[skelroot]: self.dict_armatures[skelroot].append(bone) NifLog.debug(f"'{bone.name}' marked as extra bone of armature '{skelroot.name}'") def complete_bone_tree(self, bone, skelroot): """Make sure that the complete hierarchy from bone up to skelroot is marked in dict_armatures.""" # we must already have marked both as a bone assert skelroot in self.dict_armatures # debug assert bone in self.dict_armatures[skelroot] # debug # get the node parent, this should be marked as an armature or as a bone boneparent = bone._parent if boneparent != skelroot: # parent is not the skeleton root if boneparent not in self.dict_armatures[skelroot]: # neither is it marked as a bone: so mark the parent as a bone self.dict_armatures[skelroot].append(boneparent) # store the coordinates for realignement autodetection NifLog.debug(f"'{boneparent.name}' is a bone of armature '{skelroot.name}'") # now the parent is marked as a bone # recursion: complete the bone tree, # this time starting from the parent bone self.complete_bone_tree(boneparent, skelroot) def is_bone(self, ni_block): """Tests a NiNode to see if it has been marked as a bone.""" if ni_block: for bones in self.dict_armatures.values(): if ni_block in bones: return True def is_armature_root(self, ni_block): """Tests a block to see if it's an armature.""" if isinstance(ni_block, NifFormat.NiNode): return ni_block in self.dict_armatures
def __init__(self, operator, context): NifCommon.__init__(self, operator, context) # Helper systems self.transform_anim = TransformAnimation()
def execute(self): """Main import function.""" self.load_files() # needs to be first to provide version info. self.armaturehelper = Armature() self.boundhelper = Bound() self.bhkhelper = BhkCollision() self.constrainthelper = Constraint() self.objecthelper = Object() self.object_anim = ObjectAnimation() self.transform_anim = TransformAnimation() # find and store this list now of selected objects as creating new objects adds them to the selection list self.SELECTED_OBJECTS = bpy.context.selected_objects[:] # catch nif import errors try: # check that one armature is selected in 'import geometry + parent # to armature' mode if NifOp.props.process == "GEOMETRY_ONLY": if len(self.SELECTED_OBJECTS ) != 1 or self.SELECTED_OBJECTS[0].type != 'ARMATURE': raise io_scene_niftools.utils.logging.NifError( "You must select exactly one armature in 'Import Geometry Only' mode." ) NifLog.info("Importing data") # calculate and set frames per second if NifOp.props.animation: Animation.set_frames_per_second(NifData.data.roots) # merge skeleton roots and transform geometry into the rest pose if NifOp.props.merge_skeleton_roots: pyffi.spells.nif.fix.SpellMergeSkeletonRoots( data=NifData.data).recurse() if NifOp.props.send_geoms_to_bind_pos: pyffi.spells.nif.fix.SpellSendGeometriesToBindPosition( data=NifData.data).recurse() if NifOp.props.send_detached_geoms_to_node_pos: pyffi.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition( data=NifData.data).recurse() if NifOp.props.apply_skin_deformation: VertexGroup.apply_skin_deformation(NifData.data) # store scale correction bpy.context.scene.niftools_scene.scale_correction = NifOp.props.scale_correction self.apply_scale(NifData.data, NifOp.props.scale_correction) # import all root blocks for block in NifData.data.roots: root = block # root hack for corrupt better bodies meshes and remove geometry from better bodies on skeleton import for b in (b for b in block.tree() if isinstance(b, NifFormat.NiGeometry) and b.is_skin()): # check if root belongs to the children list of the skeleton root (can only happen for better bodies meshes) if root in [ c for c in b.skin_instance.skeleton_root.children ]: # fix parenting and update transform accordingly b.skin_instance.data.set_transform( root.get_transform() * b.skin_instance.data.get_transform()) b.skin_instance.skeleton_root = root # delete non-skeleton nodes if we're importing skeleton only if NifOp.props.process == "SKELETON_ONLY": nonbip_children = (child for child in root.children if child.name[:6] != 'Bip01 ') for child in nonbip_children: root.remove_child(child) # import this root block NifLog.debug(f"Root block: {root.get_global_display()}") self.import_root(root) except NifError: return {'CANCELLED'} NifLog.info("Finished") return {'FINISHED'}
class NifImport(NifCommon): def __init__(self, operator, context): NifCommon.__init__(self, operator, context) def execute(self): """Main import function.""" self.load_files() # needs to be first to provide version info. self.armaturehelper = Armature() self.boundhelper = Bound() self.bhkhelper = BhkCollision() self.constrainthelper = Constraint() self.objecthelper = Object() self.object_anim = ObjectAnimation() self.transform_anim = TransformAnimation() # find and store this list now of selected objects as creating new objects adds them to the selection list self.SELECTED_OBJECTS = bpy.context.selected_objects[:] # catch nif import errors try: # check that one armature is selected in 'import geometry + parent # to armature' mode if NifOp.props.process == "GEOMETRY_ONLY": if len(self.SELECTED_OBJECTS ) != 1 or self.SELECTED_OBJECTS[0].type != 'ARMATURE': raise io_scene_niftools.utils.logging.NifError( "You must select exactly one armature in 'Import Geometry Only' mode." ) NifLog.info("Importing data") # calculate and set frames per second if NifOp.props.animation: Animation.set_frames_per_second(NifData.data.roots) # merge skeleton roots and transform geometry into the rest pose if NifOp.props.merge_skeleton_roots: pyffi.spells.nif.fix.SpellMergeSkeletonRoots( data=NifData.data).recurse() if NifOp.props.send_geoms_to_bind_pos: pyffi.spells.nif.fix.SpellSendGeometriesToBindPosition( data=NifData.data).recurse() if NifOp.props.send_detached_geoms_to_node_pos: pyffi.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition( data=NifData.data).recurse() if NifOp.props.apply_skin_deformation: VertexGroup.apply_skin_deformation(NifData.data) # store scale correction bpy.context.scene.niftools_scene.scale_correction = NifOp.props.scale_correction self.apply_scale(NifData.data, NifOp.props.scale_correction) # import all root blocks for block in NifData.data.roots: root = block # root hack for corrupt better bodies meshes and remove geometry from better bodies on skeleton import for b in (b for b in block.tree() if isinstance(b, NifFormat.NiGeometry) and b.is_skin()): # check if root belongs to the children list of the skeleton root (can only happen for better bodies meshes) if root in [ c for c in b.skin_instance.skeleton_root.children ]: # fix parenting and update transform accordingly b.skin_instance.data.set_transform( root.get_transform() * b.skin_instance.data.get_transform()) b.skin_instance.skeleton_root = root # delete non-skeleton nodes if we're importing skeleton only if NifOp.props.process == "SKELETON_ONLY": nonbip_children = (child for child in root.children if child.name[:6] != 'Bip01 ') for child in nonbip_children: root.remove_child(child) # import this root block NifLog.debug(f"Root block: {root.get_global_display()}") self.import_root(root) except NifError: return {'CANCELLED'} NifLog.info("Finished") return {'FINISHED'} def load_files(self): NifData.init(NifFile.load_nif(NifOp.props.filepath)) if NifOp.props.override_scene_info: scene.import_version_info(NifData.data) def import_root(self, root_block): """Main import function.""" # check that this is not a kf file if isinstance( root_block, (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)): raise io_scene_niftools.utils.logging.NifError( "Use the KF import operator to load KF files.") # divinity 2: handle CStreamableAssetData if isinstance(root_block, NifFormat.CStreamableAssetData): root_block = root_block.root # sets the root block parent to None, so that when crawling back the script won't barf root_block._parent = None # set the block parent through the tree, to ensure I can always move backward self.set_parents(root_block) # mark armature nodes and bones self.armaturehelper.mark_armatures_bones(root_block) # import the keyframe notes # if NifOp.props.animation: # self.animationhelper.import_text_keys(root_block) # read the NIF tree if isinstance(root_block, (NifFormat.NiNode, NifFormat.NiTriBasedGeom)): b_obj = self.import_branch(root_block) ObjectProperty().import_extra_datas(root_block, b_obj) # now all havok objects are imported, so we are ready to import the havok constraints self.constrainthelper.import_bhk_constraints() # parent selected meshes to imported skeleton if NifOp.props.process == "SKELETON_ONLY": # update parenting & armature modifier for child in bpy.context.selected_objects: if isinstance(child, bpy.types.Object) and not isinstance( child.data, bpy.types.Armature): child.parent = b_obj for mod in child.modifiers: if mod.type == "ARMATURE": mod.object = b_obj elif isinstance(root_block, NifFormat.NiCamera): NifLog.warn('Skipped NiCamera root') elif isinstance(root_block, NifFormat.NiPhysXProp): NifLog.warn('Skipped NiPhysXProp root') else: NifLog.warn( f"Skipped unsupported root block type '{root_block.__class__}' (corrupted nif?)." ) def import_collision(self, n_node): """ Imports a NiNode's collision_object, if present""" if n_node.collision_object: if isinstance(n_node.collision_object, NifFormat.bhkNiCollisionObject): return self.bhkhelper.import_bhk_shape( n_node.collision_object.body) elif isinstance(n_node.collision_object, NifFormat.NiCollisionData): return self.boundhelper.import_bounding_volume( n_node.collision_object.bounding_volume) return [] def import_branch(self, n_block, b_armature=None, n_armature=None): """Read the content of the current NIF tree branch to Blender recursively. :param n_block: The nif block to import. :param b_armature: The blender armature for the current branch. :param n_armature: The corresponding nif block for the armature for the current branch. """ if not n_block: return None NifLog.info(f"Importing data for block '{n_block.name.decode()}'") if isinstance(n_block, NifFormat.NiTriBasedGeom ) and NifOp.props.process != "SKELETON_ONLY": return self.objecthelper.import_geometry_object( b_armature, n_block) elif isinstance(n_block, NifFormat.NiNode): # import object if self.armaturehelper.is_armature_root(n_block): # all bones in the tree are also imported by import_armature if NifOp.props.process != "GEOMETRY_ONLY": b_obj = self.armaturehelper.import_armature(n_block) else: n_name = block_store.import_name(n_block) b_obj = math.get_armature() NifLog.info( f"Merging nif tree '{n_name}' with armature '{b_obj.name}'" ) if n_name != b_obj.name: NifLog.warn( f"Using Nif block '{n_name}' as armature '{b_obj.name}' but names do not match" ) b_armature = b_obj n_armature = n_block elif self.armaturehelper.is_bone(n_block): # bones have already been imported during import_armature n_name = block_store.import_name(n_block) if n_name in b_armature.data.bones: b_obj = b_armature.data.bones[n_name] else: # this is a fallback for a weird bug, when a node is child of a NiLodNode in a skeletal nif b_obj = self.objecthelper.create_b_obj(n_block, None, name=n_name) b_obj.niftools.flags = n_block.flags else: # import as an empty b_obj = NiTypes.import_empty(n_block) # find children b_children = [] n_children = [child for child in n_block.children] for n_child in n_children: b_child = self.import_branch(n_child, b_armature=b_armature, n_armature=n_armature) if b_child and isinstance(b_child, bpy.types.Object): b_children.append(b_child) # import collision objects & bounding box if NifOp.props.process != "SKELETON_ONLY": b_children.extend(self.import_collision(n_block)) b_children.extend( self.boundhelper.import_bounding_box(n_block)) # set bind pose for children self.objecthelper.set_object_bind(b_obj, b_children, b_armature) # import extra node data, such as node type NiTypes.import_root_collision(n_block, b_obj) NiTypes.import_billboard(n_block, b_obj) NiTypes.import_range_lod_data(n_block, b_obj, b_children) # set object transform, this must be done after all children objects have been parented to b_obj if isinstance(b_obj, bpy.types.Object): # note: bones and this object's children already have their matrix set b_obj.matrix_local = math.import_matrix(n_block) # import object level animations (non-skeletal) if NifOp.props.animation: # self.animationhelper.import_text_keys(n_block) self.transform_anim.import_transforms(n_block, b_obj) self.object_anim.import_visibility(n_block, b_obj) return b_obj # all else is currently discarded return None def set_parents(self, n_block): """Set the parent block recursively through the tree, to allow crawling back as needed.""" if isinstance(n_block, NifFormat.NiNode): # list of non-null children children = [child for child in n_block.children if child] for child in children: child._parent = n_block self.set_parents(child)
class Armature: def __init__(self): self.transform_anim = TransformAnimation() # to get access to the nif bone in object mode self.name_to_block = {} self.pose_store = {} self.bind_store = {} self.skinned = False self.n_armature = None def store_pose_matrices(self, n_node, n_root): """Stores the nif armature space matrix of a node tree""" # check that n_block is indeed a bone if not self.is_bone(n_node): return None NifLog.debug(f"Storing pose matrix for {n_node.name}") # calculate the transform relative to root, ie. turn nif local into nif armature space self.pose_store[n_node] = n_node.get_transform(n_root) # move down the hierarchy for n_child in n_node.children: self.store_pose_matrices(n_child, n_root) def get_skinned_geometries(self, n_root): """Yield all children in n_root's tree that have skinning""" # search for all NiTriShape or NiTriStrips blocks... for n_block in n_root.tree(block_type=NifFormat.NiTriBasedGeom): # yes, we found one, does it have skinning? if n_block.is_skin(): yield n_block def get_skin_bind(self, n_bone, geom, n_root): """Get armature space bind matrix for skin partition bone's inverse bind matrix""" # get the bind pose from the skin data # NiSkinData stores the inverse bind (=rest) pose for each bone, in armature space # for ZT2 elephant, the skin transform is the inverse of the geom's armature space transform # this gives a straight rest pose for MW too # return n_bone.get_transform().get_inverse(fast=False) * geom.skin_instance.data.get_transform().get_inverse(fast=False) # however, this conflicts with send_geometries_to_bind_position for MW meshes, so stick to this now return n_bone.get_transform().get_inverse(fast=False) * geom.get_transform(n_root) def bones_iter(self, skin_instance): # might want to make sure that bone_list includes no dupes too to avoid breaking the first mesh for bonenode, bonedata in zip(skin_instance.bones, skin_instance.data.bone_list): # bonenode can be None; see pyffi issue #3114079 if bonenode: yield bonenode, bonedata def store_bind_matrices(self, n_armature): """Process all geometries' skin instances to reconstruct a bind pose from their inverse bind matrices""" # improved from pyffi's send_geometries_to_bind_position & send_bones_to_bind_position NifLog.debug(f"Calculating bind for {n_armature.name}") # prioritize geometries that have most nodes in their skin instance geoms = sorted(self.get_skinned_geometries(n_armature), key=lambda g: g.skin_instance.num_bones, reverse=True) NifLog.debug(f"Found {len(geoms)} skinned geometries") for geom in geoms: NifLog.debug(f"Checking skin of {geom.name}") skininst = geom.skin_instance for bonenode, bonedata in self.bones_iter(skininst): # make sure all bone data of shared bones coincides # see if the bone has been used by a previous skin instance if bonenode in self.bind_store: # get the bind pose that has been stored diff = (bonedata.get_transform() * self.bind_store[bonenode] # * geom.skin_instance.data.get_transform()) use this if relative to skin instead of geom * geom.get_transform(n_armature).get_inverse(fast=False)) # there is a difference between the two geometries' bind poses if not diff.is_identity(): NifLog.debug(f"Fixing {geom.name} bind position") # update the skin for all bones of the new geom for bonenode, bonedata in self.bones_iter(skininst): NifLog.debug(f"Transforming bind of {bonenode.name}") bonedata.set_transform(diff.get_inverse(fast=False) * bonedata.get_transform()) # transforming verts helps with nifs where the skins differ, eg MW vampire or WLP2 Gastornis for vert in geom.data.vertices: newvert = vert * diff vert.x = newvert.x vert.y = newvert.y vert.z = newvert.z for norm in geom.data.normals: newnorm = norm * diff.get_matrix_33() norm.x = newnorm.x norm.y = newnorm.y norm.z = newnorm.z break # store bind pose for bonenode, bonedata in self.bones_iter(skininst): NifLog.debug(f"Stored {geom.name} bind position") self.bind_store[bonenode] = self.get_skin_bind(bonedata, geom, n_armature) NifLog.debug("Storing non-skeletal bone poses") self.fix_pose(n_armature, n_armature) def fix_pose(self, n_node, n_root): """reposition non-skeletal bones to maintain their local orientation to their skeletal parents""" for n_child_node in n_node.children: # only process nodes if not isinstance(n_child_node, NifFormat.NiNode): continue if n_child_node not in self.bind_store and n_child_node in self.pose_store: NifLog.debug(f"Calculating bind pose for non-skeletal bone {n_child_node.name}") # get matrices for n_node (the parent) - fallback to getter if it is not in the store n_armature_pose = self.pose_store.get(n_node, n_node.get_transform(n_root)) # get bind of parent node or pose if it has no bind pose n_armature_bind = self.bind_store.get(n_node, n_armature_pose) # the child must have a pose, no need for a fallback n_child_armature_pose = self.pose_store[n_child_node] # get the relative transform of n_child_node from pose * inverted parent pose n_child_local_pose = n_child_armature_pose * n_armature_pose.get_inverse(fast=False) # get object space transform by multiplying with bind of parent bone self.bind_store[n_child_node] = n_child_local_pose * n_armature_bind # move down the hierarchy self.fix_pose(n_child_node, n_root) def import_armature(self, n_armature): """Scans an armature hierarchy, and returns a whole armature. This is done outside the normal node tree scan to allow for positioning of the bones before skins are attached.""" armature_name = block_store.import_name(n_armature) b_armature_data = bpy.data.armatures.new(armature_name) b_armature_data.display_type = 'STICK' # use heuristics to determine a suitable orientation forward, up = self.guess_orientation(n_armature) # pass them to the matrix utility math.set_bone_orientation(forward, up) # store axis orientation for export b_armature_data.niftools.axis_forward = forward b_armature_data.niftools.axis_up = up b_armature_obj = Object.create_b_obj(n_armature, b_armature_data) b_armature_obj.show_in_front = True # store the original pose & bind matrices for all nodes self.pose_store = {} self.bind_store = {} self.store_pose_matrices(n_armature, n_armature) self.store_bind_matrices(n_armature) # make armature editable and create bones bpy.ops.object.mode_set(mode='EDIT', toggle=False) for n_child in n_armature.children: self.import_bone_bind(n_child, b_armature_data, n_armature) self.fix_bone_lengths(b_armature_data) bpy.ops.object.mode_set(mode='OBJECT', toggle=False) # The armature has been created in editmode, # now we are ready to set the bone keyframes and store the bones' long names. if NifOp.props.animation: self.transform_anim.get_bind_data(b_armature_obj) for bone_name, b_bone in b_armature_obj.data.bones.items(): n_block = self.name_to_block[bone_name] # the property is only available from object mode! block_store.store_longname(b_bone, safe_decode(n_block.name)) if NifOp.props.animation: self.transform_anim.import_transforms(n_block, b_armature_obj, bone_name) # import pose for b_name, n_block in self.name_to_block.items(): n_pose = self.pose_store[n_block] b_pose_bone = b_armature_obj.pose.bones[b_name] n_bind = math.nifformat_to_mathutils_matrix(n_pose) b_pose_bone.matrix = math.nif_bind_to_blender_bind(n_bind) # force update is required after each pbone to ensure the transforms are set properly in blender bpy.context.view_layer.update() return b_armature_obj def import_bone_bind(self, n_block, b_armature_data, n_armature, b_parent_bone=None): """Adds a bone to the armature in edit mode.""" # check that n_block is indeed a bone if not self.is_bone(n_block): return None # bone name bone_name = block_store.import_name(n_block) # create a new bone b_edit_bone = b_armature_data.edit_bones.new(bone_name) # store nif block for access from object mode self.name_to_block[b_edit_bone.name] = n_block # get the nif bone's armature space matrix (under the hood all bone space matrixes are multiplied together) n_bind = math.nifformat_to_mathutils_matrix(self.bind_store.get(n_block, NifFormat.Matrix44())) # get transformation in blender's coordinate space b_bind = math.nif_bind_to_blender_bind(n_bind) # set the bone matrix - but set the tail first to prevent issues with zero-length bone b_edit_bone.tail = mathutils.Vector([0, 0, 1]) b_edit_bone.matrix = b_bind # link to parent if b_parent_bone: b_edit_bone.parent = b_parent_bone # import and parent bone children for n_child in n_block.children: self.import_bone_bind(n_child, b_armature_data, n_armature, b_edit_bone) def guess_orientation(self, n_armature): """Analyze all bones' translations to see what the nif considers the 'forward' axis""" axis_indices = [] ids = ["X", "Y", "Z", "-X", "-Y", "-Z"] for n_child in n_armature.children: self.get_forward_axis(n_child, axis_indices) # the forward index is the most common one from the list forward_ind = max(set(axis_indices), key=axis_indices.count) # move the up index one coordinate to the right, account for end of list up_ind = (forward_ind + 1) % len(ids) # return string identifiers return ids[forward_ind], ids[up_ind] @staticmethod def argmax(values): """Return the index of the max value in values""" return max(zip(values, range(len(values))))[1] def get_forward_axis(self, n_bone, axis_indices): """Helper function to get the forward axis of a bone""" # check that n_block is indeed a bone if not self.is_bone(n_bone): return None trans = n_bone.translation.as_tuple() trans_abs = tuple(abs(v) for v in trans) # get the index of the coordinate with the biggest absolute value max_coord_ind = self.argmax(trans_abs) # now check the sign actual_value = trans[max_coord_ind] # handle sign accordingly so negative indices map to the negative identifiers in list if actual_value < 0: max_coord_ind += 3 axis_indices.append(max_coord_ind) # move down the hierarchy for n_child in n_bone.children: self.get_forward_axis(n_child, axis_indices) @staticmethod def fix_bone_lengths(b_armature_data): """Sets all edit_bones to a suitable length.""" for b_edit_bone in b_armature_data.edit_bones: # don't change root bones if b_edit_bone.parent: # take the desired length from the mean of all children's heads if b_edit_bone.children: child_heads = mathutils.Vector() for b_child in b_edit_bone.children: child_heads += b_child.head bone_length = (b_edit_bone.head - child_heads / len(b_edit_bone.children)).length if bone_length < 0.01: bone_length = 0.25 # end of a chain else: bone_length = b_edit_bone.parent.length b_edit_bone.length = bone_length def check_for_skin(self, n_root): """Checks all tri geometry for skinning, sets self.skinned accordingly""" # set these here once per run self.n_armature = None self.skinned = False for n_block in self.get_skinned_geometries(n_root): self.skinned = True NifLog.debug(f"{n_block.name} has skinning.") # one is enough to require an armature, so stop return # force import of nodes as bones, even if no geometries are present if NifOp.props.process == "SKELETON_ONLY": self.skinned = True NifLog.debug(f"Found no skinned geometries.") def is_bone(self, ni_block): """Tests a NiNode to see if it has been marked as a bone.""" if isinstance(ni_block, NifFormat.NiNode): return self.skinned def is_armature_root(self, n_block): """Tests a block to see if it's an armature.""" if isinstance(n_block, NifFormat.NiNode): # we have skinning and are waiting for a suitable start node of the tree if self.skinned and not self.n_armature: # now store it as the nif armature's root self.n_armature = n_block return True