def create_skin_inst_data(self, b_obj, b_obj_armature, polygon_parts): if bpy.context.scene.niftools_scene.game in ( 'FALLOUT_3', 'SKYRIM') and polygon_parts: skininst = block_store.create_block("BSDismemberSkinInstance", b_obj) else: skininst = block_store.create_block("NiSkinInstance", b_obj) # get skeleton root from custom property if b_obj.niftools.skeleton_root: n_root_name = b_obj.niftools.skeleton_root # or use the armature name else: n_root_name = block_store.get_full_name(b_obj_armature) # make sure that such a block exists, find it for block in block_store.block_to_obj: if isinstance(block, NifFormat.NiNode): if block.name.decode() == n_root_name: skininst.skeleton_root = block break else: raise NifError(f"Skeleton root '{n_root_name}' not found.") # create skinning data and link it skindata = block_store.create_block("NiSkinData", b_obj) skininst.data = skindata skindata.has_vertex_weights = True # fix geometry rest pose: transform relative to skeleton root skindata.set_transform(math.get_object_matrix(b_obj).get_inverse()) return skininst, skindata
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 export_furniture_marker(n_root, filebase): # oblivion and Fallout 3 furniture markers if bpy.context.scene.niftools_scene.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 io_scene_niftools.utils.logging.NifError( "Furniture marker has invalid number ({0}).\n" "Name your file 'furnituremarkerxx.nif' where xx is a number between 00 and 19." .format(filebase[15:])) # create furniture marker block furnmark = block_store.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 = block_store.create_block("NiStringExtraData") sgokeep.name = "UPB" # user property buffer sgokeep.string_data = "sgoKeep=1 ExportSel = Yes" # Unyielding = 0, sgoKeep=1ExportSel = Yes # add extra blocks n_root.add_extra_data(furnmark) n_root.add_extra_data(sgokeep)
def export_material_alpha_color_controller(self, b_material, n_mat_prop, b_dtype, n_dtype): """Export the material alpha or color controller data.""" # get fcurves fcurves = [ fcu for fcu in b_material.animation_data.action.fcurves if b_dtype in fcu.data_path ] if not fcurves: return # just set the names of the nif data types, main difference between alpha and color if b_dtype == "alpha": keydata = "NiFloatData" interpolator = "NiFloatInterpolator" controller = "NiAlphaController" else: keydata = "NiPosData" interpolator = "NiPoint3Interpolator" controller = "NiMaterialColorController" # create the key data n_key_data = block_store.create_block(keydata, fcurves) n_key_data.data.num_keys = len(fcurves[0].keyframe_points) n_key_data.data.interpolation = NifFormat.KeyType.LINEAR_KEY n_key_data.data.keys.update_size() # assumption: all curves have same amount of keys and are sampled at the same time for i, n_key in enumerate(n_key_data.data.keys): frame = fcurves[0].keyframe_points[i].co[0] # add each point of the curves n_key.arg = n_key_data.data.interpolation n_key.time = frame / self.fps if b_dtype == "alpha": n_key.value = fcurves[0].keyframe_points[i].co[1] else: n_key.value.x, n_key.value.y, n_key.value.z = [ fcu.keyframe_points[i].co[1] for fcu in fcurves ] # if key data is present # then add the controller so it is exported if fcurves[0].keyframe_points: n_mat_ctrl = block_store.create_block(controller, fcurves) n_mat_ipol = block_store.create_block(interpolator, fcurves) n_mat_ctrl.interpolator = n_mat_ipol self.set_flags_and_timing(n_mat_ctrl, fcurves) # set target color only for color controller if n_dtype: n_mat_ctrl.set_target_color(n_dtype) n_mat_ctrl.data = n_key_data n_mat_ipol.data = n_key_data # attach block to material property n_mat_prop.add_controller(n_mat_ctrl)
def export_kf_root(self, b_armature=None): """Creates and returns a KF root block and exports controllers for objects and bones""" scene = bpy.context.scene game = scene.niftools_scene.game if game in ('MORROWIND', 'FREEDOM_FORCE'): kf_root = block_store.create_block("NiSequenceStreamHelper") elif game in ('SKYRIM', 'OBLIVION', 'FALLOUT_3', 'CIVILIZATION_IV', 'ZOO_TYCOON_2', 'FREEDOM_FORCE_VS_THE_3RD_REICH', 'MEGAMI_TENSEI_IMAGINE'): kf_root = block_store.create_block("NiControllerSequence") else: raise NifError(f"Keyframe export for '{game}' is not supported.") anim_textextra = self.create_text_keys(kf_root) targetname = "Scene Root" # per-node animation if b_armature: b_action = self.get_active_action(b_armature) for b_bone in b_armature.data.bones: self.export_transforms(kf_root, b_armature, b_action, b_bone) if game in ('SKYRIM', ): targetname = "NPC Root [Root]" else: # 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" # per-object animation else: for b_obj in bpy.data.objects: b_action = self.get_active_action(b_obj) self.export_transforms(kf_root, b_obj, b_action) self.export_text_keys(b_action, anim_textextra) kf_root.name = b_action.name kf_root.unknown_int_1 = 1 kf_root.weight = 1.0 kf_root.cycle_type = NifFormat.CycleType.CYCLE_CLAMP kf_root.frequency = 1.0 if anim_textextra.num_text_keys > 0: kf_root.start_time = anim_textextra.text_keys[0].time kf_root.stop_time = anim_textextra.text_keys[ anim_textextra.num_text_keys - 1].time else: kf_root.start_time = scene.frame_start / self.fps kf_root.stop_time = scene.frame_end / self.fps kf_root.target_name = targetname return kf_root
def create_controller(parent_block, target_name, priority=0): # todo[anim] - make independent of global NifData.data.version, and move check for NifOp.props.animation outside n_kfi = None n_kfc = None try: if NifOp.props.animation == 'GEOM_NIF' and NifData.data.version < 0x0A020000: # keyframe controllers are not present in geometry only files # for more recent versions, the controller and interpolators are # present, only the data is not present (see further on) return n_kfc, n_kfi except AttributeError: # kf export has no animation mode pass # add a KeyframeController block, and refer to this block in the # parent's time controller if NifData.data.version < 0x0A020000: n_kfc = block_store.create_block("NiKeyframeController", None) else: n_kfc = block_store.create_block("NiTransformController", None) n_kfi = block_store.create_block("NiTransformInterpolator", None) # link interpolator from the controller n_kfc.interpolator = n_kfi # if parent is a node, attach controller to that node if isinstance(parent_block, NifFormat.NiNode): parent_block.add_controller(n_kfc) if n_kfi: # set interpolator default data n_kfi.scale, n_kfi.rotation, n_kfi.translation = parent_block.get_transform( ).get_scale_quat_translation() # else ControllerSequence, so create a link elif isinstance(parent_block, NifFormat.NiControllerSequence): controlled_block = parent_block.add_controlled_block() controlled_block.priority = priority if NifData.data.version < 0x0A020000: # older versions need the actual controller blocks controlled_block.target_name = target_name controlled_block.controller = n_kfc # erase reference to target node n_kfc.target = None else: # newer versions need the interpolator blocks controlled_block.interpolator = n_kfi controlled_block.node_name = target_name controlled_block.controller_type = "NiTransformController" else: raise io_scene_niftools.utils.logging.NifError( "Unsupported KeyframeController parent!") return n_kfc, n_kfi
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 get_matching_block(self, block_type, **kwargs): """Try to find a block matching block_type. Keyword arguments are a dict of parameters and required attributes of the block""" # go over all blocks of block_type NifLog.debug(f"Looking for {block_type} block. Kwargs: {kwargs}") for block in block_store.block_to_obj: # if isinstance(block, block_type): if block_type in str(type(block)): # skip blocks that don't match additional conditions for param, attribute in kwargs.items(): # now skip this block if any of the conditions does not match if attribute is not None: ret_attr = getattr(block, param, None) if ret_attr != attribute: NifLog.debug( f"break, {param} != {attribute}, returns {ret_attr}" ) break else: # we did not break out of the loop, so all checks went through, so we can use this block NifLog.debug( f"Found existing {block_type} block matching all criteria!" ) return block # we are still here, so we must create a block of this type and set all attributes accordingly NifLog.debug( f"Created new {block_type} block because none matched the required criteria!" ) block = block_store.create_block(block_type) for param, attribute in kwargs.items(): if attribute is not None: setattr(block, param, attribute) return block
def export_bhk_convex_vertices_shape(self, b_obj, fdistlist, fnormlist, radius, vertlist): colhull = block_store.create_block("bhkConvexVerticesShape", b_obj) # colhull.material = n_havok_mat[0] colhull.radius = radius unk_6 = colhull.unknown_6_floats unk_6[2] = -0.0 # enables arrow detection unk_6[5] = -0.0 # enables arrow detection # note: unknown 6 floats are usually all 0 # Vertices colhull.num_vertices = len(vertlist) colhull.vertices.update_size() for vhull, vert in zip(colhull.vertices, vertlist): vhull.x = vert[0] / self.HAVOK_SCALE vhull.y = vert[1] / self.HAVOK_SCALE vhull.z = vert[2] / self.HAVOK_SCALE # w component is 0 # Normals colhull.num_normals = len(fnormlist) colhull.normals.update_size() for nhull, norm, dist in zip(colhull.normals, fnormlist, fdistlist): nhull.x = norm[0] nhull.y = norm[1] nhull.z = norm[2] nhull.w = dist / self.HAVOK_SCALE return colhull
def export_visibility(self, n_node, b_action): """Export the visibility controller data.""" if not b_action: return # get the hide fcurve fcurves = [fcu for fcu in b_action.fcurves if "hide" in fcu.data_path] if not fcurves: return # TODO [animation] which sort of controller should be exported? # should this be driven by version number? # we probably don't want both at the same time # NiVisData = old style, NiBoolData = new style n_vis_data = block_store.create_block("NiVisData", fcurves) n_vis_data.num_keys = len(fcurves[0].keyframe_points) n_vis_data.keys.update_size() # we just leave interpolation at constant n_bool_data = block_store.create_block("NiBoolData", fcurves) n_bool_data.data.interpolation = NifFormat.KeyType.CONST_KEY n_bool_data.data.num_keys = len(fcurves[0].keyframe_points) n_bool_data.data.keys.update_size() for b_point, n_vis_key, n_bool_key in zip(fcurves[0].keyframe_points, n_vis_data.keys, n_bool_data.data.keys): # add each point of the curve b_frame, b_value = b_point.co n_vis_key.arg = n_bool_data.data.interpolation # n_vis_data has no interpolation stored n_vis_key.time = b_frame / bpy.context.scene.render.fps n_vis_key.value = b_value n_bool_key.arg = n_bool_data.data.interpolation n_bool_key.time = n_vis_key.time n_bool_key.value = n_vis_key.value # if alpha data is present (check this by checking if times were added) then add the controller so it is exported if fcurves[0].keyframe_points: n_vis_ctrl = block_store.create_block("NiVisController", fcurves) n_vis_ipol = block_store.create_block("NiBoolInterpolator", fcurves) self.set_flags_and_timing(n_vis_ctrl, fcurves) n_vis_ctrl.interpolator = n_vis_ipol n_vis_ctrl.data = n_vis_data n_vis_ipol.data = n_bool_data # attach block to node n_node.add_controller(n_vis_ctrl)
def create_text_keys(self, kf_root): """Create the text keys before filling in the data so that the extra data hierarchy is correct""" # add a NiTextKeyExtraData block n_text_extra = block_store.create_block("NiTextKeyExtraData", None) if isinstance(kf_root, NifFormat.NiControllerSequence): kf_root.text_keys = n_text_extra elif isinstance(kf_root, NifFormat.NiSequenceStreamHelper): kf_root.add_extra_data(n_text_extra) return n_text_extra
def export_bhk_blend_controller(self, b_obj, parent_block): # also add a controller for it n_blend_ctrl = block_store.create_block("bhkBlendController", b_obj) n_blend_ctrl.flags = 12 n_blend_ctrl.frequency = 1.0 n_blend_ctrl.phase = 0.0 n_blend_ctrl.start_time = consts.FLOAT_MAX n_blend_ctrl.stop_time = consts.FLOAT_MIN parent_block.add_controller(n_blend_ctrl)
def export_bs_effect_shader(self, b_mat, bsshader, b_slot): # TODO [shader][animation] Do some form of check to ensure that we actually have data effect_control = block_store.create_block("BSEffectShaderPropertyFloatController", bsshader) effect_control.flags = b_mat.niftools_alpha.textureflag effect_control.frequency = b_slot.texture.image.fps effect_control.start_time = b_slot.texture.image.frame_start effect_control.stop_time = b_slot.texture.image.frame_end bsshader.add_controller(effect_control)
def export_weapon_location(self, n_root, root_obj): # export weapon location if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): loc = root_obj.niftools.prn_location if loc != "NONE": # add string extra data prn = block_store.create_block("NiStringExtraData") prn.name = 'Prn' prn.string_data = PRN_DICT[loc] n_root.add_extra_data(prn)
def export_bhk_rigid_body(self, b_obj, n_col_obj): n_r_body = block_store.create_block("bhkRigidBody", b_obj) n_col_obj.body = n_r_body n_r_body.layer = getattr(NifFormat.OblivionLayer, b_obj.nifcollision.oblivion_layer) n_r_body.col_filter = b_obj.nifcollision.col_filter n_r_body.unknown_short = 0 n_r_body.unknown_int_1 = 0 n_r_body.unknown_int_2 = 2084020722 unk_3 = n_r_body.unknown_3_ints unk_3[0] = 0 unk_3[1] = 0 unk_3[2] = 0 n_r_body.collision_response = 1 n_r_body.unknown_byte = 0 n_r_body.process_contact_callback_delay = 65535 unk_2 = n_r_body.unknown_2_shorts unk_2[0] = 35899 unk_2[1] = 16336 n_r_body.layer_copy = n_r_body.layer n_r_body.col_filter_copy = n_r_body.col_filter # TODO [format] nif.xml update required # ukn_6 = n_r_body.unknown_6_shorts # ukn_6[0] = 21280 # ukn_6[1] = 4581 # ukn_6[2] = 62977 # ukn_6[3] = 65535 # ukn_6[4] = 44 # ukn_6[5] = 0 b_r_body = b_obj.rigid_body # mass is 1.0 at the moment (unless property was set on import or by the user) # will be fixed in update_rigid_bodies() n_r_body.mass = b_r_body.mass n_r_body.linear_damping = b_r_body.linear_damping n_r_body.angular_damping = b_r_body.angular_damping # n_r_body.linear_velocity = linear_velocity # n_r_body.angular_velocity = angular_velocity n_r_body.friction = b_r_body.friction n_r_body.restitution = b_r_body.restitution n_r_body.max_linear_velocity = b_obj.nifcollision.max_linear_velocity n_r_body.max_angular_velocity = b_obj.nifcollision.max_angular_velocity n_r_body.penetration_depth = b_obj.collision.permeability n_r_body.motion_system = b_obj.nifcollision.motion_system n_r_body.deactivator_type = b_obj.nifcollision.deactivator_type n_r_body.solver_deactivation = b_obj.nifcollision.solver_deactivation # TODO [collision][properties][ui] expose unknowns to UI & make sure to keep defaults n_r_body.unknown_byte_1 = 1 n_r_body.unknown_byte_2 = 1 n_r_body.quality_type = b_obj.nifcollision.quality_type n_r_body.unknown_int_9 = 0 return n_r_body
def export_bhk_collison_object(self, b_obj): layer = int(b_obj.nifcollision.collision_layer) col_filter = b_obj.nifcollision.col_filter n_col_obj = block_store.create_block("bhkCollisionObject", b_obj) if layer == NifFormat.OblivionLayer.OL_ANIM_STATIC and col_filter != 128: # animated collision requires flags = 41 # unless it is a constrainted but not keyframed object n_col_obj.flags = 41 else: # in all other cases this seems to be enough n_col_obj.flags = 1 return n_col_obj
def export_nicollisiondata(self, b_obj, n_parent): """ Export b_obj as a NiCollisionData """ n_coll_data = block_store.create_block("NiCollisionData", b_obj) n_coll_data.use_abv = 1 n_coll_data.target = n_parent n_parent.collision_object = n_coll_data n_bv = n_coll_data.bounding_volume if b_obj.display_bounds_type == 'SPHERE': self.export_spherebv(b_obj, n_bv) elif b_obj.display_bounds_type == 'BOX': self.export_boxbv(b_obj, n_bv) elif b_obj.display_bounds_type == 'CAPSULE': self.export_capsulebv(b_obj, n_bv)
def export_bsxflags_upb(self, root_block): # 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' 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 = block_store.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)
def create_ninode(b_obj=None): """Essentially a wrapper around create_block() that creates nodes of the right type""" # when no b_obj is passed, it means we create a root node if not b_obj: return block_store.create_block("NiNode") # get node type - some are stored as custom property of the b_obj try: n_node_type = b_obj["type"] except KeyError: n_node_type = "NiNode" # ...others by presence of constraints if has_track(b_obj): n_node_type = "NiBillboardNode" # now create the node n_node = block_store.create_block(n_node_type, b_obj) # customize the node data, depending on type if n_node_type == "NiLODNode": export_range_lod_data(n_node, b_obj) return n_node
def create_skin_inst_data(self, b_obj, n_root_name, bodypartgroups): if bpy.context.scene.niftools_scene.game in ( 'FALLOUT_3', 'SKYRIM') and bodypartgroups: skininst = block_store.create_block("BSDismemberSkinInstance", b_obj) else: skininst = block_store.create_block("NiSkinInstance", b_obj) for block in block_store.block_to_obj: if isinstance(block, NifFormat.NiNode): if block.name.decode() == n_root_name: skininst.skeleton_root = block break else: raise io_scene_niftools.utils.logging.NifError( f"Skeleton root '{n_root_name}' not found.") # create skinning data and link it skindata = block_store.create_block("NiSkinData", b_obj) skininst.data = skindata skindata.has_vertex_weights = True # fix geometry rest pose: transform relative to skeleton root skindata.set_transform(math.get_object_matrix(b_obj).get_inverse()) return skininst, skindata
def export_bhk_mopp_bv_tree_shape(self, b_obj, n_col_body): n_col_mopp = block_store.create_block("bhkMoppBvTreeShape", b_obj) n_col_body.shape = n_col_mopp # n_col_mopp.material = n_havok_mat[0] unk_8 = n_col_mopp.unknown_8_bytes unk_8[0] = 160 unk_8[1] = 13 unk_8[2] = 75 unk_8[3] = 1 unk_8[4] = 192 unk_8[5] = 207 unk_8[6] = 144 unk_8[7] = 11 n_col_mopp.unknown_float = 1.0 return n_col_mopp
def export_texture_effect(self, n_block, b_mat): # todo [texture] detect effect ref_mtex = False if ref_mtex: # create a new parent block for this shape extra_node = block_store.create_block("NiNode", ref_mtex) n_block.add_child(extra_node) # set default values for this ninode extra_node.rotation.set_identity() extra_node.scale = 1.0 extra_node.flags = 0x000C # morrowind # create texture effect block and parent the texture effect and trishape to it texeff = self.texture_helper.export_texture_effect(ref_mtex) extra_node.add_child(texeff) extra_node.add_effect(texeff) return extra_node return n_block
def export_bhk_packed_nitristrip_shape(self, b_obj, n_col_mopp): # the mopp origin, scale, and data are written later n_col_shape = block_store.create_block("bhkPackedNiTriStripsShape", b_obj) n_col_shape.unknown_int_1 = 0 n_col_shape.unknown_int_2 = 21929432 n_col_shape.unknown_float_1 = 0.1 n_col_shape.unknown_int_3 = 0 n_col_shape.unknown_float_2 = 0 n_col_shape.unknown_float_3 = 0.1 scale = n_col_shape.scale scale.x = 0 scale.y = 0 scale.z = 0 scale.unknown_float_4 = 0 n_col_shape.scale_copy = scale n_col_mopp.shape = n_col_shape return n_col_shape
def export_flip_controller(self, fliptxt, texture, target, target_tex): # TODO [animation] port code to use native Blender image strip system # despite its name a NiFlipController does not flip / mirror a texture # instead it swaps through a list of textures for a sprite animation # # fliptxt is a blender text object containing the n_flip definitions # texture is the texture object in blender ( texture is used to checked for pack and mipmap flags ) # target is the NiTexturingProperty # target_tex is the texture to n_flip ( 0 = base texture, 4 = glow texture ) # # returns exported NiFlipController tlist = fliptxt.asLines() # create a NiFlipController n_flip = block_store.create_block("NiFlipController", fliptxt) target.add_controller(n_flip) # fill in NiFlipController's values n_flip.flags = 8 # active n_flip.frequency = 1.0 start = bpy.context.scene.frame_start n_flip.start_time = (start - 1) * self.fps n_flip.stop_time = (bpy.context.scene.frame_end - start) * self.fps n_flip.texture_slot = target_tex count = 0 for t in tlist: if len(t) == 0: continue # skip empty lines # create a NiSourceTexture for each n_flip tex = TextureWriter.export_source_texture(texture, t) n_flip.num_sources += 1 n_flip.sources.update_size() n_flip.sources[n_flip.num_sources - 1] = tex count += 1 if count < 2: raise io_scene_niftools.utils.logging.NifError( f"Error in Texture Flip buffer '{fliptxt.name}': must define at least two textures" ) n_flip.delta = (n_flip.stop_time - n_flip.start_time) / count
def export_range_lod_data(n_node, b_obj): """Export range lod data for for the children of b_obj, as a NiRangeLODData block on n_node. """ # create range lod data object n_range_data = block_store.create_block("NiRangeLODData", b_obj) n_node.lod_level_data = n_range_data # get the children b_children = b_obj.children # set the data n_node.num_lod_levels = len(b_children) n_range_data.num_lod_levels = len(b_children) n_node.lod_levels.update_size() n_range_data.lod_levels.update_size() for b_child, n_lod_level, n_rd_lod_level in zip(b_children, n_node.lod_levels, n_range_data.lod_levels): n_lod_level.near_extent = b_child["near_extent"] n_lod_level.far_extent = b_child["far_extent"] n_rd_lod_level.near_extent = n_lod_level.near_extent n_rd_lod_level.far_extent = n_lod_level.far_extent
def export_collision_list(self, b_obj, n_col_body, layer, n_havok_mat): """Add collision object obj to the list of collision objects of n_col_body. If n_col_body has no collisions yet, a new list is created. If the current collision system is not a list of collisions (bhkListShape), then a ValueError is raised.""" # if no collisions have been exported yet to this parent_block # then create new collision tree on parent_block # bhkCollisionObject -> bhkRigidBody -> bhkListShape # (this works in all cases, can be simplified just before the file is written) if not n_col_body.shape: n_col_shape = block_store.create_block("bhkListShape") n_col_body.shape = n_col_shape # n_col_shape.material = n_havok_mat[0] else: n_col_shape = n_col_body.shape if not isinstance(n_col_shape, NifFormat.bhkListShape): raise ValueError('Not a list of collisions') n_col_shape.add_shape( self.export_collision_object(b_obj, layer, n_havok_mat))
def export_bsbound(self, b_obj, block_parent): box_extends = self.calculate_box_extents(b_obj) n_bbox = block_store.create_block("BSBound") # ... the following incurs double scaling because it will be added in # both the extra data list and in the old extra data sequence!!! # block_parent.add_extra_data(n_bbox) # quick hack (better solution would be to make apply_scale non-recursive) block_parent.num_extra_data_list += 1 block_parent.extra_data_list.update_size() block_parent.extra_data_list[-1] = n_bbox # set name, center, and dimensions n_bbox.name = "BBX" center = n_bbox.center center.x = b_obj.location[0] center.y = b_obj.location[1] center.z = b_obj.location[2] largest = self.calculate_largest_value(box_extends) dims = n_bbox.dimensions dims.x = largest[0] dims.y = largest[1] dims.z = largest[2]
def export_tri_shapes(self, b_obj, n_parent, n_root, trishape_name=None): """ Export a blender object ob of the type mesh, child of nif block n_parent, as NiTriShape and NiTriShapeData blocks, possibly along with some NiTexturingProperty, NiSourceTexture, NiMaterialProperty, and NiAlphaProperty blocks. We export one trishape block per mesh material. We also export vertex weights. The parameter trishape_name passes on the name for meshes that should be exported as a single mesh. """ NifLog.info(f"Exporting {b_obj}") assert (b_obj.type == 'MESH') # get mesh from b_obj b_mesh = self.get_triangulated_mesh(b_obj) b_mesh.calc_normals_split() # getVertsFromGroup fails if the mesh has no vertices # (this happens when checking for fallout 3 body parts) # so quickly catch this (rare!) case if not b_mesh.vertices: # do not export anything NifLog.warn(f"{b_obj} has no vertices, skipped.") return # get the mesh's materials, this updates the mesh material list if not isinstance(n_parent, NifFormat.RootCollisionNode): mesh_materials = b_mesh.materials else: # ignore materials on collision trishapes mesh_materials = [] # if the mesh has no materials, all face material indices should be 0, so it's ok to fake one material in the material list if not mesh_materials: mesh_materials = [None] # vertex color check mesh_hasvcol = b_mesh.vertex_colors # list of body part (name, index, vertices) in this mesh polygon_parts = self.get_polygon_parts(b_obj, b_mesh) game = bpy.context.scene.niftools_scene.game # Non-textured materials, vertex colors are used to color the mesh # Textured materials, they represent lighting details # let's now export one trishape for every mesh material # TODO [material] needs refactoring - move material, texture, etc. to separate function for materialIndex, b_mat in enumerate(mesh_materials): mesh_hasnormals = False if b_mat is not None: mesh_hasnormals = True # for proper lighting if (game == 'SKYRIM' ) and b_mat.niftools_shader.slsf_1_model_space_normals: mesh_hasnormals = False # for proper lighting # create a trishape block if not NifOp.props.stripify: trishape = block_store.create_block("NiTriShape", b_obj) else: trishape = block_store.create_block("NiTriStrips", b_obj) # fill in the NiTriShape's non-trivial values if isinstance(n_parent, NifFormat.RootCollisionNode): trishape.name = "" else: if not trishape_name: if n_parent.name: trishape.name = "Tri " + n_parent.name.decode() else: trishape.name = "Tri " + b_obj.name.decode() else: trishape.name = trishape_name # multimaterial meshes: add material index (Morrowind's child naming convention) if len(mesh_materials) > 1: trishape.name = f"{trishape.name.decode()}: {materialIndex}" else: trishape.name = block_store.get_full_name(trishape) self.set_mesh_flags(b_obj, trishape) # extra shader for Sid Meier's Railroads if game == 'SID_MEIER_S_RAILROADS': trishape.has_shader = True trishape.shader_name = "RRT_NormalMap_Spec_Env_CubeLight" trishape.unknown_integer = -1 # default # if we have an animation of a blender mesh # an intermediate NiNode has been created which holds this b_obj's transform # the trishape itself then needs identity transform (default) if trishape_name is not None: # only export the bind matrix on trishapes that were not animated math.set_object_matrix(b_obj, trishape) # check if there is a parent if n_parent: # add texture effect block (must be added as parent of the trishape) n_parent = self.export_texture_effect(n_parent, b_mat) # refer to this mesh in the parent's children list n_parent.add_child(trishape) self.object_property.export_properties(b_obj, b_mat, trishape) # -> now comes the real export ''' NIF has one uv vertex and one normal per vertex, per vert, vertex coloring. NIF uses the normal table for lighting. Smooth faces should use Blender's vertex normals, solid faces should use Blender's face normals. Blender's uv vertices and normals per face. Blender supports per face vertex coloring, ''' # We now extract vertices, uv-vertices, normals, and # vertex colors from the mesh's face list. Some vertices must be duplicated. # The following algorithm extracts all unique quads(vert, uv-vert, normal, vcol), # produce lists of vertices, uv-vertices, normals, vertex colors, and face indices. mesh_uv_layers = b_mesh.uv_layers vertquad_list = [ ] # (vertex, uv coordinate, normal, vertex color) list vertex_map = [None for _ in range(len(b_mesh.vertices)) ] # blender vertex -> nif vertices vertex_positions = [] normals = [] vertex_colors = [] uv_coords = [] triangles = [] # for each face in triangles, a body part index bodypartfacemap = [] polygons_without_bodypart = [] if b_mesh.polygons: if mesh_uv_layers: # if we have uv coordinates double check that we have uv data if not b_mesh.uv_layer_stencil: NifLog.warn( f"No UV map for texture associated with selected mesh '{b_mesh.name}'." ) use_tangents = False if mesh_uv_layers and mesh_hasnormals: if game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM') or ( game in self.texture_helper.USED_EXTRA_SHADER_TEXTURES): use_tangents = True b_mesh.calc_tangents(uvmap=mesh_uv_layers[0].name) tangents = [] bitangent_signs = [] for poly in b_mesh.polygons: # does the face belong to this trishape? if b_mat is not None and poly.material_index != materialIndex: # we have a material but this face has another material, so skip continue f_numverts = len(poly.vertices) if f_numverts < 3: continue # ignore degenerate polygons assert ((f_numverts == 3) or (f_numverts == 4)) # debug # find (vert, uv-vert, normal, vcol) quad, and if not found, create it f_index = [-1] * f_numverts for i, loop_index in enumerate(poly.loop_indices): fv_index = b_mesh.loops[loop_index].vertex_index vertex = b_mesh.vertices[fv_index] vertex_index = vertex.index fv = vertex.co # smooth = vertex normal, non-smooth = face normal) if mesh_hasnormals: if poly.use_smooth: fn = b_mesh.loops[loop_index].normal else: fn = poly.normal else: fn = None fuv = [ uv_layer.data[loop_index].uv for uv_layer in b_mesh.uv_layers ] # TODO [geomotry][mesh] Need to map b_verts -> n_verts if mesh_hasvcol: f_col = list( b_mesh.vertex_colors[0].data[loop_index].color) else: f_col = None vertquad = (fv, fuv, fn, f_col) # check for duplicate vertquad? f_index[i] = len(vertquad_list) if vertex_map[vertex_index] is not None: # iterate only over vertices with the same vertex index for j in vertex_map[vertex_index]: # check if they have the same uvs, normals and colors if self.is_new_face_corner_data( vertquad, vertquad_list[j]): continue # all tests passed: so yes, we already have a vert with the same face corner data! f_index[i] = j break if f_index[i] > 65535: raise NifError( "Too many vertices. Decimate your mesh and try again." ) if f_index[i] == len(vertquad_list): # first: add it to the vertex map if not vertex_map[vertex_index]: vertex_map[vertex_index] = [] vertex_map[vertex_index].append(len(vertquad_list)) # new (vert, uv-vert, normal, vcol) quad: add it vertquad_list.append(vertquad) # add the vertex vertex_positions.append(vertquad[0]) if mesh_hasnormals: normals.append(vertquad[2]) if use_tangents: tangents.append(b_mesh.loops[loop_index].tangent) bitangent_signs.append( [b_mesh.loops[loop_index].bitangent_sign]) if mesh_hasvcol: vertex_colors.append(vertquad[3]) if mesh_uv_layers: uv_coords.append(vertquad[1]) # now add the (hopefully, convex) face, in triangles for i in range(f_numverts - 2): if (b_obj.scale.x + b_obj.scale.y + b_obj.scale.z) > 0: f_indexed = (f_index[0], f_index[1 + i], f_index[2 + i]) else: f_indexed = (f_index[0], f_index[2 + i], f_index[1 + i]) triangles.append(f_indexed) # add body part number if game not in ('FALLOUT_3', 'SKYRIM') or not polygon_parts: # TODO: or not self.EXPORT_FO3_BODYPARTS): bodypartfacemap.append(0) else: # add the polygon's body part part_index = polygon_parts[poly.index] if part_index >= 0: bodypartfacemap.append(part_index) else: # this signals an error polygons_without_bodypart.append(poly) # check that there are no missing body part polygons if polygons_without_bodypart: self.select_unassigned_polygons(b_mesh, b_obj, polygons_without_bodypart) if len(triangles) > 65535: raise NifError( "Too many polygons. Decimate your mesh and try again.") if len(vertex_positions) == 0: continue # m_4444x: skip 'empty' material indices # add NiTriShape's data if isinstance(trishape, NifFormat.NiTriShape): tridata = block_store.create_block("NiTriShapeData", b_obj) else: tridata = block_store.create_block("NiTriStripsData", b_obj) trishape.data = tridata # data tridata.num_vertices = len(vertex_positions) tridata.has_vertices = True tridata.vertices.update_size() for i, v in enumerate(tridata.vertices): v.x, v.y, v.z = vertex_positions[i] tridata.update_center_radius() if mesh_hasnormals: tridata.has_normals = True tridata.normals.update_size() for i, v in enumerate(tridata.normals): v.x, v.y, v.z = normals[i] if mesh_hasvcol: tridata.has_vertex_colors = True tridata.vertex_colors.update_size() for i, v in enumerate(tridata.vertex_colors): v.r, v.g, v.b, v.a = vertex_colors[i] if mesh_uv_layers: if game in ('FALLOUT_3', 'SKYRIM'): if len(mesh_uv_layers) > 1: raise NifError( f"{game} does not support multiple UV layers.") tridata.num_uv_sets = len(mesh_uv_layers) tridata.bs_num_uv_sets = len(mesh_uv_layers) tridata.has_uv = True tridata.uv_sets.update_size() for j, uv_layer in enumerate(mesh_uv_layers): for i, uv in enumerate(tridata.uv_sets[j]): if len(uv_coords[i]) == 0: continue # skip non-uv textures uv.u = uv_coords[i][j][0] # NIF flips the texture V-coordinate (OpenGL standard) uv.v = 1.0 - uv_coords[i][j][1] # opengl standard # set triangles stitch strips for civ4 tridata.set_triangles(triangles, stitchstrips=NifOp.props.stitch_strips) # update tangent space (as binary extra data only for Oblivion) # for extra shader texture games, only export it if those textures are actually exported # (civ4 seems to be consistent with not using tangent space on non shadered nifs) if use_tangents: if game == 'SKYRIM': tridata.bs_num_uv_sets = tridata.bs_num_uv_sets + 4096 # calculate the bitangents using the normals, tangent list and bitangent sign bitangents = bitangent_signs * np.cross(normals, tangents) # B_tan: +d(B_u), B_bit: +d(B_v) and N_tan: +d(N_v), N_bit: +d(N_u) # moreover, N_v = 1 - B_v, so d(B_v) = - d(N_v), therefore N_tan = -B_bit and N_bit = B_tan self.add_defined_tangents(trishape, tangents=-bitangents, bitangents=tangents, as_extra_data=(game == 'OBLIVION')) # todo [mesh/object] use more sophisticated armature finding, also taking armature modifier into account # now export the vertex weights, if there are any if b_obj.parent and b_obj.parent.type == 'ARMATURE': b_obj_armature = b_obj.parent vertgroups = { vertex_group.name for vertex_group in b_obj.vertex_groups } bone_names = set(b_obj_armature.data.bones.keys()) # the vertgroups that correspond to bone_names are bones that influence the mesh boneinfluences = vertgroups & bone_names if boneinfluences: # yes we have skinning! # create new skinning instance block and link it skininst, skindata = self.create_skin_inst_data( b_obj, b_obj_armature, polygon_parts) trishape.skin_instance = skininst # Vertex weights, find weights and normalization factors vert_list = {} vert_norm = {} unweighted_vertices = [] for bone_group in boneinfluences: b_list_weight = [] b_vert_group = b_obj.vertex_groups[bone_group] for b_vert in b_mesh.vertices: if len(b_vert.groups ) == 0: # check vert has weight_groups unweighted_vertices.append(b_vert.index) continue for g in b_vert.groups: if b_vert_group.name in boneinfluences: if g.group == b_vert_group.index: b_list_weight.append( (b_vert.index, g.weight)) break vert_list[bone_group] = b_list_weight # create normalisation groupings for v in vert_list[bone_group]: if v[0] in vert_norm: vert_norm[v[0]] += v[1] else: vert_norm[v[0]] = v[1] self.select_unweighted_vertices(b_obj, unweighted_vertices) # for each bone, first we get the bone block then we get the vertex weights and then we add it to the NiSkinData # note: allocate memory for faster performance vert_added = [False for _ in range(len(vertex_positions))] for b_bone_name in boneinfluences: # find bone in exported blocks bone_block = self.get_bone_block( b_obj_armature.data.bones[b_bone_name]) # find vertex weights vert_weights = {} for v in vert_list[b_bone_name]: # v[0] is the original vertex index # v[1] is the weight # vertex_map[v[0]] is the set of vertices (indices) to which v[0] was mapped # so we simply export the same weight as the original vertex for each new vertex # write the weights # extra check for multi material meshes if vertex_map[v[0]] and vert_norm[v[0]]: for vert_index in vertex_map[v[0]]: vert_weights[ vert_index] = v[1] / vert_norm[v[0]] vert_added[vert_index] = True # add bone as influence, but only if there were actually any vertices influenced by the bone if vert_weights: trishape.add_bone(bone_block, vert_weights) # update bind position skinning data # trishape.update_bind_position() # override pyffi trishape.update_bind_position with custom one that is relative to the nif root self.update_bind_position(trishape, n_root, b_obj_armature) # calculate center and radius for each skin bone data block trishape.update_skin_center_radius() if NifData.data.version >= 0x04020100 and NifOp.props.skin_partition: NifLog.info("Creating skin partition") part_order = [ getattr(NifFormat.BSDismemberBodyPartType, face_map.name, None) for face_map in b_obj.face_maps ] part_order = [ body_part for body_part in part_order if body_part is not None ] # override pyffi trishape.update_skin_partition with custom one (that allows ordering) trishape.update_skin_partition = update_skin_partition.__get__( trishape) lostweight = trishape.update_skin_partition( maxbonesperpartition=NifOp.props. max_bones_per_partition, maxbonespervertex=NifOp.props.max_bones_per_vertex, stripify=NifOp.props.stripify, stitchstrips=NifOp.props.stitch_strips, padbones=NifOp.props.pad_bones, triangles=triangles, trianglepartmap=bodypartfacemap, maximize_bone_sharing=(game in ('FALLOUT_3', 'SKYRIM')), part_sort_order=part_order) # warn on bad config settings if game == 'OBLIVION': if NifOp.props.pad_bones: NifLog.warn( "Using padbones on Oblivion export. Disable the pad bones option to get higher quality skin partitions." ) if game in ('OBLIVION', 'FALLOUT_3'): if NifOp.props.max_bones_per_partition < 18: NifLog.warn( "Using less than 18 bones per partition on Oblivion/Fallout 3 export." "Set it to 18 to get higher quality skin partitions." ) if game == 'SKYRIM': if NifOp.props.max_bones_per_partition < 24: NifLog.warn( "Using less than 24 bones per partition on Skyrim export." "Set it to 24 to get higher quality skin partitions." ) if lostweight > NifOp.props.epsilon: NifLog.warn( f"Lost {lostweight:f} in vertex weights while creating a skin partition for Blender object '{b_obj.name}' (nif block '{trishape.name}')" ) # clean up del vert_weights del vert_added # fix data consistency type tridata.consistency_flags = b_obj.niftools.consistency_flags # export EGM or NiGeomMorpherController animation self.morph_anim.export_morph(b_mesh, trishape, vertex_map) return trishape
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}") # 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 # protect against null nif versions if bpy.context.scene.niftools_scene.game == 'NONE': raise NifError( "You have not selected a game. Please select a game and" " nif version in the scene tab.") # 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: if 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.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 = "x" if bpy.context.scene.niftools_scene.game in ( 'MORROWIND', ) else "" 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") # find nif version to write self.version, data = scene.get_version_data() NifData.init(data) # 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'): self.transform_anim.add_dummy_controllers() # 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 data.roots = [root_block] scale_correction = bpy.context.scene.niftools_scene.scale_correction if abs(scale_correction) > NifOp.props.epsilon: self.apply_scale(data, round(1 / NifOp.props.scale_correction)) # 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'}
def export_constraints(self, b_obj, root_block): """Export the constraints of an object. @param b_obj: The object whose constraints to export. @param root_block: The root of the nif tree (required for update_a_b).""" if isinstance(b_obj, bpy.types.Bone): # bone object has its constraints stored in the posebone # so now we should get the posebone, but no constraints for # bones are exported anyway for now # so skip this object return if not hasattr(b_obj, "constraints"): # skip text buffers etc return for b_constr in b_obj.constraints: # rigid body joints if b_constr.type == 'RIGID_BODY_JOINT': if bpy.context.scene.niftools_scene.game not in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): NifLog.warn( f"Only Oblivion/Fallout/Skyrim rigid body constraints currently supported: Skipping {b_constr}." ) continue # check that the object is a rigid body for otherbody, otherobj in block_store.block_to_obj.items(): if isinstance( otherbody, NifFormat.bhkRigidBody) and otherobj is b_obj: hkbody = otherbody break else: # no collision body for this object raise io_scene_niftools.utils.logging.NifError( f"Object {b_obj.name} has a rigid body constraint, but is not exported as collision object" ) # yes there is a rigid body constraint # is it of a type that is supported? if b_constr.pivot_type == 'CONE_TWIST': # ball if b_obj.rigid_body.enabled: n_bhkconstraint = block_store.create_block( "bhkRagdollConstraint", b_constr) else: n_bhkconstraint = block_store.create_block( "bhkMalleableConstraint", b_constr) n_bhkconstraint.type = 7 n_bhkdescriptor = n_bhkconstraint.ragdoll elif b_constr.pivot_type == 'HINGE': # hinge if b_obj.rigid_body.enabled: n_bhkconstraint = block_store.create_block( "bhkLimitedHingeConstraint", b_constr) else: n_bhkconstraint = block_store.create_block( "bhkMalleableConstraint", b_constr) n_bhkconstraint.type = 2 n_bhkdescriptor = n_bhkconstraint.limited_hinge else: raise io_scene_niftools.utils.logging.NifError( f"Unsupported rigid body joint type ({b_constr.type}), only ball and hinge are supported." ) # defaults and getting object properties for user settings (should use constraint properties, # but blender does not have those...) if b_constr.limit_angle_max_x != 0: max_angle = b_constr.limit_angle_max_x else: max_angle = 1.5 if b_constr.limit_angle_min_x != 0: min_angle = b_constr.limit_angle_min_x else: min_angle = 0.0 # friction: again, just picking a reasonable value if no real value given if b_obj.niftools_constraint.LHMaxFriction != 0: max_friction = b_obj.niftools_constraint.LHMaxFriction else: if isinstance(n_bhkconstraint, NifFormat.bhkMalleableConstraint): # malleable typically have 0 (perhaps because they have a damping parameter) max_friction = 0 else: # non-malleable typically have 10 if bpy.context.scene.niftools_scene.game == 'FALLOUT_3': max_friction = 100 else: # oblivion max_friction = 10 # parent constraint to hkbody hkbody.num_constraints += 1 hkbody.constraints.update_size() hkbody.constraints[-1] = n_bhkconstraint # export n_bhkconstraint settings n_bhkconstraint.num_entities = 2 n_bhkconstraint.entities.update_size() n_bhkconstraint.entities[0] = hkbody # is there a target? targetobj = b_constr.target if not targetobj: NifLog.warn( f"Constraint {b_constr} has no target, skipped") continue # find target's bhkRigidBody for otherbody, otherobj in block_store.block_to_obj.items(): if isinstance( otherbody, NifFormat.bhkRigidBody) and otherobj == targetobj: n_bhkconstraint.entities[1] = otherbody break else: # not found raise io_scene_niftools.utils.logging.NifError( f"Rigid body target not exported in nif tree - check that {targetobj} is selected during export." ) # priority n_bhkconstraint.priority = 1 # extra malleable constraint settings if isinstance(n_bhkconstraint, NifFormat.bhkMalleableConstraint): # unknowns n_bhkconstraint.unknown_int_2 = 2 n_bhkconstraint.unknown_int_3 = 1 # force required to keep bodies together n_bhkconstraint.tau = b_obj.niftools_constraint.tau n_bhkconstraint.damping = b_obj.niftools_constraint.damping # calculate pivot point and constraint matrix pivot = mathutils.Vector([ b_constr.pivot_x, b_constr.pivot_y, b_constr.pivot_z ]) / self.HAVOK_SCALE constr_matrix = mathutils.Euler( (b_constr.axis_x, b_constr.axis_y, b_constr.axis_z)) constr_matrix = constr_matrix.to_matrix() # transform pivot point and constraint matrix into bhkRigidBody # coordinates (also see import_nif.py, the # NifImport.import_bhk_constraints method) # the pivot point v' is in object coordinates # however nif expects it in hkbody 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' * O * T * B' * B^{-1} * R^{-1} # for the rotation matrix, we transform in the same way # but ignore all translation parts # assume R is unit transform... # apply object transform relative to the bone head # (this is O * T * B' * B^{-1} at once) transform = mathutils.Matrix(b_obj.matrix_local) # pivot = pivot * transform constr_matrix = constr_matrix * transform.to_3x3() # export n_bhkdescriptor pivot point # n_bhkdescriptor.pivot_a.x = pivot[0] # n_bhkdescriptor.pivot_a.y = pivot[1] # n_bhkdescriptor.pivot_a.z = pivot[2] # export n_bhkdescriptor axes and other parameters # (also see import_nif.py NifImport.import_bhk_constraints) axis_x = mathutils.Vector([1, 0, 0]) * constr_matrix axis_y = mathutils.Vector([0, 1, 0]) * constr_matrix axis_z = mathutils.Vector([0, 0, 1]) * constr_matrix if isinstance(n_bhkdescriptor, NifFormat.RagdollDescriptor): # z axis is the twist vector n_bhkdescriptor.twist_a.x = axis_z[0] n_bhkdescriptor.twist_a.y = axis_z[1] n_bhkdescriptor.twist_a.z = axis_z[2] # x axis is the plane vector n_bhkdescriptor.plane_a.x = axis_x[0] n_bhkdescriptor.plane_a.y = axis_x[1] n_bhkdescriptor.plane_a.z = axis_x[2] # angle limits # take them twist and plane to be 45 deg (3.14 / 4 = 0.8) n_bhkdescriptor.plane_min_angle = b_constr.limit_angle_min_x n_bhkdescriptor.plane_max_angle = b_constr.limit_angle_max_x n_bhkdescriptor.cone_max_angle = b_constr.limit_angle_max_y n_bhkdescriptor.twist_min_angle = b_constr.limit_angle_min_z n_bhkdescriptor.twist_max_angle = b_constr.limit_angle_max_z # same for maximum cone angle n_bhkdescriptor.max_friction = max_friction elif isinstance(n_bhkdescriptor, NifFormat.LimitedHingeDescriptor): # y axis is the zero angle vector on the plane of rotation n_bhkdescriptor.perp_2_axle_in_a_1.x = axis_y[0] n_bhkdescriptor.perp_2_axle_in_a_1.y = axis_y[1] n_bhkdescriptor.perp_2_axle_in_a_1.z = axis_y[2] # x axis is the axis of rotation n_bhkdescriptor.axle_a.x = axis_x[0] n_bhkdescriptor.axle_a.y = axis_x[1] n_bhkdescriptor.axle_a.z = axis_x[2] # z is the remaining axis determining the positive direction of rotation n_bhkdescriptor.perp_2_axle_in_a_2.x = axis_z[0] n_bhkdescriptor.perp_2_axle_in_a_2.y = axis_z[1] n_bhkdescriptor.perp_2_axle_in_a_2.z = axis_z[2] # angle limits typically, the constraint on one side is defined by the z axis n_bhkdescriptor.min_angle = min_angle # the maximum axis is typically about 90 degrees # 3.14 / 2 = 1.5 n_bhkdescriptor.max_angle = max_angle # friction n_bhkdescriptor.max_friction = max_friction else: raise ValueError( f"unknown descriptor {n_bhkdescriptor.__class__.__name__}" ) # do AB n_bhkconstraint.update_a_b(root_block) n_bhkdescriptor.pivot_b.x = pivot[0] n_bhkdescriptor.pivot_b.y = pivot[1] n_bhkdescriptor.pivot_b.z = pivot[2]