mirror of
https://github.com/DarklightGames/io_scene_psk_psa.git
synced 2024-11-24 06:50:13 +01:00
WIP import functionality (PSK working, PSA in the works)
This commit is contained in:
parent
0622cf43e5
commit
4a9a921583
@ -16,55 +16,83 @@ if 'bpy' in locals():
|
||||
importlib.reload(psk_data)
|
||||
importlib.reload(psk_builder)
|
||||
importlib.reload(psk_exporter)
|
||||
importlib.reload(psk_importer)
|
||||
importlib.reload(psk_reader)
|
||||
importlib.reload(psk_operator)
|
||||
importlib.reload(psa_data)
|
||||
importlib.reload(psa_builder)
|
||||
importlib.reload(psa_exporter)
|
||||
importlib.reload(psa_operator)
|
||||
importlib.reload(psa_reader)
|
||||
importlib.reload(psa_importer)
|
||||
else:
|
||||
# if i remove this line, it can be enabled just fine
|
||||
from .psk import data as psk_data
|
||||
from .psk import builder as psk_builder
|
||||
from .psk import exporter as psk_exporter
|
||||
from .psk import reader as psk_reader
|
||||
from .psk import importer as psk_importer
|
||||
from .psk import operator as psk_operator
|
||||
from .psa import data as psa_data
|
||||
from .psa import builder as psa_builder
|
||||
from .psa import exporter as psa_exporter
|
||||
from .psa import operator as psa_operator
|
||||
from .psa import reader as psa_reader
|
||||
from .psa import importer as psa_importer
|
||||
|
||||
import bpy
|
||||
from bpy.props import IntProperty, CollectionProperty
|
||||
|
||||
classes = [
|
||||
psk_operator.PskExportOperator,
|
||||
psk_operator.PskImportOperator,
|
||||
psa_operator.PsaExportOperator,
|
||||
psa_operator.PsaImportOperator,
|
||||
psa_operator.PSA_UL_ActionList,
|
||||
psa_operator.ActionListItem
|
||||
psa_operator.PSA_UL_ImportActionList,
|
||||
psa_operator.ActionListItem,
|
||||
psa_operator.ImportActionListItem
|
||||
]
|
||||
|
||||
|
||||
def psk_menu_func(self, context):
|
||||
def psk_export_menu_func(self, context):
|
||||
self.layout.operator(psk_operator.PskExportOperator.bl_idname, text ='Unreal PSK (.psk)')
|
||||
|
||||
|
||||
def psa_menu_func(self, context):
|
||||
def psk_import_menu_func(self, context):
|
||||
self.layout.operator(psk_operator.PskImportOperator.bl_idname, text ='Unreal PSK (.psk)')
|
||||
|
||||
|
||||
def psa_export_menu_func(self, context):
|
||||
self.layout.operator(psa_operator.PsaExportOperator.bl_idname, text='Unreal PSA (.psa)')
|
||||
|
||||
|
||||
def psa_import_menu_func(self, context):
|
||||
self.layout.operator(psa_operator.PsaImportOperator.bl_idname, text ='Unreal PSA (.psa)')
|
||||
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.TOPBAR_MT_file_export.append(psk_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_export.append(psa_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_export.append(psk_export_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_import.append(psa_import_menu_func)
|
||||
bpy.types.Scene.psa_action_list = CollectionProperty(type=psa_operator.ActionListItem)
|
||||
bpy.types.Scene.psa_import_action_list = CollectionProperty(type=psa_operator.ImportActionListItem)
|
||||
bpy.types.Scene.psa_action_list_index = IntProperty(name='index for list??', default=0)
|
||||
bpy.types.Scene.psa_import_action_list_index = IntProperty(name='index for list??', default=0)
|
||||
|
||||
|
||||
def unregister():
|
||||
del bpy.types.Scene.psa_action_list_index
|
||||
del bpy.types.Scene.psa_import_action_list_index
|
||||
del bpy.types.Scene.psa_action_list
|
||||
bpy.types.TOPBAR_MT_file_export.remove(psa_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(psk_menu_func)
|
||||
del bpy.types.Scene.psa_import_action_list
|
||||
bpy.types.TOPBAR_MT_file_export.remove(psk_export_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func)
|
||||
bpy.types.TOPBAR_MT_file_import.remove(psa_import_menu_func)
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
@ -17,6 +17,12 @@ class Quaternion(Structure):
|
||||
('w', c_float),
|
||||
]
|
||||
|
||||
def __iter__(self):
|
||||
yield self.x
|
||||
yield self.y
|
||||
yield self.z
|
||||
yield self.w
|
||||
|
||||
|
||||
class Section(Structure):
|
||||
_fields_ = [
|
||||
|
@ -22,7 +22,7 @@ class Psa(object):
|
||||
('bone_count', c_int32),
|
||||
('root_include', c_int32),
|
||||
('compression_style', c_int32),
|
||||
('key_quotum', c_int32), # what the fuck is a quotum
|
||||
('key_quotum', c_int32),
|
||||
('key_reduction', c_float),
|
||||
('track_time', c_float),
|
||||
('fps', c_float),
|
||||
|
15
io_export_psk_psa/psa/importer.py
Normal file
15
io_export_psk_psa/psa/importer.py
Normal file
@ -0,0 +1,15 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
from .data import Psa
|
||||
|
||||
|
||||
class PsaImporter(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def import_psa(self, psa: Psa, context):
|
||||
print('importing yay')
|
||||
print(psa.sequences)
|
||||
for sequence in psa.sequences:
|
||||
print(sequence.name, sequence.frame_start_index, sequence.frame_count)
|
@ -1,12 +1,23 @@
|
||||
from bpy.types import Operator, Action, UIList, PropertyGroup
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty
|
||||
from .builder import PsaBuilder, PsaBuilderOptions
|
||||
from .exporter import PsaExporter
|
||||
from .reader import PsaReader
|
||||
from .importer import PsaImporter
|
||||
import bpy
|
||||
import re
|
||||
|
||||
|
||||
class ImportActionListItem(PropertyGroup):
|
||||
action_name: StringProperty()
|
||||
is_selected: BoolProperty(default=True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.action_name
|
||||
|
||||
|
||||
class ActionListItem(PropertyGroup):
|
||||
action: PointerProperty(type=Action)
|
||||
is_selected: BoolProperty(default=False)
|
||||
@ -16,6 +27,22 @@ class ActionListItem(PropertyGroup):
|
||||
return self.action.name
|
||||
|
||||
|
||||
class PSA_UL_ImportActionList(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
layout.alignment = 'LEFT'
|
||||
layout.prop(item, 'is_selected', icon_only=True)
|
||||
layout.label(text=item.action_name)
|
||||
|
||||
# def filter_items(self, context, data, property):
|
||||
# # TODO: returns two lists, apparently
|
||||
# actions = getattr(data, property)
|
||||
# flt_flags = []
|
||||
# flt_neworder = []
|
||||
# if self.filter_name:
|
||||
# flt_flags = bpy.types.UI_UL_list.filter_items_by_name(self.filter_name, self.bitflag_filter_item, actions, 'name', reverse=self.use_filter_invert)
|
||||
# return flt_flags, flt_neworder
|
||||
|
||||
|
||||
class PSA_UL_ActionList(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
layout.alignment = 'LEFT'
|
||||
@ -37,8 +64,8 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
bl_label = 'Export'
|
||||
__doc__ = 'PSA Exporter (.psa)'
|
||||
filename_ext = '.psa'
|
||||
filter_glob : StringProperty(default='*.psa', options={'HIDDEN'})
|
||||
filepath : StringProperty(
|
||||
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
||||
filepath: StringProperty(
|
||||
name='File Path',
|
||||
description='File path used for exporting the PSA file',
|
||||
maxlen=1024,
|
||||
@ -103,3 +130,47 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
exporter = PsaExporter(psk)
|
||||
exporter.export(self.filepath)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PsaImportOperator(Operator, ImportHelper):
|
||||
# TODO: list out the actions to be imported
|
||||
bl_idname = 'import.psa'
|
||||
bl_label = 'Import'
|
||||
__doc__ = 'PSA Importer (.psa)'
|
||||
filename_ext = '.psa'
|
||||
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
||||
filepath: StringProperty(
|
||||
name='File Path',
|
||||
description='File path used for importing the PSA file',
|
||||
maxlen=1024,
|
||||
default='')
|
||||
|
||||
def invoke(self, context, event):
|
||||
action_names = []
|
||||
try:
|
||||
action_names = PsaReader().scan_sequence_names(self.filepath)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
context.scene.psa_import_action_list.clear()
|
||||
for action_name in action_names:
|
||||
item = context.scene.psa_action_list.add()
|
||||
item.action_name = action_name
|
||||
item.is_selected = True
|
||||
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
box = layout.box()
|
||||
box.label(text='Actions', icon='ACTION')
|
||||
row = box.row()
|
||||
row.template_list('PSA_UL_ImportActionList', 'asd', scene, 'psa_import_action_list', scene, 'psa_import_action_list_index', rows=len(context.scene.psa_import_action_list))
|
||||
|
||||
def execute(self, context):
|
||||
reader = PsaReader()
|
||||
psa = reader.read(self.filepath)
|
||||
PsaImporter().import_psa(psa, context)
|
||||
return {'FINISHED'}
|
||||
|
53
io_export_psk_psa/psa/reader.py
Normal file
53
io_export_psk_psa/psa/reader.py
Normal file
@ -0,0 +1,53 @@
|
||||
from .data import *
|
||||
from typing import AnyStr
|
||||
import ctypes
|
||||
|
||||
|
||||
class PsaReader(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def read_types(fp, data_class: ctypes.Structure, section: Section, data):
|
||||
buffer_length = section.data_size * section.data_count
|
||||
buffer = fp.read(buffer_length)
|
||||
offset = 0
|
||||
for _ in range(section.data_count):
|
||||
data.append(data_class.from_buffer_copy(buffer, offset))
|
||||
offset += section.data_size
|
||||
|
||||
def scan_sequence_names(self, path) -> List[AnyStr]:
|
||||
sequences = []
|
||||
with open(path, 'rb') as fp:
|
||||
if fp.read(8) != b'ANIMINFO':
|
||||
raise IOError('Unexpected file format')
|
||||
fp.seek(0, 0)
|
||||
while fp.read(1):
|
||||
fp.seek(-1, 1)
|
||||
section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section)))
|
||||
if section.name == b'ANIMINFO':
|
||||
PsaReader.read_types(fp, Psa.Sequence, section, sequences)
|
||||
return [sequence.name for sequence in sequences]
|
||||
else:
|
||||
fp.seek(section.data_size * section.data_count, 1)
|
||||
return []
|
||||
|
||||
def read(self, path) -> Psa:
|
||||
psa = Psa()
|
||||
with open(path, 'rb') as fp:
|
||||
while fp.read(1):
|
||||
fp.seek(-1, 1)
|
||||
section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section)))
|
||||
if section.name == b'ANIMHEAD':
|
||||
pass
|
||||
elif section.name == b'BONENAMES':
|
||||
PsaReader.read_types(fp, Psa.Bone, section, psa.bones)
|
||||
elif section.name == b'ANIMINFO':
|
||||
PsaReader.read_types(fp, Psa.Sequence, section, psa.sequences)
|
||||
elif section.name == b'ANIMKEYS':
|
||||
PsaReader.read_types(fp, Psa.Key, section, psa.keys)
|
||||
else:
|
||||
raise RuntimeError(f'Unrecognized section "{section.name}"')
|
||||
return psa
|
||||
1
|
@ -159,9 +159,9 @@ class PskBuilder(object):
|
||||
for f in object.data.loop_triangles:
|
||||
face = Psk.Face()
|
||||
face.material_index = material_indices[f.material_index]
|
||||
face.wedge_index_1 = f.loops[2] + wedge_offset
|
||||
face.wedge_index_2 = f.loops[1] + wedge_offset
|
||||
face.wedge_index_3 = f.loops[0] + wedge_offset
|
||||
face.wedge_indices[0] = f.loops[2] + wedge_offset
|
||||
face.wedge_indices[1] = f.loops[1] + wedge_offset
|
||||
face.wedge_indices[2] = f.loops[0] + wedge_offset
|
||||
face.smoothing_groups = poly_groups[f.polygon_index]
|
||||
psk.faces.append(face)
|
||||
# update the material index of the wedges
|
||||
|
@ -25,9 +25,7 @@ class Psk(object):
|
||||
|
||||
class Face(Structure):
|
||||
_fields_ = [
|
||||
('wedge_index_1', c_int16),
|
||||
('wedge_index_2', c_int16),
|
||||
('wedge_index_3', c_int16),
|
||||
('wedge_indices', c_int16 * 3),
|
||||
('material_index', c_int8),
|
||||
('aux_material_index', c_int8),
|
||||
('smoothing_groups', c_int32)
|
||||
|
71
io_export_psk_psa/psk/importer.py
Normal file
71
io_export_psk_psa/psk/importer.py
Normal file
@ -0,0 +1,71 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
from .data import Psk
|
||||
|
||||
|
||||
class PskImporter(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def import_psk(self, psk: Psk, context):
|
||||
# ARMATURE
|
||||
armature_data = bpy.data.armatures.new('armature')
|
||||
armature_object = bpy.data.objects.new('new_ao', armature_data)
|
||||
context.scene.collection.objects.link(armature_object)
|
||||
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
except:
|
||||
pass
|
||||
|
||||
armature_object.select_set(state=True)
|
||||
bpy.context.view_layer.objects.active = armature_object
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
for bone in psk.bones:
|
||||
edit_bone = armature_data.edit_bones.new(bone.name.decode('utf-8'))
|
||||
edit_bone.parent = armature_data.edit_bones[bone.parent_index]
|
||||
edit_bone.head = (bone.location.x, bone.location.y, bone.location.z)
|
||||
rotation = mathutils.Quaternion(*bone.rotation)
|
||||
edit_bone.tail = edit_bone.head + (mathutils.Vector(0, 0, 1) @ rotation)
|
||||
|
||||
# MESH
|
||||
mesh_data = bpy.data.meshes.new('mesh')
|
||||
mesh_object = bpy.data.objects.new('new_mo', mesh_data)
|
||||
|
||||
# MATERIALS
|
||||
for material in psk.materials:
|
||||
bpy_material = bpy.data.materials.new(material.name.decode('utf-8'))
|
||||
mesh_data.materials.append(bpy_material)
|
||||
|
||||
bm = bmesh.new()
|
||||
|
||||
# VERTICES
|
||||
for point in psk.points:
|
||||
bm.verts.new((point.x, point.y, point.z))
|
||||
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
for face in psk.faces:
|
||||
point_indices = [bm.verts[psk.wedges[i].point_index] for i in reversed(face.wedge_indices)]
|
||||
bm_face = bm.faces.new(point_indices)
|
||||
bm_face.material_index = face.material_index
|
||||
|
||||
bm.to_mesh(mesh_data)
|
||||
|
||||
# TEXTURE COORDINATES
|
||||
data_index = 0
|
||||
uv_layer = mesh_data.uv_layers.new()
|
||||
for face_index, face in enumerate(psk.faces):
|
||||
face_wedges = [psk.wedges[i] for i in reversed(face.wedge_indices)]
|
||||
for wedge in face_wedges:
|
||||
uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v
|
||||
data_index += 1
|
||||
|
||||
bm.free()
|
||||
|
||||
# TODO: weights (vertex grorups etc.)
|
||||
|
||||
context.scene.collection.objects.link(mesh_object)
|
@ -1,8 +1,29 @@
|
||||
from bpy.types import Operator
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from bpy.props import StringProperty, BoolProperty, FloatProperty
|
||||
from .builder import PskBuilder
|
||||
from .exporter import PskExporter
|
||||
from .reader import PskReader
|
||||
from .importer import PskImporter
|
||||
|
||||
|
||||
class PskImportOperator(Operator, ImportHelper):
|
||||
bl_idname = 'import.psk'
|
||||
bl_label = 'Export'
|
||||
__doc__ = 'PSK Importer (.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):
|
||||
reader = PskReader()
|
||||
psk = reader.read(self.filepath)
|
||||
PskImporter().import_psk(psk, context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PskExportOperator(Operator, ExportHelper):
|
||||
|
46
io_export_psk_psa/psk/reader.py
Normal file
46
io_export_psk_psa/psk/reader.py
Normal file
@ -0,0 +1,46 @@
|
||||
from .data import *
|
||||
import ctypes
|
||||
|
||||
|
||||
class PskReader(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def read_types(fp, data_class: ctypes.Structure, section: Section, data):
|
||||
buffer_length = section.data_size * section.data_count
|
||||
buffer = fp.read(buffer_length)
|
||||
offset = 0
|
||||
for _ in range(section.data_count):
|
||||
data.append(data_class.from_buffer_copy(buffer, offset))
|
||||
offset += section.data_size
|
||||
|
||||
def read(self, path) -> Psk:
|
||||
psk = Psk()
|
||||
with open(path, 'rb') as fp:
|
||||
while fp.read(1):
|
||||
fp.seek(-1, 1)
|
||||
section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section)))
|
||||
if section.name == b'ACTRHEAD':
|
||||
pass
|
||||
elif section.name == b'PNTS0000':
|
||||
PskReader.read_types(fp, Vector3, section, psk.points)
|
||||
elif section.name == b'VTXW0000':
|
||||
if section.data_size == ctypes.sizeof(Psk.Wedge16):
|
||||
PskReader.read_types(fp, Psk.Wedge16, section, psk.wedges)
|
||||
elif section.data_size == ctypes.sizeof(Psk.Wedge32):
|
||||
PskReader.read_types(fp, Psk.Wedge32, section, psk.wedges)
|
||||
else:
|
||||
raise RuntimeError('Unrecognized wedge format')
|
||||
elif section.name == b'FACE0000':
|
||||
PskReader.read_types(fp, Psk.Face, section, psk.faces)
|
||||
elif section.name == b'MATT0000':
|
||||
PskReader.read_types(fp, Psk.Material, section, psk.materials)
|
||||
elif section.name == b'REFSKELT':
|
||||
PskReader.read_types(fp, Psk.Bone, section, psk.bones)
|
||||
elif section.name == b'RAWWEIGHTS':
|
||||
PskReader.read_types(fp, Psk.Weight, section, psk.weights)
|
||||
else:
|
||||
raise RuntimeError(f'Unrecognized section "{section.name}"')
|
||||
return psk
|
Loading…
Reference in New Issue
Block a user