Files
P2EP_Export/event_scene_import.py
2025-12-08 20:28:56 +05:00

392 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

bl_info = {
"name": "Persona2 PSX Scene Importer",
"author": "Sergey Shemet",
"version": (1, 2),
"blender": (4, 5, 0),
"location": "File > Import",
"description": "Import Persona 2 Eternal Punishment PSX 3D scenes (.p2e)",
"category": "Import-Export",
}
import bpy
import struct
from bpy_extras.io_utils import ImportHelper
from bpy.props import FloatProperty, EnumProperty, BoolProperty
class ImportP2E(bpy.types.Operator, ImportHelper):
bl_idname = "import_scene.p2e"
bl_label = "Import Persona2 PSX Scene"
bl_description = "Import Persona 2 Eternal Punishment PSX 3D scenes"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
filename_ext = ".p2e"
filter_glob: bpy.props.StringProperty(
default="*.p2e",
options={'HIDDEN'},
maxlen=255,
)
coord_scale: FloatProperty(
name="Coordinate Scale",
description="PSX fixed-point scale multiplier",
default=0.01,
min=0.00001,
max=1.0,
step=0.01,
precision=5
)
scale_preset: EnumProperty(
name="PSX Scale Preset",
description="Standard PSX fixed-point scales",
items=[
('CUSTOM', "Custom", "Custom scale"),
('100', "1/100", "Life-like scale"),
('4096', "1/4096 (12.4)", "High precision"),
('256', "1/256 (8.8)", "Alternative scale"),
('16', "1/16 (12.4)", "Typical PSX vertex scale"),
('1', "1/1 (Raw)", "No scaling"),
],
default='100'
)
show_vertices: BoolProperty(
name="Show Vertices",
description="Show vertex points for debugging",
default=False
)
vertex_size: FloatProperty(
name="Vertex Size",
description="Size of debug vertex markers",
default=0.1,
min=0.001,
max=1.0
)
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="PSX Import Settings", icon='SETTINGS')
box.prop(self, "scale_preset")
if self.scale_preset != 'CUSTOM':
preset_scales = {
'100': 1/100.0,
'4096': 1/4096.0,
'256': 1/256.0,
'16': 1/16.0,
'1': 1.0
}
self.coord_scale = preset_scales[self.scale_preset]
if self.scale_preset == 'CUSTOM':
box.prop(self, "coord_scale")
box.separator()
box.label(text="Debug Options", icon='MODIFIER')
box.prop(self, "show_vertices")
if self.show_vertices:
box.prop(self, "vertex_size")
box.separator()
box.label(text=f"Current scale: {self.coord_scale:.6f}", icon='INFO')
def execute(self, context):
# Читаем файл
vertices, objects_data = self.read_p2e_file(self.filepath)
if not vertices:
self.report({'ERROR'}, "No vertices found")
return {'CANCELLED'}
# Создаем меши для каждого объекта
self.create_objects_from_data(vertices, objects_data)
# Опционально показываем вершины для отладки
if self.show_vertices:
self.create_debug_vertices(vertices)
self.report({'INFO'}, f"Imported {len(vertices)} vertices, {len(objects_data)} objects")
return {'FINISHED'}
def read_p2e_file(self, filepath):
"""Чтение .p2e файла: вершины и полигоны объектов"""
try:
with open(filepath, 'rb') as f:
# Пропускаем заголовок (16 байт sourceName)
f.seek(0x10)
# Читаем заголовок сцены
data = f.read(12)
if len(data) < 12:
self.report({'ERROR'}, "File too small")
return [], []
objects_cnt, some_obj_ctr, vertex_ptr, shift_ptr = struct.unpack('<HHII', data)
# Вычисляем количество вершин
vertex_count = (shift_ptr - vertex_ptr) // 8
print(f"=== P2E File Info ===")
print(f"Objects: {objects_cnt}")
print(f"Vertices: {vertex_count}")
print(f"Vertex pointer: 0x{vertex_ptr:08X}")
print(f"Shift pointer: 0x{shift_ptr:08X}")
# 1. Читаем вершины
vertices = []
f.seek(vertex_ptr)
for i in range(vertex_count):
vertex_data = f.read(8)
if len(vertex_data) < 8:
break
x, y, z, d = struct.unpack('<4h', vertex_data)
# PSX координаты: X=X, Y=Z, Z=-Y
vertex = (
x * self.coord_scale,
z * self.coord_scale,
-y * self.coord_scale
)
vertices.append(vertex)
print(f"Read {len(vertices)} vertices")
# 2. Читаем объекты
objects_data = []
f.seek(0x10 + 12) # После заголовка сцены начинаются объекты
for obj_idx in range(objects_cnt):
# Читаем указатель на объект
obj_ptr_data = f.read(4)
if len(obj_ptr_data) < 4:
break
obj_ptr = struct.unpack('<I', obj_ptr_data)[0]
# Сохраняем текущую позицию
current_pos = f.tell()
# Переходим к данным объекта
f.seek(obj_ptr)
# Читаем gpuPacket заголовок
packet_data = f.read(8) # u16 globalChunkCnt, u16 localChunkCnt, u32 objectId
if len(packet_data) < 8:
f.seek(current_pos)
continue
global_chunk_cnt, local_chunk_cnt, object_id = struct.unpack('<HHI', packet_data)
print(f"\nObject {obj_idx}:")
print(f" Pointer: 0x{obj_ptr:08X}")
print(f" ID: {object_id}")
print(f" Chunks: {global_chunk_cnt}")
# Собираем полигоны этого объекта
obj_polygons = []
for chunk_idx in range(global_chunk_cnt):
# Читаем заголовок chunk
chunk_header = f.read(4)
if len(chunk_header) < 4:
break
cmd_cnt, chunk_size_words, header_fuck = struct.unpack('<BBH', chunk_header)
# print(f" Chunk {chunk_idx}: cmd={cmd_cnt}, size={chunk_size_words} words")
# Определяем тип полигона по cmd_cnt
poly_type = self.get_polygon_type(cmd_cnt)
if poly_type is None:
# Пропускаем неизвестный chunk
bytes_to_skip = chunk_size_words * 4 # chunk_size в словах (4 байта)
f.read(bytes_to_skip)
continue
# Читаем ВЕСЬ chunk (включая индексы вершин)
# chunk_size_words - количество 4-байтных слов данных ПОСЛЕ заголовка
chunk_data_size = chunk_size_words * 4
poly_data = f.read(chunk_data_size)
if len(poly_data) < chunk_data_size:
break
# Извлекаем индексы вершин из конца chunk
vertex_indices = self.extract_vertex_indices(poly_type, poly_data)
if vertex_indices:
obj_polygons.append({
'type': poly_type,
'cmd': cmd_cnt,
'vertices': vertex_indices,
'chunk_size': chunk_size_words
})
# Сохраняем данные объекта
if obj_polygons:
objects_data.append({
'id': object_id,
'ptr': obj_ptr,
'polygons': obj_polygons,
'vertex_count': len(obj_polygons) * 4 # Примерно
})
# Возвращаемся к списку объектов
f.seek(current_pos)
print(f"\n=== Summary ===")
print(f"Total objects with polygons: {len(objects_data)}")
return vertices, objects_data
except Exception as e:
self.report({'ERROR'}, f"Error reading file: {str(e)}")
import traceback
traceback.print_exc()
return [], []
def get_polygon_type(self, cmd_cnt):
"""Определяем тип полигона по команде"""
poly_types = {
7: 'shaded_textured_quad', # shadedTexturedPoly
6: 'textured_quad', # texturedPoly
5: 'shaded_quad', # shadedPoly
3: 'shaded_textured_tri', # shadedTextured3Poly
2: 'textured_tri', # textured3Poly
}
return poly_types.get(cmd_cnt, None)
def extract_vertex_indices(self, poly_type, poly_data):
"""Извлекаем индексы вершин из конца chunk данных"""
try:
if poly_type in ['shaded_textured_quad', 'textured_quad', 'shaded_quad']:
# Квады: 4 индекса uint32 в конце (16 байт)
if len(poly_data) >= 16:
# Последние 16 байт: vertexId1-4
v1, v2, v3, v4 = struct.unpack_from('<4I', poly_data, len(poly_data) - 16)
return [v1, v2, v4, v3]
elif poly_type in ['shaded_textured_tri', 'textured_tri']:
# Треугольники: 3 индекса uint32 в конце (12 байт)
if len(poly_data) >= 12:
# Последние 12 байт: vertexId1-3
v1, v2, v3 = struct.unpack_from('<3I', poly_data, len(poly_data) - 12)
return [v1, v2, v3]
except Exception as e:
print(f"Error extracting indices from {poly_type}: {e}")
return []
def create_objects_from_data(self, vertices, objects_data):
"""Создание отдельных мешей для каждого объекта"""
if not objects_data:
return
# Создаем коллекцию для импортированных объектов
collection_name = "PSX_Objects"
if collection_name not in bpy.data.collections:
collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(collection)
else:
collection = bpy.data.collections[collection_name]
total_polygons = 0
successful_objects = 0
for obj_idx, obj_data in enumerate(objects_data):
# Собираем все полигоны этого объекта
all_faces = []
for poly in obj_data['polygons']:
vertex_ids = poly['vertices']
# Проверяем, что индексы в пределах массива вершин
valid_ids = []
for vid in vertex_ids:
if vid < len(vertices):
valid_ids.append(vid)
else:
print(f"Warning: Vertex index {vid} out of range (max {len(vertices)-1})")
if len(valid_ids) >= 3: # Нужно минимум 3 вершины для полигона
all_faces.append(valid_ids)
total_polygons += 1
if not all_faces:
print(f"Object {obj_idx} has no valid polygons")
continue
# Создаем меш
mesh_name = f"PSX_Obj_{obj_idx:03d}_ID{obj_data['id']}"
mesh = bpy.data.meshes.new(mesh_name)
# Создаем вершины и полигоны
mesh.from_pydata(vertices, [], all_faces)
# Обновляем меш
mesh.update()
# Создаем объект
obj = bpy.data.objects.new(mesh_name, mesh)
# Добавляем кастомные свойства
obj["psx_object_id"] = obj_data['id']
obj["psx_object_ptr"] = obj_data['ptr']
obj["psx_poly_count"] = len(all_faces)
obj["psx_poly_types"] = ','.join(str(p['cmd']) for p in obj_data['polygons'])
# Добавляем в коллекцию
collection.objects.link(obj)
successful_objects += 1
print(f" Created object {obj_idx} with {len(all_faces)} polygons")
print(f"\nCreated {successful_objects} objects with {total_polygons} total polygons")
def create_debug_vertices(self, vertices):
"""Создание мета-шаров для отладки вершин"""
if not vertices:
return
meta = bpy.data.metaballs.new("Debug_Vertices")
meta_obj = bpy.data.objects.new("PSX_Debug_Vertices", meta)
meta.resolution = 0.1
meta.threshold = 0.05
for i, vertex in enumerate(vertices):
elem = meta.elements.new()
elem.co = vertex
elem.radius = self.vertex_size
bpy.context.collection.objects.link(meta_obj)
print(f"Created {len(vertices)} debug vertices")
def menu_func_import(self, context):
self.layout.operator(ImportP2E.bl_idname, text="Persona2 PSX Scene (.p2e)")
classes = (ImportP2E,)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
if __name__ == "__main__":
register()