diff --git a/blender/exportsimple.py b/blender/exportsimple.py new file mode 100644 index 0000000..3bca708 --- /dev/null +++ b/blender/exportsimple.py @@ -0,0 +1,310 @@ +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 = {} + +def export_mesh(obj, output_path, export_luv = False, export_bones = False): + 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 + + # 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 + + 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) + + key = (pos, normal, uv, luv) + + 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)) + + 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") + + f.write(f"# x y z nx ny nz u v{' lu lv' if export_luv 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):.2f} {rad_to_deg(rotation.y):.2f} {rad_to_deg(rotation.z):.2f}" + +def position_str(position): + return f"{position.x:.3f} {position.y:.3f} {position.z:.3f}" + +def scale_str(scale): + return f"{scale.x:.3f}" + +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 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_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("# name parent x y z yaw pitch roll scale\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") + + 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.001): + _, 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} + + start, end = map(int, action.frame_range) + for frame in range(start, 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: + 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}") + export_mesh(object, get_path(mesh_name, "mesh")) + + parent = object.parent + if parent and parent.type == 'ARMATURE': + armatures[parent.name] = parent + print(f" Is skeletal, Parent Armature: {parent.name}") + +armature_names = [name for name, _ in armatures.items()] +print("Armatures will be exported: ", armature_names) + +actions = {} + +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: + actions[action.name] = (action, action_name, armatures[action_armature]) + +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, "sk")) + +for action_name, (action, simple_action_name, armature) in actions.items(): + export_animation(action, armature, get_path(f"{armature.name}_{simple_action_name}", "anim")) + +with open(get_path("rooms", "list"), 'w') as rooms_file: + for room_name in roomnames: + rooms_file.write(f"{room_name}\n") +