many text work and scripts
This commit is contained in:
421
event_scene_export.py
Normal file
421
event_scene_export.py
Normal 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()
|
||||
Reference in New Issue
Block a user