def export_collision(self, b_obj, n_parent): """Main function for adding collision object b_obj to a node. Returns True if this object is exported as a collision""" if b_obj.display_type != "BOUNDS": return if b_obj.name.lower().startswith('bsbound'): # add a bounding box self.bs_helper.export_bounds(b_obj, n_parent, bsbound=True) elif b_obj.name.lower().startswith("bounding box"): # Morrowind bounding box self.bs_helper.export_bounds(b_obj, n_parent, bsbound=False) if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): nodes = [n_parent] nodes.extend([ block for block in n_parent.children if block.name[:14] == 'collisiondummy' ]) for node in nodes: try: self.bhk_helper.export_collision_helper(b_obj, node) break except ValueError: # adding collision failed continue else: # all nodes failed so add new one node = types.create_ninode(b_obj) node.name = 'collisiondummy{:d}'.format(n_parent.num_children) if b_obj.niftools.flags != 0: node_flag_hex = hex(b_obj.niftools.flags) else: node_flag_hex = 0x000E # default node.flags = node_flag_hex n_parent.add_child(node) self.bhk_helper.export_collision_helper(b_obj, node) elif bpy.context.scene.niftools_scene.game in ('ZOO_TYCOON_2', ): self.bound_helper.export_nicollisiondata(b_obj, n_parent) else: NifLog.warn( f"Collisions not supported for game '{bpy.context.scene.niftools_scene.game}', skipped collision object '{b_obj.name}'" ) return True
def get_b_blend_type_from_n_apply_mode(n_apply_mode): # TODO [material] Check out n_apply_modes if n_apply_mode == NifFormat.ApplyMode.APPLY_MODULATE: return "MIX" elif n_apply_mode == NifFormat.ApplyMode.APPLY_REPLACE: return "COLOR" elif n_apply_mode == NifFormat.ApplyMode.APPLY_DECAL: return "OVERLAY" elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT: return "LIGHTEN" elif n_apply_mode == NifFormat.ApplyMode.APPLY_HILIGHT2: # used by Oblivion for parallax return "MULTIPLY" else: NifLog.warn( f"Unknown apply mode ({n_apply_mode}) in material, using blend type 'MIX'" ) return "MIX"
def load_image(tex_path): """Returns an image or a generated image if none was found""" name = os.path.basename(tex_path) if name not in bpy.data.images: try: b_image = bpy.data.images.load(tex_path) except: NifLog.warn( f"Texture '{name}' not found or not supported and no alternate available" ) b_image = bpy.data.images.new(name=name, width=1, height=1, alpha=True) else: b_image = bpy.data.images[name] return b_image
def import_bhk_simple_shape_phantom(self, bhkshape): """Imports a bhkSimpleShapePhantom block and applies the transform to the collision object""" # import shapes collision_objs = self.import_bhk_shape(bhkshape.shape) NifLog.warn("Support for bhkSimpleShapePhantom is limited, transform is ignored") # todo [pyffi/collision] current nifskope shows a transform, our nif xml doesn't, so ignore it for now # # find transformation matrix # transform = mathutils.Matrix(bhkshape.transform.as_list()) # # # fix scale # transform.translation = transform.translation * self.HAVOK_SCALE # # # apply transform # for b_col_obj in collision_objs: # b_col_obj.matrix_local = b_col_obj.matrix_local @ transform # return a list of transformed collision shapes return collision_objs
def update_bind_position(self, n_geom, n_root, b_obj_armature): """Transfer the Blender bind position to the nif bind position. Sets the NiSkinData overall transform to the inverse of the geometry transform relative to the skeleton root, and sets the NiSkinData of each bone to the inverse of the transpose of the bone transform relative to the skeleton root, corrected for the overall transform.""" if not n_geom.is_skin(): return # validate skin and set up quick links n_geom._validate_skin() skininst = n_geom.skin_instance skindata = skininst.data skelroot = skininst.skeleton_root # calculate overall offset (including the skeleton root transform) and use its inverse geomtransform = (n_geom.get_transform(skelroot) * skelroot.get_transform()).get_inverse(fast=False) skindata.set_transform(geomtransform) # for some nifs, somehow n_root is not set properly?! if not n_root: NifLog.warn(f"n_root was not set, bug") n_root = skelroot old_position = b_obj_armature.data.pose_position b_obj_armature.data.pose_position = 'POSE' # calculate bone offsets for i, bone in enumerate(skininst.bones): bone_name = block_store.block_to_obj[bone].name pose_bone = b_obj_armature.pose.bones[bone_name] n_bind = math.mathutils_to_nifformat_matrix( math.blender_bind_to_nif_bind(pose_bone.matrix)) # todo [armature] figure out the correct transform that works universally # inverse skin bind in nif armature space, relative to root / geom?? skindata.bone_list[i].set_transform( (n_bind * geomtransform).get_inverse(fast=False)) # this seems to be correct for skyrim heads, but breaks stuff like ZT2 elephant # skindata.bone_list[i].set_transform(bone.get_transform(n_root).get_inverse()) b_obj_armature.data.pose_position = old_position
def decompose_srt(b_matrix): """Decompose Blender transform matrix as a scale, 4x4 rotation matrix, and translation vector.""" # get matrix components trans_vec, rot_quat, scale_vec = b_matrix.decompose() rotmat = rot_quat.to_matrix() # todo [armature] negative scale is not generated on armature end # no need to run costly operations here for now # and fix the sign of scale # if b_matrix.determinant() < 0: # scale_vec.negate() # only uniform scaling allow rather large error to accommodate some nifs if abs(scale_vec[0] - scale_vec[1]) + abs(scale_vec[1] - scale_vec[2]) > 0.02: NifLog.warn( "Non-uniform scaling not supported. Workaround: apply size and rotation (CTRL-A)." ) return scale_vec[0], rotmat.to_4x4(), trans_vec
def export_source_texture(n_texture=None, filename=None): """Export a NiSourceTexture. :param n_texture: The n_texture object in blender to be exported. :param filename: The full or relative path to the n_texture file (this argument is used when exporting NiFlipControllers and when exporting default shader slots that have no use in being imported into Blender). :return: The exported NiSourceTexture block. """ # create NiSourceTexture srctex = NifFormat.NiSourceTexture() srctex.use_external = True if filename is not None: # preset filename srctex.file_name = filename elif n_texture is not None: srctex.file_name = TextureWriter.export_texture_filename(n_texture) else: # this probably should not happen NifLog.warn( "Exporting source texture without texture or filename (bug?).") # fill in default values (TODO: can we use 6 for everything?) if bpy.context.scene.niftools_scene.nif_version >= 0x0A000100: srctex.pixel_layout = 6 else: srctex.pixel_layout = 5 srctex.use_mipmaps = 1 srctex.alpha_format = 3 srctex.unknown_byte = 1 # search for duplicate for block in block_store.block_to_obj: if isinstance(block, NifFormat.NiSourceTexture) and block.get_hash( ) == srctex.get_hash(): return block # no identical source texture found, so use and register the new one return block_store.register_block(srctex, n_texture)
def import_version_info(data): scene = bpy.context.scene.niftools_scene nif_version = data._version_value_._value user_version = data._user_version_value_._value user_version_2 = data._user_version_2_value_._value # filter possible games by nif version possible_games = [] for game, versions in NifFormat.games.items(): if game != '?': if nif_version in versions: game_enum = _game_to_enum(game) # go to next game if user version for this game does not match defined if game_enum in scene.USER_VERSION: if scene.USER_VERSION[game_enum] != user_version: continue # or user version in scene is not 0 when this game has no associated user version elif user_version != 0: continue # same checks for user version 2 if game_enum in scene.USER_VERSION_2: if scene.USER_VERSION_2[game_enum] != user_version_2: continue elif user_version_2 != 0: continue # passed all checks, add to possible games list possible_games.append(game_enum) if len(possible_games) == 1: scene.game = possible_games[0] elif len(possible_games) > 1: scene.game = possible_games[0] # todo[version] - check if this nif's version is marked as default for any of the possible games and use that NifLog.warn( f"Game set to '{possible_games[0]}', but multiple games qualified") scene.nif_version = nif_version scene.user_version = user_version scene.user_version_2 = user_version_2
def import_root(self, root_block): """Main import function.""" # check that this is not a kf file if isinstance( root_block, (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)): raise io_scene_niftools.utils.logging.NifError( "Use the KF import operator to load KF files.") # divinity 2: handle CStreamableAssetData if isinstance(root_block, NifFormat.CStreamableAssetData): root_block = root_block.root # sets the root block parent to None, so that when crawling back the script won't barf root_block._parent = None # set the block parent through the tree, to ensure I can always move backward self.set_parents(root_block) # mark armature nodes and bones self.armaturehelper.mark_armatures_bones(root_block) # import the keyframe notes # if NifOp.props.animation: # self.animationhelper.import_text_keys(root_block) # read the NIF tree if isinstance(root_block, (NifFormat.NiNode, NifFormat.NiTriBasedGeom)): b_obj = self.import_branch(root_block) ObjectProperty().import_extra_datas(root_block, b_obj) # now all havok objects are imported, so we are ready to import the havok constraints self.constrainthelper.import_bhk_constraints() # parent selected meshes to imported skeleton if NifOp.props.process == "SKELETON_ONLY": # update parenting & armature modifier for child in bpy.context.selected_objects: if isinstance(child, bpy.types.Object) and not isinstance( child.data, bpy.types.Armature): child.parent = b_obj for mod in child.modifiers: if mod.type == "ARMATURE": mod.object = b_obj elif isinstance(root_block, NifFormat.NiCamera): NifLog.warn('Skipped NiCamera root') elif isinstance(root_block, NifFormat.NiPhysXProp): NifLog.warn('Skipped NiPhysXProp root') else: NifLog.warn( f"Skipped unsupported root block type '{root_block.__class__}' (corrupted nif?)." )
def get_global_uv_transform_clip(self): # get the values from the nodes, find the nodes by name, or search back in the node tree x_scale = y_scale = x_offset = y_offset = clamp_x = clamp_y = None # first check if there are any of the preset name - much more time efficient try: combine_node = self.b_mat.node_tree.nodes["Combine UV0"] if not isinstance(combine_node, bpy.types.ShaderNodeCombineXYZ): combine_node = None NifLog.warn( f"Found node with name 'Combine UV0', but it was of the wrong type." ) except: # if there is a combine node, it does not have the standard name combine_node = None NifLog.warn(f"Did not find node with 'Combine UV0' name.") if combine_node is None: # did not find a (correct) combine node, search through the first existing texture node vector input b_texture_node = None for slot_name, slot_node in self.slots.items(): if slot_node is not None: break if slot_node is not None: combine_node = self.get_input_node_of_type( slot_node.inputs[0], bpy.types.ShaderNodeCombineXYZ) NifLog.warn( f"Searching through vector input of {slot_name} texture gave {combine_node}" ) if combine_node: x_link = combine_node.inputs[0].links if x_link: x_node = x_link[0].from_node x_scale = x_node.inputs[1].default_value x_offset = x_node.inputs[2].default_value clamp_x = x_node.use_clamp y_link = combine_node.inputs[1].links if y_link: y_node = y_link[0].from_node y_scale = y_node.inputs[1].default_value y_offset = y_node.inputs[2].default_value clamp_y = y_node.use_clamp return x_scale, y_scale, x_offset, y_offset, clamp_x, clamp_y
def import_root(self, root_block): """Main import function.""" # check that this is not a kf file if isinstance( root_block, (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)): raise io_scene_niftools.utils.logging.NifError( "Use the KF import operator to load KF files.") # divinity 2: handle CStreamableAssetData if isinstance(root_block, NifFormat.CStreamableAssetData): root_block = root_block.root # mark armature nodes and bones self.armaturehelper.check_for_skin(root_block) # read the NIF tree if isinstance(root_block, (NifFormat.NiNode, NifFormat.NiTriBasedGeom)): b_obj = self.import_branch(root_block) ObjectProperty().import_extra_datas(root_block, b_obj) # now all havok objects are imported, so we are ready to import the havok constraints self.constrainthelper.import_bhk_constraints() # parent selected meshes to imported skeleton if NifOp.props.process == "SKELETON_ONLY": for b_child in self.SELECTED_OBJECTS: self.objecthelper.remove_armature_modifier(b_child) self.objecthelper.append_armature_modifier(b_child, b_obj) elif isinstance(root_block, NifFormat.NiCamera): NifLog.warn('Skipped NiCamera root') elif isinstance(root_block, NifFormat.NiPhysXProp): NifLog.warn('Skipped NiPhysXProp root') else: NifLog.warn( f"Skipped unsupported root block type '{root_block.__class__}' (corrupted nif?)." )
def 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]
def export_material_property(self, b_mat, flags=0x0001): """Return existing material property with given settings, or create a new one if a material property with these settings is not found.""" # don't export material properties for these games if bpy.context.scene.niftools_scene.game in ('SKYRIM', ): return name = block_store.get_full_name(b_mat) # create n_block n_mat_prop = NifFormat.NiMaterialProperty() # list which determines whether the material name is relevant or not only for particular names this holds, # such as EnvMap2 by default, the material name does not affect rendering specialnames = ("EnvMap2", "EnvMap", "skin", "Hair", "dynalpha", "HideSecret", "Lava") # hack to preserve EnvMap2, skinm, ... named blocks (even if they got renamed to EnvMap2.xxx or skin.xxx on import) if bpy.context.scene.niftools_scene.game in ('OBLIVION', 'FALLOUT_3', 'SKYRIM'): for specialname in specialnames: if name.lower() == specialname.lower() or name.lower( ).startswith(specialname.lower() + "."): if name != specialname: NifLog.warn( f"Renaming material '{name}' to '{specialname}'") name = specialname # clear noname materials if name.lower().startswith("noname"): NifLog.warn(f"Renaming material '{name}' to ''") name = "" n_mat_prop.name = name # TODO: - standard flag, check? material and texture properties in morrowind style nifs had a flag n_mat_prop.flags = flags ambient = b_mat.niftools.ambient_color n_mat_prop.ambient_color.r = ambient.r n_mat_prop.ambient_color.g = ambient.g n_mat_prop.ambient_color.b = ambient.b # todo [material] some colors in the b2.8 api allow rgb access, others don't - why?? # diffuse mat n_mat_prop.diffuse_color.r, n_mat_prop.diffuse_color.g, n_mat_prop.diffuse_color.b, _ = b_mat.diffuse_color n_mat_prop.specular_color.r, n_mat_prop.specular_color.g, n_mat_prop.specular_color.b = b_mat.specular_color emissive = b_mat.niftools.emissive_color n_mat_prop.emissive_color.r = emissive.r n_mat_prop.emissive_color.g = emissive.g n_mat_prop.emissive_color.b = emissive.b # gloss mat 'Hardness' scrollbar in Blender, takes values between 1 and 511 (MW -> 0.0 - 128.0) n_mat_prop.glossiness = b_mat.specular_intensity n_mat_prop.alpha = b_mat.niftools.emissive_alpha.v # todo [material] this float is used by FO3's material properties # n_mat_prop.emit_multi = emitmulti # search for duplicate # (ignore the name string as sometimes import needs to create different materials even when NiMaterialProperty is the same) for n_block in block_store.block_to_obj: if not isinstance(n_block, NifFormat.NiMaterialProperty): continue # when optimization is enabled, ignore material name if EXPORT_OPTIMIZE_MATERIALS: ignore_strings = not (n_block.name in specialnames) else: ignore_strings = False # check hash first_index = 1 if ignore_strings else 0 if n_block.get_hash()[first_index:] == n_mat_prop.get_hash( )[first_index:]: NifLog.warn( f"Merging materials '{n_mat_prop.name}' and '{n_block.name}' (they are identical in nif)" ) n_mat_prop = n_block break block_store.register_block(n_mat_prop) # material animation self.material_anim.export_material(b_mat, n_mat_prop) # no material property with given settings found, so use and register the new one return n_mat_prop
def export_collision_helper(self, b_obj, parent_block): """Helper function to add collision objects to a node. This function exports the rigid body, and calls the appropriate function to export the collision geometry in the desired format. @param b_obj: The object to export as collision. @param parent_block: The NiNode parent of the collision. """ rigid_body = b_obj.rigid_body if not rigid_body: NifLog.warn( f"'{b_obj.name}' has no rigid body, skipping rigid body export" ) return # is it packed coll_ispacked = (rigid_body.collision_shape == 'MESH') # Set Havok Scale ratio b_scene = bpy.context.scene.niftools_scene if b_scene.user_version == 12 and b_scene.user_version_2 == 83: self.HAVOK_SCALE = consts.HAVOK_SCALE * 10 # find physics properties/defaults # get havok material name from material name if b_obj.data.materials: n_havok_mat = b_obj.data.materials[0].name else: n_havok_mat = "HAV_MAT_STONE" # linear_velocity = b_obj.rigid_body.deactivate_linear_velocity # angular_velocity = b_obj.rigid_body.deactivate_angular_velocity layer = int(b_obj.nifcollision.collision_layer) # TODO [object][collision][flags] export bsxFlags # self.export_bsx_upb_flags(b_obj, parent_block) # if no collisions have been exported yet to this parent_block # then create new collision tree on parent_block # bhkCollisionObject -> bhkRigidBody if not parent_block.collision_object: # note: collision settings are taken from lowerclasschair01.nif if layer == NifFormat.OblivionLayer.OL_BIPED: # special collision object for creatures n_col_obj = self.export_bhk_blend_collision(b_obj) # TODO [collsion][annimation] add detection for this self.export_bhk_blend_controller(b_obj, parent_block) else: # usual collision object n_col_obj = self.export_bhk_collison_object(b_obj) parent_block.collision_object = n_col_obj n_col_obj.target = parent_block n_bhkrigidbody = self.export_bhk_rigid_body(b_obj, n_col_obj) # we will use n_col_body to attach shapes to below n_col_body = n_bhkrigidbody else: n_col_body = parent_block.collision_object.body # fix total mass n_col_body.mass += rigid_body.mass if coll_ispacked: self.export_collision_packed(b_obj, n_col_body, layer, n_havok_mat) else: if b_obj.nifcollision.export_bhklist: self.export_collision_list(b_obj, n_col_body, layer, n_havok_mat) else: self.export_collision_single(b_obj, n_col_body, layer, n_havok_mat)
def export_collision_object(self, b_obj, layer, n_havok_mat): """Export object obj as box, sphere, capsule, or convex hull. Note: polyheder is handled by export_collision_packed.""" # find bounding box data if not b_obj.data.vertices: NifLog.warn(f"Skipping collision object {b_obj} without vertices.") return None box_extends = self.calculate_box_extents(b_obj) calc_bhkshape_radius = (box_extends[0][1] - box_extends[0][0] + box_extends[1][1] - box_extends[1][0] + box_extends[2][1] - box_extends[2][0]) / (6.0 * self.HAVOK_SCALE) b_r_body = b_obj.rigid_body if b_r_body.use_margin: margin = b_r_body.collision_margin if margin - calc_bhkshape_radius > NifOp.props.epsilon: radius = calc_bhkshape_radius else: radius = margin collision_shape = b_r_body.collision_shape if collision_shape in {'BOX', 'SPHERE'}: # note: collision settings are taken from lowerclasschair01.nif n_coltf = block_store.create_block("bhkConvexTransformShape", b_obj) # n_coltf.material = n_havok_mat[0] n_coltf.unknown_float_1 = 0.1 unk_8 = n_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 hktf = math.get_object_bind(b_obj) # the translation part must point to the center of the data # so calculate the center in local coordinates # TODO [collsion] Replace when method moves to bound class, causes circular dependency center = mathutils.Vector( ((box_extends[0][0] + box_extends[0][1]) / 2.0, (box_extends[1][0] + box_extends[1][1]) / 2.0, (box_extends[2][0] + box_extends[2][1]) / 2.0)) # and transform it to global coordinates center = center @ hktf hktf[0][3] = center[0] hktf[1][3] = center[1] hktf[2][3] = center[2] # we need to store the transpose of the matrix hktf.transpose() n_coltf.transform.set_rows(*hktf) # fix matrix for havok coordinate system n_coltf.transform.m_14 /= self.HAVOK_SCALE n_coltf.transform.m_24 /= self.HAVOK_SCALE n_coltf.transform.m_34 /= self.HAVOK_SCALE if collision_shape == 'BOX': n_colbox = block_store.create_block("bhkBoxShape", b_obj) n_coltf.shape = n_colbox # n_colbox.material = n_havok_mat[0] n_colbox.radius = radius unk_8 = n_colbox.unknown_8_bytes unk_8[0] = 0x6b unk_8[1] = 0xee unk_8[2] = 0x43 unk_8[3] = 0x40 unk_8[4] = 0x3a unk_8[5] = 0xef unk_8[6] = 0x8e unk_8[7] = 0x3e # fix dimensions for havok coordinate system box_extends = self.calculate_box_extents(b_obj) dims = n_colbox.dimensions dims.x = (box_extends[0][1] - box_extends[0][0]) / (2.0 * self.HAVOK_SCALE) dims.y = (box_extends[1][1] - box_extends[1][0]) / (2.0 * self.HAVOK_SCALE) dims.z = (box_extends[2][1] - box_extends[2][0]) / (2.0 * self.HAVOK_SCALE) n_colbox.minimum_size = min(dims.x, dims.y, dims.z) elif collision_shape == 'SPHERE': n_colsphere = block_store.create_block("bhkSphereShape", b_obj) n_coltf.shape = n_colsphere # n_colsphere.material = n_havok_mat[0] # TODO [object][collision] find out what this is: fix for havok coordinate system (6 * 7 = 42) # take average radius n_colsphere.radius = radius return n_coltf elif collision_shape in {'CYLINDER', 'CAPSULE'}: length = b_obj.dimensions.z - b_obj.dimensions.x radius = b_obj.dimensions.x / 2 matrix = math.get_object_bind(b_obj) length_half = length / 2 # calculate the direction unit vector v_dir = (mathutils.Vector( (0, 0, 1)) @ matrix.to_3x3().inverted()).normalized() first_point = matrix.translation + v_dir * length_half second_point = matrix.translation - v_dir * length_half radius /= self.HAVOK_SCALE first_point /= self.HAVOK_SCALE second_point /= self.HAVOK_SCALE n_col_caps = block_store.create_block("bhkCapsuleShape", b_obj) # n_col_caps.material = n_havok_mat[0] # n_col_caps.skyrim_material = n_havok_mat[1] cap_1 = n_col_caps.first_point cap_1.x = first_point.x cap_1.y = first_point.y cap_1.z = first_point.z cap_2 = n_col_caps.second_point cap_2.x = second_point.x cap_2.y = second_point.y cap_2.z = second_point.z n_col_caps.radius = radius n_col_caps.radius_1 = radius n_col_caps.radius_2 = radius return n_col_caps elif collision_shape == 'CONVEX_HULL': b_mesh = b_obj.data b_transform_mat = math.get_object_bind(b_obj) b_rot_quat = b_transform_mat.decompose()[1] b_scale_vec = b_transform_mat.decompose()[0] ''' scale = math.avg(b_scale_vec.to_tuple()) if scale < 0: scale = - (-scale) ** (1.0 / 3) else: scale = scale ** (1.0 / 3) rotation /= scale ''' # calculate vertices, normals, and distances vertlist = [b_transform_mat @ vert.co for vert in b_mesh.vertices] fnormlist = [ b_rot_quat @ b_face.normal for b_face in b_mesh.polygons ] fdistlist = [(b_transform_mat @ (-1 * b_mesh.vertices[ b_mesh.polygons[b_face.index].vertices[0]].co)).dot( b_rot_quat.to_matrix() @ b_face.normal) for b_face in b_mesh.polygons] # remove duplicates through dictionary vertdict = {} for i, vert in enumerate(vertlist): vertdict[(int(vert[0] * consts.VERTEX_RESOLUTION), int(vert[1] * consts.VERTEX_RESOLUTION), int(vert[2] * consts.VERTEX_RESOLUTION))] = i fdict = {} for i, (norm, dist) in enumerate(zip(fnormlist, fdistlist)): fdict[(int(norm[0] * consts.NORMAL_RESOLUTION), int(norm[1] * consts.NORMAL_RESOLUTION), int(norm[2] * consts.NORMAL_RESOLUTION), int(dist * consts.VERTEX_RESOLUTION))] = i # sort vertices and normals vertkeys = sorted(vertdict.keys()) fkeys = sorted(fdict.keys()) vertlist = [vertlist[vertdict[hsh]] for hsh in vertkeys] fnormlist = [fnormlist[fdict[hsh]] for hsh in fkeys] fdistlist = [fdistlist[fdict[hsh]] for hsh in fkeys] if len(fnormlist) > 65535 or len(vertlist) > 65535: raise io_scene_niftools.utils.logging.NifError( "Mesh has too many polygons/vertices. Simply/split your mesh and try again." ) return self.export_bhk_convex_vertices_shape( b_obj, fdistlist, fnormlist, radius, vertlist) else: raise io_scene_niftools.utils.logging.NifError( f'Cannot export collision type {collision_shape} to collision shape list' )
def import_kf_root(self, kf_root, b_armature_obj, bind_data): """Base method to warn user that this root type is not supported""" NifLog.warn(f"Unknown KF root block found : {kf_root.name:s}") NifLog.warn(f"This type isn't currently supported: {type(kf_root)}")
def export_node(self, b_obj, n_parent): """Export a mesh/armature/empty object b_obj as child of n_parent. Export also all children of b_obj. :param n_parent: :param b_obj: """ if not b_obj: return None b_action = self.object_anim.get_active_action(b_obj) # can we export this b_obj? if b_obj.type not in self.export_types: return None if b_obj.type == 'MESH': if self.export_collision(b_obj, n_parent): return else: # -> mesh data. is_multimaterial = len(set([f.material_index for f in b_obj.data.polygons])) > 1 # determine if object tracks camera # nb normally, imported models will have tracking constraints on their parent empty # but users may create track_to constraints directly on objects, so keep it for now has_track = types.has_track(b_obj) # If this has children or animations or more than one material it gets wrapped in a purpose made NiNode. if not (b_action or b_obj.children or is_multimaterial or has_track): mesh = self.mesh_helper.export_tri_shapes(b_obj, n_parent, self.n_root, b_obj.name) if not self.n_root: self.n_root = mesh return mesh # set transform on trishapes rather than NiNodes for skinned meshes to fix an issue with clothing slots if b_obj.parent and b_obj.parent.type == 'ARMATURE' and b_action: # mesh with armature parent should not have animation! NifLog.warn(f"Mesh {b_obj.name} is skinned but also has object animation. " f"The nif format does not support this, ignoring object animation.") b_action = False # -> everything else (empty/armature) is a (more or less regular) node node = types.create_ninode(b_obj) # set parenting here so that it can be accessed if not self.n_root: self.n_root = node # make it child of its parent in the nif, if it has one if n_parent: n_parent.add_child(node) # and fill in this node's non-trivial values node.name = block_store.get_full_name(b_obj) self.set_node_flags(b_obj, node) math.set_object_matrix(b_obj, node) # export object animation self.transform_anim.export_transforms(node, b_obj, b_action) self.object_anim.export_visibility(node, b_action) # if it is a mesh, export the mesh as trishape children of this ninode if b_obj.type == 'MESH': return self.mesh_helper.export_tri_shapes(b_obj, node, self.n_root) # if it is an armature, export the bones as ninode children of this ninode elif b_obj.type == 'ARMATURE': self.armaturehelper.export_bones(b_obj, node) # export all children of this b_obj as children of this NiNode self.export_children(b_obj, node) return node
def import_branch(self, n_block, b_armature=None, n_armature=None): """Read the content of the current NIF tree branch to Blender recursively. :param n_block: The nif block to import. :param b_armature: The blender armature for the current branch. :param n_armature: The corresponding nif block for the armature for the current branch. """ if not n_block: return None NifLog.info(f"Importing data for block '{n_block.name.decode()}'") if isinstance(n_block, NifFormat.NiTriBasedGeom ) and NifOp.props.process != "SKELETON_ONLY": return self.objecthelper.import_geometry_object( b_armature, n_block) elif isinstance(n_block, NifFormat.NiNode): # import object if self.armaturehelper.is_armature_root(n_block): # all bones in the tree are also imported by import_armature if NifOp.props.process != "GEOMETRY_ONLY": b_obj = self.armaturehelper.import_armature(n_block) else: n_name = block_store.import_name(n_block) b_obj = math.get_armature() NifLog.info( f"Merging nif tree '{n_name}' with armature '{b_obj.name}'" ) if n_name != b_obj.name: NifLog.warn( f"Using Nif block '{n_name}' as armature '{b_obj.name}' but names do not match" ) b_armature = b_obj n_armature = n_block elif self.armaturehelper.is_bone(n_block): # bones have already been imported during import_armature n_name = block_store.import_name(n_block) if n_name in b_armature.data.bones: b_obj = b_armature.data.bones[n_name] else: # this is a fallback for a weird bug, when a node is child of a NiLodNode in a skeletal nif b_obj = self.objecthelper.create_b_obj(n_block, None, name=n_name) b_obj.niftools.flags = n_block.flags else: # import as an empty b_obj = NiTypes.import_empty(n_block) # find children b_children = [] n_children = [child for child in n_block.children] for n_child in n_children: b_child = self.import_branch(n_child, b_armature=b_armature, n_armature=n_armature) if b_child and isinstance(b_child, bpy.types.Object): b_children.append(b_child) # import collision objects & bounding box if NifOp.props.process != "SKELETON_ONLY": b_children.extend(self.import_collision(n_block)) b_children.extend( self.boundhelper.import_bounding_box(n_block)) # set bind pose for children self.objecthelper.set_object_bind(b_obj, b_children, b_armature) # import extra node data, such as node type NiTypes.import_root_collision(n_block, b_obj) NiTypes.import_billboard(n_block, b_obj) NiTypes.import_range_lod_data(n_block, b_obj, b_children) # set object transform, this must be done after all children objects have been parented to b_obj if isinstance(b_obj, bpy.types.Object): # note: bones and this object's children already have their matrix set b_obj.matrix_local = math.import_matrix(n_block) # import object level animations (non-skeletal) if NifOp.props.animation: # self.animationhelper.import_text_keys(n_block) self.transform_anim.import_transforms(n_block, b_obj) self.object_anim.import_visibility(n_block, b_obj) return b_obj # all else is currently discarded return None
def import_constraint(self, hkbody): """Imports a bone havok constraint as Blender object constraint.""" assert (isinstance(hkbody, NifFormat.bhkRigidBody)) # check for constraints if not hkbody.constraints: return # find objects if len(collision.DICT_HAVOK_OBJECTS[hkbody]) != 1: NifLog.warn( "Rigid body with no or multiple shapes, constraints skipped") return b_hkobj = collision.DICT_HAVOK_OBJECTS[hkbody][0] NifLog.info(f"Importing constraints for b_hkobj.name") # now import all constraints for hkconstraint in hkbody.constraints: # check constraint entities if not hkconstraint.num_entities == 2: NifLog.warn("Constraint with more than 2 entities, skipped") continue if not hkconstraint.entities[0] is hkbody: NifLog.warn("First constraint entity not self, skipped") continue if not hkconstraint.entities[1] in collision.DICT_HAVOK_OBJECTS: NifLog.warn("Second constraint entity not imported, skipped") continue # get constraint descriptor if isinstance(hkconstraint, NifFormat.bhkRagdollConstraint): hkdescriptor = hkconstraint.ragdoll b_hkobj.rigid_body.enabled = True elif isinstance(hkconstraint, NifFormat.bhkLimitedHingeConstraint): hkdescriptor = hkconstraint.limited_hinge b_hkobj.rigid_body.enabled = True elif isinstance(hkconstraint, NifFormat.bhkHingeConstraint): hkdescriptor = hkconstraint.hinge b_hkobj.rigid_body.enabled = True elif isinstance(hkconstraint, NifFormat.bhkMalleableConstraint): if hkconstraint.type == 7: hkdescriptor = hkconstraint.ragdoll b_hkobj.rigid_body.enabled = False elif hkconstraint.type == 2: hkdescriptor = hkconstraint.limited_hinge b_hkobj.rigid_body.enabled = False else: NifLog.warn( f"Unknown malleable type ({hkconstraint.type:s}), skipped" ) # TODO [constraint][flag] Damping parameters not yet in Blender Python API # TODO [constraint][flag] tau (force between bodies) not supported by Blender else: NifLog.warn( f"Unknown constraint type ({hkconstraint.__class__.__name__}), skipped" ) continue # todo [constraints] the following is no longer possible, fixme return # add the constraint as a rigid body joint b_constr = b_hkobj.constraints.new('RIGID_BODY_JOINT') b_constr.name = b_hkobj.name b_constr.show_pivot = True # note: rigidbodyjoint parameters (from Constraint.c) # CONSTR_RB_AXX 0.0 # CONSTR_RB_AXY 0.0 # CONSTR_RB_AXZ 0.0 # CONSTR_RB_EXTRAFZ 0.0 # CONSTR_RB_MAXLIMIT0 0.0 # CONSTR_RB_MAXLIMIT1 0.0 # CONSTR_RB_MAXLIMIT2 0.0 # CONSTR_RB_MAXLIMIT3 0.0 # CONSTR_RB_MAXLIMIT4 0.0 # CONSTR_RB_MAXLIMIT5 0.0 # CONSTR_RB_MINLIMIT0 0.0 # CONSTR_RB_MINLIMIT1 0.0 # CONSTR_RB_MINLIMIT2 0.0 # CONSTR_RB_MINLIMIT3 0.0 # CONSTR_RB_MINLIMIT4 0.0 # CONSTR_RB_MINLIMIT5 0.0 # CONSTR_RB_PIVX 0.0 # CONSTR_RB_PIVY 0.0 # CONSTR_RB_PIVZ 0.0 # CONSTR_RB_TYPE 12 # LIMIT 63 # PARSIZEY 63 # TARGET [Object "capsule.002"] # limit 3, 4, 5 correspond to angular limits along x, y and z # and are measured in degrees # pivx/y/z is the pivot point # set constraint target b_constr.target = collision.DICT_HAVOK_OBJECTS[ hkconstraint.entities[1]][0] # set rigid body type (generic) b_constr.pivot_type = 'GENERIC_6_DOF' # limiting parameters (limit everything) b_constr.use_angular_limit_x = True b_constr.use_angular_limit_y = True b_constr.use_angular_limit_z = True # get pivot point pivot = mathutils.Vector( (hkdescriptor.pivot_b.x, hkdescriptor.pivot_b.y, hkdescriptor.pivot_b.z)) * self.HAVOK_SCALE # get z- and x-axes of the constraint # (also see export_nif.py NifImport.export_constraints) if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): b_constr.pivot_type = 'CONE_TWIST' # for ragdoll, take z to be the twist axis (central axis of the # cone, that is) axis_z = mathutils.Vector( (hkdescriptor.twist_a.x, hkdescriptor.twist_a.y, hkdescriptor.twist_a.z)) # for ragdoll, let x be the plane vector axis_x = mathutils.Vector( (hkdescriptor.plane_a.x, hkdescriptor.plane_a.y, hkdescriptor.plane_a.z)) # set the angle limits # (see http://niftools.sourceforge.net/wiki/Oblivion/Bhk_Objects/Ragdoll_Constraint # for a nice picture explaining this) b_constr.limit_angle_min_x = hkdescriptor.plane_min_angle b_constr.limit_angle_max_x = hkdescriptor.plane_max_angle b_constr.limit_angle_min_y = -hkdescriptor.cone_max_angle b_constr.limit_angle_max_y = hkdescriptor.cone_max_angle b_constr.limit_angle_min_z = hkdescriptor.twist_min_angle b_constr.limit_angle_max_z = hkdescriptor.twist_max_angle b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction elif isinstance(hkdescriptor, NifFormat.LimitedHingeDescriptor): # for hinge, y is the vector on the plane of rotation defining # the zero angle axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x, hkdescriptor.perp_2_axle_in_a_1.y, hkdescriptor.perp_2_axle_in_a_1.z)) # for hinge, take x to be the the axis of rotation # (this corresponds with Blender's convention for hinges) axis_x = mathutils.Vector( (hkdescriptor.axle_a.x, hkdescriptor.axle_a.y, hkdescriptor.axle_a.z)) # for hinge, z is the vector on the plane of rotation defining # the positive direction of rotation axis_z = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_2.x, hkdescriptor.perp_2_axle_in_a_2.y, hkdescriptor.perp_2_axle_in_a_2.z)) # they should form a orthogonal basis if (mathutils.Vector.cross(axis_x, axis_y) - axis_z).length > 0.01: # either not orthogonal, or negative orientation if (mathutils.Vector.cross(-axis_x, axis_y) - axis_z).length > 0.01: NifLog.warn( f"Axes are not orthogonal in {hkdescriptor.__class__.__name__}; Arbitrary orientation has been chosen" ) axis_z = mathutils.Vector.cross(axis_x, axis_y) else: # fix orientation NifLog.warn( f"X axis flipped in {hkdescriptor.__class__.__name__} to fix orientation" ) axis_x = -axis_x # getting properties with no blender constraint equivalent and setting as obj properties b_constr.limit_angle_max_x = hkdescriptor.max_angle b_constr.limit_angle_min_x = hkdescriptor.min_angle b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction if hasattr(hkconstraint, "tau"): b_hkobj.niftools_constraint.tau = hkconstraint.tau b_hkobj.niftools_constraint.damping = hkconstraint.damping elif isinstance(hkdescriptor, NifFormat.HingeDescriptor): # for hinge, y is the vector on the plane of rotation defining # the zero angle axis_y = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_1.x, hkdescriptor.perp_2_axle_in_a_1.y, hkdescriptor.perp_2_axle_in_a_1.z)) # for hinge, z is the vector on the plane of rotation defining # the positive direction of rotation axis_z = mathutils.Vector((hkdescriptor.perp_2_axle_in_a_2.x, hkdescriptor.perp_2_axle_in_a_2.y, hkdescriptor.perp_2_axle_in_a_2.z)) # take x to be the the axis of rotation # (this corresponds with Blender's convention for hinges) axis_x = mathutils.Vector.cross(axis_y, axis_z) b_hkobj.niftools_constraint.LHMaxFriction = hkdescriptor.max_friction else: raise ValueError("Unknown descriptor {0}".format( hkdescriptor.__class__.__name__)) # transform pivot point and constraint matrix into object # coordinates # (also see export_nif.py NifImport.export_constraints) # the pivot point v is in hkbody coordinates # however blender expects it in object coordinates, v' # v * R * B = v' * O * T * B' # with R = rigid body transform (usually unit tf) # B = nif bone matrix # O = blender object transform # T = bone tail matrix (translation in Y direction) # B' = blender bone matrix # so we need to cancel out the object transformation by # v' = v * R * B * B'^{-1} * T^{-1} * O^{-1} # the local rotation L at the pivot point must be such that # (axis_z + v) * R * B = ([0 0 1] * L + v') * O * T * B' # so (taking the rotation parts of all matrices!!!) # [0 0 1] * L = axis_z * R * B * B'^{-1} * T^{-1} * O^{-1} # and similarly # [1 0 0] * L = axis_x * R * B * B'^{-1} * T^{-1} * O^{-1} # hence these give us the first and last row of L # which is exactly enough to provide the euler angles # multiply with rigid body transform if isinstance(hkbody, NifFormat.bhkRigidBodyT): # set rotation transform = mathutils.Quaternion( (hkbody.rotation.w, hkbody.rotation.x, hkbody.rotation.y, hkbody.rotation.z)).to_matrix() transform.resize_4x4() # set translation transform[0][3] = hkbody.translation.x * self.HAVOK_SCALE transform[1][3] = hkbody.translation.y * self.HAVOK_SCALE transform[2][3] = hkbody.translation.z * self.HAVOK_SCALE # apply transform # pivot = pivot * transform transform = transform.to_3x3() axis_z = axis_z * transform axis_x = axis_x * transform # TODO [armature] update this to use the new bone system # next, cancel out bone matrix correction # note that B' = X * B with X = self.nif_import.dict_bones_extra_matrix[B] # so multiply with the inverse of X # for niBone in self.nif_import.dict_bones_extra_matrix: # if niBone.collision_object \ # and niBone.collision_object.body is hkbody: # transform = mathutils.Matrix( # self.nif_import.dict_bones_extra_matrix[niBone]) # transform.invert() # pivot = pivot * transform # transform = transform.to_3x3() # axis_z = axis_z * transform # axis_x = axis_x * transform # break # # cancel out bone tail translation # if b_hkobj.parent_bone: # pivot[1] -= b_hkobj.parent.data.bones[ # b_hkobj.parent_bone].length # cancel out object transform transform = mathutils.Matrix(b_hkobj.matrix_local) transform.invert() # pivot = pivot * transform transform = transform.to_3x3() axis_z = axis_z * transform axis_x = axis_x * transform # set pivot point b_constr.pivot_x = pivot[0] b_constr.pivot_y = pivot[1] b_constr.pivot_z = pivot[2] # set euler angles constr_matrix = mathutils.Matrix( (axis_x, mathutils.Vector.cross(axis_z, axis_x), axis_z)) constr_euler = constr_matrix.to_euler() b_constr.axis_x = constr_euler.x b_constr.axis_y = constr_euler.y b_constr.axis_z = constr_euler.z # DEBUG assert ((axis_x - mathutils.Vector( (1, 0, 0)) * constr_matrix).length < 0.0001) assert ((axis_z - mathutils.Vector( (0, 0, 1)) * constr_matrix).length < 0.0001) # the generic rigid body type is very buggy... so for simulation purposes let's transform it into ball and hinge if isinstance(hkdescriptor, NifFormat.RagdollDescriptor): # cone_twist b_constr.pivot_type = 'CONE_TWIST' elif isinstance( hkdescriptor, (NifFormat.LimitedHingeDescriptor, NifFormat.HingeDescriptor)): # (limited) hinge b_constr.pivot_type = 'HINGE' else: raise ValueError("Unknown descriptor {0}".format( hkdescriptor.__class__.__name__))
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 process_bhk(self, bhk_shape): """Base method to warn user that this property is not supported""" NifLog.warn(f"Unsupported bhk shape {bhk_shape.__class__.__name__}") NifLog.warn(f"This type isn't currently supported: {type(bhk_shape)}") return []
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 process_property(self, prop): """Base method to warn user that this property is not supported""" NifLog.warn(f"Unknown property block found : {prop.name:s}") NifLog.warn(f"This type isn't currently supported: {type(prop)}")