1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2025-02-17 10:08:31 +01:00

* PSK importer now working

* Fleshing out PSA importer (not done yet but getting there)
This commit is contained in:
Colin Basnett 2022-01-14 12:26:35 -08:00
parent d578350980
commit 9fa0780032
11 changed files with 504 additions and 296 deletions

View File

@ -18,11 +18,9 @@ if 'bpy' in locals():
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:
@ -32,43 +30,48 @@ else:
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
from bpy.props import PointerProperty
classes = [
psk_operator.PskExportOperator,
psk_operator.PskImportOperator,
psa_operator.PsaExportOperator,
psa_operator.PsaImportOperator,
psa_operator.PSA_UL_ActionList,
psa_operator.PSA_UL_ImportActionList,
psa_operator.ActionListItem,
psa_operator.ImportActionListItem
psk_exporter.PskExportOperator,
psk_importer.PskImportOperator,
psa_exporter.PsaExportOperator,
psa_importer.PsaImportOperator,
psa_importer.PsaImportFileSelectOperator,
psa_importer.PSA_UL_ActionList,
psa_importer.PSA_UL_ImportActionList,
psa_exporter.PsaExportActionListItem,
psa_importer.PsaImportActionListItem,
psa_importer.PsaImportSelectAll,
psa_importer.PsaImportDeselectAll,
psa_importer.PSA_PT_ImportPanel,
psa_importer.PsaImportPropertyGroup,
psa_exporter.PsaExportPropertyGroup,
]
def psk_export_menu_func(self, context):
self.layout.operator(psk_operator.PskExportOperator.bl_idname, text ='Unreal PSK (.psk)')
self.layout.operator(psk_exporter.PskExportOperator.bl_idname, text='Unreal PSK (.psk)')
def psk_import_menu_func(self, context):
self.layout.operator(psk_operator.PskImportOperator.bl_idname, text ='Unreal PSK (.psk)')
self.layout.operator(psk_importer.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)')
self.layout.operator(psa_exporter.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)')
self.layout.operator(psa_importer.PsaImportOperator.bl_idname, text='Unreal PSA (.psa)')
def register():
@ -78,17 +81,13 @@ def register():
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)
bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup)
bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
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
del bpy.types.Scene.psa_import_action_list
del bpy.types.Scene.psa_export
del bpy.types.Scene.psa_import
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)

View File

@ -8,6 +8,11 @@ class Vector3(Structure):
('z', c_float),
]
def __iter__(self):
yield self.x
yield self.y
yield self.z
class Quaternion(Structure):
_fields_ = [
@ -18,10 +23,10 @@ class Quaternion(Structure):
]
def __iter__(self):
yield self.w
yield self.x
yield self.y
yield self.z
yield self.w
class Section(Structure):

View File

@ -1,4 +1,4 @@
from typing import List
from typing import List, Dict
from ..data import *
@ -40,5 +40,5 @@ class Psa(object):
def __init__(self):
self.bones: List[Psa.Bone] = []
self.sequences: List[Psa.Sequence] = []
self.sequences: Dict[Psa.Sequence] = {}
self.keys: List[Psa.Key] = []

View File

