boss contacts & many small fixes

This commit is contained in:
sShemet
2026-03-19 17:13:28 +05:00
parent 5bb16aa4d5
commit 3b19924266
535 changed files with 1333 additions and 1321 deletions

View File

@@ -0,0 +1,421 @@
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('<I', vid)
obj_data['chunk_data'] += chunk
obj_data['polygon_count'] += 1
# Обновляем смещение
global_vertex_offset += len(unique_vertices)
objects_data.append(obj_data)
return {
'objects': objects_data,
'vertices': global_vertices,
'vertex_count': len(global_vertices),
'object_count': len(objects_data)
}
def calc_tex_params(self, uvs):
"""Расчет CLUT и TexPage по UV координатам"""
# Среднее U координат
avg_u = sum(u for u, v in uvs) / len(uvs)
# Расчет CLUT (0-7)
# CLUT - это индекс в палитре текстур, нам нужно определить в каком из блоков 1/8 текстуры находится среднее U
# Блоки CLUT расположены в VRAM линиями, начиная с 0x480. Поэтому нам нужно просто смещаться по Y по 1 для каждого блока
clut_index = min(int(avg_u * 8), 7)
clut = (clut_index + 480) << 6 # CLUT хранится как int16, YYYYYYYYYYXXXXX. Так как X всегда 0, то просто смещаем Y на 6 бит
# Расчет TexPage
tex_page_raw = int(avg_u * 4)
tex_page = ((tex_page_raw & 15) + 10) | 16 | 128 #default p2 texpage shift (10) & enabling lower page (5th bit) & 8-bit mode (8th bit)
# Расчет X координат - нам нужно сместить x относительно текстурной страницы
tex_page_u_offset = (tex_page_raw & 3) * 0.25 # Коэффициент текстурной страницы в 0...1
xs = []
for u, v in uvs:
# X = (U - смещение страницы) * 512.
# 512 - это ширина всей текстуры. Отнимаем коэффициент от X, умножая на 512,
# чтобы получить положение X относительно начала страницы в координатах всей текстуры
x = int((u - tex_page_u_offset) * 512) & 0xFF
xs.append(x)
return {
'clut': clut,
'tex_page': tex_page,
'xs': xs
}
def create_textured_tri_chunk(self, uvs, shade):
"""Создание чанка для текстурированного треугольника"""
params = self.calc_tex_params(uvs)
chunk = bytearray()
# Заглушка (4 байта)
chunk += b'\x5c\x64\x61\x07'
# Цвет и команда GPU
chunk += struct.pack('<BBBB', shade[0], shade[1], shade[2], 0x24)
# Вершина 1
chunk += b'\x70\x5c\x30\x30'
chunk += struct.pack('<BBH',
params['xs'][0], # X1
int((1 - uvs[0][1]) * 255), # Y1 = V1 * 256
params['clut'] # CLUT
)
# Вершина 2
chunk += b'\x30\x32\x34\x2e'
chunk += struct.pack('<BBH',
params['xs'][1], # X2
int((1 - uvs[2][1]) * 255), # Y2
params['tex_page'] # TexPage
)
# Вершина 3
chunk += b'\x1e\xc1\x00\x09'
chunk += struct.pack('<BBH',
params['xs'][2], # X3
int((1 - uvs[3][1]) * 255), # Y3
0 # Заглушка
)
# Дублируем массив 2 раза (технический нюанс)
full_chunk = chunk * 2
# Добавляем заголовок чанка
header = struct.pack('<BBH', 0x2, 0x13, 0)
return header + full_chunk
def create_textured_quad_chunk(self, uvs, shade):
"""Создание чанка для текстурированного квадра"""
params = self.calc_tex_params(uvs)
chunk = bytearray()
# Магические байты
chunk += b'\x5c\x64\x61\x09'
# Цвет и команда
chunk += struct.pack('<BBBB', shade[0], shade[1], shade[2], 0x2C)
# Вершина 1
chunk += b'\x70\x5c\x30\x30'
chunk += struct.pack('<BBH',
params['xs'][3], # X1
# params['xs'][0], # X1
int((1 - uvs[3][1]) * 255), # Y1
# int((1 - uvs[0][1]) * 255), # Y1
params['clut'] # CLUT
)
# Вершина 2
chunk += b'\x30\x32\x34\x2e'
chunk += struct.pack('<BBH',
params['xs'][2], # X2
# params['xs'][1], # X2
int((1 - uvs[2][1]) * 255), # Y2
# int((1 - uvs[1][1]) * 255), # Y2
params['tex_page'] # TexPage
)
# Вершина 4
chunk += b'\x1e\xC1\x00\x09'
chunk += struct.pack('<BBBB',
params['xs'][0], # X4
# params['xs'][3], # X4
int((1 - uvs[0][1]) * 255), # Y4 = V4 * 256
# int((1 - uvs[3][1]) * 255), # Y4 = V4 * 256
84, 12 # Заглушка
)
# Вершина 3
chunk += b'\x5c\x64\x61\x07'
chunk += struct.pack('<BBBB',
params['xs'][1], # X3
# params['xs'][2], # X3
int((1 - uvs[1][1]) * 255), # Y3 = V3 * 256
# int((1 - uvs[2][1]) * 255), # Y3 = V3 * 256
128, 36 # Заглушка
)
# Дублируем массив 2 раза
full_chunk = chunk * 2
# Добавляем заголовок
header = struct.pack('<BBH', 0x6, 0x18, 0)
return header + full_chunk
def write_p2e_file(self, filepath, scene_data):
"""Запись .p2e файла"""
with open(filepath, 'wb') as f:
# 1. sourceName (16 байт)
f.write(b'Hello, blya.\x00\x00\x00\x00')
# 2. Заголовок сцены (пока заглушки)
header_pos = f.tell() # Должно быть 0x10
f.write(struct.pack('<HHII',
0, # objectsCnt - заполним позже
0, # someObjCtr
0, # vertexArrayPtr - заполним позже
0 # shiftInfoPtr - заполним позже
))
# 3. Таблица указателей на объекты (начинается с 0x1C)
# Каждый objectPtr - 4 байта
object_ptr_table_pos = f.tell() # Должно быть 0x1C
# Резервируем места для указателей
object_ptr_slots = []
for i in range(scene_data['object_count']):
slot_pos = f.tell()
object_ptr_slots.append(slot_pos)
f.write(b'\x00\x00\x00\x00') # Заполним позже
# 4. Запись gpuPacket объектов
object_data_addresses = []
for obj_idx, obj_data in enumerate(scene_data['objects']):
# Запоминаем адрес начала данных объекта
object_data_pos = f.tell()
object_data_addresses.append(object_data_pos)
# Записываем gpuPacket заголовок
global_chunk_cnt = obj_data['polygon_count']
f.write(struct.pack('<HHI',
global_chunk_cnt, # globalChunkCnt
0, # localChunkCnt (обычно 1)
obj_data['id'] # objectId
))
# Записываем чанки полигонов
f.write(obj_data['chunk_data'])
# 5. Обновляем указатели в таблице
current_pos = f.tell()
for i, slot_pos in enumerate(object_ptr_slots):
f.seek(slot_pos)
f.write(struct.pack('<I', object_data_addresses[i]))
f.seek(current_pos)
# 6. Запись массива вершин
vertex_ptr = f.tell()
for x, y, z, d in scene_data['vertices']:
f.write(struct.pack('<4h', x, y, z, d))
# 7. Запись таблицы смещений
shift_ptr = f.tell()
self.write_shift_table(f, scene_data['object_count'])
# 8. Обновляем заголовок сцены
f.seek(header_pos)
f.write(struct.pack('<HHII',
scene_data['object_count'], # objectsCnt
0, # someObjCtr
vertex_ptr, # vertexArrayPtr
shift_ptr # shiftInfoPtr
))
def write_shift_table(self, f, object_count):
"""Запись таблицы смещений (objShiftInfo)"""
for i in range(object_count):
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<B', 16)) # 0
f.write(struct.pack('<B', 0)) # 16
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<i', 0))
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<B', 16)) # 16
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<i', 0))
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<B', 16)) # 16
f.write(struct.pack('<B', 0)) # 0
f.write(struct.pack('<B', 0)) # 0
# shiftX, shiftY, shiftZ (3 x int32)
# (0,0,0,0) # комментарий о координатах инициализации
f.write(struct.pack('<3i', 0, 0, 0))
# # Последние 5 x u32 hz2
f.write(struct.pack('<i', 0) * 5)
# Регистрация
# Регистрация плагина
def menu_func_export(self, context):
self.layout.operator(ExportP2E.bl_idname, text="Persona2 PSX Scene (.p2e)")
def register():
bpy.utils.register_class(ExportP2E)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(ExportP2E)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,392 @@
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()

