def import_texture_extra_shader(self, b_mat, n_texture_prop, extra_datas): # extra texture shader slots for shader_tex_desc in n_texture_prop.shader_textures: if not shader_tex_desc.is_used: continue # it is used, figure out the slot it is used for for extra in extra_datas: if extra.integer_data == shader_tex_desc.map_index: shader_name = extra.name break else: NifLog.warn("No slot for shader texture {0}.".format( shader_tex_desc.texture_data.source.file_name)) continue try: extra_shader_index = ( self.nif_import.EXTRA_SHADER_TEXTURES.index(shader_name)) except ValueError: # shader_name not in self.EXTRA_SHADER_TEXTURES NifLog.warn("No slot for shader texture {0}.".format( shader_tex_desc.texture_data.source.file_name)) continue self.import_shader_by_type(shader_tex_desc, extra_shader_index)
def import_material_color_controller(self, b_material, b_channel, n_geom, n_target_color): # find material color controller with matching target color n_matprop = nif_utils.find_property(n_geom, NifFormat.NiMaterialProperty) if not n_matprop: return for ctrl in n_matprop.get_controllers(): if isinstance(ctrl, NifFormat.NiMaterialColorController): if ctrl.get_target_color() == n_target_color: n_matcolor_ctrl = ctrl break else: return NifLog.info( "Importing material color controller for target color {0} into blender channel {0}" .format(n_target_color, b_channel)) # import data as curves b_mat_action = self.nif_import.animationhelper.create_action( b_material, "MaterialAction") fcurves = create_fcurves(b_mat_action, b_channel, range(3)) interp = self.nif_import.animationhelper.get_b_interp_from_n_interp( n_matcolor_ctrl.data.data.interpolation) self.nif_import.animationhelper.set_extrapolation( n_matcolor_ctrl.flags, fcurves) for key in n_matcolor_ctrl.data.data.keys: self.nif_import.animationhelper.add_key(fcurves, key.time, key.value.as_list(), interp)
def import_kf_standalone(self, kf_root, b_armature_obj, bind_data): """ Import a kf animation. Needs a suitable armature in blender scene. """ NifLog.info("Importing KF tree") # check that this is an Oblivion style kf file if not isinstance(kf_root, NifFormat.NiControllerSequence): raise nif_utils.NifError("non-Oblivion .kf import not supported") # import text keys self.import_text_keys(kf_root) self.create_action(b_armature_obj, kf_root.name.decode()) # go over all controlled blocks (NiKeyframeController) for controlledblock in kf_root.controlled_blocks: # nb: this yielded just an empty bytestring # nodename = controlledblock.get_node_name() kfc = controlledblock.controller bone_name = armature.get_bone_name_for_blender( controlledblock.target_name) if bone_name in bind_data: niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans = bind_data[ bone_name] self.armature_animation.import_keyframe_controller( kfc, b_armature_obj, bone_name, niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans)
def import_material_uv_controller(self, b_material, n_geom): """Import UV controller data.""" # search for the block n_ctrl = nif_utils.find_controller(n_geom, NifFormat.NiUVController) if not (n_ctrl and n_ctrl.data): return NifLog.info("Importing UV controller") b_mat_action = self.nif_import.animationhelper.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.nif_import.animationhelper.get_b_interp_from_n_interp( n_uvgroup.interpolation) #in blender, UV offset is stored per 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 = create_fcurves( b_mat_action, "texture_slots[" + str(i) + "]." + data_path, (array_ind, )) for key in n_uvgroup.keys: if "offset" in data_path: self.nif_import.animationhelper.add_key( fcurves, key.time, (-key.value, ), interp) else: self.nif_import.animationhelper.add_key( fcurves, key.time, (key.value, ), interp) self.nif_import.animationhelper.set_extrapolation( n_ctrl.flags, fcurves)
def get_extend_from_flags(self, flags): if flags & 6 == 4: # 0b100 return "CONST" elif flags & 6 == 0: # 0b000 return "CYCLIC" NifLog.warn("Unsupported cycle mode in nif, using clamped.") return "CONST"
def set_alpha(self, b_mat, ShaderProperty, n_alpha_prop): NifLog.debug("Alpha prop detected") b_mat.use_transparency = True if hasattr(ShaderProperty, 'alpha'): b_mat.alpha = (1 - ShaderProperty.alpha) else: b_mat.alpha = 0 b_mat.transparency_method = 'Z_TRANSPARENCY' # enable z-buffered transparency b_mat.offset_z = n_alpha_prop.threshold # Transparency threshold b_mat.niftools_alpha.alphaflag = n_alpha_prop.flags return b_mat
def get_n_apply_mode_from_b_blend_type(self, 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 get_b_interp_from_n_interp(self, n_ipol): if n_ipol == NifFormat.KeyType.LINEAR_KEY: return "LINEAR" elif n_ipol == NifFormat.KeyType.QUADRATIC_KEY: return "BEZIER" elif n_ipol == 0: # guessing, not documented in nif.xml return "CONSTANT" NifLog.warn( "Unsupported interpolation mode ({0}) in nif, using quadratic/bezier." .format(n_ipol)) return "BEZIER"
def get_n_interp_from_b_interp(self, 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( "Unsupported interpolation mode ({0}) in blend, using quadratic/bezier." .format(b_ipol)) return NifFormat.KeyType.QUADRATIC_KEY
def export_tex_desc(self, texdesc=None, uvlayers=None, b_mat_texslot=None): """Helper function for export_texturing_property to export each texture slot.""" try: texdesc.uv_set = uvlayers.index( b_mat_texslot.uv_layer) if b_mat_texslot.uv_layer else 0 except ValueError: # mtex.uv_layer not in uvlayers list NifLog.warn( "Bad uv layer name '{0}' in texture '{1}'. Using first uv layer" .format(b_mat_texslot.uv_layer, b_mat_texslot.texture.name)) texdesc.uv_set = 0 # assume 0 is active layer texdesc.source = self.export_source_texture(b_mat_texslot.texture)
def import_texture_source(self, source): """Convert a NiSourceTexture block, or simply a path string, to a Blender Texture object, return the Texture object and stores it in the self.nif_import.dict_textures dictionary to avoid future duplicate imports. """ # if the source block is not linked then return None if not source: return None # calculate the texture hash key texture_hash = self.get_texture_hash(source) try: # look up the texture in the dictionary of imported textures # and return it if found return self.nif_import.dict_textures[texture_hash] except KeyError: pass b_image = None if (isinstance(source, NifFormat.NiSourceTexture) and not source.use_external): fn, b_image = self.import_embedded_texture_source(source) else: fn, b_image = self.import_source(source) # create a stub image if the image could not be loaded b_text_name = os.path.basename(fn) if not b_image: NifLog.warn( "Texture '{0}' not found or not supported and no alternate available" .format(fn)) b_image = bpy.data.images.new(name=b_text_name, width=1, height=1, alpha=False) b_image.filepath = fn # create a texture b_texture = bpy.data.textures.new(name=b_text_name, type='IMAGE') b_texture.image = b_image b_texture.use_interpolation = True b_texture.use_mipmap = True # save texture to avoid duplicate imports, and return it self.nif_import.dict_textures[texture_hash] = b_texture return b_texture
def import_shader_by_type(self, b_mat, shader_tex_desc, extra_shader_index): if extra_shader_index == 0: # EnvironmentMapIndex if shader_tex_desc.texture_data.source.file_name.lower( ).startswith("rrt_engine_env_map"): # sid meier's railroads: env map generated by engine # we can skip this NifLog.info( "Skipping environment map texture. Env Map is generated by Engine" ) envTexDesc = shader_tex_desc.texture_data self.has_envtex = True self.env_map = self.import_image_texture(b_mat, envTexDesc) elif extra_shader_index == 1: # NormalMapIndex bumpTexDesc = shader_tex_desc.texture_data self.has_bumptex = True self.bump_map = self.import_image_texture(b_mat, bumpTexDesc) elif extra_shader_index == 2: # SpecularIntensityIndex glossTexDesc = shader_tex_desc.texture_data self.has_glosstex = True self.gloss_map = self.import_image_texture(b_mat, glossTexDesc) elif extra_shader_index == 3: # EnvironmentIntensityIndex (this is reflection) refTexDesc = shader_tex_desc.texture_data self.has_reftex = True self.reflection_map = self.reflection_map = self.import_image_texture( b_mat, refTexDesc) elif extra_shader_index == 4: # LightCubeMapIndex if shader_tex_desc.texture_data.source.file_name.lower( ).startswith("rrt_cube_light_map"): # sid meier's railroads: light map generated by engine # we can skip this NifLog.info("Ignoring Env Map as generated by Engine") NifLog.warn("Skipping light cube texture.") elif extra_shader_index == 5: # ShadowTextureIndex NifLog.warn("Skipping shadow texture.") else: NifLog.warn("Unknown texture type found in extra_shader_index")
def __init__(self, operator): """Common initialization functions for executing the import/export operators: """ NifOp.init(operator) # print scripts info from . import bl_info niftools_ver = (".".join(str(i) for i in bl_info["version"])) NifLog.info( "Executing - Niftools : Blender Nif Plugin v{0} (running on Blender {1}, PyFFI {2})" .format(niftools_ver, bpy.app.version_string, pyffi.__version__)) # 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[:]
def import_bhk_shape(self, bhkshape): """Imports any supported collision shape as list of blender meshes.""" if self.nif_import.data._user_version_value_._value == 12: if self.nif_import.data._user_version_2_value_._value == 83: self.HAVOK_SCALE = self.nif_import.HAVOK_SCALE * 10 else: self.HAVOK_SCALE = self.nif_import.HAVOK_SCALE if isinstance(bhkshape, NifFormat.bhkTransformShape): return self.import_bhktransform(bhkshape) elif isinstance(bhkshape, NifFormat.bhkRigidBody): return self.import_bhkridgidbody(bhkshape) elif isinstance(bhkshape, NifFormat.bhkBoxShape): return self.import_bhkbox_shape(bhkshape) elif isinstance(bhkshape, NifFormat.bhkSphereShape): return self.import_bhksphere_shape(bhkshape) elif isinstance(bhkshape, NifFormat.bhkCapsuleShape): return self.import_bhkcapsule_shape(bhkshape) elif isinstance(bhkshape, NifFormat.bhkConvexVerticesShape): return self.import_bhkconvex_vertices_shape(bhkshape) elif isinstance(bhkshape, NifFormat.bhkPackedNiTriStripsShape): return self.import_bhkpackednitristrips_shape(bhkshape) elif isinstance(bhkshape, NifFormat.bhkNiTriStripsShape): self.havok_mat = bhkshape.material return reduce(operator.add, (self.import_bhk_shape(strips) for strips in bhkshape.strips_data)) elif isinstance(bhkshape, NifFormat.NiTriStripsData): return self.import_nitristrips(bhkshape) elif isinstance(bhkshape, NifFormat.bhkMoppBvTreeShape): return self.import_bhk_shape(bhkshape.shape) elif isinstance(bhkshape, NifFormat.bhkListShape): return reduce(operator.add, (self.import_bhk_shape(subshape) for subshape in bhkshape.sub_shapes)) NifLog.warn("Unsupported bhk shape {0}".format( bhkshape.__class__.__name__)) return []
def get_b_blend_type_from_n_apply_mode(self, n_apply_mode): # TODO: - Check out n_apply_modes if n_apply_mode == NifFormat.ApplyMode.APPLY_MODULATE: return "MIX" elif n_apply_mode == NifFormat.ApplyMode.APPLY_REPLACE: return "COLOR" elif n_apply_mode == NifFormat.ApplyMode.APPLY_DECAL: return "OVERLAY" elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT: return "LIGHTEN" elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT2: # used by Oblivion for parallax return "MULTIPLY" else: NifLog.warn( "Unknown apply mode (%i) in material, using blend type 'MIX'". format(n_apply_mode)) return "MIX"
def import_bone_animation(self, n_block, b_armature_obj, bone_name): """ Imports an animation contained in the NIF itself. """ if NifOp.props.animation: NifLog.debug('Importing animation for bone %s'.format(bone_name)) bone_bm = nif_utils.import_matrix(n_block) # base pose niBone_bind_scale, niBone_bind_rot, niBone_bind_trans = nif_utils.decompose_srt( bone_bm) niBone_bind_rot_inv = niBone_bind_rot.to_4x4().inverted() kfc = nif_utils.find_controller(n_block, NifFormat.NiKeyframeController) self.import_keyframe_controller(kfc, b_armature_obj, bone_name, niBone_bind_scale, niBone_bind_rot_inv, niBone_bind_trans)
def set_frames_per_second(self, roots): """Scan all blocks and set a reasonable number for FPS to this class and the scene.""" # find all key times key_times = [] for root in roots: for kfd in root.tree(block_type=NifFormat.NiKeyframeData): key_times.extend(key.time for key in kfd.translations.keys) key_times.extend(key.time for key in kfd.scales.keys) key_times.extend(key.time for key in kfd.quaternion_keys) key_times.extend(key.time for key in kfd.xyz_rotations[0].keys) key_times.extend(key.time for key in kfd.xyz_rotations[1].keys) key_times.extend(key.time for key in kfd.xyz_rotations[2].keys) for kfi in root.tree(block_type=NifFormat.NiBSplineInterpolator): if not kfi.basis_data: # skip bsplines without basis data (eg bowidle.kf in # Oblivion) continue key_times.extend( point * (kfi.stop_time - kfi.start_time) / (kfi.basis_data.num_control_points - 2) for point in range(kfi.basis_data.num_control_points - 2)) for uvdata in root.tree(block_type=NifFormat.NiUVData): for uvgroup in uvdata.uv_groups: key_times.extend(key.time for key in uvgroup.keys) fps = self.fps # not animated, return a reasonable default if not key_times: return # calculate FPS key_times = sorted(set(key_times)) lowest_diff = sum( abs(int(time * fps + 0.5) - (time * fps)) for time in key_times) # for test_fps in range(1,120): #disabled, used for testing for test_fps in [20, 24, 25, 35]: diff = sum( abs(int(time * test_fps + 0.5) - (time * test_fps)) for time in key_times) if diff < lowest_diff: lowest_diff = diff fps = test_fps NifLog.info("Animation estimated at %i frames per second." % fps) self.fps = fps bpy.context.scene.render.fps = fps bpy.context.scene.frame_set(0)
def export_source_texture(self, texture=None, filename=None): """Export a NiSourceTexture. :param texture: The texture object in blender to be exported. :param filename: The full or relative path to the texture file (this argument is used when exporting NiFlipControllers and when exporting default shader slots that have no use in being imported into Blender). :return: The exported NiSourceTexture block. """ # create NiSourceTexture srctex = NifFormat.NiSourceTexture() srctex.use_external = True if not filename is None: # preset filename srctex.file_name = filename elif not texture is None: srctex.file_name = self.export_texture_filename(texture) else: # this probably should not happen NifLog.warn( "Exporting source texture without texture or filename (bug?).") # fill in default values (TODO: can we use 6 for everything?) if bpy.context.scene.niftools_scene.nif_version >= 0x0A000100: srctex.pixel_layout = 6 else: srctex.pixel_layout = 5 srctex.use_mipmaps = 1 srctex.alpha_format = 3 srctex.unknown_byte = 1 # search for duplicate for block in self.nif_export.nif_export.dict_blocks: if isinstance(block, NifFormat.NiSourceTexture) and block.get_hash( ) == srctex.get_hash(): return block # no identical source texture found, so use and register # the new one return self.nif_export.nif_export.objecthelper.register_block( srctex, texture)
def import_object_vis_controller(self, n_node, b_obj): """Import vis controller for blender object.""" n_vis_ctrl = nif_utils.find_controller(n_node, NifFormat.NiVisController) if not (n_vis_ctrl and n_vis_ctrl.data): return NifLog.info("Importing vis controller") b_obj_action = self.nif_import.animationhelper.create_action( b_obj, b_obj.name + "-Anim") fcurves = create_fcurves(b_obj_action, "hide", (0, )) for key in n_vis_ctrl.data.keys: self.nif_import.animationhelper.add_key(fcurves, key.time, (key.value, ), "CONSTANT") #get extrapolation from flags and set it to fcurves self.nif_import.animationhelper.set_extrapolation( n_vis_ctrl.flags, fcurves)
def export_collision(self, b_obj, parent_block): """Main function for adding collision object b_obj to a node.""" if NifOp.props.game == 'MORROWIND': if b_obj.game.collision_bounds_type != 'TRIANGLE_MESH': raise nif_utils.NifError( "Morrowind only supports Triangle Mesh collisions.") node = self.objecthelper.create_block("RootCollisionNode", b_obj) parent_block.add_child(node) node.flags = 0x0003 # default self.objecthelper.set_object_matrix(b_obj, node) for child in b_obj.children: self.objecthelper.export_node(child, node, None) elif NifOp.props.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): nodes = [parent_block] nodes.extend([ block for block in parent_block.children if block.name[:14] == 'collisiondummy' ]) for node in nodes: try: self.bhkshapehelper.export_collision_helper(b_obj, node) break except ValueError: # adding collision failed continue else: # all nodes failed so add new one node = self.objecthelper.create_ninode(b_obj) node.set_transform(self.IDENTITY44) node.name = 'collisiondummy%i' % parent_block.num_children if b_obj.niftools.objectflags != 0: node_flag_hex = hex(b_obj.niftools.objectflags) else: node_flag_hex = 0x000E # default node.flags = node_flag_hex parent_block.add_child(node) self.bhkshapehelper.export_collision_helper(b_obj, node) else: NifLog.warn( "Only Morrowind, Oblivion, and Fallout 3 collisions are supported, skipped collision object '{0}'" .format(b_obj.name))
def import_material_alpha_controller(self, b_material, n_geom): # find alpha controller n_matprop = nif_utils.find_property(n_geom, NifFormat.NiMaterialProperty) if not n_matprop: return n_alphactrl = nif_utils.find_controller(n_matprop, NifFormat.NiAlphaController) if not (n_alphactrl and n_alphactrl.data): return NifLog.info("Importing alpha controller") b_mat_action = self.nif_import.animationhelper.create_action( b_material, "MaterialAction") fcurves = create_fcurves(b_mat_action, "alpha", (0, )) interp = self.nif_import.animationhelper.get_b_interp_from_n_interp( n_alphactrl.data.data.interpolation) self.nif_import.animationhelper.set_extrapolation( n_alphactrl.flags, fcurves) for key in n_alphactrl.data.data.keys: self.nif_import.animationhelper.add_key(fcurves, key.time, (key.value, ), interp)
def complete_bone_tree(self, bone, skelroot): """Make sure that the bones actually form a tree all the way down to the armature node. Call this function on all bones of a skin instance. """ # we must already have marked this one as a bone assert skelroot in self.nif_import.dict_armatures # debug assert bone in self.nif_import.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.nif_import.dict_armatures[skelroot]: # neither is it marked as a bone: so mark the parent as a bone self.nif_import.dict_armatures[skelroot].append(boneparent) # store the coordinates for realignement autodetection NifLog.debug("'{0}' is a bone of armature '{1}'".format( boneparent.name, 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 decompose_srt(matrix): """Decompose Blender transform matrix as a scale, rotation matrix, and translation vector.""" # get matrix components trans_vec, rot_quat, scale_vec = matrix.decompose() #obtain a combined scale and rotation matrix to test determinate rotmat = rot_quat.to_matrix() scalemat = mathutils.Matrix( ((scale_vec[0], 0.0, 0.0), (0.0, scale_vec[1], 0.0), (0.0, 0.0, scale_vec[2]))) scale_rot = scalemat * rotmat # and fix their sign if (scale_rot.determinant() < 0): scale_vec.negate() # only uniform scaling # allow rather large error to accomodate some nifs if abs(scale_vec[0] - scale_vec[1]) + abs(scale_vec[1] - scale_vec[2]) > 0.02: NifLog.warn("Non-uniform scaling not supported." + " Workaround: apply size and rotation (CTRL-A).") return [scale_vec[0], rotmat, trans_vec]
def import_embedded_texture_source(self, source): fn = None # find a file name (but avoid overwriting) n = 0 while True: fn = "image%03i.dds" % n tex = os.path.join(os.path.dirname(NifOp.props.filepath), fn) if not os.path.exists(tex): break n += 1 if self.nif_import.IMPORT_EXPORTEMBEDDEDTEXTURES: # save embedded texture as dds file stream = open(tex, "wb") try: NifLog.info("Saving embedded texture as {0}".format(tex)) source.pixel_data.save_as_dds(stream) except ValueError: # value error means that the pixel format is not supported b_image = None else: # saving dds succeeded so load the file b_image = bpy.ops.image.open(tex) # Blender will return an image object even if the # file format is not supported, # so to check if the image is actually loaded an error # is forced via "b_image.size" try: b_image.size except: # RuntimeError: couldn't load image data in Blender b_image = None # not supported, delete image object finally: stream.close() else: b_image = None return [fn, b_image]
def load_kf(file_path): """Loads a Kf file from the given path""" NifLog.info("Loading {0}".format(file_path)) kf_file = NifFormat.Data() # open keyframe file for binary reading with open(file_path, "rb") as kf_stream: # check if nif file is valid kf_file.inspect_version_only(kf_stream) if kf_file.version >= 0: # it is valid, so read the file NifLog.info("KF file version: {0}".format(kf_file.version, "x")) NifLog.info("Reading keyframe file") kf_file.read(kf_stream) elif kf_file.version == -1: raise NifError("Unsupported KF version.") else: raise NifError("Not a KF file.") return kf_file
def load_nif(file_path): """Loads a nif from the given file path""" NifLog.info("Importing {0}".format(file_path)) nif_data = NifFormat.Data() # open file for binary reading with open(file_path, "rb") as nif_stream: # check if nif file is valid nif_data.inspect_version_only(nif_stream) if nif_data.version >= 0: # it is valid, so read the file NifLog.info("NIF file version: {0}".format( nif_data.version, "x")) NifLog.info("Reading file") nif_data.read(nif_stream) elif nif_data.version == -1: raise NifError("Unsupported NIF version.") else: raise NifError("Not a NIF file.") return nif_data
def load_egm(file_path): """Loads an egm file from the given path""" NifLog.info("Loading {0}".format(file_path)) egm_file = EgmFormat.Data() # open keyframe file for binary reading with open(file_path, "rb") as egm_stream: # check if nif file is valid egm_file.inspect_version_only(egm_stream) if egm_file.version >= 0: # it is valid, so read the file NifLog.info("EGM file version: {0}".format( egm_file.version, "x")) NifLog.info("Reading FaceGen egm file") egm_file.read(egm_stream) elif egm_file.version == -1: raise NifError("Unsupported EGM version.") else: raise NifError("Not a EGM file.") return egm_file
def export_text_keys( self, block_parent, ): """Parse the animation groups buffer and write an extra string data block, and attach it to an existing block (typically, the root of the nif tree).""" if NifOp.props.animation == 'GEOM_NIF': # animation group extra data is not present in geometry only files return if "Anim" not in bpy.data.texts: return animtxt = bpy.data.texts["Anim"] NifLog.info("Exporting animation groups") # -> get animation groups information # parse the anim text descriptor # the format is: # frame/string1[/string2[.../stringN]] # example: # 001/Idle: Start/Idle: Stop/Idle2: Start/Idle2: Loop Start # 051/Idle2: Stop/Idle3: Start # 101/Idle3: Loop Start/Idle3: Stop slist = animtxt.asLines() flist = [] dlist = [] for s in slist: # ignore empty lines if not s: continue # parse line t = s.split('/') if (len(t) < 2): raise nif_utils.NifError("Syntax error in Anim buffer ('%s')" % s) f = int(t[0]) if ((f < bpy.context.scene.frame_start) or (f > bpy.context.scene.frame_end)): NifLog.warn( "Frame in animation buffer out of range ({0} not between [{1}, {2}])" .format(str(f), str(bpy.context.scene.frame_start), str(bpy.context.scene.frame_end))) d = t[1].strip() for i in range(2, len(t)): d = d + '\r\n' + t[i].strip() #print 'frame %d'%f + ' -> \'%s\''%d # debug flist.append(f) dlist.append(d) # -> now comes the real export # add a NiTextKeyExtraData block, and refer to this block in the # parent node (we choose the root block) textextra = self.nif_export.objecthelper.create_block( "NiTextKeyExtraData", animtxt) block_parent.add_extra_data(textextra) # create a text key for each frame descriptor textextra.num_text_keys = len(flist) textextra.text_keys.update_size() for i, key in enumerate(textextra.text_keys): key.time = flist[i] / self.fps key.value = dlist[i] return textextra
def import_constraint(self, hkbody): """Imports a bone havok constraint as Blender object constraint.""" assert (isinstance(hkbody, NifFormat.bhkRigidBody)) # check for constraints if not hkbody.constraints: return # find objects if len(self.nif_import.dict_havok_objects[hkbody]) != 1: NifLog.warn( "Rigid body with no or multiple shapes, constraints skipped") return b_hkobj = self.nif_import.dict_havok_objects[hkbody][0] NifLog.info("Importing constraints for %s" % b_hkobj.name) # now import all constraints for hkconstraint in hkbody.constraints: # check constraint entities if not hkconstraint.num_entities == 2: NifLog.warn("Constraint with more than 2 entities, skipped") continue if not hkconstraint.entities[0] is hkbody: NifLog.warn("First constraint entity not self, skipped") continue if not hkconstraint.entities[ 1] in self.nif_import.dict_havok_objects: NifLog.warn("Second constraint entity not imported, skipped") continue # get constraint descriptor if isinstance(hkconstraint, NifFormat.bhkRagdollConstraint): hkdescriptor = hkconstraint.ragdoll b_hkobj.rigid_body.enabled = True elif isinstance(hkconstraint, NifFormat.bhkLimitedHingeConstraint): hkdescriptor = hkconstraint.limited_hinge b_hkobj.rigid_body.enabled = True elif isinstance(hkconstraint, NifFormat.bhkHingeConstraint): hkdescriptor = hkconstraint.hinge b_hkobj.rigid_body.enabled = True elif isinstance(hkconstraint, NifFormat.bhkMalleableConstraint): if hkconstraint.type == 7: hkdescriptor = hkconstraint.ragdoll b_hkobj.rigid_body.enabled = False elif hkconstraint.type == 2: hkdescriptor = hkconstraint.limited_hinge b_hkobj.rigid_body.enabled = False else: NifLog.warn("Unknown malleable type ({0}), skipped".format( str(hkconstraint.type))) # extra malleable constraint settings ### damping parameters not yet in Blender Python API ### tau (force between bodies) not supported by Blender else: NifLog.warn("Unknown constraint type ({0}), skipped".format( hkconstraint.__class__.__name__)) continue # add the constraint as a rigid body joint b_constr = b_hkobj.constraints.new('RIGID_BODY_JOINT') b_constr.name = b_hkobj.name b_constr.show_pivot = True # note: rigidbodyjoint parameters (from Constraint.c) # CONSTR_RB_AXX 0.0 # CONSTR_RB_AXY 0.0 # CONSTR_RB_AXZ 0.0 # CONSTR_RB_EXTRAFZ 0.0 # CONSTR_RB_MAXLIMIT0 0.0 # CONSTR_RB_MAXLIMIT1 0.0 # CONSTR_RB_MAXLIMIT2 0.0 # CONSTR_RB_MAXLIMIT3 0.0 # CONSTR_RB_MAXLIMIT4 0.0 # CONSTR_RB_MAXLIMIT5 0.0 # CONSTR_RB_MINLIMIT0 0.0 # CONSTR_RB_MINLIMIT1 0.0 # CONSTR_RB_MINLIMIT2 0.0 # CONSTR_RB_MINLIMIT3 0.0 # CONSTR_RB_MINLIMIT4 0.0 # CONSTR_RB_MINLIMIT5 0.0 # CONSTR_RB_PIVX 0.0 # CONSTR_RB_PIVY 0.0 # CONSTR_RB_PIVZ 0.0 # CONSTR_RB_TYPE 12 # LIMIT 63 # PARSIZEY 63 # TARGET [Object "capsule.002"] # limit 3, 4, 5 correspond to angular limits along x, y and z # and are measured in degrees # pivx/y/z is the pivot point # set constraint target b_constr.target = \ self.nif_import.dict_havok_objects[hkconstraint.entities[1]][0] # set rigid body type (generic) b_constr.pivot_type = 'GENERIC_6_DOF' # limiting parameters (limit everything) b_constr.use_angular_limit_x = True b_constr.use_angular_limit_y = True b_constr.use_angular_limit_z = True # get pivot point pivot = mathutils.Vector( (hkdescriptor.pivot_a.x * self.HAVOK_SCALE, hkdescriptor.pivot_a.y * self.HAVOK_SCALE, hkdescriptor.pivot_a.z * self.HAVOK_SCALE)) # get z- and x-axes of the constraint # (also see export_nif.py NifImport.export_constraints) if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): b_constr.pivot_type = 'CONE_TWIST' # for ragdoll, take z to be the twist axis (central axis of the # cone, that is) axis_z = mathutils.Vector( (hkdescriptor.twist_a.x, hkdescriptor.twist_a.y, hkdescriptor.twist_a.z)) # for ragdoll, let x be the plane vector axis_x = mathutils.Vector( (hkdescriptor.plane_a.x, hkdescriptor.plane_a.y, hkdescriptor.plane_a.z)) # set the angle limits # (see http://niftools.sourceforge.net/wiki/Oblivion/Bhk_Objects/Ragdoll_Constraint # for a nice picture explaining this) b_constr.limit_angle_min_x = \ hkdescriptor.plane_min_angle b_constr.limit_angle_max_x = \ hkdescriptor.plane_max_angle b_constr.limit_angle_min_y = \ -hkdescriptor.cone_max_angle b_constr.limit_angle_max_y = \ hkdescriptor.cone_max_angle b_constr.limit_angle_min_z = \ hkdescriptor.twist_min_angle b_constr.limit_angle_max_z = \ hkdescriptor.twist_max_angle b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction elif isinstance(hkdescriptor, NifFormat.LimitedHingeDescriptor): # for hinge, y is the vector on the plane of rotation defining # the zero angle axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x, hkdescriptor.perp_2_axle_in_a_1.y, hkdescriptor.perp_2_axle_in_a_1.z)) # for hinge, take x to be the the axis of rotation # (this corresponds with Blender's convention for hinges) axis_x = mathutils.Vector( (hkdescriptor.axle_a.x, hkdescriptor.axle_a.y, hkdescriptor.axle_a.z)) # for hinge, z is the vector on the plane of rotation defining # the positive direction of rotation axis_z = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_2.x, hkdescriptor.perp_2_axle_in_a_2.y, hkdescriptor.perp_2_axle_in_a_2.z)) # they should form a orthogonal basis if (mathutils.Vector.cross(axis_x, axis_y) - axis_z).length > 0.01: # either not orthogonal, or negative orientation if (mathutils.Vector.cross(-axis_x, axis_y) - axis_z).length > 0.01: NifLog.warn( "Axes are not orthogonal in {0}; Arbitrary orientation has been chosen" .format(hkdescriptor.__class__.__name__)) axis_z = mathutils.Vector.cross(axis_x, axis_y) else: # fix orientation NifLog.warn( "X axis flipped in {0} to fix orientation".format( hkdescriptor.__class__.__name__)) axis_x = -axis_x # getting properties with no blender constraint # equivalent and setting as obj properties b_constr.limit_angle_max_x = hkdescriptor.max_angle b_constr.limit_angle_min_x = hkdescriptor.min_angle b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction if hasattr(hkconstraint, "tau"): b_hkobj.niftools_constraint.tau = hkconstraint.tau b_hkobj.niftools_constraint.damping = hkconstraint.damping elif isinstance(hkdescriptor, NifFormat.HingeDescriptor): # for hinge, y is the vector on the plane of rotation defining # the zero angle axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x, hkdescriptor.perp_2_axle_in_a_1.y, hkdescriptor.perp_2_axle_in_a_1.z)) # for hinge, z is the vector on the plane of rotation defining # the positive direction of rotation axis_z = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_2.x, hkdescriptor.perp_2_axle_in_a_2.y, hkdescriptor.perp_2_axle_in_a_2.z)) # take x to be the the axis of rotation # (this corresponds with Blender's convention for hinges) axis_x = mathutils.Vector.cross(axis_y, axis_z) b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction else: raise ValueError("unknown descriptor %s" % hkdescriptor.__class__.__name__) # transform pivot point and constraint matrix into object # coordinates # (also see export_nif.py NifImport.export_constraints) # the pivot point v is in hkbody coordinates # however blender expects it in object coordinates, v' # v * R * B = v' * O * T * B' # with R = rigid body transform (usually unit tf) # B = nif bone matrix # O = blender object transform # T = bone tail matrix (translation in Y direction) # B' = blender bone matrix # so we need to cancel out the object transformation by # v' = v * R * B * B'^{-1} * T^{-1} * O^{-1} # the local rotation L at the pivot point must be such that # (axis_z + v) * R * B = ([0 0 1] * L + v') * O * T * B' # so (taking the rotation parts of all matrices!!!) # [0 0 1] * L = axis_z * R * B * B'^{-1} * T^{-1} * O^{-1} # and similarly # [1 0 0] * L = axis_x * R * B * B'^{-1} * T^{-1} * O^{-1} # hence these give us the first and last row of L # which is exactly enough to provide the euler angles # multiply with rigid body transform if isinstance(hkbody, NifFormat.bhkRigidBodyT): # set rotation transform = mathutils.Quaternion( (hkbody.rotation.w, hkbody.rotation.x, hkbody.rotation.y, hkbody.rotation.z)).to_matrix() transform.resize_4x4() # set translation transform[0][3] = hkbody.translation.x * self.HAVOK_SCALE transform[1][3] = hkbody.translation.y * self.HAVOK_SCALE transform[2][3] = hkbody.translation.z * self.HAVOK_SCALE # apply transform pivot = pivot * transform transform = transform.to_3x3() axis_z = axis_z * transform axis_x = axis_x * transform # TODO: update this to use the new bone system # # next, cancel out bone matrix correction # # note that B' = X * B with X = self.nif_import.dict_bones_extra_matrix[B] # # so multiply with the inverse of X # for niBone in self.nif_import.dict_bones_extra_matrix: # if niBone.collision_object \ # and niBone.collision_object.body is hkbody: # transform = mathutils.Matrix( # self.nif_import.dict_bones_extra_matrix[niBone]) # transform.invert() # pivot = pivot * transform # transform = transform.to_3x3() # axis_z = axis_z * transform # axis_x = axis_x * transform # break # cancel out bone tail translation if b_hkobj.parent_bone: pivot[1] -= b_hkobj.parent.data.bones[ b_hkobj.parent_bone].length # cancel out object transform transform = mathutils.Matrix(b_hkobj.matrix_local) transform.invert() pivot = pivot * transform transform = transform.to_3x3() axis_z = axis_z * transform axis_x = axis_x * transform # set pivot point b_constr.pivot_x = pivot[0] b_constr.pivot_y = pivot[1] b_constr.pivot_z = pivot[2] # set euler angles constr_matrix = mathutils.Matrix( (axis_x, mathutils.Vector.cross(axis_z, axis_x), axis_z)) constr_euler = constr_matrix.to_euler() b_constr.axis_x = constr_euler.x b_constr.axis_y = constr_euler.y b_constr.axis_z = constr_euler.z # DEBUG assert ((axis_x - mathutils.Vector( (1, 0, 0)) * constr_matrix).length < 0.0001) assert ((axis_z - mathutils.Vector( (0, 0, 1)) * constr_matrix).length < 0.0001) # the generic rigid body type is very buggy... so for simulation # purposes let's transform it into ball and hinge if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): # cone_twist b_constr.pivot_type = 'CONE_TWIST' elif isinstance( hkdescriptor, (NifFormat.LimitedHingeDescriptor, NifFormat.HingeDescriptor)): # (limited) hinge b_constr.pivot_type = 'HINGE' else: raise ValueError("unknown descriptor %s" % hkdescriptor.__class__.__name__)
def mark_armatures_bones(self, niBlock): """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 ( self.nif_import.data.version in (0x14000005, 0x14020007) and (os.path.basename(NifOp.props.filepath).lower() in ('skeleton.nif', 'skeletonbeast.nif'))): if not isinstance(niBlock, NifFormat.NiNode): raise nif_utils.NifError( "cannot import skeleton: root is not a NiNode") # for morrowind, take the Bip01 node to be the skeleton root if self.nif_import.data.version == 0x04000002: skelroot = niBlock.find(block_name='Bip01', block_type=NifFormat.NiNode) if not skelroot: skelroot = niBlock else: skelroot = niBlock if skelroot not in self.nif_import.dict_armatures: self.nif_import.dict_armatures[skelroot] = [] NifLog.info("Selecting node '%s' as skeleton root".format( skelroot.name)) # add bones for bone in skelroot.tree(): if bone is skelroot: continue if not isinstance(bone, NifFormat.NiNode): continue if self.nif_import.is_grouping_node(bone): continue if bone not in self.nif_import.dict_armatures[skelroot]: self.nif_import.dict_armatures[skelroot].append(bone) return # done! # attaching to selected armature -> first identify armature and bones elif NifOp.props.skeleton == "GEOMETRY_ONLY" and not self.nif_import.dict_armatures: skelroot = niBlock.find( block_name=self.nif_import.selected_objects[0].name) if not skelroot: raise nif_utils.NifError( "nif has no armature '%s'" % self.nif_import.selected_objects[0].name) NifLog.debug("Identified '{0}' as armature".format(skelroot.name)) self.nif_import.dict_armatures[skelroot] = [] for bone_name in self.nif_import.selected_objects[ 0].data.bones.keys(): # blender bone naming -> nif bone naming nif_bone_name = armature.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( "Identified nif block '{0}' with bone '{1}' in selected armature" .format(nif_bone_name, bone_name)) self.nif_import.dict_names[bone_block] = bone_name self.nif_import.dict_armatures[skelroot].append(bone_block) self.complete_bone_tree(bone_block, skelroot) # search for all NiTriShape or NiTriStrips blocks... if isinstance(niBlock, NifFormat.NiTriBasedGeom): # yes, we found one, get its skin instance if niBlock.is_skin(): NifLog.debug("Skin found on block '{0}'".format(niBlock.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 = niBlock.skin_instance skelroot = skininst.skeleton_root if NifOp.props.skeleton == "EVERYTHING": if skelroot not in self.nif_import.dict_armatures: self.nif_import.dict_armatures[skelroot] = [] NifLog.debug("'{0}' is an armature".format( skelroot.name)) elif NifOp.props.skeleton == "GEOMETRY_ONLY": if skelroot not in self.nif_import.dict_armatures: raise nif_utils.NifError( "nif structure incompatible with '%s' as armature:" " node '%s' has '%s' as armature" % (self.nif_import.selected_objects[0].name, niBlock.name, skelroot.name)) for boneBlock in skininst.bones: # boneBlock can be None; see pyffi issue #3114079 if not boneBlock: continue if boneBlock not in self.nif_import.dict_armatures[ skelroot]: self.nif_import.dict_armatures[skelroot].append( boneBlock) NifLog.debug( "'{0}' is a bone of armature '{1}'".format( boneBlock.name, 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 if asked if self.nif_import.IMPORT_EXTRANODES: # add bones 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 self.nif_import.is_grouping_node(bone): continue if bone not in self.nif_import.dict_armatures[ skelroot]: self.nif_import.dict_armatures[skelroot].append( bone) NifLog.debug( "'{0}' marked as extra bone of armature '{1}'". format(bone.name, skelroot.name)) # we make sure all NiNodes from this bone # all the way down to the armature NiNode # are marked as bones self.complete_bone_tree(bone, skelroot) # continue down the tree for child in niBlock.get_refs(): if not isinstance(child, NifFormat.NiAVObject): continue # skip blocks that don't have transforms self.mark_armatures_bones(child)