@ -1,5 +1,11 @@
import bpy
from bpy.types import Operator, PropertyGroup, Action
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty
from bpy_extras.io_utils import ExportHelper
from typing import Type
from .builder import PsaBuilder, PsaBuilderOptions
from .data import *
import re
class PsaExporter(object):
@ -25,3 +31,92 @@ class PsaExporter(object):
self.write_section(fp, b'BONENAMES', Psa.Bone, self.psa.bones)
self.write_section(fp, b'ANIMINFO', Psa.Sequence, self.psa.sequences)
self.write_section(fp, b'ANIMKEYS', Psa.Key, self.psa.keys)
class PsaExportActionListItem(PropertyGroup):
action: PointerProperty(type=Action)
is_selected: BoolProperty(default=False)
@property
def name(self):
return self.action.name
class PsaExportPropertyGroup(bpy.types.PropertyGroup):
action_list: CollectionProperty(type=PsaExportActionListItem)
import_action_list: CollectionProperty(type=PsaExportActionListItem)
action_list_index: IntProperty(name='index for list??', default=0)
import_action_list_index: IntProperty(name='index for list??', default=0)
class PsaExportOperator(Operator, ExportHelper):
bl_idname = 'export.psa'
bl_label = 'Export'
__doc__ = 'PSA Exporter (.psa)'
filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSA file',
maxlen=1024,
default='')
def __init__(self):
self.armature = None
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_ActionList', 'asd', scene, 'psa_export.action_list', scene, 'psa_export.action_list_index', rows=len(context.scene.psa_export.action_list))
def is_action_for_armature(self, action):
if len(action.fcurves) == 0:
return False
bone_names = set([x.name for x in self.armature.data.bones])
for fcurve in action.fcurves:
match = re.match(r'pose\.bones\["(.+)"\].\w+', fcurve.data_path)
if not match:
continue
bone_name = match.group(1)
if bone_name in bone_names:
return True
return False
def invoke(self, context, event):
if context.view_layer.objects.active.type != 'ARMATURE':
self.report({'ERROR_INVALID_CONTEXT'}, 'The selected object must be an armature.')
return {'CANCELLED'}
self.armature = context.view_layer.objects.active
context.scene.psa_export.action_list.clear()
for action in bpy.data.actions:
item = context.scene.psa_export.action_list.add()
item.action = action
if self.is_action_for_armature(action):
item.is_selected = True
if len(context.scene.psa_export.action_list) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.')
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
actions = [x.action for x in context.scene.psa_export.action_list if x.is_selected]
if len(actions) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.')
return {'CANCELLED'}
options = PsaBuilderOptions()
options.actions = actions
builder = PsaBuilder()
psa = builder.build(context, options)
exporter = PsaExporter(psa)
exporter.export(self.filepath)
return {'FINISHED'}

View File

@ -1,15 +1,215 @@
import bpy
import bmesh
import mathutils
from .data import Psa
from typing import List, AnyStr
import bpy
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
from .reader import PsaReader
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)
def import_psa(self, psa: Psa, sequence_names: List[AnyStr], context):
properties = context.scene.psa_import
sequences = map(lambda x: psa.sequences[x], sequence_names)
armature_object = properties.armature_object
armature_data = armature_object.data
# create an index mapping from bones in the PSA to bones in the target armature.
bone_indices = {}
data_bone_names = [x.name for x in armature_data.bones]
for index, psa_bone in enumerate(psa.bones):
psa_bone_name = psa_bone.name.decode()
try:
bone_indices[index] = data_bone_names.index(psa_bone_name)
except ValueError:
pass
del data_bone_names
for sequence in sequences:
action = bpy.data.actions.new(name=sequence.name.decode())
for psa_bone_index, armature_bone_index in bone_indices.items():
psa_bone = psa.bones[psa_bone_index]
pose_bone = armature_object.pose.bones[armature_bone_index]
# rotation
rotation_data_path = pose_bone.path_from_id('rotation_quaternion')
fcurve_quat_w = action.fcurves.new(rotation_data_path, index=0)
fcurve_quat_x = action.fcurves.new(rotation_data_path, index=0)
fcurve_quat_y = action.fcurves.new(rotation_data_path, index=0)
fcurve_quat_z = action.fcurves.new(rotation_data_path, index=0)
# location
location_data_path = pose_bone.path_from_id('location')
fcurve_location_x = action.fcurves.new(location_data_path, index=0)
fcurve_location_y = action.fcurves.new(location_data_path, index=1)
fcurve_location_z = action.fcurves.new(location_data_path, index=2)
# add keyframes
fcurve_quat_w.keyframe_points.add(sequence.frame_count)
fcurve_quat_x.keyframe_points.add(sequence.frame_count)
fcurve_quat_y.keyframe_points.add(sequence.frame_count)
fcurve_quat_z.keyframe_points.add(sequence.frame_count)
fcurve_location_x.keyframe_points.add(sequence.frame_count)
fcurve_location_y.keyframe_points.add(sequence.frame_count)
fcurve_location_z.keyframe_points.add(sequence.frame_count)
raw_key_index = 0 # ?
for frame_index in range(sequence.frame_count):
for psa_bone_index in range(len(psa.bones)):
if psa_bone_index not in bone_indices:
# bone does not exist in the armature, skip it
raw_key_index += 1
continue
psa_bone = psa.bones[psa_bone_index]
# ...
raw_key_index += 1
class PsaImportActionListItem(PropertyGroup):
action_name: StringProperty()
is_selected: BoolProperty(default=True)
@property
def name(self):
return self.action_name
class PsaImportPropertyGroup(bpy.types.PropertyGroup):
cool_filepath: StringProperty(default='')
armature_object: PointerProperty(type=bpy.types.Object) # TODO: figure out how to filter this to only objects of a specific type
action_list: CollectionProperty(type=PsaImportActionListItem)
import_action_list: CollectionProperty(type=PsaImportActionListItem)
action_list_index: IntProperty(name='index for list??', default=0)
import_action_list_index: IntProperty(name='index for list??', default=0)
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,
'action_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'
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 PsaImportSelectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_select_all'
bl_label = 'Select All'
def execute(self, context):
for action in context.scene.psa_import.action_list:
action.is_selected = True
return {'FINISHED'}
class PsaImportDeselectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_deselect_all'
bl_label = 'Deselect All'
def execute(self, context):
for action in context.scene.psa_import.action_list:
action.is_selected = False
return {'FINISHED'}
class PSA_PT_ImportPanel(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = 'PSA Import'
bl_context = 'objectmode'
bl_category = 'PSA Import'
def draw(self, context):
layout = self.layout
scene = context.scene
row = layout.row()
row.operator('psa_import.file_select', icon='FILE_FOLDER', text='')
row.label(text=scene.psa_import.cool_filepath)
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=10)
row = box.row()
row.operator('psa_import.actions_select_all', text='Select All')
row.operator('psa_import.actions_deselect_all', text='Deselect All')
layout.prop(scene.psa_import, 'armature_object', icon_only=True)
layout.operator('psa_import.import', text='Import')
class PsaImportOperator(Operator):
bl_idname = 'psa_import.import'
bl_label = 'Import'
def execute(self, context):
psa = PsaReader().read(context.scene.psa_import.cool_filepath)
sequence_names = [x.action_name for x in context.scene.psa_import.action_list if x.is_selected]
PsaImporter().import_psa(psa, sequence_names, context)
return {'FINISHED'}
class PsaImportFileSelectOperator(Operator, ImportHelper):
bl_idname = 'psa_import.file_select'
bl_label = 'File Select'
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):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
context.scene.psa_import.cool_filepath = self.filepath
# Load the sequence names from the selected file
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_import.action_list.add()
item.action_name = action_name.decode()
item.is_selected = True
return {'FINISHED'}

