many text work and scripts

This commit is contained in:
sShemet
2025-12-08 20:28:56 +05:00
parent 37dca7ee15
commit c09c3db56c
697 changed files with 12308 additions and 9392 deletions

421
event_scene_export.py Normal file
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()