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 mathutils_to_nifformat_matrix(b_matrix): """Convert a blender matrix to a NifFormat.Matrix44""" # transpose to swap columns for rows so we can use pyffi's set_rows() directly # instead of setting every single value manually n_matrix = NifFormat.Matrix44() n_matrix.set_rows(*b_matrix.transposed()) return n_matrix
def build_nif_matrix(cls): n_mat = NifFormat.Matrix44() translation = (2.0, 3.0, 4.0) scale = 2.0 n_rhs_rot_x = (1.0, 0.0, 0.0, 0.0, 0.866, 0.5, 0.0, -0.5, 0.866) n_rhs_rot_y = (0.5, 0.0, -0.866, 0.0, 1.0, 0.0, 0.866, 0.0, 0.5) n_rhs_rot_z = (0, 1, 0, -1, 0, 0, 0, 0, 1) n_rhs_rot_x = cls.create_matrix(n_rhs_rot_x) n_rhs_rot_y = cls.create_matrix(n_rhs_rot_y) n_rhs_rot_z = cls.create_matrix(n_rhs_rot_z) n_mat33 = n_rhs_rot_z * n_rhs_rot_y * n_rhs_rot_x n_vec3 = NifFormat.Vector3() n_vec3.x = translation[0] n_vec3.y = translation[1] n_vec3.z = translation[2] n_mat.set_scale_rotation_translation(scale, n_mat33, n_vec3) return n_mat
def build_nif_matrix(cls): n_mat = NifFormat.Matrix44() translation = (2.0, 3.0, 4.0) scale = 2.0 rhsrotx = (1.0, 0.0, 0.0, 0.0, 0.866, 0.5, 0.0, -0.5, 0.866) rhsroty = (0.5, 0.0, -0.866, 0.0, 1.0, 0.0, 0.866, 0.0, 0.5) rhsrotz = (0, 1, 0, -1, 0, 0, 0, 0, 1) rhsrotx = cls.create_matrix(rhsrotx) rhsroty = cls.create_matrix(rhsroty) rhsrotz = cls.create_matrix(rhsrotz) n_mat33 = rhsrotz * rhsroty * rhsrotx n_vec3 = NifFormat.Vector3() n_vec3.x = translation[0] n_vec3.y = translation[1] n_vec3.z = translation[2] n_mat.set_scale_rotation_translation(scale, n_mat33, n_vec3) return n_mat
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 branchentry(self, branch): if (isinstance(branch, NifFormat.NiGeometry) and branch.skin_instance and branch.skin_instance.data): for skelroot, skeldata, bonenode, bonedata in zip( repeat(branch.skin_instance.skeleton_root), repeat(branch.skin_instance.data), branch.skin_instance.bones, branch.skin_instance.data.bone_list): for refskelroot, refskeldata, refbonenode, refbonedata \ in self.toaster.refbonedata: if bonenode.name == refbonenode.name: self.toaster.msgblockbegin("checking bone %s" % bonenode.name) # check that skeleton roots are identical if skelroot.name == refskelroot.name: # no extra transform branchtransform_extra = NifFormat.Matrix44() branchtransform_extra.set_identity() else: self.toaster.msg( "skipping: skeleton roots are not identical") self.toaster.msgblockend() continue # the following is an experimental way of # compensating for different skeleton roots # (disabled by default) # can we find skeleton root of data in reference # data? for refskelroot_branch \ in self.toaster.refdata.get_global_iterator(): if not isinstance(refskelroot_branch, NifFormat.NiAVObject): continue if skelroot.name == refskelroot_branch.name: # yes! found! #self.toaster.msg( # "found alternative in reference nif") branchtransform_extra = \ refskelroot_branch.get_transform(refskelroot).get_inverse() break else: for skelroot_ref \ in self.data.get_global_iterator(): if not isinstance(skelroot_ref, NifFormat.NiAVObject): continue if refskelroot.name == skelroot_ref.name: # yes! found! #self.toaster.msg( # "found alternative in nif") branchtransform_extra = \ skelroot_ref.get_transform(skelroot) break else: self.toaster.msgblockbegin("""\ skipping: skeleton roots are not identical and no alternative found""") self.toaster.msgblockend() continue # calculate total transform matrix that would be applied # to a vertex in the reference geometry in the position # of the reference bone reftransform = ( refbonedata.get_transform() * refbonenode.get_transform(refskelroot) * refskeldata.get_transform()) # calculate total transform matrix that would be applied # to a vertex in this branch in the position of the # reference bone branchtransform = ( bonedata.get_transform() * refbonenode.get_transform(refskelroot) # NOT a typo * skeldata.get_transform() * branchtransform_extra) # skelroot differences # compare if not self.are_matrices_equal(reftransform, branchtransform): #raise ValueError( self.toaster.msg( "transform mismatch\n%s\n!=\n%s\n" % (reftransform, branchtransform)) self.toaster.msgblockend() # stop in this branch return False else: # keep iterating return True
class NifExport(NifCommon): IDENTITY44 = NifFormat.Matrix44() IDENTITY44.set_identity() FLOAT_MIN = -3.4028234663852886e+38 FLOAT_MAX = +3.4028234663852886e+38 # TODO: - Expose via properties EXPORT_OPTIMIZE_MATERIALS = True EXPORT_OB_COLLISION_DO_NOT_USE_BLENDER_PROPERTIES = False EXPORT_BHKLISTSHAPE = False EXPORT_OB_BSXFLAGS = 2 EXPORT_OB_MASS = 10.0 EXPORT_OB_SOLID = True EXPORT_OB_MOTIONSYSTEM = 7, # MO_SYS_FIXED EXPORT_OB_UNKNOWNBYTE1 = 1 EXPORT_OB_UNKNOWNBYTE2 = 1 EXPORT_OB_QUALITYTYPE = 1 # MO_QUAL_FIXED EXPORT_OB_WIND = 0 EXPORT_OB_LAYER = 1 # static EXPORT_OB_MATERIAL = 9 # wood EXPORT_OB_PRN = "NONE" # Todo with location on character. For weapons, rings, helmets, Sheilds ect def __init__(self, operator, context): NifCommon.__init__(self, operator) # Helper systems self.bhkshapehelper = bhkshape_export(parent=self) self.boundhelper = bound_export(parent=self) self.armaturehelper = Armature(parent=self) self.animationhelper = AnimationHelper(parent=self) self.propertyhelper = PropertyHelper(parent=self) self.constrainthelper = constraint_export(parent=self) self.texturehelper = TextureHelper(parent=self) self.objecthelper = ObjectHelper(parent=self) def execute(self): """Main export function.""" if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT', toggle=False) NifLog.info("Exporting {0}".format(NifOp.props.filepath)) # TODO: ''' if NifOp.props.animation == 'ALL_NIF_XNIF_XKF' and NifOp.props.game == 'MORROWIND': # if exporting in nif+xnif+kf mode, then first export # the nif with geometry + animation, which is done by: NifOp.props.animation = 'ALL_NIF' ''' # extract directory, base name, extension directory = os.path.dirname(NifOp.props.filepath) filebase, fileext = os.path.splitext( os.path.basename(NifOp.props.filepath)) self.dict_armatures = {} self.dict_bone_priorities = {} self.dict_havok_objects = {} self.dict_names = {} self.dict_blocks = {} self.dict_block_names = [] self.dict_materials = {} self.dict_textures = {} self.dict_mesh_uvlayers = [] # if an egm is exported, this will contain the data self.egm_data = None try: # catch export errors for b_obj in bpy.data.objects: # armatures should not be in rest position if b_obj.type == 'ARMATURE': # ensure we get the mesh vertices in animation mode, # and not in rest position! b_obj.data.pose_position = 'POSE' if b_obj.type == 'MESH': # TODO - Need to implement correct armature checking # b_armature_modifier = None b_obj_name = b_obj.name if b_obj.parent: for b_mod in bpy.data.objects[b_obj_name].modifiers: if b_mod.type == 'ARMATURE': # b_armature_modifier = b_mod.name if b_mod.use_bone_envelopes: raise nif_utils.NifError( "'%s': Cannot export envelope skinning. If you have vertex groups, turn off envelopes.\n" "If you don't have vertex groups, select the bones one by one press W to " "convert their envelopes to vertex weights, and turn off envelopes." % b_obj.name) # if not b_armature_modifier: # raise nif_utils.NifError("'%s': is parented but does not have" # " the armature modifier set. This will" # " cause animations to fail." # % b_obj.name) # check for non-uniform transforms # (lattices are not exported so ignore them as they often tend # to have non-uniform scaling) if b_obj.type != 'LATTICE': scale = b_obj.matrix_local.to_scale() if abs(scale.x - scale.y) > NifOp.props.epsilon or abs( scale.y - scale.z) > NifOp.props.epsilon: raise nif_utils.NifError( "Non-uniform scaling not supported.\n " "Workaround: apply size and rotation (CTRL-A) on '%s'." % b_obj.name) root_name = filebase # get the root object from selected object # only export empties, meshes, and armatures if not bpy.context.selected_objects: raise nif_utils.NifError( "Please select the object(s) to export, and run this script again." ) root_objects = set() export_types = ('EMPTY', 'MESH', 'ARMATURE') exportable_objects = [ b_obj for b_obj in bpy.context.selected_objects if b_obj.type in export_types ] for root_object in exportable_objects: while root_object.parent: root_object = root_object.parent if NifOp.props.game in ('CIVILIZATION_IV', 'OBLIVION', 'FALLOUT_3', 'ZOO_TYCOON_2'): if (root_object.type == 'ARMATURE') or (root_object.name.lower() == "bip01"): root_name = 'Scene Root' # TODO remove as already filtered if root_object.type not in export_types: raise nif_utils.NifError( "Root object (%s) must be an 'EMPTY', 'MESH', or 'ARMATURE' object." % root_object.name) root_objects.add(root_object) # smooth seams of objects if NifOp.props.smooth_object_seams: self.objecthelper.mesh_helper.smooth_mesh_seams( bpy.context.scene.objects) # TODO: use Blender actions for animation groups # check for animation groups definition in a text buffer 'Anim' try: animtxt = None # Changed for testing needs fix bpy.data.texts["Anim"] except NameError: animtxt = None # rebuild the full name dictionary from the 'FullNames' text buffer self.objecthelper.rebuild_full_names() # export nif: # ----------- NifLog.info("Exporting") # find nif version to write # TODO Move fully to scene level self.version = NifOp.op.version[NifOp.props.game] self.user_version, self.user_version_2 = scene_export.get_version_info( NifOp.props) #the axes used for bone correction depend on the nif version armature.set_bone_correction_from_version(self.version) # create a nif object # export the root node (the name is fixed later to avoid confusing the # exporter with duplicate names) root_block = self.objecthelper.export_node(None, None, '') # TODO Move to object system and redo # export objects NifLog.info("Exporting objects") for root_object in root_objects: if NifOp.props.game in 'SKYRIM': if root_object.niftools_bs_invmarker: for extra_item in root_block.extra_data_list: if isinstance(extra_item, NifFormat.BSInvMarker): raise nif_utils.NifError( "Multiple Items have Inventory marker data only one item may contain this data" ) else: n_extra_list = NifFormat.BSInvMarker() n_extra_list.name = root_object.niftools_bs_invmarker[ 0].name.encode() n_extra_list.rotation_x = root_object.niftools_bs_invmarker[ 0].bs_inv_x n_extra_list.rotation_y = root_object.niftools_bs_invmarker[ 0].bs_inv_y n_extra_list.rotation_z = root_object.niftools_bs_invmarker[ 0].bs_inv_z n_extra_list.zoom = root_object.niftools_bs_invmarker[ 0].bs_inv_zoom root_block.add_extra_data(n_extra_list) # export the root objects as a NiNodes; their children are # exported as well self.objecthelper.export_node(root_object, root_block, root_object.name) # post-processing: # ---------------- # if we exported animations, but no animation groups are defined, # define a default animation group NifLog.info("Checking animation groups") if not animtxt: has_controllers = False for block in self.dict_blocks: # has it a controller field? if isinstance(block, NifFormat.NiObjectNET): if block.controller: has_controllers = True break if has_controllers: NifLog.info("Defining default animation group.") # write the animation group text buffer animtxt = bpy.data.texts.new("Anim") animtxt.write( "%i/Idle: Start/Idle: Loop Start\n%i/Idle: Loop Stop/Idle: Stop" % (bpy.context.scene.frame_start, bpy.context.scene.frame_end)) # animations without keyframe animations crash the TESCS # if we are in that situation, add a trivial keyframe animation NifLog.info("Checking controllers") if animtxt and NifOp.props.game == 'MORROWIND': has_keyframecontrollers = False for block in self.dict_blocks: if isinstance(block, NifFormat.NiKeyframeController): has_keyframecontrollers = True break if ((not has_keyframecontrollers) and (not NifOp.props.bs_animation_node)): NifLog.info("Defining dummy keyframe controller") # add a trivial keyframe controller on the scene root self.animationhelper.export_keyframes(root_block) if NifOp.props.bs_animation_node and NifOp.props.game == 'MORROWIND': for block in self.dict_blocks: if isinstance(block, NifFormat.NiNode): # if any of the shape children has a controller # or if the ninode has a controller # convert its type if block.controller or any( child.controller for child in block.children if isinstance(child, NifFormat.NiGeometry)): new_block = NifFormat.NiBSAnimationNode().deepcopy( block) # have to change flags to 42 to make it work new_block.flags = 42 root_block.replace_global_node(block, new_block) if root_block is block: root_block = new_block # oblivion skeleton export: check that all bones have a # transform controller and transform interpolator if NifOp.props.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM') and filebase.lower() in ( 'skeleton', 'skeletonbeast'): # here comes everything that is Oblivion skeleton export specific NifLog.info( "Adding controllers and interpolators for skeleton") for block in list(self.dict_blocks.keys()): if isinstance(block, NifFormat.NiNode ) and block.name.decode() == "Bip01": for bone in block.tree(block_type=NifFormat.NiNode): ctrl = self.objecthelper.create_block( "NiTransformController") interp = self.objecthelper.create_block( "NiTransformInterpolator") ctrl.interpolator = interp bone.add_controller(ctrl) ctrl.flags = 12 ctrl.frequency = 1.0 ctrl.phase = 0.0 ctrl.start_time = self.FLOAT_MAX ctrl.stop_time = self.FLOAT_MIN interp.translation.x = bone.translation.x interp.translation.y = bone.translation.y interp.translation.z = bone.translation.z scale, quat = bone.rotation.get_scale_quat() interp.rotation.x = quat.x interp.rotation.y = quat.y interp.rotation.z = quat.z interp.rotation.w = quat.w interp.scale = bone.scale else: # here comes everything that should be exported EXCEPT # for Oblivion skeleton exports # export animation groups (not for skeleton.nif export!) if animtxt: # TODO: removed temorarily to process bseffectshader export anim_textextra = None # self.animationhelper.export_text_keys(root_block) else: anim_textextra = None # oblivion and Fallout 3 furniture markers if NifOp.props.game in ( 'OBLIVION', 'FALLOUT_3', 'SKYRIM') and filebase[:15].lower() == 'furnituremarker': # exporting a furniture marker for Oblivion/FO3 try: furniturenumber = int(filebase[15:]) except ValueError: raise nif_utils.NifError( "Furniture marker has invalid number (%s).\n" "Name your file 'furnituremarkerxx.nif' where xx is a number between 00 and 19." % filebase[15:]) # name scene root name the file base name root_name = filebase # create furniture marker block furnmark = self.objecthelper.create_block("BSFurnitureMarker") furnmark.name = "FRN" furnmark.num_positions = 1 furnmark.positions.update_size() furnmark.positions[0].position_ref_1 = furniturenumber furnmark.positions[0].position_ref_2 = furniturenumber # create extra string data sgoKeep sgokeep = self.objecthelper.create_block("NiStringExtraData") sgokeep.name = "UPB" # user property buffer sgokeep.string_data = "sgoKeep=1 ExportSel = Yes" # Unyielding = 0, sgoKeep=1ExportSel = Yes # add extra blocks root_block.add_extra_data(furnmark) root_block.add_extra_data(sgokeep) # FIXME: NifLog.info("Checking collision") # activate oblivion/Fallout 3 collision and physics if NifOp.props.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): hascollision = False for b_obj in bpy.data.objects: if b_obj.game.use_collision_bounds: hascollision = True break if hascollision: # enable collision bsx = self.objecthelper.create_block("BSXFlags") bsx.name = 'BSX' bsx.integer_data = b_obj.niftools.bsxflags root_block.add_extra_data(bsx) # 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 = self.objecthelper.create_block( "NiStringExtraData") upb.name = 'UPB' if b_obj.niftools.upb == '': upb.string_data = 'Mass = 0.000000\r\nEllasticity = 0.300000\r\nFriction = 0.300000\r\nUnyielding = 0\r\nSimulation_Geometry = 2\r\nProxy_Geometry = <None>\r\nUse_Display_Proxy = 0\r\nDisplay_Children = 1\r\nDisable_Collisions = 0\r\nInactive = 0\r\nDisplay_Proxy = <None>\r\n' else: upb.string_data = b_obj.niftools.upb.encode() root_block.add_extra_data(upb) # update rigid body center of gravity and mass if self.EXPORT_OB_COLLISION_DO_NOT_USE_BLENDER_PROPERTIES: # we are not using blender properties to set the mass # so calculate mass automatically first calculate distribution of mass total_mass = 0 for block in self.dict_blocks: if isinstance(block, NifFormat.bhkRigidBody): block.update_mass_center_inertia( solid=self.EXPORT_OB_SOLID) total_mass += block.mass if total_mass == 0: # to avoid zero division error later (if mass is zero then this does not matter anyway) total_mass = 1 # now update the mass ensuring that total mass is self.EXPORT_OB_MASS for block in self.dict_blocks: if isinstance(block, NifFormat.bhkRigidBody): mass = self.EXPORT_OB_MASS * block.mass / total_mass # lower bound on mass if mass < 0.0001: mass = 0.05 block.update_mass_center_inertia( mass=mass, solid=self.EXPORT_OB_SOLID) else: # using blender properties, so block.mass *should* have # been set properly for block in self.dict_blocks: if isinstance(block, NifFormat.bhkRigidBody): # lower bound on mass if block.mass < 0.0001: block.mass = 0.05 block.update_mass_center_inertia( mass=block.mass, solid=self.EXPORT_OB_SOLID) # bhkConvexVerticesShape of children of bhkListShapes need an extra bhkConvexTransformShape (see issue #3308638, reported by Koniption) # note: self.dict_blocks changes during iteration, so need list copy for block in list(self.dict_blocks): if isinstance(block, NifFormat.bhkListShape): for i, sub_shape in enumerate(block.sub_shapes): if isinstance(sub_shape, NifFormat.bhkConvexVerticesShape): coltf = self.objecthelper.create_block( "bhkConvexTransformShape") coltf.material = sub_shape.material coltf.unknown_float_1 = 0.1 coltf.unknown_8_bytes[0] = 96 coltf.unknown_8_bytes[1] = 120 coltf.unknown_8_bytes[2] = 53 coltf.unknown_8_bytes[3] = 19 coltf.unknown_8_bytes[4] = 24 coltf.unknown_8_bytes[5] = 9 coltf.unknown_8_bytes[6] = 253 coltf.unknown_8_bytes[7] = 4 coltf.transform.set_identity() coltf.shape = sub_shape block.sub_shapes[i] = coltf # export constraints for b_obj in self.objecthelper.get_exported_objects(): if isinstance(b_obj, bpy.types.Object) and b_obj.constraints: self.constrainthelper.export_constraints(b_obj, root_block) # export weapon location if NifOp.props.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): if self.EXPORT_OB_PRN != "NONE": # add string extra data prn = self.objecthelper.create_block("NiStringExtraData") prn.name = 'Prn' prn.string_data = { "BACK": "BackWeapon", "SIDE": "SideWeapon", "QUIVER": "Quiver", "SHIELD": "Bip01 L ForearmTwist", "HELM": "Bip01 Head", "RING": "Bip01 R Finger1" }[self.EXPORT_OB_PRN] root_block.add_extra_data(prn) # add vertex color and zbuffer properties for civ4 and railroads if NifOp.props.game in ('CIVILIZATION_IV', 'SID_MEIER_S_RAILROADS'): self.propertyhelper.object_property.export_vertex_color_property( root_block) self.propertyhelper.object_property.export_z_buffer_property( root_block) elif NifOp.props.game in ('EMPIRE_EARTH_II', ): self.propertyhelper.object_property.export_vertex_color_property( root_block) self.propertyhelper.object_property.export_z_buffer_property( root_block, flags=15, function=1) # FIXME: """ if self.EXPORT_FLATTENSKIN: # (warning: trouble if armatures parent other armatures or # if bones parent geometries, or if object is animated) # flatten skins skelroots = set() affectedbones = [] for block in self.dict_blocks: if isinstance(block, NifFormat.NiGeometry) and block.is_skin(): NifLog.info("Flattening skin on geometry {0}".format(block.name)) affectedbones.extend(block.flatten_skin()) skelroots.add(block.skin_instance.skeleton_root) # remove NiNodes that do not affect skin for skelroot in skelroots: NifLog.info("Removing unused NiNodes in '{0}'".format(skelroot.name)) skelrootchildren = [child for child in skelroot.children if ((not isinstance(child, NifFormat.NiNode)) or (child in affectedbones))] skelroot.num_children = len(skelrootchildren) skelroot.children.update_size() for i, child in enumerate(skelrootchildren): skelroot.children[i] = child """ # apply scale if abs(NifOp.props.scale_correction_export) > NifOp.props.epsilon: NifLog.info("Applying scale correction {0}".format( str(NifOp.props.scale_correction_export))) data = NifFormat.Data() data.roots = [root_block] toaster = pyffi.spells.nif.NifToaster() toaster.scale = NifOp.props.scale_correction_export pyffi.spells.nif.fix.SpellScale(data=data, toaster=toaster).recurse() # also scale egm if self.egm_data: self.egm_data.apply_scale( NifOp.props.scale_correction_export) # generate mopps (must be done after applying scale!) if NifOp.props.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): for block in self.dict_blocks: if isinstance(block, NifFormat.bhkMoppBvTreeShape): NifLog.info("Generating mopp...") block.update_mopp() # print "=== DEBUG: MOPP TREE ===" # block.parse_mopp(verbose = True) # print "=== END OF MOPP TREE ===" # warn about mopps on non-static objects if any(sub_shape.layer != 1 for sub_shape in block.shape.sub_shapes): NifLog.warn( "Mopps for non-static objects may not function correctly in-game. You may wish to use simple primitives for collision." ) # delete original scene root if a scene root object was already defined if root_block.num_children == 1 and ( root_block.children[0].name in ['Scene Root', 'Bip01'] or root_block.children[0].name[-3:] == 'nif'): if root_block.children[0].name[-3:] == 'nif': root_block.children[0].name = filebase NifLog.info("Making '{0}' the root block".format( root_block.children[0].name)) # remove root_block from self.dict_blocks self.dict_blocks.pop(root_block) # set new root block old_root_block = root_block root_block = old_root_block.children[0] # copy extra data and properties for extra in old_root_block.get_extra_datas(): # delete links in extras to avoid parentship problems extra.next_extra_data = None # now add it root_block.add_extra_data(extra) for b in old_root_block.get_controllers(): root_block.add_controller(b) for b in old_root_block.properties: root_block.add_property(b) for b in old_root_block.effects: root_block.add_effect(b) else: root_block.name = root_name self.root_ninode = None for root_obj in root_objects: if root_obj.niftools.rootnode == 'BSFadeNode': self.root_ninode = 'BSFadeNode' elif self.root_ninode is None: self.root_ninode = 'NiNode' # making root block a fade node if NifOp.props.game in ('FALLOUT_3', 'SKYRIM' ) and self.root_ninode == 'BSFadeNode': NifLog.info("Making root block a BSFadeNode") fade_root_block = NifFormat.BSFadeNode().deepcopy(root_block) fade_root_block.replace_global_node(root_block, fade_root_block) root_block = fade_root_block export_animation = NifOp.props.animation if export_animation == 'ALL_NIF': NifLog.info("Exporting geometry and animation") elif export_animation == 'GEOM_NIF': # for morrowind: everything except keyframe controllers NifLog.info("Exporting geometry only") elif export_animation == 'ANIM_KF': # for morrowind: only keyframe controllers NifLog.info("Exporting animation only (as .kf file)") # export nif file: # ---------------- NifLog.info("Writing NIF version 0x%08X" % self.version) if export_animation != 'ANIM_KF': if NifOp.props.game == 'EMPIRE_EARTH_II': ext = ".nifcache" else: ext = ".nif" NifLog.info("Writing {0} file".format(ext)) # make sure we have the right file extension if fileext.lower() != ext: NifLog.warn( "Changing extension from {0} to {1} on output file". format(fileext, ext)) niffile = os.path.join(directory, filebase + ext) data = NifFormat.Data(version=self.version, user_version=self.user_version, user_version_2=self.user_version_2) data.roots = [root_block] if NifOp.props.game == 'NEOSTEAM': data.modification = "neosteam" elif NifOp.props.game == 'ATLANTICA': data.modification = "ndoors" elif NifOp.props.game == 'HOWLING_SWORD': data.modification = "jmihs1" with open(niffile, "wb") as stream: data.write(stream) # create and export keyframe file and xnif file: # ---------------------------------------------- # convert root_block tree into a keyframe tree if export_animation == 'ANIM_KF' or export_animation == 'ALL_NIF_XNIF_XKF': NifLog.info("Creating keyframe tree") # find all nodes and relevant controllers node_kfctrls = {} for node in root_block.tree(): if not isinstance(node, NifFormat.NiAVObject): continue # get list of all controllers for this node ctrls = node.get_controllers() for ctrl in ctrls: if NifOp.props.game == 'MORROWIND': # morrowind: only keyframe controllers if not isinstance(ctrl, NifFormat.NiKeyframeController): continue if node not in node_kfctrls: node_kfctrls[node] = [] node_kfctrls[node].append(ctrl) # morrowind if NifOp.props.game in ('MORROWIND', 'FREEDOM_FORCE'): # create kf root header kf_root = self.objecthelper.create_block( "NiSequenceStreamHelper") kf_root.add_extra_data(anim_textextra) # reparent controller tree for node, ctrls in node_kfctrls.items(): for ctrl in ctrls: # create node reference by name nodename_extra = self.objecthelper.create_block( "NiStringExtraData") nodename_extra.bytes_remaining = len(node.name) + 4 nodename_extra.string_data = node.name # break the controller chain ctrl.next_controller = None # add node reference and controller kf_root.add_extra_data(nodename_extra) kf_root.add_controller(ctrl) # wipe controller target ctrl.target = None # oblivion elif NifOp.props.game in ('OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV', 'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH'): # TODO [animation] allow for object kf only # create kf root header kf_root = self.objecthelper.create_block( "NiControllerSequence") # if self.EXPORT_ANIMSEQUENCENAME: # kf_root.name = self.EXPORT_ANIMSEQUENCENAME # else: kf_root.name = filebase kf_root.unknown_int_1 = 1 kf_root.weight = 1.0 kf_root.text_keys = anim_textextra kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP kf_root.frequency = 1.0 kf_root.start_time = bpy.context.scene.frame_start * bpy.context.scene.render.fps kf_root.stop_time = (bpy.context.scene.frame_end - bpy.context.scene.frame_start ) * bpy.context.scene.render.fps # quick hack to set correct target name if "Bip01" in b_armature.data.bones: targetname = "Bip01" elif "Bip02" in b_armature.data.bones: targetname = "Bip02" else: targetname = root_block.name kf_root.target_name = targetname kf_root.string_palette = NifFormat.NiStringPalette() b_armature = armature.get_armature() # per-node animation if b_armature: for b_bone in b_armature.data.bones: self.animationhelper.export_keyframes( kf_root, b_armature, b_bone) # per-object animation else: for b_obj in bpy.data.objects: self.animationhelper.export_keyframes( kf_root, b_obj) # for node, ctrls in zip(iter(node_kfctrls.keys()), iter(node_kfctrls.values())): # # export a block for every interpolator in every controller # for ctrl in ctrls: # # XXX add get_interpolators to pyffi interface # if isinstance(ctrl, NifFormat.NiSingleInterpController): # interpolators = [ctrl.interpolator] # elif isinstance( ctrl, (NifFormat.NiGeomMorpherController, NifFormat.NiMorphWeightsController)): # interpolators = ctrl.interpolators # if isinstance(ctrl, NifFormat.NiGeomMorpherController): # variable_2s = [morph.frame_name for morph in ctrl.data.morphs] # else: # variable_2s = [None for interpolator in interpolators] # for interpolator, variable_2 in zip(interpolators, variable_2s): # # create ControlledLink for each interpolator # controlledblock = kf_root.add_controlled_block() # if self.version < 0x0A020000: # # older versions need the actual controller blocks # controlledblock.target_name = node.name # controlledblock.controller = ctrl # # erase reference to target node # ctrl.target = None # else: # # newer versions need the interpolator blocks # controlledblock.interpolator = interpolator # # get bone animation priority (previously fetched from the constraints during export_bones) # if not node.name in self.dict_bone_priorities or self.EXPORT_ANIM_DO_NOT_USE_BLENDER_PROPERTIES: # if self.EXPORT_ANIMPRIORITY != 0: # priority = self.EXPORT_ANIMPRIORITY # else: # priority = 26 # NifLog.warn("No priority set for bone {0}, falling back on default value ({1})".format(node.name, str(priority))) # else: # priority = self.dict_bone_priorities[node.name] # controlledblock.priority = priority # # set palette, and node and controller type names, and variables # controlledblock.string_palette = kf_root.string_palette # controlledblock.set_node_name(node.name) # controlledblock.set_controller_type(ctrl.__class__.__name__) # if variable_2: # controlledblock.set_variable_2(variable_2) else: raise nif_utils.NifError( "Keyframe export for '%s' is not supported.\nOnly Morrowind, Oblivion, Fallout 3, Civilization IV," " Zoo Tycoon 2, Freedom Force, and Freedom Force vs. the 3rd Reich keyframes are supported." % NifOp.props.game) # write kf (and xnif if asked) prefix = "" if ( export_animation != 'ALL_NIF_XNIF_XKF') else "x" ext = ".kf" NifLog.info("Writing {0} file".format(prefix + ext)) kffile = os.path.join(directory, prefix + filebase + ext) data = NifFormat.Data(version=self.version, user_version=self.user_version, user_version_2=self.user_version_2) data.roots = [kf_root] data.neosteam = (NifOp.props.game == 'NEOSTEAM') stream = open(kffile, "wb") try: data.write(stream) finally: stream.close() if export_animation == 'ALL_NIF_XNIF_XKF': NifLog.info("Detaching keyframe controllers from nif") # detach the keyframe controllers from the nif (for xnif) for node in root_block.tree(): if not isinstance(node, NifFormat.NiNode): continue # remove references to keyframe controllers from node # (for xnif) while isinstance(node.controller, NifFormat.NiKeyframeController): node.controller = node.controller.next_controller ctrl = node.controller while ctrl: if isinstance(ctrl.next_controller, NifFormat.NiKeyframeController): ctrl.next_controller = ctrl.next_controller.next_controller else: ctrl = ctrl.next_controller NifLog.info("Detaching animation text keys from nif") # detach animation text keys if root_block.extra_data is not anim_textextra: raise RuntimeError( "Oops, you found a bug! Animation extra data" " wasn't where expected...") root_block.extra_data = None prefix = "x" # we are in morrowind 'nifxnifkf mode' ext = ".nif" NifLog.info("Writing {0} file".format(prefix + ext)) xniffile = os.path.join(directory, prefix + filebase + ext) data = NifFormat.Data(version=self.version, user_version=self.user_version, user_version_2=self.user_version_2) data.roots = [root_block] data.neosteam = (NifOp.props.game == 'NEOSTEAM') stream = open(xniffile, "wb") try: data.write(stream) finally: stream.close() # export egm file: # ----------------- if self.egm_data: ext = ".egm" NifLog.info("Writing {0} file".format(ext)) egmfile = os.path.join(directory, filebase + ext) stream = open(egmfile, "wb") try: self.egm_data.write(stream) finally: stream.close() finally: # clear progress bar NifLog.info("Finished") # save exported file (this is used by the test suite) self.root_blocks = [root_block] return {'FINISHED'} 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 export_egm(self, keyblocks): self.egm_data = EgmFormat.Data(num_vertices=len(keyblocks[0].data)) for keyblock in keyblocks: if keyblock.name.startswith("EGM SYM"): morph = self.egm_data.add_sym_morph() elif keyblock.name.startswith("EGM ASYM"): morph = self.egm_data.add_asym_morph() else: continue self.info("Exporting morph %s to egm" % keyblock.name) relative_vertices = [] # note: keyblocks[0] is base key for vert, key_vert in zip(keyblocks[0].data, keyblock.data): relative_vertices.append(key_vert - vert) morph.set_relative_vertices(relative_vertices)
class NifExport(NifCommon): IDENTITY44 = NifFormat.Matrix44() IDENTITY44.set_identity() # TODO: - Expose via properties def __init__(self, operator, context): NifCommon.__init__(self, operator, context) # Helper systems self.transform_anim = TransformAnimation() self.constrainthelper = Constraint() self.objecthelper = Object() self.exportable_objects = [] self.root_objects = [] def execute(self): """Main export function.""" if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT', toggle=False) NifLog.info(f"Exporting {NifOp.props.filepath}") # TODO [animation[ Fix morrowind animation support ''' if NifOp.props.animation == 'ALL_NIF_XNIF_XKF' and bpy.context.scene.niftools_scene.game == 'MORROWIND': # if exporting in nif+xnif+kf mode, then first export # the nif with geometry + animation, which is done by: NifOp.props.animation = 'ALL_NIF' ''' # extract directory, base name, extension directory = os.path.dirname(NifOp.props.filepath) filebase, fileext = os.path.splitext( os.path.basename(NifOp.props.filepath)) block_store.block_to_obj = {} # clear out previous iteration try: # catch export errors # find all objects that do not have a parent self.exportable_objects, self.root_objects = self.objecthelper.get_export_objects( ) if not self.exportable_objects: NifLog.warn("No objects can be exported!") return {'FINISHED'} for b_obj in self.exportable_objects: # armatures should not be in rest position if b_obj.type == 'ARMATURE': b_obj.data.pose_position = 'POSE' elif b_obj.type == 'MESH': if b_obj.parent and b_obj.parent.type == 'ARMATURE': for b_mod in b_obj.modifiers: if b_mod.type == 'ARMATURE' and b_mod.use_bone_envelopes: raise NifError( f"'{b_obj.name}': Cannot export envelope skinning. If you have vertex groups, turn off envelopes.\n" f"If you don't have vertex groups, select the bones one by one press W to " f"convert their envelopes to vertex weights, and turn off envelopes." ) # check for non-uniform transforms scale = b_obj.matrix_local.to_scale() if abs(scale.x - scale.y) > NifOp.props.epsilon or abs( scale.y - scale.z) > NifOp.props.epsilon: NifLog.warn( f"Non-uniform scaling not supported.\n" f"Workaround: apply size and rotation (CTRL-A) on '{b_obj.name}'." ) b_armature = math.get_armature() # some scenes may not have an armature, so nothing to do here if b_armature: math.set_bone_orientation( b_armature.data.niftools.axis_forward, b_armature.data.niftools.axis_up) prefix = "" NifLog.info("Exporting") if NifOp.props.animation == 'ALL_NIF': NifLog.info("Exporting geometry and animation") elif NifOp.props.animation == 'GEOM_NIF': # for morrowind: everything except keyframe controllers NifLog.info("Exporting geometry only") elif NifOp.props.animation == 'ANIM_KF': # for morrowind: only keyframe controllers NifLog.info("Exporting animation only (as .kf file)") elif NifOp.props.animation == 'ALL_NIF_XNIF_XKF': prefix = "x" NifLog.info("Exporting geometry and animation in xnif-style") # find nif version to write self.version, data = scene.get_version_data() NifData.init(data) # write external animation to a KF tree if NifOp.props.animation in ('ANIM_KF', 'ALL_NIF_XNIF_XKF'): NifLog.info("Creating keyframe tree") kf_root = self.transform_anim.export_kf_root(b_armature) # write kf (and xkf if asked) ext = ".kf" NifLog.info(f"Writing {prefix}{ext} file") kffile = os.path.join(directory, prefix + filebase + ext) data.roots = [kf_root] data.neosteam = ( bpy.context.scene.niftools_scene.game == 'NEOSTEAM') # scale correction for the skeleton if bpy.context.scene.niftools_scene.game in ('SKYRIM'): toaster = pyffi.spells.nif.NifToaster() toaster.scale = round(1 / NifOp.props.scale_correction) pyffi.spells.nif.fix.SpellScale(data=data, toaster=toaster).recurse() NifLog.info( f"Scale Correction set to {round(1 / NifOp.props.scale_correction)}" ) with open(kffile, "wb") as stream: data.write(stream) # if only anim, no need to do the time consuming nif export if NifOp.props.animation == 'ANIM_KF': # clear progress bar NifLog.info("Finished") return {'FINISHED'} # export the actual root node (the name is fixed later to avoid confusing the exporter with duplicate names) root_block = self.objecthelper.export_root_node( self.root_objects, filebase) # post-processing: # ---------------- NifLog.info("Checking controllers") if bpy.context.scene.niftools_scene.game == 'MORROWIND': # animations without keyframe animations crash the TESCS # if we are in that situation, add a trivial keyframe animation has_keyframecontrollers = False for block in block_store.block_to_obj: if isinstance(block, NifFormat.NiKeyframeController): has_keyframecontrollers = True break if (not has_keyframecontrollers) and ( not NifOp.props.bs_animation_node): NifLog.info("Defining dummy keyframe controller") # add a trivial keyframe controller on the scene root self.transform_anim.create_controller( root_block, root_block.name) if NifOp.props.bs_animation_node: for block in block_store.block_to_obj: if isinstance(block, NifFormat.NiNode): # if any of the shape children has a controller or if the ninode has a controller convert its type if block.controller or any( child.controller for child in block.children if isinstance( child, NifFormat.NiGeometry)): new_block = NifFormat.NiBSAnimationNode( ).deepcopy(block) # have to change flags to 42 to make it work new_block.flags = 42 root_block.replace_global_node( block, new_block) if root_block is block: root_block = new_block # oblivion skeleton export: check that all bones have a transform controller and transform interpolator if bpy.context.scene.niftools_scene.game in ( 'OBLIVION', 'FALLOUT_3', 'SKYRIM') and filebase.lower() in ('skeleton', 'skeletonbeast'): # TODO [armature] Extract out to armature animation # here comes everything that is Oblivion skeleton export specific NifLog.info( "Adding controllers and interpolators for skeleton") # note: block_store.block_to_obj changes during iteration, so need list copy for n_block in list(block_store.block_to_obj.keys()): if isinstance(n_block, NifFormat.NiNode ) and n_block.name.decode() == "Bip01": for n_bone in n_block.tree( block_type=NifFormat.NiNode): n_kfc, n_kfi = self.transform_anim.create_controller( n_bone, n_bone.name.decode()) # todo [anim] use self.nif_export.animationhelper.set_flags_and_timing n_kfc.flags = 12 n_kfc.frequency = 1.0 n_kfc.phase = 0.0 n_kfc.start_time = consts.FLOAT_MAX n_kfc.stop_time = consts.FLOAT_MIN else: # here comes everything that should be exported EXCEPT for Oblivion skeleton exports # export animation groups (not for skeleton.nif export!) # anim_textextra = self.animation_helper.export_text_keys(b_action) pass # bhkConvexVerticesShape of children of bhkListShapes need an extra bhkConvexTransformShape (see issue #3308638, reported by Koniption) # note: block_store.block_to_obj changes during iteration, so need list copy for block in list(block_store.block_to_obj.keys()): if isinstance(block, NifFormat.bhkListShape): for i, sub_shape in enumerate(block.sub_shapes): if isinstance(sub_shape, NifFormat.bhkConvexVerticesShape): coltf = block_store.create_block( "bhkConvexTransformShape") coltf.material = sub_shape.material coltf.unknown_float_1 = 0.1 unk_8 = coltf.unknown_8_bytes unk_8[0] = 96 unk_8[1] = 120 unk_8[2] = 53 unk_8[3] = 19 unk_8[4] = 24 unk_8[5] = 9 unk_8[6] = 253 unk_8[7] = 4 coltf.transform.set_identity() coltf.shape = sub_shape block.sub_shapes[i] = coltf # export constraints for b_obj in self.exportable_objects: if b_obj.constraints: self.constrainthelper.export_constraints(b_obj, root_block) object_prop = ObjectProperty() object_prop.export_root_node_properties(root_block) # FIXME: """ if self.EXPORT_FLATTENSKIN: # (warning: trouble if armatures parent other armatures or # if bones parent geometries, or if object is animated) # flatten skins skelroots = set() affectedbones = [] for block in block_store.block_to_obj: if isinstance(block, NifFormat.NiGeometry) and block.is_skin(): NifLog.info("Flattening skin on geometry {0}".format(block.name)) affectedbones.extend(block.flatten_skin()) skelroots.add(block.skin_instance.skeleton_root) # remove NiNodes that do not affect skin for skelroot in skelroots: NifLog.info("Removing unused NiNodes in '{0}'".format(skelroot.name)) skelrootchildren = [child for child in skelroot.children if ((not isinstance(child, NifFormat.NiNode)) or (child in affectedbones))] skelroot.num_children = len(skelrootchildren) skelroot.children.update_size() for i, child in enumerate(skelrootchildren): skelroot.children[i] = child """ # apply scale scale_correction = bpy.context.scene.niftools_scene.scale_correction if abs(scale_correction) > NifOp.props.epsilon: NifLog.info(f"Applying scale correction {scale_correction}") data.roots = [root_block] toaster = pyffi.spells.nif.NifToaster() toaster.scale = 1 / scale_correction pyffi.spells.nif.fix.SpellScale(data=data, toaster=toaster).recurse() # also scale egm if EGMData.data: EGMData.data.apply_scale(1 / scale_correction) # generate mopps (must be done after applying scale!) if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): for block in block_store.block_to_obj: if isinstance(block, NifFormat.bhkMoppBvTreeShape): NifLog.info("Generating mopp...") block.update_mopp() # print "=== DEBUG: MOPP TREE ===" # block.parse_mopp(verbose = True) # print "=== END OF MOPP TREE ===" # warn about mopps on non-static objects if any(sub_shape.layer != 1 for sub_shape in block.shape.sub_shapes): NifLog.warn( "Mopps for non-static objects may not function correctly in-game. You may wish to use simple primitives for collision." ) # export nif file: # ---------------- if bpy.context.scene.niftools_scene.game == 'EMPIRE_EARTH_II': ext = ".nifcache" else: ext = ".nif" NifLog.info(f"Writing {ext} file") # make sure we have the right file extension if fileext.lower() != ext: NifLog.warn( f"Changing extension from {fileext} to {ext} on output file" ) niffile = os.path.join(directory, prefix + filebase + ext) data.roots = [root_block] # todo [export] I believe this is redundant and setting modification only is the current way? data.neosteam = ( bpy.context.scene.niftools_scene.game == 'NEOSTEAM') if bpy.context.scene.niftools_scene.game == 'NEOSTEAM': data.modification = "neosteam" elif bpy.context.scene.niftools_scene.game == 'ATLANTICA': data.modification = "ndoors" elif bpy.context.scene.niftools_scene.game == 'HOWLING_SWORD': data.modification = "jmihs1" with open(niffile, "wb") as stream: data.write(stream) # export egm file: # ----------------- if EGMData.data: ext = ".egm" NifLog.info(f"Writing {ext} file") egmfile = os.path.join(directory, filebase + ext) with open(egmfile, "wb") as stream: EGMData.data.write(stream) # save exported file (this is used by the test suite) self.root_blocks = [root_block] except NifError: return {'CANCELLED'} NifLog.info("Finished") return {'FINISHED'}