View File

@ -1,176 +0,0 @@
from bpy.types import Operator, Action, UIList, PropertyGroup
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)
@property
def name(self):
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'
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 PsaExportOperator(Operator, ExportHelper):
bl_idname = 'export.psa'
bl_label = 'Export'
__doc__ = 'PSA Exporter (.psa)'
filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSA file',
maxlen=1024,
default='')
def __init__(self):
self.armature = None
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_ActionList', 'asd', scene, 'psa_action_list', scene, 'psa_action_list_index', rows=len(context.scene.psa_action_list))
def is_action_for_armature(self, action):
if len(action.fcurves) == 0:
return False
bone_names = set([x.name for x in self.armature.data.bones])
for fcurve in action.fcurves:
match = re.match(r'pose\.bones\["(.+)"\].\w+', fcurve.data_path)
if not match:
continue
bone_name = match.group(1)
if bone_name in bone_names:
return True
return False
def invoke(self, context, event):
if context.view_layer.objects.active.type != 'ARMATURE':
self.report({'ERROR_INVALID_CONTEXT'}, 'The selected object must be an armature.')
return {'CANCELLED'}
self.armature = context.view_layer.objects.active
context.scene.psa_action_list.clear()
for action in bpy.data.actions:
item = context.scene.psa_action_list.add()
item.action = action
if self.is_action_for_armature(action):
item.is_selected = True
if len(context.scene.psa_action_list) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.')
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
actions = [x.action for x in context.scene.psa_action_list if x.is_selected]
if len(actions) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.')
return {'CANCELLED'}
options = PsaBuilderOptions()
options.actions = actions
builder = PsaBuilder()
psk = builder.build(context, options)
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'}

View File

