bl_info = { "name": "Persona2 PSX Scene Exporter", "author": "Sergey Shemet", "version": (1, 0), "blender": (4, 5, 0), "location": "File > Export", "description": "Export Persona 2 Eternal Punishment PSX 3D scenes (.p2e)", "category": "Import-Export", } import bpy import struct import math import binascii from bpy_extras.io_utils import ExportHelper from bpy.props import StringProperty, BoolProperty class ExportP2E(bpy.types.Operator, ExportHelper): bl_idname = "export_scene.p2e" bl_label = "Export Persona2 PSX Scene" bl_description = "Export Persona 2 Eternal Punishment PSX 3D scenes" bl_options = {'REGISTER', 'UNDO', 'PRESET'} filename_ext = ".p2e" filter_glob: StringProperty( default="*.p2e", options={'HIDDEN'}, maxlen=255, ) def execute(self, context): # 1. Сбор объектов из коллекции PSX_Export export_collection = bpy.data.collections.get("PSX_Export") if not export_collection: self.report({'ERROR'}, "Collection 'PSX_Export' not found") return {'CANCELLED'} scene_data = self.prepare_scene_data(export_collection) if scene_data is None: # Если prepare_scene_data провалился return {'CANCELLED'} self.write_p2e_file(self.filepath, scene_data) self.report({'INFO'}, f"Exported {len(scene_data['objects'])} objects") return {'FINISHED'} def prepare_scene_data(self, export_collection): """Подготовка всех данных сцены""" if not export_collection: self.report({'ERROR'}, "Export collection is None") return None objects_data = [] global_vertices = [] global_vertex_offset = 0 for obj in export_collection.objects: if obj.type != 'MESH': continue # Данные объекта obj_data = { 'id': len(objects_data), 'custom_props': {k: v for k, v in obj.items() if k not in ['_RNA_UI']}, 'polygon_count': 0, 'chunk_data': bytearray(), 'local_vertices': [], 'vertex_map': {} } # Обработка вершин меша mesh = obj.data mesh.calc_loop_triangles() # Убедимся, что полигональные данные актуальны local_vertices = [] for vert in mesh.vertices: world_co = obj.matrix_world @ vert.co psx_vert = ( int(world_co.x * 100), -int(world_co.z * 100), int(world_co.y * 100), 0 ) local_vertices.append(psx_vert) # Уникальные вершины unique_vertices = [] vertex_map = {} for i, vert in enumerate(local_vertices): found = False for j, unique_vert in enumerate(unique_vertices): if vert == unique_vert: vertex_map[i] = j found = True break if not found: vertex_map[i] = len(unique_vertices) unique_vertices.append(vert) for vert in unique_vertices: global_vertices.append(vert) obj_data['local_vertices'].append(vert) obj_data['vertex_map'] = vertex_map # === ПРОВЕРКА UV СЛОЯ === if not mesh.uv_layers.active: self.report({'ERROR'}, f"Object '{obj.name}' has no active UV map. Please unwrap UVs.") return None uv_layer = mesh.uv_layers.active.data if len(uv_layer) != len(mesh.loops): self.report({'WARNING'}, f"UV layer size mismatch for '{obj.name}': {len(uv_layer)} UVs vs {len(mesh.loops)} loops") # === ОБРАБОТКА ПОЛИГОНОВ === for poly in mesh.polygons: vertex_count = len(poly.vertices) if vertex_count not in (3, 4): self.report({'ERROR'}, f"Polygon in '{obj.name}' has {vertex_count} vertices. Only triangles (3) or quads (4) allowed.") return None # Собираем UV координаты с проверкой индексов uvs = [] for loop_idx in poly.loop_indices: if loop_idx >= len(uv_layer): self.report({'ERROR'}, f"Loop index {loop_idx} out of range in '{obj.name}'. UV data is broken or inconsistent.") return None uv = uv_layer[loop_idx].uv uvs.append((uv.x, uv.y)) # Получаем индексы вершин vertex_ids = [obj_data['vertex_map'][v] + global_vertex_offset for v in poly.vertices] # Получаем цвет затенения shade = obj.get("shade", (128, 128, 128)) # Формируем чанк if vertex_count == 3: chunk = self.create_textured_tri_chunk(uvs, shade) else: # 4 chunk = self.create_textured_quad_chunk(uvs, shade) # тестовый разворот вершин vertex_ids = vertex_ids[::-1] # Меняем 3 и 4 вершины местами для квадов (если 4 вершины) if vertex_count == 4: vertex_ids[2], vertex_ids[3] = vertex_ids[3], vertex_ids[2] # Добавляем индексы в чанк for vid in vertex_ids: chunk += struct.pack('