コード例 #1
0
    def read_base_file(self):
        if not os.path.exists(self.filepath):
            raise GMDError("Must export into an existing file")

        with open(self.filepath, "rb") as in_file:
            data = in_file.read()

        can_write, base_header = can_write_over(data)
        if not can_write:
            raise GMDError(f"Can't write over files with version {base_header.version_str()}")
        self.initial_data, self.scene = read_to_legacy(data)
コード例 #2
0
    def read_base_file(self):
        if not os.path.exists(self.filepath):
            raise GMDError("Must export into an existing file")

        with open(self.filepath, "rb") as in_file:
            data = in_file.read()

        self.gmd_file = GMDFileIOAbstraction(GMDFile(data).structs)
コード例 #3
0
    def read(self):
        with open(self.filepath, "rb") as in_file:
            data = in_file.read()

        can_read, base_header = can_read_from(data)
        if not can_read:
            raise GMDError(
                f"Can't read files with version {base_header.version_str()}")
        self.initial_data, self.scene = read_to_legacy(data)
コード例 #4
0
 def check_bone_sets_match(parent_name: str, blender_bones: List[bpy.types.Bone], gmd_bones: List[GMDBone]):
     blender_bone_dict = {x.name:x for x in blender_bones}
     gmd_bone_dict = {x.name:x for x in gmd_bones}
     if blender_bone_dict.keys() != gmd_bone_dict.keys():
         blender_bone_names = set(blender_bone_dict.keys())
         gmd_bone_names = set(gmd_bone_dict.keys())
         missing_names = gmd_bone_names - blender_bone_names
         unexpected_names = blender_bone_names - gmd_bone_names
         raise GMDError(f"Bones under {parent_name} didn't match between the file and the Blender object. Missing {missing_names}, and found unexpected names {unexpected_names}")
     for (name, gmd_bone) in gmd_bone_dict.items():
         blender_bone = blender_bone_dict[name]
         check_bone_sets_match(name, blender_bone.children, gmd_bone.children)
コード例 #5
0
    def check(self):
        #if bpy.context.object.mode != "OBJECT":
        #    raise GMDError("Can only export in object mode")

        # Check that only one object is selected
        if len(bpy.context.selected_objects) != 1:
            raise GMDError("Exactly one object should be selected to export into the file")
        root = bpy.context.active_object
        if root.type != "EMPTY":
            raise GMDError("The selected object must not be a mesh or armature.")
        if len(root.children) != 1:
            raise GMDError("The selected object should have exactly one child, which should be an armature")
        armature_obj = root.children[0]
        if armature_obj.type != "ARMATURE":
            raise GMDError("The selected object should have exactly one child, which should be an armature")
        if self.strict:
            # Check that the name of the file == the name of the selected object
            expected_name = root_name_for_gmd_file(self.scene)
            if root.name != expected_name:
                raise GMDError(f"Strict Check: The name of the selected object should match the file name: {expected_name}")

        # Check that the bones in the file == the connected bones in the armature?
        def check_bone_sets_match(parent_name: str, blender_bones: List[bpy.types.Bone], gmd_bones: List[GMDBone]):
            blender_bone_dict = {x.name:x for x in blender_bones}
            gmd_bone_dict = {x.name:x for x in gmd_bones}
            if blender_bone_dict.keys() != gmd_bone_dict.keys():
                blender_bone_names = set(blender_bone_dict.keys())
                gmd_bone_names = set(gmd_bone_dict.keys())
                missing_names = gmd_bone_names - blender_bone_names
                unexpected_names = blender_bone_names - gmd_bone_names
                raise GMDError(f"Bones under {parent_name} didn't match between the file and the Blender object. Missing {missing_names}, and found unexpected names {unexpected_names}")
            for (name, gmd_bone) in gmd_bone_dict.items():
                blender_bone = blender_bone_dict[name]
                check_bone_sets_match(name, blender_bone.children, gmd_bone.children)

        self.armature = armature_obj.data
        check_bone_sets_match("root", [b for b in self.armature.bones if not b.parent], self.scene.bone_roots)

        # Gather a list of all of the meshes connected to the selected armature
        # Must be MESH type objects that have an ARMATURE modifier
        self.mesh_objs = [obj for obj in armature_obj.children if (obj.type == "MESH") and (m for m in obj.modifiers if m.types == "ARMATURE")]
        if len(self.mesh_objs) != len(armature_obj.children):
            #raise GMDError(f"Found {len(self.mesh_objs)} valid exportable meshes, but {len(armature_obj.children)} total objects were present.")
            raise GMDError(f"All armature children must be meshes and actually deformed by the armature.")

        # Collect all of the blender materials for the GMDFile material
        self.blender_mat_name_map: Dict[str, int] = {material_name(material):material.id for material in self.scene.materials}

        pass