@ -20,9 +20,6 @@ class PsaReader(object):
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)))
@ -44,9 +41,14 @@ class PsaReader(object):
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)
sequences = []
PsaReader.read_types(fp, Psa.Sequence, section, sequences)
for sequence in sequences:
psa.sequences[sequence.name.decode()] = sequence
elif section.name == b'ANIMKEYS':
PsaReader.read_types(fp, Psa.Key, section, psa.keys)
elif section.name in [b'SCALEKEYS']:
fp.seek(section.data_size * section.data_count, 1)
else:
raise RuntimeError(f'Unrecognized section "{section.name}"')
return psa

View File

@ -36,8 +36,8 @@ class Psk(object):
class Face(Structure):
_fields_ = [
('wedge_indices', c_uint16 * 3),
('material_index', c_int8),
('aux_material_index', c_int8),
('material_index', c_uint8),
('aux_material_index', c_uint8),
('smoothing_groups', c_int32)
]

View File

@ -1,8 +1,18 @@
from typing import Type
from .data import *
from .builder import PskBuilder
from typing import Type
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty
MAX_WEDGE_COUNT = 65536
MAX_POINT_COUNT = 4294967296
MAX_BONE_COUNT = 256
MAX_MATERIAL_COUNT = 256
class PskExporter(object):
def __init__(self, psk: Psk):
self.psk: Psk = psk
@ -19,27 +29,61 @@ class PskExporter(object):
fp.write(datum)
def export(self, path: str):
if len(self.psk.wedges) > MAX_WEDGE_COUNT:
raise RuntimeError(f'Number of wedges ({len(self.psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}')
if len(self.psk.bones) > MAX_BONE_COUNT:
raise RuntimeError(f'Number of bones ({len(self.psk.bones)}) exceeds limit of {MAX_BONE_COUNT}')
if len(self.psk.points) > MAX_POINT_COUNT:
raise RuntimeError(f'Numbers of vertices ({len(self.psk.points)}) exceeds limit of {MAX_POINT_COUNT}')
if len(self.psk.materials) > MAX_MATERIAL_COUNT:
raise RuntimeError(f'Number of materials ({len(self.psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}')
with open(path, 'wb') as fp:
self.write_section(fp, b'ACTRHEAD')
self.write_section(fp, b'PNTS0000', Vector3, self.psk.points)
# WEDGES
if len(self.psk.wedges) <= 65536:
wedge_type = Psk.Wedge16
else:
wedge_type = Psk.Wedge32
wedges = []
for index, w in enumerate(self.psk.wedges):
wedge = wedge_type()
wedge = Psk.Wedge16()
wedge.material_index = w.material_index
wedge.u = w.u
wedge.v = w.v
wedge.point_index = w.point_index
wedges.append(wedge)
self.write_section(fp, b'VTXW0000', wedge_type, wedges)
self.write_section(fp, b'VTXW0000', Psk.Wedge16, wedges)
self.write_section(fp, b'FACE0000', Psk.Face, self.psk.faces)
self.write_section(fp, b'MATT0000', Psk.Material, self.psk.materials)
self.write_section(fp, b'REFSKELT', Psk.Bone, self.psk.bones)
self.write_section(fp, b'RAWWEIGHTS', Psk.Weight, self.psk.weights)
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 invoke(self, context, event):
try:
PskBuilder.get_input_objects(context)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
builder = PskBuilder()
psk = builder.build(context)
exporter = PskExporter(psk)
exporter.export(self.filepath)
return {'FINISHED'}

View File

