예제 #1
0
    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)
예제 #3
0
    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
예제 #4
0
    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)
예제 #7
0
    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'
            )