def import_bhkcapsule_shape(self, bhk_shape): """Import a BhkCapsule block as a simple cylinder collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") radius = bhk_shape.radius * self.HAVOK_SCALE p_1 = bhk_shape.first_point p_2 = bhk_shape.second_point length = (p_1 - p_2).norm() * self.HAVOK_SCALE first_point = p_1 * self.HAVOK_SCALE second_point = p_2 * self.HAVOK_SCALE minx = miny = -radius maxx = maxy = +radius minz = -radius - length / 2 maxz = length / 2 + radius # create blender object b_obj = Object.box_from_extents("capsule", minx, maxx, miny, maxy, minz, maxz) # here, these are not encoded as a direction so we must first calculate the direction b_obj.matrix_local = self.center_origin_to_matrix( (first_point + second_point) / 2, first_point - second_point) self.set_b_collider(b_obj, bounds_type="CAPSULE", display_type="CAPSULE", radius=radius, n_obj=bhk_shape) return [b_obj]
def import_bhksphere_shape(self, bhk_shape): """Import a BhkSphere block as a simple sphere collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") r = bhk_shape.radius * self.HAVOK_SCALE b_obj = Object.box_from_extents("sphere", -r, r, -r, r, -r, r) self.set_b_collider(b_obj, display_type="SPHERE", bounds_type='SPHERE', radius=r, n_obj=bhk_shape) return [b_obj]
def import_nitristrips(self, bhk_shape): """Import a NiTriStrips block as a Triangle-Mesh collision object""" # no factor 7 correction!!! verts = [(v.x, v.y, v.z) for v in bhk_shape.vertices] faces = list(bhk_shape.get_triangles()) b_obj = Object.mesh_from_data("poly", verts, faces) # TODO [collision] self.havok_mat! self.set_b_collider(b_obj, bounds_type="MESH", radius=bhk_shape.radius) return [b_obj]
def import_boxbv(self, box): offset = box.center # ignore for now, seems to be a unity 3x3 matrix axes = box.axis x, y, z = box.extent b_obj = Object.box_from_extents("box", -x, x, -y, y, -z, z) b_obj.location = (offset.x, offset.y, offset.z) self.set_b_collider(b_obj, radius=(x + y + z) / 3) return [b_obj]
def import_spherebv(self, sphere): r = sphere.radius c = sphere.center b_obj = Object.box_from_extents("sphere", -r, r, -r, r, -r, r) b_obj.location = (c.x, c.y, c.z) self.set_b_collider(b_obj, bounds_type="SPHERE", display_type='SPHERE', radius=r) return [b_obj]
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, 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 = 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_bounding_box(self, n_block): """Import a NiNode's bounding box or attached BSBound extra data.""" if not n_block or not isinstance(n_block, NifFormat.NiNode): return [] # we have a ninode with bounding box if n_block.has_bounding_box: b_name = 'Bounding Box' # Ninode's bbox behaves like a seperate mesh. # bounding_box center(n_block.bounding_box.translation) is relative to the bound_box n_bl_trans = n_block.translation n_bbox = n_block.bounding_box n_b_trans = n_bbox.translation minx = n_b_trans.x - n_bl_trans.x - n_bbox.radius.x miny = n_b_trans.y - n_bl_trans.y - n_bbox.radius.y minz = n_b_trans.z - n_bl_trans.z - n_bbox.radius.z maxx = n_b_trans.x - n_bl_trans.x + n_bbox.radius.x maxy = n_b_trans.y - n_bl_trans.y + n_bbox.radius.y maxz = n_b_trans.z - n_bl_trans.z + n_bbox.radius.z bbox_center = n_b_trans.as_list() # we may still have a BSBound extra data attached to this node else: for n_extra in n_block.get_extra_datas(): # TODO [extra][data] Move to property processor if isinstance(n_extra, NifFormat.BSBound): b_name = 'BSBound' center = n_extra.center dims = n_extra.dimensions minx = -dims.x miny = -dims.y minz = -dims.z maxx = +dims.x maxy = +dims.y maxz = +dims.z bbox_center = center.as_list() break # none was found else: return [] # create blender object b_obj = Object.box_from_extents(b_name, minx, maxx, miny, maxy, minz, maxz) # probably only on NiNodes with BB if hasattr(n_block, "flags"): b_obj.niftools.flags = n_block.flags b_obj.location = bbox_center self.set_b_collider(b_obj, radius=max(maxx, maxy, maxz)) return [ b_obj, ]
def import_bhkconvex_vertices_shape(self, bhk_shape): """Import a BhkConvexVertex block as a convex hull collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # find vertices (and fix scale) scaled_verts = [(self.HAVOK_SCALE * n_vert.x, self.HAVOK_SCALE * n_vert.y, self.HAVOK_SCALE * n_vert.z) for n_vert in bhk_shape.vertices] verts, faces = qhull3d(scaled_verts) b_obj = Object.mesh_from_data("convexpoly", verts, faces) radius = bhk_shape.radius * self.HAVOK_SCALE self.set_b_collider(b_obj, bounds_type="CONVEX_HULL", radius=radius, n_obj=bhk_shape) return [b_obj]
def import_bhkpackednitristrips_shape(self, bhk_shape): """Import a BhkPackedNiTriStrips block as a Triangle-Mesh collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # create mesh for each sub shape hk_objects = [] vertex_offset = 0 subshapes = bhk_shape.sub_shapes if not subshapes: # fallout 3 stores them in the data subshapes = bhk_shape.data.sub_shapes for subshape_num, subshape in enumerate(subshapes): verts = [] faces = [] for vert_index in range(vertex_offset, vertex_offset + subshape.num_vertices): n_vert = bhk_shape.data.vertices[vert_index] verts.append( (n_vert.x * self.HAVOK_SCALE, n_vert.y * self.HAVOK_SCALE, n_vert.z * self.HAVOK_SCALE)) for bhk_triangle in bhk_shape.data.triangles: bhk_tri = bhk_triangle.triangle if (vertex_offset <= bhk_tri.v_1) and ( bhk_tri.v_1 < vertex_offset + subshape.num_vertices): faces.append((bhk_tri.v_1 - vertex_offset, bhk_tri.v_2 - vertex_offset, bhk_tri.v_3 - vertex_offset)) else: continue b_obj = Object.mesh_from_data(f'poly{subshape_num:d}', verts, faces) radius = min(vert.co.length for vert in b_obj.data.vertices) self.set_b_collider(b_obj, bounds_type="MESH", radius=radius, n_obj=subshape) vertex_offset += subshape.num_vertices hk_objects.append(b_obj) return hk_objects
def import_billboard(n_node, b_obj): """ Import a NiBillboardNode """ if isinstance(n_node, NifFormat.NiBillboardNode) and not isinstance( b_obj, bpy.types.Bone): # find camera object for obj in bpy.context.scene.objects: if obj.type == 'CAMERA': b_obj_camera = obj break # none exists, create one else: b_obj_camera_data = bpy.data.cameras.new("Camera") b_obj_camera = Object.create_b_obj(None, b_obj_camera_data) # make b_obj track camera object constr = b_obj.constraints.new('TRACK_TO') constr.target = b_obj_camera constr.track_axis = 'TRACK_Z' constr.up_axis = 'UP_Y'
def import_bhkbox_shape(self, bhk_shape): """Import a BhkBox block as a simple Box collision object""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # create box r = bhk_shape.radius * self.HAVOK_SCALE dims = bhk_shape.dimensions minx = -dims.x * self.HAVOK_SCALE maxx = +dims.x * self.HAVOK_SCALE miny = -dims.y * self.HAVOK_SCALE maxy = +dims.y * self.HAVOK_SCALE minz = -dims.z * self.HAVOK_SCALE maxz = +dims.z * self.HAVOK_SCALE # create blender object b_obj = Object.box_from_extents("box", minx, maxx, miny, maxy, minz, maxz) self.set_b_collider(b_obj, radius=r, n_obj=bhk_shape) return [b_obj]
def import_capsulebv(self, capsule): offset = capsule.center # always a normalized vector direction = capsule.origin # nb properly named in newer nif.xmls extent = capsule.unknown_float_1 radius = capsule.unknown_float_2 # positions of the box verts minx = miny = -radius maxx = maxy = +radius minz = -(extent + 2 * radius) / 2 maxz = +(extent + 2 * radius) / 2 # create blender object b_obj = Object.box_from_extents("capsule", minx, maxx, miny, maxy, minz, maxz) # apply transform in local space b_obj.matrix_local = self.center_origin_to_matrix(offset, direction) self.set_b_collider(b_obj, bounds_type="CAPSULE", display_type="CAPSULE", radius=radius) return [b_obj]
def import_empty(n_block): """Creates and returns a grouping empty.""" b_empty = Object.create_b_obj(n_block, None) # TODO [flags] Move out to generic processing b_empty.niftools.flags = n_block.flags return b_empty
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)