コード例 #6
0
                    def bonesplit(x: SubmeshHelperSubset):
                        bones = set()
                        for tri in x.referenced_triangles:
                            tri_bones = x.base.triangle_referenced_bones(tri)
                            if len(tri_bones) + len(bones) < 32:
                                bones += tri_bones

                        x_withbones = SubmeshHelperSubset.empty(x.base)
                        x_withoutbones = SubmeshHelperSubset.empty(x.base)
                        for tri in x.referenced_triangles:
                            tri_bones = x.base.triangle_referenced_bones(tri)
                            if bones.issuperset(tri_bones):
                                x_withbones.add_triangle(tri)
                            else:
                                x_withoutbones.add_triangle(tri)

                        if len(x_withoutbones.referenced_triangles) == len(x.referenced_triangles):
                            raise GMDError("bonesplit() did not reduce triangle count!")

                        return x_withbones, x_withoutbones
コード例 #7
0
    def update_gmd_submeshes(self):
        # Convert to REST pose
        old_pose_position = self.armature.pose_position
        if old_pose_position != "REST":
            self.armature.pose_position = "REST"
            #bpy.context.scene.update()

        depsgraph = bpy.context.evaluated_depsgraph_get()

        gmd_submeshes = []
        # Extract mesh data while in the rest pose
        for mesh_obj in self.mesh_objs:
            # Check the mesh uses at least one approved materials
            if len(mesh_obj.material_slots) == 0:
                raise GMDError(f"Mesh {mesh_obj.name} doesn't use any materials! Each mesh must use at least one material")
            unexpected_material_names = {m.name for m in mesh_obj.material_slots if m.name not in self.blender_mat_name_map}
            if unexpected_material_names:
                raise GMDError(f"Mesh {mesh_obj.name} uses materials {unexpected_material_names} which are not already present in the file")
            # Mapping of material slot -> material ID in the GMD file
            obj_mat_slot_to_gmd_id: List[int] = [self.blender_mat_name_map[m.name] for i,m in enumerate(mesh_obj.material_slots)]
            # Mapping of vertex group ID -> bone ID in the GMD file
            # Bones have already been checked, this mesh is a child of a correct armature so the bones in blender == the bones in the file
            # TODO - However, the object itself may still contain other vertex groups
            obj_vertex_group_to_gmd_id = {i:self.scene.bone_name_map[group.name].id for i,group in enumerate(mesh_obj.vertex_groups) if group.name in self.scene.bone_name_map}
            if len(obj_vertex_group_to_gmd_id) != len(mesh_obj.vertex_groups):
                # TODO: Report!
                blender_vgroups = set(g.name for g in mesh_obj.vertex_groups)
                mesh_vgroups = set(self.scene.bone_name_map.keys())
                expected_groups = mesh_vgroups.difference(blender_vgroups)
                new_groups = blender_vgroups.difference(mesh_vgroups)
                print(f"Mesh {mesh_obj.name} is missing vertex groups for {expected_groups}, and has extra groups {new_groups}")

            # Generate a mesh with modifiers applied, and put it into a bmesh
            mesh = mesh_obj.evaluated_get(depsgraph).data
            #mesh_obj.to_mesh(apply_modifiers=True, calc_undeformed=True)
            bm = bmesh.new()
            bm.from_mesh(mesh)
            bm.verts.ensure_lookup_table()
            bm.verts.index_update()

            material_submeshes = [SubmeshHelper() for m in self.scene.materials]

            deform_layer = bm.verts.layers.deform.active

            # TODO: In situations where >2 layers are present, do we want to prompt the user to remove one?
            # TODO: Make sure, if we're doing this, to remember that the else: blocks also cover len(layers) == 0

            if len(bm.loops.layers.color) == 1:
                col0_layer = bm.loops.layers.color[0]
                col1_layer = None
            elif len(bm.loops.layers.color) == 2:
                col0_layer = bm.loops.layers.color[0]
                col1_layer = bm.loops.layers.color[1]
            else:
                col0_layer = bm.loops.layers.color["color0"] if "color0" in bm.loops.layers.color else None
                col1_layer = bm.loops.layers.color["color1"] if "color1" in bm.loops.layers.color else None

            if len(bm.loops.layers.uv) == 1:
                uv0_layer = bm.loops.layers.uv[0]
                uv1_layer = None
            elif len(bm.loops.layers.uv) == 2:
                uv0_layer = bm.loops.layers.uv[0]
                uv1_layer = bm.loops.layers.uv[1]
            else:
                uv0_layer = bm.loops.layers.uv["TexCoords0"] if "TexCoords0" in bm.loops.layers.uv else None
                uv1_layer = bm.loops.layers.uv["TexCoords1"] if "TexCoords1" in bm.loops.layers.uv else None

            # TODO: Check vertex layout against expected layers?
            print(f"Exporting {mesh_obj.name}:")
            print(f"\tmaterial slot mapping: {obj_mat_slot_to_gmd_id}")
            print(f"\tvertex group mapping: {obj_vertex_group_to_gmd_id}")
            print(f"\tdeform_layer: {deform_layer.name}")
            print(f"\tuv_layers: {uv0_layer.name if uv0_layer else None} {uv1_layer.name if uv1_layer else None}")
            print(f"\tcolor_layers: {col0_layer.name if col0_layer else None} {col1_layer.name if col1_layer else None}")

            for tri_loops in bm.calc_loop_triangles():
                l0 = tri_loops[0]
                l1 = tri_loops[1]
                l2 = tri_loops[2]

                if not (0 <= l0.face.material_index < len(obj_mat_slot_to_gmd_id)):
                    # TODO: Report
                    print(f"Mesh {mesh_obj.name} has a face with out-of-bounds material index {l0.face.material_index}. It will be skipped!")
                    continue
                material_id = obj_mat_slot_to_gmd_id[l0.face.material_index]
                sm = material_submeshes[material_id]

                def vertex_of(l, normal, tangent):
                    b_vert = bm.verts[l.vert.index]
                    v = GMDVertex()
                    v.pos = blender_to_yk_space(b_vert.co)
                    v.normal = blender_to_yk_space_vec4(normal if normal else b_vert.normal, 1)
                    v.tangent = blender_to_yk_space_vec4(tangent, 1)

                    # TODO: Color0, Color1
                    if col0_layer:
                        v.col0 = blender_to_yk_color(l[col0_layer])
                    else:
                        v.col0 = Vec4(1, 1, 1, 1)

                    if col1_layer:
                        v.col1 = blender_to_yk_color(l[col1_layer])
                    else:
                        v.col1 = Vec4(1, 1, 1, 1)

                    # Get a list of (vertex group ID, weight) items sorted in descending order of weight
                    # Take the top 4 elements, for the top 4 most deforming bones
                    # Normalize the weights so they sum to 1
                    b_weights = sorted(b_vert[deform_layer].items(), key=lambda i: 1-i[1])
                    if len(b_weights) > 4:
                        b_weights = b_weights[:4]
                    elif len(b_weights) < 4:
                        # Add zeroed elements to b_weights so it's 4 elements long
                        b_weights += [(0, 0.0)] * (4 - len(b_weights))
                    weight_sum = sum(weight for (vtx,weight) in b_weights)
                    if weight_sum <= 0.0:
                        # TODO: Report this with self.report()
                        pass
                    else:
                        b_weights = [(vtx,weight/weight_sum) for (vtx,weight) in b_weights]

                    # Convert the weights to the yk_gmd abstract BoneWeight format
                    weights_list = [BoneWeight(bone=obj_vertex_group_to_gmd_id[vtx], weight=weight) for vtx, weight in b_weights]
                    v.weights = (
                        weights_list[0],
                        weights_list[1],
                        weights_list[2],
                        weights_list[3],
                    )

                    if uv0_layer:
                        v.uv0 = uv_blender_to_yk_space(l[uv0_layer].uv)
                    else:
                        v.uv0 = (0, 0)

                    if uv1_layer:
                        v.uv1 = uv_blender_to_yk_space(l[uv1_layer].uv)
                    else:
                        v.uv1 = (0, 0)

                    return v

                def parse_loop_elem(l):
                    if l.face.smooth:
                        # Smoothed vertices can be shared between different triangles that use them
                        return sm.add_vertex(l.vert.index, lambda: vertex_of(l, None, l.calc_tangent()))
                    else:
                        # Vertices on hard edges cannot be shared and must be duplicated per-face
                        return sm.add_unique_vertex(vertex_of(l, l.calc_normal(), l.calc_tangent()))

                # if face.smooth:
                #     # Vertices can be reused from the base vertex buffer.
                #     # If they're being created from this face, the face tangent for this vert is taken.
                #     # Otherwise, the vertex is taken from
                #     triangle = (
                #         sm.add_vertex(l0.vert.index, ),
                #         sm.add_vertex(l1.vert.index, lambda: vertex_of(l1.vert.index, None, l1.calc_tangent())),
                #         sm.add_vertex(l2.vert.index, lambda: vertex_of(l2.vert.index, None, l2.calc_tangent())),
                #     )
                # else:
                #     # Make copies of the vertices and add them
                #     triangle = (
                #         sm.add_unique_vertex(vertex_of(l0.vert.index, l0.calc_normal(), l0.calc_tangent())),
                #         sm.add_unique_vertex(vertex_of(l1.vert.index, l1.calc_normal(), l1.calc_tangent())),
                #         sm.add_unique_vertex(vertex_of(l2.vert.index, l2.calc_normal(), l2.calc_tangent())),
                #     )
                triangle = (
                    parse_loop_elem(l0),
                    parse_loop_elem(l1),
                    parse_loop_elem(l2),
                )
                sm.add_triangle(triangle)
                pass

            # Free the memory associated with the meshes we created
            bm.free()
            # The mesh isn't in the main database, so don't remove it
            # bpy.data.meshes.remove(mesh)

            # Filter the generated submeshes and put them in the overall submesh list
            for material_id, sm in enumerate(material_submeshes):
                if len(sm.vertices) == 0:
                    continue
                bones = [b for b,vs in sm.weighted_bone_verts.items() if len(vs) > 0]

                split_submeshes = []
                if len(bones) <= 32:
                    split_submeshes.append(sm)
                else:
                    # Split SubmeshHelpers so that you never get >32 unique bones weighting a single submesh
                    # This will always be possible, as any triangle can reference at most 12 bones (3 verts * 4 bones/vert)
                    # so a naive solution of 2 triangles per SubmeshHelper will always reference at most 24 bones which is <32.

                    x_too_many_bones = SubmeshHelperSubset.complete(sm)

                    def bonesplit(x: SubmeshHelperSubset):
                        bones = set()
                        print(x.referenced_triangles)
                        for tri in x.referenced_triangles:
                            tri_bones = x.base.triangle_referenced_bones(tri)
                            if len(tri_bones) + len(bones) < 32:
                                bones = bones.union(tri_bones)

                        x_withbones = SubmeshHelperSubset.empty(x.base)
                        x_withoutbones = SubmeshHelperSubset.empty(x.base)
                        for tri in x.referenced_triangles:
                            tri_bones = x.base.triangle_referenced_bones(tri)
                            if bones.issuperset(tri_bones):
                                x_withbones.add_triangle(tri)
                            else:
                                x_withoutbones.add_triangle(tri)

                        if len(x_withoutbones.referenced_triangles) == len(x.referenced_triangles):
                            raise GMDError("bonesplit() did not reduce triangle count!")

                        return x_withbones, x_withoutbones


                    # Start by selecting 32 bones.
                        # bones = {}
                        # for tri in submesh:
                            # tri_bones = tri.referenced_bones() (at max 24)
                            # if len(tri_bones) + len(bones) > 32
                                # break
                            # bones += tri_bones
                        # This algorithm guarantees that at least one triangle uses ONLY those bones.
                    # Then put all of the triangles that reference ONLY those bones in a new mesh.
                    # Put the other triangles in a separate mesh. If they reference > 32 bones, apply the process again.
                    # This splitting transformation bonesplit(x, bones) -> x_thosebones, x_otherbones will always produce x_otherbones with fewer triangles than x
                        # We know that at least one triangle uses only the selected bones
                            # => len(x_thosebones) >= 1
                            # len(x_otherbones) = len(x) - len(x_thosebones)
                            # => len(x_otherbones) <= len(x) - 1
                            # => len(x_otherbones) < len(x)
                    # => applying bonesplit to x_otherbones recursively will definitely reduce the amount of triangles to 0
                    # it will produce at maximum len(x) new meshes
                    split_meshes = []
                    while len(x_too_many_bones.referenced_triangles) > 0:
                        new_submesh,x_too_many_bones = bonesplit(x_too_many_bones)
                        split_meshes.append(new_submesh)

                    # these can then be merged back together!!!!
                    # TODO: Check if it's even worth it
                    print(f"Mesh {mesh_obj.name} had >32 bone references ({len(bones)}) and was split into {len(split_meshes)} chunks")

                    for split_mesh in split_meshes:
                        split_submeshes.append(split_mesh.convert_to_submeshhelper())

                    pass
                # Convert the SubmeshHelpers to Submeshes
                for sm in split_submeshes:
                    relevant_bone_list = list(sm.total_referenced_bones())
                    bone_id_map = {bone_id:idx for idx,bone_id in enumerate(relevant_bone_list)}

                    def remap_weight(w: BoneWeight):
                        if w.weight == 0:
                            return w
                        else:
                            return BoneWeight(bone=bone_id_map[w.bone], weight=w.weight)

                    def remap_vertex(v: GMDVertex):
                        new_v = GMDVertex()
                        new_v.pos = v.pos
                        new_v.normal = v.normal
                        new_v.tangent = v.tangent
                        new_v.uv0 = v.uv0
                        new_v.uv1 = v.uv1
                        new_v.col0 = v.col0
                        new_v.col1 = v.col1
                        new_v.weights = ((
                            remap_weight(v.weights[0]),
                            remap_weight(v.weights[1]),
                            remap_weight(v.weights[2]),
                            remap_weight(v.weights[3]),
                        ))
                        return new_v

                    vertices = [remap_vertex(v) for v in sm.vertices]

                    # TODO: Better strip handling?
                    # The newer games only render triangle_strip_reset_indices, but the file should contain all variants
                    # TODO: Enable configurable import for each triangle strip type - import only one type, or all?
                    triangle_indices = []
                    triangle_strip_noreset_indices = []
                    triangle_strip_reset_indices = []
                    for t in sm.triangles:
                        # Blender uses reversed winding order
                        triangle_indices.append(t[0])
                        triangle_indices.append(t[2])
                        triangle_indices.append(t[1])

                        # If we can continue the strip, do so
                        if not triangle_strip_noreset_indices:
                            # Add the triangle as normal
                            triangle_strip_noreset_indices.append(t[0])
                            triangle_strip_noreset_indices.append(t[2])
                            triangle_strip_noreset_indices.append(t[1])
                        elif (triangle_strip_noreset_indices[-2] == t[0] and
                                triangle_strip_noreset_indices[-1] == t[2]):
                            triangle_strip_noreset_indices.append(t[1])
                        else:
                            # Two extra verts to create a degenerate triangle, signalling the end of the strip
                            triangle_strip_noreset_indices.append(triangle_strip_noreset_indices[-1])
                            triangle_strip_noreset_indices.append(t[0])
                            # Add the triangle as normal
                            triangle_strip_noreset_indices.append(t[0])
                            triangle_strip_noreset_indices.append(t[2])
                            triangle_strip_noreset_indices.append(t[1])

                        # If we can continue the strip, do so
                        if not triangle_strip_reset_indices:
                            # Add the triangle as normal
                            triangle_strip_reset_indices.append(t[0])
                            triangle_strip_reset_indices.append(t[2])
                            triangle_strip_reset_indices.append(t[1])
                        elif (triangle_strip_reset_indices[-2] == t[0] and
                                triangle_strip_reset_indices[-1] == t[2]):
                            triangle_strip_reset_indices.append(t[1])
                        else:
                            # Reset index signalling the end of the strip
                            triangle_strip_reset_indices.append(0xFFFF)
                            # Add the triangle as normal
                            triangle_strip_reset_indices.append(t[0])
                            triangle_strip_reset_indices.append(t[2])
                            triangle_strip_reset_indices.append(t[1])

                    # triangle_indices = [sm.triangles[0][0], sm.triangles[0][2], sm.triangles[0][1]]
                    # triangle_strip_noreset_indices = [sm.triangles[1][0], sm.triangles[1][2], sm.triangles[1][1]]
                    # triangle_strip_reset_indices = functools.reduce(lambda a,b: a + [0xFFFF] + b, [[t[0], t[2], t[1]] for t in sm.triangles])

                    gmd_submeshes.append(GMDSubmesh(
                        material=self.scene.materials[material_id],

                        relevant_bones=relevant_bone_list,

                        vertices=vertices,

                        triangle_indices=triangle_indices,
                        triangle_strip_noreset_indices=triangle_strip_noreset_indices,
                        triangle_strip_reset_indices=triangle_strip_reset_indices,
                    ))
                    # Remove the submesh data, avoid massive memory usage
                    del sm
            pass

        # Put the submesh list into self.gmd_file
        self.scene.submeshes = gmd_submeshes

        print(f"Built list of submeshes: {len(self.scene.submeshes)}")

        # Convert back to normal pose
        if old_pose_position != "REST":
            self.armature.pose_position = old_pose_position
            #bpy.context.scene.update()
        pass