import bpy import os import re print("=== EXPORT STARTED ===") blend_dir = os.path.dirname(bpy.data.filepath) out_path = os.path.join(blend_dir, "pgout") os.makedirs(out_path, exist_ok=True) roomnames = [] armatures = {} armature_anims = {} def get_armature_keep_bones(armature: bpy.types.Armature): keep_bones = set() for bone in armature.data.bones: bone_name = bone.name is_tag = re.match(r"Tag.(\w+)", bone_name) if bone.use_deform or is_tag: keep_bones.add(bone) while bone.parent: bone = bone.parent keep_bones.add(bone) return keep_bones def export_mesh(obj, output_path, export_luv = False, armature = None): if obj.type != 'MESH': raise TypeError("Selected object is not a mesh") mesh = obj.data mesh.calc_loop_triangles() # Prepare UV layers uv_layer = mesh.uv_layers.active.data lightmap_uv_data = None if export_luv: lightmap_layer = mesh.uv_layers.get("LightmapUV") lightmap_uv_data = lightmap_layer.data if lightmap_layer else None export_bones = armature is not None keep_bone_names = None if export_bones: keep_bone_names = set(bone.name for bone in get_armature_keep_bones(armature)) # Vertex deduplication map and list vertex_map = {} # (position, normal, uv, lightmap_uv) -> vertex_index unique_vertices = [] # list of strings (v ...) # faces = [] # list of (v1, v2, v3) indices material_faces = {} # material_index -> list of face indices bone_map = {} unique_bones = [] def get_bone_index(bone_name): if bone_name not in bone_map: bone_map[bone_name] = len(unique_bones) unique_bones.append(bone_name) return bone_map[bone_name] for tri in mesh.loop_triangles: face_indices = [] material_index = tri.material_index # Get the material from the object's material slots if material_index < len(obj.material_slots): material = obj.material_slots[material_index].material material_name = material.name if material else "Unknown" else: material_name = "Unknown" if material_name not in material_faces: material_faces[material_name] = [] faces = material_faces[material_name] for loop_index in tri.loops: loop = mesh.loops[loop_index] vertex = mesh.vertices[loop.vertex_index] pos = tuple(round(c, 6) for c in vertex.co) normal = tuple(round(n, 6) for n in loop.normal) uv = tuple(round(c, 6) for c in uv_layer[loop_index].uv) if lightmap_uv_data: luv = tuple(round(c, 6) for c in lightmap_uv_data[loop_index].uv) else: luv = (0.0, 0.0) if export_bones: deform_bones = [] for vert_group in vertex.groups: weight = round(vert_group.weight, 3) if weight < 0.001: continue group = obj.vertex_groups[vert_group.group] if group.name not in keep_bone_names: continue deform_bones.append((group.name, weight)) deform_bones.sort(key=lambda x: x[1], reverse=True) deform_bones = deform_bones[:4] weight_sum = sum(w for _, w in deform_bones) bones_data = tuple((get_bone_index(name), w / weight_sum) for name, w in deform_bones) else: bones_data = tuple() key = (pos, normal, uv, luv, bones_data) if key in vertex_map: index = vertex_map[key] else: index = len(unique_vertices) vertex_map[key] = index # Create vertex line vertex_line_comps = [ " ".join(f"{c:.3f}" for c in pos), " ".join(f"{n:.3f}" for n in normal), " ".join(f"{c:.3f}" for c in uv) ] if export_luv: vertex_line_comps.append(" ".join(f"{c:.3f}" for c in luv)) if export_bones: vertex_line_comps.append(str(len(bones_data))) vertex_line_comps.append(" ".join(f"{b[0]} {b[1]:.3f}" for b in bones_data)) vertex_line = "v " + " ".join(vertex_line_comps) unique_vertices.append(vertex_line) face_indices.append(index) if len(face_indices) == 3: faces.append(face_indices) # Write to file with open(output_path, 'w') as f: if export_luv: f.write("luv\n") if export_bones: f.write(f"skeleton {armature.name}\n") for bone in unique_bones: f.write(f"d {bone}\n") f.write(f"# v {' ' if export_luv else ''}{' [...]' if export_bones else ''}\n") for v in unique_vertices: f.write(v + "\n") for material_name, faces in material_faces.items(): f.write(f"m {material_name}\n") for face in faces: f.write("f {} {} {}\n".format(*face)) print(f"Exported {obj.name} to: {output_path}") # Output file path # output_path = os.path.join(os.path.expanduser("~"), "Desktop", "exported_model.txt") # obj = bpy.context.active_object def rad_to_deg(radians): return radians * (180.0 / 3.141592653589793) def rotation_str(rotation): return f"{rad_to_deg(rotation.x):.0f} {rad_to_deg(rotation.y):.0f} {rad_to_deg(rotation.z):.0f}" def rotation_str2(rotation): return f"{rad_to_deg(rotation.x):.3f} {rad_to_deg(rotation.y):.3f} {rad_to_deg(rotation.z):.3f}" def position_str(position): return f"{position.x:.4f} {position.y:.4f} {position.z:.4f}" def scale_str(scale): return f"{scale.x:.4f}" def matrix_decompose_str(matrix): translation, rotation, scale = matrix.decompose() return f"{position_str(translation)} {rotation_str2(rotation.to_euler())} {scale_str(scale)}" def get_path(name, ext): return os.path.join(out_path, f"{name}.{ext}") def export_armature(armature: bpy.types.Armature, output_path: str): keep_bones = get_armature_keep_bones(armature) # Export armature data (bones, etc.) with open(output_path, 'w') as f: f.write("# b \n") for bone in armature.data.bones: if not bone in keep_bones: continue parent = bone.parent parent_name = parent.name if parent else "NONE" bind_matrix = bone.matrix_local f.write(f"b {bone.name} {parent_name} {matrix_decompose_str(bind_matrix)}\n") anims = armature_anims.get(armature.name, []) for (anim_name, anim_file_name) in anims: f.write(f"anim {anim_name} {anim_file_name}\n") print(f"Exported Armature: {armature.name} to {output_path}") def vectors_similar(v1, v2, threshold): return (v1 - v2).length < threshold def frames_similar(f1, f2, threshold=0.00001): _, t1, r1, s1, _ = f1 _, t2, r2, s2, _ = f2 if not vectors_similar(t1, t2, threshold): return False if r1.x - r2.x > threshold or r1.y - r2.y > threshold or r1.z - r2.z > threshold: return False if not vectors_similar(s1, s2, threshold): return False return True def export_animation(action: bpy.types.Action, armature: bpy.types.Armature, output_path: str): original_action = armature.animation_data.action original_frame = bpy.context.scene.frame_current armature.animation_data.action = action keep_bones = get_armature_keep_bones(armature) bone_frames = {bone.name: [] for bone in keep_bones} _, end = map(int, action.frame_range) fps = bpy.context.scene.render.fps for frame in range(0, end): bpy.context.scene.frame_set(frame) bpy.context.view_layer.update() for bone in keep_bones: pose_bone = armature.pose.bones.get(bone.name) if not pose_bone: continue matrix = (pose_bone.parent.matrix.inverted() @ pose_bone.matrix) if pose_bone.parent else pose_bone.matrix.copy() translation, rotation, scale = matrix.decompose() rotation_euler = rotation.to_euler() current_frame = (frame, translation, rotation_euler, scale, matrix) frame_list = bone_frames[bone.name] if len(frame_list) > 0: last_frame = frame_list[-1] if frames_similar(last_frame, current_frame): continue frame_list.append(current_frame) with open(output_path, 'w') as f: f.write(f"frames {end}\n") f.write(f"fps {fps}\n") for bone_name, frames in bone_frames.items(): f.write(f"ch {bone_name}\n") for frame in frames: idx, _, _, _, matrix = frame f.write(f"f {idx} {matrix_decompose_str(matrix)}\n") print(f"Exported Animation: {action.name} to {output_path}") bpy.context.scene.frame_set(original_frame) armature.animation_data.action = original_action print("Exporting rooms...") for collection in bpy.context.scene.collection.children: match = re.search(r"Room.(\w+)", collection.name) if match: room_id = match.group(1) print(f"Found Room: {room_id}") room_name = f"room_{room_id}" roomnames.append(room_name) info_path = os.path.join(blend_dir, "pgout", f"{room_name}.info") os.makedirs(os.path.dirname(info_path), exist_ok=True) with open(info_path, 'w') as info_file: for obj in collection.objects: if obj.type == 'MESH' and obj.name == collection.name: export_mesh(obj, get_path(room_name, "mesh"), export_luv=True) portal_match = re.search(r"Portal.(\w+).(\w+)", obj.name) if portal_match: portal_type = portal_match.group(1) portal_id = portal_match.group(2) #extract position, rotation and scale from the portal object position = obj.location rotation = obj.rotation_euler scale = obj.scale info_file.write(f"portal {portal_type} {portal_id} {position_str(position)} {rotation_str(rotation)} {scale_str(scale)}\n") print("Exporting meshes...") for object in bpy.data.objects: match = re.search(r"Mesh.(\w+)", object.name) if match: mesh_name = match.group(1) print(f"Found Non-Room Mesh: {mesh_name}") armature = None parent = object.parent if parent and parent.type == 'ARMATURE': armatures[parent.name] = parent print(f" Is skeletal, Parent Armature: {parent.name}") armature = parent export_mesh(object, get_path(mesh_name, "mesh"), armature=armature) armature_names = [name for name, _ in armatures.items()] print("Armatures will be exported: ", armature_names) actions = {} armature_anims = {name: [] for name in armature_names} for action in bpy.data.actions: match = re.search(r"Anim.(\w+).(\w+)", action.name) if match: action_armature = match.group(1) action_name = match.group(2) if action_armature in armatures: armature = armatures[action_armature] action_file_name = f"{armature.name}_{action_name}" actions[action.name] = (action, action_file_name, armature) armature_anims[action_armature].append((action_name, action_file_name)) action_names = [f"{data[1]} ({data[2].name})" for name, data in actions.items()] print("Actions will be exported: ", action_names) for armature_name, armature in armatures.items(): export_armature(armature, get_path(armature_name, "skel")) for action_name, (action, action_file_name, armature) in actions.items(): export_animation(action, armature, get_path(action_file_name, "anim")) with open(get_path("rooms", "list"), 'w') as rooms_file: for room_name in roomnames: rooms_file.write(f"{room_name}\n")