@ -1,17 +1,25 @@
import os
import bpy
import bmesh
import mathutils
from typing import Optional
from .data import Psk
from mathutils import Quaternion, Vector, Matrix
from .reader import PskReader
from bpy.props import StringProperty
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper
class PskImporter(object):
def __init__(self):
pass
def import_psk(self, psk: Psk, context):
def import_psk(self, psk: Psk, name: str, context):
# ARMATURE
armature_data = bpy.data.armatures.new('armature')
armature_object = bpy.data.objects.new('new_ao', armature_data)
armature_data = bpy.data.armatures.new(name)
armature_object = bpy.data.objects.new(name, armature_data)
armature_object.show_in_front = True
context.scene.collection.objects.link(armature_object)
try:
@ -24,19 +32,68 @@ class PskImporter(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)
# Intermediate bone type for the purpose of construction.
class ImportBone(object):
def __init__(self, index: int, psk_bone: Psk.Bone):
self.index: int = index
self.psk_bone: Psk.Bone = psk_bone
self.parent: Optional[ImportBone] = None
self.local_rotation: Quaternion = Quaternion()
self.local_translation: Vector = Vector()
self.world_rotation_matrix: Matrix = Matrix()
self.world_matrix: Matrix = Matrix()
self.vertex_group = None
import_bones = []
should_invert_root = False
new_bone_size = 8.0
for bone_index, psk_bone in enumerate(psk.bones):
import_bone = ImportBone(bone_index, psk_bone)
psk_bone.parent_index = max(0, psk_bone.parent_index)
import_bone.local_rotation = Quaternion(tuple(psk_bone.rotation))
import_bone.local_translation = Vector(tuple(psk_bone.location))
if psk_bone.parent_index == 0 and bone_index == 0:
if should_invert_root:
import_bone.world_rotation_matrix = import_bone.local_rotation.conjugated().to_matrix()
else:
import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix()
import_bone.world_matrix = Matrix.Translation(import_bone.local_translation)
import_bones.append(import_bone)
for bone_index, bone in enumerate(import_bones):
if bone.psk_bone.parent_index == 0 and bone_index == 0:
continue
parent = import_bones[bone.psk_bone.parent_index]
bone.parent = parent
bone.world_matrix = parent.world_rotation_matrix.to_4x4()
translation = bone.local_translation.copy()
translation.rotate(parent.world_rotation_matrix)
bone.world_matrix.translation = parent.world_matrix.translation + translation
bone.world_rotation_matrix = bone.local_rotation.conjugated().to_matrix()
bone.world_rotation_matrix.rotate(parent.world_rotation_matrix)
for bone in import_bones:
edit_bone = armature_data.edit_bones.new(bone.psk_bone.name.decode('utf-8'))
if bone.parent is not None:
edit_bone.parent = armature_data.edit_bones[bone.psk_bone.parent_index]
elif not should_invert_root:
bone.local_rotation.conjugate()
post_quat = bone.local_rotation.conjugated()
edit_bone.tail = Vector((0.0, new_bone_size, 0.0))
m = post_quat.copy()
m.rotate(bone.world_matrix)
m = m.to_matrix().to_4x4()
m.translation = bone.world_matrix.translation
edit_bone.matrix = m
# MESH
mesh_data = bpy.data.meshes.new('mesh')
mesh_object = bpy.data.objects.new('new_mo', mesh_data)
mesh_data = bpy.data.meshes.new(name)
mesh_object = bpy.data.objects.new(name, mesh_data)
# MATERIALS
for material in psk.materials:
# TODO: re-use of materials should be an option
bpy_material = bpy.data.materials.new(material.name.decode('utf-8'))
mesh_data.materials.append(bpy_material)
@ -44,7 +101,7 @@ class PskImporter(object):
# VERTICES
for point in psk.points:
bm.verts.new((point.x, point.y, point.z))
bm.verts.new(tuple(point))
bm.verts.ensure_lookup_table()
@ -64,8 +121,47 @@ class PskImporter(object):
uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v
data_index += 1
bm.normal_update()
bm.free()
# TODO: weights (vertex grorups etc.)
# VERTEX WEIGHTS
# Get a list of all bones that have weights associated with them.
vertex_group_bone_indices = set(map(lambda weight: weight.bone_index, psk.weights))
for bone_index in sorted(list(vertex_group_bone_indices)):
import_bones[bone_index].vertex_group = mesh_object.vertex_groups.new(name=import_bones[bone_index].psk_bone.name.decode('windows-1252'))
for weight in psk.weights:
import_bones[weight.bone_index].vertex_group.add((weight.point_index,), weight.weight, 'ADD')
# Add armature modifier to our mesh object.
armature_modifier = mesh_object.modifiers.new(name='Armature', type='ARMATURE')
armature_modifier.object = armature_object
mesh_object.parent = armature_object
context.scene.collection.objects.link(mesh_object)
try:
bpy.ops.object.mode_set(mode='OBJECT')
except:
pass
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)
name = os.path.splitext(os.path.basename(self.filepath))[0]
PskImporter().import_psk(psk, name, context)
return {'FINISHED'}

View File

@ -1,57 +0,0 @@
from bpy.types import Operator
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):
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 invoke(self, context, event):
try:
PskBuilder.get_input_objects(context)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
builder = PskBuilder()
psk = builder.build(context)
exporter = PskExporter(psk)
exporter.export(self.filepath)
return {'FINISHED'}