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 determine_texture_types(self, b_mat): """Checks all texture nodes of a material and checks their labels for relevant texture cues. Stores all slots as class properties.""" self.b_mat = b_mat self._reset_fields() for b_texture_node in self.get_used_textslots(b_mat): shown_label = b_texture_node.label if shown_label == '': shown_label = b_texture_node.image.name NifLog.debug( f"Found node {b_texture_node.name} of type {shown_label}") # go over all slots for slot_name in self.slots.keys(): if slot_name in shown_label: # slot has already been populated if self.slots[slot_name]: raise NifError( f"Multiple {slot_name} textures in material '{b_mat.name}''.\n" f"Make sure there is only one texture node labeled as '{slot_name}'" ) # it's a new slot so store it self.slots[slot_name] = b_texture_node break # unsupported texture type else: raise NifError( f"Do not know how to export texture node '{b_texture_node.name}' in material '{b_mat.name}' with label '{shown_label}'." f"Delete it or change its label.")
def init(operator, context): NifOp.op = operator NifOp.props = operator.properties NifOp.context = context # init loggers logging level NifLog.init(operator)
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_sequence_stream_helper(self, kf_root, b_armature_obj, bind_data): NifLog.debug('Importing NiSequenceStreamHelper...') b_action = self.create_action(b_armature_obj, kf_root.name.decode(), retrieve=False) # import parallel trees of extra datas and keyframe controllers extra = kf_root.extra_data controller = kf_root.controller while extra and controller: # textkeys in the stack do not specify node names, import as markers while isinstance(extra, NifFormat.NiTextKeyExtraData): self.import_text_key_extra_data(extra, b_action) extra = extra.next_extra_data # grabe the node name from string data bone_name = None if isinstance(extra, NifFormat.NiStringExtraData): node_name = extra.string_data.decode() bone_name = block_registry.get_bone_name_for_blender(node_name) # import keyframe controller if bone_name in bind_data: niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans = bind_data[ bone_name] self.import_keyframe_controller(controller, b_armature_obj, bone_name, niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans) # grab next pair of extra and controller extra = extra.next_extra_data controller = controller.next_controller
def import_material_uv_controller(self, b_material, n_geom): """Import UV controller data.""" # search for the block n_ctrl = math.find_controller(n_geom, NifFormat.NiUVController) if not n_ctrl: return NifLog.info("Importing UV controller") b_mat_action = self.create_action(b_material, "MaterialAction") dtypes = ("offset", 0), ("offset", 1), ("scale", 0), ("scale", 1) for n_uvgroup, (data_path, array_ind) in zip(n_ctrl.data.uv_groups, dtypes): if n_uvgroup.keys: interp = self.get_b_interp_from_n_interp( n_uvgroup.interpolation) # in blender, UV offset is stored per n_texture slot # so we have to repeat the import for each used tex slot for i, texture_slot in enumerate(b_material.texture_slots): if texture_slot: fcurves = self.create_fcurves( b_mat_action, f"texture_slots[{i}]." + data_path, (array_ind, ), n_ctrl.flags) for key in n_uvgroup.keys: if "offset" in data_path: self.add_key(fcurves, key.time, (-key.value, ), interp) else: self.add_key(fcurves, key.time, (key.value, ), interp)
def import_version_info(data): scene = bpy.context.scene.niftools_scene scene.nif_version = data._version_value_._value scene.user_version = data._user_version_value_._value scene.user_version_2 = data._user_version_2_value_._value # filter possible games by nif version possible_games = [] for game, versions in NifFormat.games.items(): if game != '?': if scene.nif_version in versions: game_enum = _game_to_enum(game) # go to next game if user version for this game does not match defined if game_enum in scene.USER_VERSION: if scene.USER_VERSION[game_enum] != scene.user_version: continue # or user version in scene is not 0 when this game has no associated user version elif scene.user_version != 0: continue # same checks for user version 2 if game_enum in scene.USER_VERSION_2: if scene.USER_VERSION_2[game_enum] != scene.user_version_2: continue elif scene.user_version_2 != 0: continue # passed all checks, add to possible games list possible_games.append(game_enum) if len(possible_games) == 1: scene.game = possible_games[0] elif len(possible_games) > 1: scene.game = possible_games[0] # todo[version] - check if this nif's version is marked as default for any of the possible games and use that NifLog.warn( f"Game set to '{possible_games[0]}', but multiple games qualified")
def process_nimaterial_property(self, prop): """Import a NiMaterialProperty based material""" NiMaterial().import_material(self.n_block, self.b_mat, prop) # TODO [animation][material] merge this call into import_material MaterialAnimation().import_material_controllers( self.n_block, self.b_mat) NifLog.debug("NiMaterialProperty property processed")
def export_bsxflags_upb(self, root_block, root_objects): # TODO [object][property] Fixme NifLog.info("Checking collision") # activate oblivion/Fallout 3 collision and physics if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): b_obj = self.has_collision() if b_obj: # enable collision bsx = block_store.create_block("BSXFlags") bsx.name = 'BSX' root_block.add_extra_data(bsx) found_bsx = False for root_object in root_objects: if root_object.niftools.bsxflags: if found_bsx: raise NifError( "Multiple objects have BSXFlags. Only one onject may contain this data" ) else: found_bxs = True bsx.integer_data = root_object.niftools.bsxflags # many Oblivion nifs have a UPB, but export is disabled as # they do not seem to affect anything in the game if b_obj.niftools.upb: upb = block_store.create_block("NiStringExtraData") upb.name = 'UPB' if b_obj.niftools.upb == '': upb.string_data = UPB_DEFAULT else: upb.string_data = b_obj.niftools.upb.encode() root_block.add_extra_data(upb)
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 process_nispecular_property(self, prop): """SpecularProperty based specular""" # TODO [material][property] if NifData.data.version == 0x14000004: self.b_mat.specular_intensity = 0.0 # no specular prop NifLog.debug("NiSpecularProperty property processed")
def import_bhk_ridgidbody_t(self, bhk_shape): """Imports a BhkRigidBody block and applies the transform to the collision objects""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # import shapes collision_objs = self.import_bhk_shape(bhk_shape.shape) # find transformation matrix in case of the T version # set rotation b_rot = bhk_shape.rotation transform = mathutils.Quaternion([b_rot.w, b_rot.x, b_rot.y, b_rot.z]).to_matrix().to_4x4() # set translation b_trans = bhk_shape.translation transform.translation = mathutils.Vector( (b_trans.x, b_trans.y, b_trans.z)) * self.HAVOK_SCALE # apply transform for b_col_obj in collision_objs: b_col_obj.matrix_local = b_col_obj.matrix_local @ transform self._import_bhk_rigid_body(bhk_shape, collision_objs) # and return a list of transformed collision shapes return collision_objs
def register(): # addon updater code and configurations in case of broken version, try to register the updater first # so that users can revert back to a working version NifLog.debug("Starting registration") configure_autoupdater() register_modules(MODS, __name__)
def export_text_keys(self, b_action): """Process b_action's pose markers and return an extra string data block.""" if NifOp.props.animation == 'GEOM_NIF': # animation group extra data is not present in geometry only files return NifLog.info("Exporting animation groups") self.add_dummy_markers(b_action) # add a NiTextKeyExtraData block n_text_extra = block_store.create_block("NiTextKeyExtraData", b_action.pose_markers) # create a text key for each frame descriptor n_text_extra.num_text_keys = len(b_action.pose_markers) n_text_extra.text_keys.update_size() f0, f1 = b_action.frame_range for key, marker in zip(n_text_extra.text_keys, b_action.pose_markers): f = marker.frame if (f < f0) or (f > f1): NifLog.warn( f"Marker out of animated range ({f} not between [{f0}, {f1}])" ) key.time = f / self.fps key.value = marker.name.replace('/', '\r\n') return n_text_extra
def apply_skin_deformation(n_data): """ Process all geometries in NIF tree to apply their skin """ # get all geometries with skin n_geoms = [ g for g in n_data.get_global_iterator() if isinstance(g, NifFormat.NiGeometry) and g.is_skin() ] # make sure that each skin is applied only once to avoid distortions when a model is referred to twice for n_geom in set(n_geoms): NifLog.info('Applying skin deformation on geometry {0}'.format( n_geom.name)) skininst = n_geom.skin_instance skindata = skininst.data if skindata.has_vertex_weights: vertices = n_geom.get_skin_deformation()[0] else: NifLog.info( "PyFFI does not support this type of skinning, so here's a workaround..." ) vertices = VertexGroup.get_skin_deformation_from_partition( n_geom) # finally we can actually set the data for vold, vnew in zip(n_geom.data.vertices, vertices): vold.x = vnew.x vold.y = vnew.y vold.z = vnew.z
def export_root_node(self, root_objects, filebase): """ Exports a nif's root node; use blender root if there is only one, else create a meta root """ # TODO [collsion] detect root collision -> root collision node (can be mesh or empty) # self.nif_export.collisionhelper.export_collision(b_obj, n_parent) # return None # done; stop here self.n_root = None # there is only one root object so that will be our final root if len(root_objects) == 1: b_obj = root_objects[0] self.export_node(b_obj, None) # there is more than one root object so we create a meta root else: NifLog.info("Created meta root because blender scene had multiple root objects") self.n_root = types.create_ninode() self.n_root.name = "Scene Root" for b_obj in root_objects: self.export_node(b_obj, self.n_root) # TODO [object] How dow we know we are selecting the right node in the case of multi-root? # making root block a fade node root_type = b_obj.niftools.rootnode if bpy.context.scene.niftools_scene.game in ('FALLOUT_3', 'SKYRIM') and root_type == 'BSFadeNode': NifLog.info("Making root block a BSFadeNode") fade_root_block = NifFormat.BSFadeNode().deepcopy(self.n_root) fade_root_block.replace_global_node(self.n_root, fade_root_block) self.n_root = fade_root_block # various extra datas object_property = ObjectDataProperty() object_property.export_bsxflags_upb(self.n_root, root_objects) object_property.export_inventory_marker(self.n_root, root_objects) object_property.export_weapon_location(self.n_root, b_obj) types.export_furniture_marker(self.n_root, filebase) return self.n_root
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_controller_manager(self, n_block, b_obj, b_armature): ctrlm = n_block.controller if ctrlm and isinstance(ctrlm, NifFormat.NiControllerManager): NifLog.debug(f'Importing NiControllerManager') if b_armature: self.get_bind_data(b_armature) for ctrl in ctrlm.controller_sequences: self.import_kf_root(ctrl, b_armature)
def get_extend_from_flags(flags): if flags & 6 == 4: # 0b100 return "CONSTANT" elif flags & 6 == 0: # 0b000 return "CYCLIC" NifLog.warn("Unsupported cycle mode in nif, using clamped.") return "CONSTANT"
def export_texture_filename(b_texture_node): """Returns image file name from b_texture_node. @param b_texture_node: The b_texture_node object in blender. @return: The file name of the image used in the b_texture_node. """ if not isinstance(b_texture_node, bpy.types.ShaderNodeTexImage): raise io_scene_niftools.utils.logging.NifError( f"Expected a Shader node texture, got {type(b_texture_node)}") # get filename from image # TODO [b_texture_node] still needed? can b_texture_node.image be None in current blender? # check that image is loaded if b_texture_node.image is None: raise io_scene_niftools.utils.logging.NifError( f"Image type texture has no file loaded ('{b_texture_node.name}')" ) filename = b_texture_node.image.filepath # warn if packed flag is enabled if b_texture_node.image.packed_file: NifLog.warn( f"Packed image in texture '{b_texture_node.name}' ignored, exporting as '{filename}' instead." ) # try and find a DDS alternative, force it if required ddsfilename = f"{(filename[:-4])}.dds" if os.path.exists( bpy.path.abspath(ddsfilename)) or NifOp.props.force_dds: filename = ddsfilename # sanitize file path if bpy.context.scene.niftools_scene.game not in ('MORROWIND', 'OBLIVION', 'FALLOUT_3', 'SKYRIM'): # strip b_texture_node file path filename = os.path.basename(filename) else: # strip the data files prefix from the b_texture_node's file name filename = filename.lower() idx = filename.find("textures") if idx >= 0: filename = filename[idx:] elif not os.path.exists(bpy.path.abspath(filename)): pass else: NifLog.warn( f"{filename} does not reside in a 'Textures' folder; texture path will be stripped and textures may not display in-game" ) filename = os.path.basename(filename) # for linux export: fix path separators return filename.replace('/', '\\')
def create_and_link(self, slot_name, n_tex_info): """""" slot_lower = slot_name.lower().replace(' ', '_') import_func_name = f"link_{slot_lower}_node" import_func = getattr(self, import_func_name, None) if not import_func: NifLog.debug(f"Could not find texture linking function {import_func_name}") return b_texture = self.create_texture_slot(n_tex_info) import_func(b_texture)
def get_n_apply_mode_from_b_blend_type(b_blend_type): if b_blend_type == "LIGHTEN": return NifFormat.ApplyMode.APPLY_HILIGHT elif b_blend_type == "MULTIPLY": return NifFormat.ApplyMode.APPLY_HILIGHT2 elif b_blend_type == "MIX": return NifFormat.ApplyMode.APPLY_MODULATE NifLog.warn("Unsupported blend type ({0}) in material, using apply mode APPLY_MODULATE".format(b_blend_type)) return NifFormat.ApplyMode.APPLY_MODULATE
def import_bhk_ridgid_body(self, bhk_shape): """Imports a BhkRigidBody block and applies the transform to the collision objects""" NifLog.debug(f"Importing {bhk_shape.__class__.__name__}") # import shapes collision_objs = self.import_bhk_shape(bhk_shape.shape) self._import_bhk_rigid_body(bhk_shape, collision_objs) # and return a list of transformed collision shapes return collision_objs
def add_dummy_markers(self, b_action): # if we exported animations, but no animation groups are defined, # define a default animation group NifLog.info("Checking action pose markers.") if not b_action.pose_markers: NifLog.info("Defining default action pose markers.") for frame, text in zip(b_action.frame_range, ("Idle: Start/Idle: Loop Start", "Idle: Loop Stop/Idle: Stop")): marker = b_action.pose_markers.new(text) marker.frame = frame
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_version_data(): """ Returns NifFormat.Data of the correct version and user versions """ b_scene = bpy.context.scene.niftools_scene game = b_scene.game version = b_scene.nif_version NifLog.info(f"Writing NIF version 0x{version:08X}") # get user version and user version 2 for export user_version = b_scene.user_version if b_scene.user_version else b_scene.USER_VERSION.get(game, 0) user_version_2 = b_scene.user_version_2 if b_scene.user_version_2 else b_scene.USER_VERSION_2.get(game, 0) return version, NifFormat.Data(version, user_version, user_version_2)
def load_image(tex_path): """Returns an image or a generated image if none was found""" name = os.path.basename(tex_path) if name not in bpy.data.images: try: b_image = bpy.data.images.load(tex_path) except: NifLog.warn(f"Texture '{name}' not found or not supported and no alternate available") b_image = bpy.data.images.new(name=name, width=1, height=1, alpha=True) else: b_image = bpy.data.images[name] return b_image
def configure_autoupdater(): NifLog.debug("Configuring auto-updater") addon_updater_ops.register(bl_info) addon_updater_ops.updater.select_link = select_zip_file addon_updater_ops.updater.use_releases = True addon_updater_ops.updater.remove_pre_update_patterns = [ "*.py", "*.pyc", "*.xml", "*.exe", "*.rst", "VERSION", "*.xsd" ] addon_updater_ops.updater.user = "******" addon_updater_ops.updater.repo = "blender_niftools_addon" addon_updater_ops.updater.website = "https://github.com/niftools/blender_niftools_addon/" addon_updater_ops.updater.version_min_update = (0, 0, 4)
def set_extrapolation(extend_type, fcurves): if extend_type == "CONSTANT": for fcurve in fcurves: fcurve.extrapolation = 'CONSTANT' elif extend_type == "CYCLIC": for fcurve in fcurves: fcurve.modifiers.new('CYCLES') # don't support reverse for now, not sure if it is even possible in blender else: NifLog.warn("Unsupported extrapolation mode, using clamped.") for fcurve in fcurves: fcurve.extrapolation = 'CONSTANT'
def get_n_interp_from_b_interp(b_ipol): if b_ipol == "LINEAR": return NifFormat.KeyType.LINEAR_KEY elif b_ipol == "BEZIER": return NifFormat.KeyType.QUADRATIC_KEY elif b_ipol == "CONSTANT": return NifFormat.KeyType.CONST_KEY NifLog.warn( f"Unsupported interpolation mode ({b_ipol}) in blend, using quadratic/bezier." ) return NifFormat.KeyType.QUADRATIC_KEY