def export_capsulebv(self, b_obj, n_bv): """ Export b_obj as a NiCollisionData's bounding_volume capsule """ n_bv.collision_type = 2 matrix = math.get_object_bind(b_obj) offset = matrix.translation # calculate the direction unit vector v_dir = (mathutils.Vector( (0, 0, 1)) @ matrix.to_3x3().inverted()).normalized() extent = b_obj.dimensions.z - b_obj.dimensions.x radius = b_obj.dimensions.x / 2 # store data capsule = n_bv.capsule center = capsule.center center.x = offset.x center.y = offset.y center.z = offset.z origin = capsule.origin origin.x = v_dir.x origin.y = v_dir.y origin.z = v_dir.z # TODO [collision] nb properly named in newer nif.xmls capsule.unknown_float_1 = extent capsule.unknown_float_2 = radius
def get_bind_data(self, b_armature): """Get the required bind data of an armature. Used by standalone KF import and export. """ self.bind_data = {} if b_armature: for b_bone in b_armature.data.bones: n_bind_scale, n_bind_rot, n_bind_trans = math.decompose_srt(math.get_object_bind(b_bone)) self.bind_data[b_bone.name] = (n_bind_rot.inverted(), n_bind_trans)
def export_boxbv(self, b_obj, n_bv): """ Export b_obj as a NiCollisionData's bounding_volume box """ n_bv.collision_type = 1 matrix = math.get_object_bind(b_obj) # set center center = matrix.translation box_center = n_bv.box.center box_center.x = center.x box_center.y = center.y box_center.z = center.z # set axes to unity 3x3 matrix axis = n_bv.box.axis axis[0].x = 1 axis[1].y = 1 axis[2].z = 1 # set extent extent = b_obj.dimensions / 2 box_extent = n_bv.box.extent box_extent[0] = extent.x box_extent[1] = extent.y box_extent[2] = extent.z
def export_spherebv(self, b_obj, n_bv): """ Export b_obj as a NiCollisionData's bounding_volume sphere """ n_bv.collision_type = 0 matrix = math.get_object_bind(b_obj) center = matrix.translation n_bv.sphere.radius = b_obj.dimensions.x / 2 sphere_center = n_bv.sphere.center sphere_center.x = center.x sphere_center.y = center.y sphere_center.z = center.z
def export_collision_packed(self, b_obj, n_col_body, layer, n_havok_mat): """Add object ob as packed collision object to collision body n_col_body. If parent_block hasn't any collisions yet, a new packed list is created. If the current collision system is not a packed list of collisions (bhkPackedNiTriStripsShape), then a ValueError is raised. """ if not n_col_body.shape: n_col_mopp = self.export_bhk_mopp_bv_tree_shape(b_obj, n_col_body) n_col_shape = self.export_bhk_packed_nitristrip_shape( b_obj, n_col_mopp) else: raise ValueError('Multi-material mopps not supported for now') # TODO [object][collision] this code will do the trick once multi-material mopps work # n_col_mopp = n_col_body.shape # if not isinstance(n_col_mopp, NifFormat.bhkMoppBvTreeShape): # raise ValueError('Not a packed list of collisions') # n_col_shape = n_col_mopp.shape # if not isinstance(n_col_shape, NifFormat.bhkPackedNiTriStripsShape): # raise ValueError('Not a packed list of collisions') b_mesh = b_obj.data transform = math.get_object_bind(b_obj) rotation = transform.decompose()[1] vertices = [vert.co * transform for vert in b_mesh.vertices] triangles = [] normals = [] for face in b_mesh.polygons: if len(face.vertices) < 3: continue # ignore degenerate polygons triangles.append([face.vertices[i] for i in (0, 1, 2)]) normals.append(rotation * face.normal) if len(face.vertices) == 4: triangles.append([face.vertices[i] for i in (0, 2, 3)]) normals.append(rotation * face.normal) # TODO [collision][havok] Redo this as a material lookup havok_mat = NifFormat.HavokMaterial() havok_mat.material = n_havok_mat n_col_shape.add_shape(triangles, normals, vertices, layer, havok_mat.material)
def export_bone(self, b_obj, b_bone, n_parent_node, n_root_node): """Exports a bone and all of its children.""" # create a new nif block for this b_bone n_node = types.create_ninode(b_bone) n_node.name = block_store.get_full_name(b_bone) # link to nif parent node n_parent_node.add_child(n_node) self.export_bone_flags(b_bone, n_node) # set the pose on the nodes p_mat = math.get_object_bind(b_obj.pose.bones[b_bone.name]) math.set_b_matrix_to_n_block(p_mat, n_node) # per-bone animation self.transform_anim.export_transforms(n_node, b_obj, self.b_action, b_bone) # continue down the bone tree for b_child in b_bone.children: self.export_bone(b_obj, b_child, n_node, n_root_node)
def export_transforms(self, parent_block, b_obj, b_action, bone=None): """ If bone == None, object level animation is exported. If a bone is given, skeletal animation is exported. """ # b_action may be None, then nothing is done. if not b_action: return # blender object must exist assert b_obj # if a bone is given, b_obj must be an armature if bone: assert type(b_obj.data) == bpy.types.Armature # just for more detailed error reporting later on bonestr = "" # skeletal animation - with bone correction & coordinate corrections if bone and bone.name in b_action.groups: # get bind matrix for bone or object bind_matrix = math.get_object_bind(bone) exp_fcurves = b_action.groups[bone.name].channels # just for more detailed error reporting later on bonestr = " in bone " + bone.name target_name = block_store.get_full_name(bone) priority = bone.niftools.priority # object level animation - no coordinate corrections elif not bone: # raise error on any objects parented to bones if b_obj.parent and b_obj.parent_type == "BONE": raise io_scene_niftools.utils.logging.NifError( "{} is parented to a bone AND has animations. The nif format does not support this!" .format(b_obj.name)) target_name = block_store.get_full_name(b_obj) priority = 0 # we have either a root object (Scene Root), in which case we take the coordinates without modification # or a generic object parented to an empty = node # objects may have an offset from their parent that is not apparent in the user input (ie. UI values and keyframes) # we want to export matrix_local, and the keyframes are in matrix_basis, so do: # matrix_local = matrix_parent_inverse * matrix_basis bind_matrix = b_obj.matrix_parent_inverse exp_fcurves = [ fcu for fcu in b_action.fcurves if fcu.data_path in ("rotation_quaternion", "rotation_euler", "location", "scale") ] else: # bone isn't keyframed in this action, nothing to do here return # decompose the bind matrix bind_scale, bind_rot, bind_trans = math.decompose_srt(bind_matrix) n_kfc, n_kfi = self.create_controller(parent_block, target_name, priority) # fill in the non-trivial values start_frame, stop_frame = b_action.frame_range self.set_flags_and_timing(n_kfc, exp_fcurves, start_frame, stop_frame) # get the desired fcurves for each data type from exp_fcurves quaternions = [ fcu for fcu in exp_fcurves if fcu.data_path.endswith("quaternion") ] translations = [ fcu for fcu in exp_fcurves if fcu.data_path.endswith("location") ] eulers = [ fcu for fcu in exp_fcurves if fcu.data_path.endswith("euler") ] scales = [ fcu for fcu in exp_fcurves if fcu.data_path.endswith("scale") ] # ensure that those groups that are present have all their fcurves for fcus, num_fcus in ((quaternions, 4), (eulers, 3), (translations, 3), (scales, 3)): if fcus and len(fcus) != num_fcus: raise io_scene_niftools.utils.logging.NifError( "Incomplete key set {} for action {}. Ensure that if a bone is keyframed for a property, all channels are keyframed." .format(bonestr, b_action.name)) # go over all fcurves collected above and transform and store all their keys quat_curve = [] euler_curve = [] trans_curve = [] scale_curve = [] for frame, quat in self.iter_frame_key(quaternions, mathutils.Quaternion): quat = math.export_keymat(bind_rot, quat.to_matrix().to_4x4(), bone).to_quaternion() quat_curve.append((frame, quat)) for frame, euler in self.iter_frame_key(eulers, mathutils.Euler): keymat = math.export_keymat(bind_rot, euler.to_matrix().to_4x4(), bone) euler = keymat.to_euler("XYZ", euler) euler_curve.append((frame, euler)) for frame, trans in self.iter_frame_key(translations, mathutils.Vector): keymat = math.export_keymat(bind_rot, mathutils.Matrix.Translation(trans), bone) trans = keymat.to_translation() + bind_trans trans_curve.append((frame, trans)) for frame, scale in self.iter_frame_key(scales, mathutils.Vector): # just use the first scale curve and assume even scale over all curves scale_curve.append((frame, scale[0])) if n_kfi: if max( len(c) for c in (quat_curve, euler_curve, trans_curve, scale_curve)) > 1: # number of frames is > 1, so add transform data n_kfd = block_store.create_block("NiTransformData", exp_fcurves) n_kfi.data = n_kfd else: # only add data if number of keys is > 1 # (see importer comments with import_kf_root: a single frame # keyframe denotes an interpolator without further data) # insufficient keys, so set the data and we're done! if trans_curve: trans = trans_curve[0][1] n_kfi.translation.x, n_kfi.translation.y, n_kfi.translation.z = trans if quat_curve: quat = quat_curve[0][1] elif euler_curve: quat = euler_curve[0][1].to_quaternion() if quat_curve or euler_curve: n_kfi.rotation.x = quat.x n_kfi.rotation.y = quat.y n_kfi.rotation.z = quat.z n_kfi.rotation.w = quat.w # ignore scale for now... n_kfi.scale = 1.0 # no need to add any keys, done return else: # add the keyframe data n_kfd = block_store.create_block("NiKeyframeData", exp_fcurves) n_kfc.data = n_kfd # TODO [animation] support other interpolation modes, get interpolation from blender? # probably requires additional data like tangents and stuff # finally we can export the data calculated above if euler_curve: n_kfd.rotation_type = NifFormat.KeyType.XYZ_ROTATION_KEY n_kfd.num_rotation_keys = 1 # *NOT* len(frames) this crashes the engine! for i, coord in enumerate(n_kfd.xyz_rotations): coord.num_keys = len(euler_curve) coord.interpolation = NifFormat.KeyType.LINEAR_KEY coord.keys.update_size() for key, (frame, euler) in zip(coord.keys, euler_curve): key.time = frame / self.fps key.value = euler[i] elif quat_curve: n_kfd.rotation_type = NifFormat.KeyType.QUADRATIC_KEY n_kfd.num_rotation_keys = len(quat_curve) n_kfd.quaternion_keys.update_size() for key, (frame, quat) in zip(n_kfd.quaternion_keys, quat_curve): key.time = frame / self.fps key.value.w = quat.w key.value.x = quat.x key.value.y = quat.y key.value.z = quat.z n_kfd.translations.interpolation = NifFormat.KeyType.LINEAR_KEY n_kfd.translations.num_keys = len(trans_curve) n_kfd.translations.keys.update_size() for key, (frame, trans) in zip(n_kfd.translations.keys, trans_curve): key.time = frame / self.fps key.value.x, key.value.y, key.value.z = trans n_kfd.scales.interpolation = NifFormat.KeyType.LINEAR_KEY n_kfd.scales.num_keys = len(scale_curve) n_kfd.scales.keys.update_size() for key, (frame, scale) in zip(n_kfd.scales.keys, scale_curve): key.time = frame / self.fps key.value = scale
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' )