diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..b8a8ec7 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,47 @@ +bl_info = { + "name": "PSK/PSA Exporter", + "author": "Colin Basnett", + "version": ( 1, 0, 0 ), + "blender": ( 2, 80, 0 ), + "location": "File > Export > PSK Export (.psk)", + "description": "PSK/PSA Export (.psk)", + "warning": "", + "wiki_url": "https://github.com/DarklightGames/io_export_psk_psa", + "tracker_url": "https://github.com/DarklightGames/io_export_psk_psa/issues", + "category": "Import-Export" +} + +if 'bpy' in locals(): + import importlib + importlib.reload(psk) + importlib.reload(exporter) + importlib.reload(builder) +else: + # if i remove this line, it can be enabled just fine + from . import psk + from . import exporter + from . import builder + +import bpy + +classes = [ + exporter.PskExportOperator +] + +def menu_func(self, context): + self.layout.operator(exporter.PskExportOperator.bl_idname, text = "Unreal PSK (.psk)") + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + bpy.types.TOPBAR_MT_file_export.append(menu_func) + +def unregister(): + bpy.types.TOPBAR_MT_file_export.remove(menu_func) + from bpy.utils import unregister_class + for cls in reversed(classes): + unregister_class(cls) + +if __name__ == '__main__': + register() diff --git a/src/builder.py b/src/builder.py new file mode 100644 index 0000000..e18ff28 --- /dev/null +++ b/src/builder.py @@ -0,0 +1,65 @@ +import bpy +import bmesh +from .psk import Psk, Vector3 + + +class PskBuilder(object): + def __init__(self): + pass + + def build(self, context) -> Psk: + object = context.view_layer.objects.active + if object.type != 'MESH': + raise RuntimeError('Selected object must be a Mesh') + + # ensure that there is exactly one armature modifier + modifiers = [x for x in object.modifiers if x.type == 'ARMATURE'] + if len(modifiers) != 1: + raise RuntimeError('the mesh must have one armature modifier') + armature_modifier = modifiers[0] + armature_object = armature_modifier.object + + if armature_object is None: + raise RuntimeError('the armature modifier has no linked object') + + # TODO: probably requires at least one bone? not sure + mesh_data = object.data + + # TODO: if there is an edge-split modifier, we need to apply it. + + # TODO: duplicate all the data + mesh = bpy.data.meshes.new('export') + + # copy the contents of the mesh + bm = bmesh.new() + bm.from_mesh(mesh_data) + # triangulate everything + bmesh.ops.triangulate(bm, faces=bm.faces) + bm.to_mesh(mesh) + bm.free() + del bm + + psk = Psk() + + # vertices + for vertex in mesh.vertices: + psk.points.append(Vector3(*vertex.co)) + + # TODO: wedges (a "wedge" is actually a UV'd vertex, basically) + # for wedge in mesh.wedges: + # pass + + # materials + for i, m in enumerate(object.data.materials): + material = Psk.Material() + material.name = m.name + material.texture_index = i + psk.materials.append(material) + + # TODO: should we make the wedges/faces at the same time?? + f = Psk.Face() + # f.wedge_index_1 = 0 + + # TODO: weights + + return psk diff --git a/src/exporter.py b/src/exporter.py new file mode 100644 index 0000000..c4eae0d --- /dev/null +++ b/src/exporter.py @@ -0,0 +1,82 @@ +from bpy.types import Operator +from bpy_extras.io_utils import ExportHelper +from bpy.props import StringProperty, BoolProperty, FloatProperty +import struct +import io +from .psk import Psk +from .builder import PskBuilder + +# https://me3explorer.fandom.com/wiki/PSK_File_Format +# https://docs.unrealengine.com/udk/Two/rsrc/Two/BinaryFormatSpecifications/UnrealAnimDataStructs.h +class PskExportOperator(Operator, ExportHelper): + bl_idname = 'export.psk' + bl_label = 'Export' + __doc__ = 'PSK Exporter (.psk)' + filename_ext = '.psk' + # filter_glob : StringProperty(default='*.psk', options={'HIDDEN'}) + + filepath : StringProperty( + name='File Path', + description='File path used for exporting the PSK file', + maxlen=1024, + default='') + + def execute(self, context): + builder = PskBuilder() + psk = builder.build(context) + exporter = PskExporter(psk) + exporter.export(self.filepath) + return {'FINISHED'} + + +class PskExporter(object): + def __init__(self, psk: Psk): + self.psk: Psk = psk + + @staticmethod + def write_section(f, id: bytes, data_size: int, data_count: int, data: bytes): + write(f, '20s', bytearray(id).ljust(20, b'\0')) + write(f, 'I', 1999801) + write(f, 'I', data_size) + write(f, 'I', data_count) + f.write(data) + + + def export(self, path: str): + with open(path, 'wb') as fp: + PskExporter.write_section(fp, b'ACTRHEAD', 0, 0, b'') + + # POINTS + data = io.BytesIO() + fmt = '3f' + for point in self.psk.points: + write(data, fmt, point.x, point.y, point.z) + PskExporter.write_section(fp, b'PNTS0000', struct.calcsize(fmt), len(self.psk.points), data.getvalue()) + + # WEDGES + buffer = io.BytesIO() + if len(self.psk.wedges) <= 65536: + for w in self.psk.wedges: + write(buffer, 'ssffbbs', w.point_index, 0, w.u, w.v, w.material_index, 0, 0) + else: + for w in self.psk.wedges: + write(buffer, 'iffi', w.point_index, w.u, w.v, w.material_index) + fp.write(buffer.getvalue()) + + # FACES + buffer = io.BytesIO() + for f in self.psk.faces: + write(buffer, 'sssbbi', f.wedge_index_1, f.wedge_index_2, f.wedge_index_3, f.material_index, + f.aux_material_index, f.smoothing_groups) + fp.write(buffer.getvalue()) + + # MATERIALS + buffer = io.BytesIO() + fmt = '64s6i' + for m in self.psk.materials: + write(buffer, fmt, bytes(m.name, encoding='utf-8'), m.texture_index, m.poly_flags, m.aux_material_index, m.aux_flags, m.lod_bias, m.lod_style) + self.write_section(fp, b'MATT0000', struct.calcsize(fmt), len(self.psk.materials), buffer.getvalue()) + + +def write(f, fmt, *values): + f.write(struct.pack(fmt, *values)) diff --git a/src/psk.py b/src/psk.py new file mode 100644 index 0000000..6250c24 --- /dev/null +++ b/src/psk.py @@ -0,0 +1,74 @@ +from typing import List + +class Vector3(object): + def __init__(self, x = 0, y = 0, z = 0): + self.x = x + self.y = y + self.z = z + + def __iter__(self): + yield self.x + yield self.y + yield self.z + + +class Quaternion(object): + def __init__(self): + self.x = 0.0 + self.y = 0.0 + self.z = 0.0 + self.w = 0.0 + +class Psk(object): + + class Wedge(object): + def __init__(self): + self.point_index = -1 + self.u = 0.0 + self.v = 0.0 + self.material_index = -1 + + class Face(object): + def __init__(self): + self.wedge_index_1 = -1 + self.wedge_index_2 = -1 + self.wedge_index_3 = -1 + self.material_index = -1 + self.aux_material_index = -1 + self.smoothing_groups = -1 + + class Material(object): + def __init__(self): + self.name = '' + self.texture_index = -1 + self.poly_flags = 0 + self.aux_material_index = -1 + self.aux_flags = -1 + self.lod_bias = 0 + self.lod_style = 0 + + class Bone(object): + def __init__(self): + self.name = '' + self.flags = 0 + self.children_count = 0 + self.parent_index = -1 + self.rotation = Quaternion() + self.position = Vector3() + self.length = 0.0 + self.size = Vector3() + + class Weight(object): + def __init__(self): + self.weight = 0.0 + self.point_index = -1 + self.bone_index = -1 + + + def __init__(self): + self.points = [] + self.wedges: List[Psk.Wedge] = [] + self.faces: List[Psk.Face] = [] + self.materials: List[Psk.Material] = [] + self.weights: List[Psk.Weight] = [] + self.bones: List[Psk.Bone] = []