Added initial files.
This commit is contained in:
47
src/__init__.py
Normal file
47
src/__init__.py
Normal file
@@ -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()
|
||||||
65
src/builder.py
Normal file
65
src/builder.py
Normal file
@@ -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
|
||||||
82
src/exporter.py
Normal file
82
src/exporter.py
Normal file
@@ -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))
|
||||||
74
src/psk.py
Normal file
74
src/psk.py
Normal file
@@ -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] = []
|
||||||
Reference in New Issue
Block a user