View File

@@ -0,0 +1,250 @@
import bpy
import struct
import math
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty
from bpy.types import Operator
bl_info = {
"name": "Persona2 PSX Scene Collisions Importer",
"author": "Sergey Shemet",
"version": (1, 0),
"blender": (4, 5, 0),
"location": "File > Import",
"description": "Imports Persona 2 Eternal Punishment PSX 3D scenes Collisions (.p2coll)",
"category": "Import-Export",
}
class P2CollisionImporter(Operator, ImportHelper):
"""Import Persona 2 PSX Collisions"""
bl_idname = "import_scene.p2coll"
bl_label = "Import P2Coll"
bl_options = {'PRESET', 'UNDO'}
filename_ext = ".p2coll"
filter_glob: StringProperty(
default="*.p2coll",
options={'HIDDEN'},
)
def execute(self, context):
return read_p2coll_file(context, self.filepath)
def create_cylinder_at_location(location, radius, height, vertices=16, name="cylinder"):
"""Создает цилиндр с заданными параметрами в указанной позиции"""
# Создаем цилиндр
bpy.ops.mesh.primitive_cylinder_add(
vertices=vertices,
radius=radius,
depth=height,
location=location
)
# Получаем созданный объект
cylinder = context.active_object
cylinder.name = name
return cylinder
def read_p2coll_file(context, filepath):
"""Чтение файла .p2coll и создание объектов в Blender"""
with open(filepath, 'rb') as file:
# Чтение всего файла
data = file.read()
# Чтение блоков из заголовка
# Первый блок: vertexMap
vertex_map_size = struct.unpack_from('<I', data, 0)[0]
vertex_map_block_offset = struct.unpack_from('<I', data, 4)[0]
# Второй блок: collisionLines
collision_lines_size = struct.unpack_from('<I', data, 8)[0]
collision_lines_block_offset = struct.unpack_from('<I', data, 12)[0]
# Третий блок: arrow (теперь cylCollision)
cyl_collision_size = struct.unpack_from('<I', data, 16)[0]
cyl_collision_block_offset = struct.unpack_from('<I', data, 20)[0]
print(f"Vertex count: {vertex_map_size}")
print(f"Wall collisions count: {collision_lines_size}")
print(f"Cylinder collisions count: {cyl_collision_size}")
# Чтение массива вершин
vertices = []
vertex_data_offset = vertex_map_block_offset
for i in range(vertex_map_size):
offset = vertex_data_offset + i * 8 # 8 bytes per coord struct (4 x short)
if offset + 8 <= len(data):
x, y, z, d = struct.unpack_from('<4h', data, offset)
# Преобразование координат: y=z, z=-y, деление на 100
vertices.append((
x / 100.0,
z / 100.0,
-y / 100.0
))
# Создание коллекции
coll_collection_name = "PSX_Collisions"
# Проверяем, существует ли коллекция
if coll_collection_name in bpy.data.collections:
coll_collection = bpy.data.collections[coll_collection_name]
else:
coll_collection = bpy.data.collections.new(coll_collection_name)
context.scene.collection.children.link(coll_collection)
# Сохраняем активную коллекцию для возврата
current_collection = context.collection
# Чтение и создание объектов коллизий (стен)
collision_data_offset = collision_lines_block_offset
for i in range(collision_lines_size):
offset = collision_data_offset + i * 8 # 8 bytes per collisionObj struct
if offset + 8 <= len(data):
# Чтение данных коллизии
vert_id1 = struct.unpack_from('<H', data, offset)[0]
vert_id2 = struct.unpack_from('<H', data, offset + 2)[0]
col_id = struct.unpack_from('<H', data, offset + 4)[0]
collis_height = struct.unpack_from('<B', data, offset + 6)[0]
collis_flag = struct.unpack_from('<B', data, offset + 7)[0]
print(f"Wall Collision {i}: v1={vert_id1}, v2={vert_id2}, height={collis_height}, flag={collis_flag}")
# Проверка, что индексы вершин валидны
if vert_id1 < len(vertices) and vert_id2 < len(vertices):
# Создание меша для стены
mesh = bpy.data.meshes.new(f"wall_collision_{i}")
obj = bpy.data.objects.new(f"wall_collision_{i}", mesh)
# Получаем координаты вершин
v1 = vertices[vert_id1]
v2 = vertices[vert_id2]
# Высота стены
height = collis_height / 100.0
# Создаём вершины для прямоугольника (стены)
verts = [
(v1[0], v1[1], v1[2]), # Нижняя левая
(v2[0], v2[1], v2[2]), # Нижняя правая
(v2[0], v2[1], v2[2] + height), # Верхняя правая
(v1[0], v1[1], v1[2] + height), # Верхняя левая
]
# Грани (два треугольника для прямоугольника)
faces = [(0, 1, 2, 3)]
# Создание меша
mesh.from_pydata(verts, [], faces)
mesh.update()
# Добавляем метаданные
obj["collisHeight"] = collis_height
obj["collisFlag"] = collis_flag
obj["collisionId"] = col_id
obj["vertexId1"] = vert_id1
obj["vertexId2"] = vert_id2
obj["collisionType"] = "wall"
# Добавляем объект в коллекцию
coll_collection.objects.link(obj)
obj.update_tag()
# Отключаем отображение в основной сцене
if obj.name in context.scene.collection.objects:
context.scene.collection.objects.unlink(obj)
else:
print(f"Warning: Invalid vertex indices in wall collision {i}: {vert_id1}, {vert_id2}")
# Чтение и создание цилиндрических коллизий
cyl_collision_data_offset = cyl_collision_block_offset
for i in range(cyl_collision_size):
offset = cyl_collision_data_offset + i * 8 # 8 bytes per cylCollisionObj struct
if offset + 8 <= len(data):
# Чтение данных цилиндрической коллизии
coords_vertex = struct.unpack_from('<H', data, offset)[0]
radius = struct.unpack_from('<H', data, offset + 2)[0] # Радиус вместо вращения
object_id = struct.unpack_from('<H', data, offset + 4)[0]
collis_height = struct.unpack_from('<B', data, offset + 6)[0]
collis_flag = struct.unpack_from('<B', data, offset + 7)[0]
print(f"Cylinder Collision {i}: vertex={coords_vertex}, radius={radius}, height={collis_height}, flag={collis_flag}")
# Проверка, что индекс вершины валиден
if coords_vertex < len(vertices):
# Устанавливаем активную коллекцию для создания объектов
context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_collection.name]
# Получаем позицию для цилиндра
pos = vertices[coords_vertex]
# Преобразуем радиус и высоту (делим на 100)
cyl_radius = radius / 100.0
cyl_height = collis_height / 100.0
# Создаем цилиндр
bpy.ops.mesh.primitive_cylinder_add(
vertices=16, # Количество вершин для гладкости
radius=cyl_radius,
depth=cyl_height,
location=pos,
rotation=(0, 0, 0) # Без вращения
)
# Получаем созданный объект
cyl_obj = context.active_object
cyl_obj.name = f"cyl_collision_{i}"
# Смещаем цилиндр так, чтобы его основание было в позиции вершины
cyl_obj.location.z += cyl_height / 2.0
# Добавляем метаданные
cyl_obj["collisHeight"] = collis_height
cyl_obj["collisFlag"] = collis_flag
cyl_obj["objectId"] = object_id
cyl_obj["vertexId"] = coords_vertex
cyl_obj["radius"] = radius
cyl_obj["collisionType"] = "cylinder"
# Обновляем объект
cyl_obj.update_tag()
# Убеждаемся, что объект в правильной коллекции
if coll_collection.name not in cyl_obj.users_collection:
# Удаляем из всех текущих коллекций
for coll in cyl_obj.users_collection:
coll.objects.unlink(cyl_obj)
# Добавляем в нашу коллекцию
coll_collection.objects.link(cyl_obj)
else:
print(f"Warning: Invalid vertex index in cylinder collision {i}: {coords_vertex}")
# Возвращаем активную коллекцию
context.view_layer.active_layer_collection = context.view_layer.layer_collection
# Выводим информацию о результате
print(f"Импортировано {len(vertices)} вершин")
print(f"Создано {collision_lines_size} стен-коллизий")
print(f"Создано {cyl_collision_size} цилиндрических коллизий")
return {'FINISHED'}
def menu_func_import(self, context):
self.layout.operator(P2CollisionImporter.bl_idname, text="Persona 2 Collisions (.p2coll)")
def register():
bpy.utils.register_class(P2CollisionImporter)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.utils.unregister_class(P2CollisionImporter)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
if __name__ == "__main__":
register()