From afe598f671ac9b7db9d13b180e63cd0851a75ea3 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sat, 29 Jul 2023 20:51:23 -0700 Subject: [PATCH] File structure/naming overhaul on the PSK importer and exporter * Inverted to the option to "ignore bone name restrictions". It is now "Enforce Bone Name Restrictions" and it is off by default. * Added option to filter out "reversed" sequences from the sequence list --- io_scene_psk_psa/__init__.py | 32 ++-- io_scene_psk_psa/helpers.py | 3 +- io_scene_psk_psa/psa/builder.py | 2 +- io_scene_psk_psa/psa/export/operators.py | 4 +- io_scene_psk_psa/psa/export/properties.py | 13 +- io_scene_psk_psa/psa/export/ui.py | 1 + io_scene_psk_psa/psk/builder.py | 4 +- io_scene_psk_psa/psk/export/__init__.py | 0 .../psk/{exporter.py => export/operators.py} | 149 ++++-------------- io_scene_psk_psa/psk/export/properties.py | 39 +++++ io_scene_psk_psa/psk/export/ui.py | 12 ++ io_scene_psk_psa/psk/import_/__init__.py | 0 io_scene_psk_psa/psk/import_/operators.py | 143 +++++++++++++++++ io_scene_psk_psa/psk/importer.py | 143 +---------------- io_scene_psk_psa/psk/writer.py | 54 +++++++ io_scene_psk_psa/types.py | 10 +- 16 files changed, 323 insertions(+), 286 deletions(-) create mode 100644 io_scene_psk_psa/psk/export/__init__.py rename io_scene_psk_psa/psk/{exporter.py => export/operators.py} (51%) create mode 100644 io_scene_psk_psa/psk/export/properties.py create mode 100644 io_scene_psk_psa/psk/export/ui.py create mode 100644 io_scene_psk_psa/psk/import_/__init__.py create mode 100644 io_scene_psk_psa/psk/import_/operators.py create mode 100644 io_scene_psk_psa/psk/writer.py diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index d0bd736..5acf976 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -18,10 +18,14 @@ if 'bpy' in locals(): importlib.reload(psx_types) importlib.reload(psk_data) - importlib.reload(psk_builder) - importlib.reload(psk_exporter) - importlib.reload(psk_importer) importlib.reload(psk_reader) + importlib.reload(psk_writer) + importlib.reload(psk_builder) + importlib.reload(psk_importer) + importlib.reload(psk_export_properties) + importlib.reload(psk_export_operators) + importlib.reload(psk_export_ui) + importlib.reload(psk_import_operators) importlib.reload(psa_data) importlib.reload(psa_reader) @@ -39,10 +43,14 @@ else: from . import helpers as psx_helpers from . import types as psx_types 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 writer as psk_writer + from .psk import builder as psk_builder from .psk import importer as psk_importer + from .psk.export import properties as psk_export_properties + from .psk.export import operators as psk_export_operators + from .psk.export import ui as psk_export_ui + from .psk.import_ import operators as psk_import_operators from .psa import data as psa_data from .psa import reader as psa_reader @@ -60,8 +68,10 @@ import bpy from bpy.props import PointerProperty classes = psx_types.classes +\ - psk_importer.classes +\ - psk_exporter.classes +\ + psk_import_operators.classes +\ + psk_export_properties.classes +\ + psk_export_operators.classes +\ + psk_export_ui.classes + \ psa_export_properties.classes +\ psa_export_operators.classes +\ psa_export_ui.classes + \ @@ -71,11 +81,11 @@ classes = psx_types.classes +\ def psk_export_menu_func(self, context): - self.layout.operator(psk_exporter.PskExportOperator.bl_idname, text='Unreal PSK (.psk)') + self.layout.operator(psk_export_operators.PSK_OT_export.bl_idname, text='Unreal PSK (.psk)') def psk_import_menu_func(self, context): - self.layout.operator(psk_importer.PskImportOperator.bl_idname, text='Unreal PSK (.psk/.pskx)') + self.layout.operator(psk_import_operators.PSK_OT_import.bl_idname, text='Unreal PSK (.psk/.pskx)') def psa_export_menu_func(self, context): @@ -95,8 +105,8 @@ def register(): bpy.types.TOPBAR_MT_file_import.append(psa_import_menu_func) bpy.types.Scene.psa_import = PointerProperty(type=psa_import_properties.PSA_PG_import) bpy.types.Scene.psa_export = PointerProperty(type=psa_export_properties.PSA_PG_export) - bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup) - bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_ActionExportPropertyGroup) + bpy.types.Scene.psk_export = PointerProperty(type=psk_export_properties.PSK_PG_export) + bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_action_export) def unregister(): diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index 238c42f..fe683e0 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -93,7 +93,8 @@ def check_bone_names(bone_names: Iterable[str]): invalid_bone_names = [x for x in bone_names if pattern.match(x) is None] if len(invalid_bone_names) > 0: raise RuntimeError(f'The following bone names are invalid: {invalid_bone_names}.\n' - f'Bone names must only contain letters, numbers, spaces, hyphens and underscores.') + f'Bone names must only contain letters, numbers, spaces, hyphens and underscores.\n' + f'You can bypass this by disabling "Enforce Bone Name Restrictions" in the export settings.') def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_group_indices: List[int]) -> List[str]: diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index eed8ce9..fcabebc 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -27,7 +27,7 @@ class PsaBuildOptions: self.sequences: List[PsaBuildSequence] = [] self.bone_filter_mode: str = 'ALL' self.bone_group_indices: List[int] = [] - self.should_ignore_bone_name_restrictions: bool = False + self.should_enforce_bone_name_restrictions: bool = False self.sequence_name_prefix: str = '' self.sequence_name_suffix: str = '' self.root_motion: bool = False diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 7d0c260..a5ee85e 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -313,7 +313,7 @@ class PSA_OT_export(Operator, ExportHelper): layout.template_list('PSX_UL_bone_group_list', '', pg, 'bone_group_list', pg, 'bone_group_list_index', rows=rows) - layout.prop(pg, 'should_ignore_bone_name_restrictions') + layout.prop(pg, 'should_enforce_bone_name_restrictions') layout.separator() @@ -397,7 +397,7 @@ class PSA_OT_export(Operator, ExportHelper): options.sequences = export_sequences options.bone_filter_mode = pg.bone_filter_mode options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected] - options.should_ignore_bone_name_restrictions = pg.should_ignore_bone_name_restrictions + options.should_ignore_bone_name_restrictions = pg.should_enforce_bone_name_restrictions options.sequence_name_prefix = pg.sequence_name_prefix options.sequence_name_suffix = pg.sequence_name_suffix options.root_motion = pg.root_motion diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index 8e6f625..2baa4fe 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -116,10 +116,16 @@ class PSA_PG_export(PropertyGroup): options=empty_set, description='Show actions that belong to an asset library') sequence_filter_pose_marker: BoolProperty( - default=False, + default=True, name='Show pose markers', options=empty_set) sequence_use_filter_sort_reverse: BoolProperty(default=True, options=empty_set) + sequence_filter_reversed: BoolProperty( + default=True, + options=empty_set, + name='Show Reversed', + description='Show reversed sequences' + ) def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: @@ -147,6 +153,11 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: if hasattr(sequence, 'is_pose_marker') and sequence.is_pose_marker: flt_flags[i] &= ~bitflag_filter_item + if not pg.sequence_filter_reversed: + for i, sequence in enumerate(sequences): + if sequence.frame_start > sequence.frame_end: + flt_flags[i] &= ~bitflag_filter_item + return flt_flags diff --git a/io_scene_psk_psa/psa/export/ui.py b/io_scene_psk_psa/psa/export/ui.py index 9dfb221..ad9d29a 100644 --- a/io_scene_psk_psa/psa/export/ui.py +++ b/io_scene_psk_psa/psa/export/ui.py @@ -38,6 +38,7 @@ class PSA_UL_export_sequences(UIList): subrow = row.row(align=True) subrow.prop(pg, 'sequence_filter_asset', icon_only=True, icon='ASSET_MANAGER') subrow.prop(pg, 'sequence_filter_pose_marker', icon_only=True, icon='PMARKER') + subrow.prop(pg, 'sequence_filter_reversed', text="", icon='FRAME_PREV') def filter_items(self, context, data, prop): pg = getattr(context.scene, 'psa_export') diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 4c15380..99a3784 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -18,7 +18,7 @@ class PskBuildOptions(object): self.bone_group_indices: List[int] = [] self.use_raw_mesh_data = True self.material_names: List[str] = [] - self.should_ignore_bone_name_restrictions = False + self.should_enforce_bone_name_restrictions = False def get_psk_input_objects(context) -> PskInputObjects: @@ -83,7 +83,7 @@ def build_psk(context, options: PskBuildOptions) -> Psk: bones = [armature_data.bones[bone_name] for bone_name in bone_names] # Check that all bone names are valid. - if not options.should_ignore_bone_name_restrictions: + if options.should_enforce_bone_name_restrictions: check_bone_names(map(lambda x: x.name, bones)) for bone in bones: diff --git a/io_scene_psk_psa/psk/export/__init__.py b/io_scene_psk_psa/psk/export/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/io_scene_psk_psa/psk/exporter.py b/io_scene_psk_psa/psk/export/operators.py similarity index 51% rename from io_scene_psk_psa/psk/exporter.py rename to io_scene_psk_psa/psk/export/operators.py index 0386b4c..cb48caa 100644 --- a/io_scene_psk_psa/psk/exporter.py +++ b/io_scene_psk_psa/psk/export/operators.py @@ -1,62 +1,10 @@ -from typing import Type - -from bpy.props import BoolProperty, StringProperty, CollectionProperty, IntProperty, EnumProperty -from bpy.types import Operator, PropertyGroup, UIList +from bpy.props import StringProperty +from bpy.types import Operator from bpy_extras.io_utils import ExportHelper -from .builder import build_psk, PskBuildOptions, get_psk_input_objects -from .data import * -from ..helpers import populate_bone_group_list -from ..types import PSX_PG_bone_group_list_item - -MAX_WEDGE_COUNT = 65536 -MAX_POINT_COUNT = 4294967296 -MAX_BONE_COUNT = 256 -MAX_MATERIAL_COUNT = 256 - - -def _write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None): - section = Section() - section.name = name - if data_type is not None and data is not None: - section.data_size = sizeof(data_type) - section.data_count = len(data) - fp.write(section) - if data is not None: - for datum in data: - fp.write(datum) - - -def export_psk(psk: Psk, path: str): - if len(psk.wedges) > MAX_WEDGE_COUNT: - raise RuntimeError(f'Number of wedges ({len(psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}') - if len(psk.points) > MAX_POINT_COUNT: - raise RuntimeError(f'Numbers of vertices ({len(psk.points)}) exceeds limit of {MAX_POINT_COUNT}') - if len(psk.materials) > MAX_MATERIAL_COUNT: - raise RuntimeError(f'Number of materials ({len(psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}') - if len(psk.bones) > MAX_BONE_COUNT: - raise RuntimeError(f'Number of bones ({len(psk.bones)}) exceeds limit of {MAX_BONE_COUNT}') - elif len(psk.bones) == 0: - raise RuntimeError(f'At least one bone must be marked for export') - - with open(path, 'wb') as fp: - _write_section(fp, b'ACTRHEAD') - _write_section(fp, b'PNTS0000', Vector3, psk.points) - - wedges = [] - for index, w in enumerate(psk.wedges): - 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) - - _write_section(fp, b'VTXW0000', Psk.Wedge16, wedges) - _write_section(fp, b'FACE0000', Psk.Face, psk.faces) - _write_section(fp, b'MATT0000', Psk.Material, psk.materials) - _write_section(fp, b'REFSKELT', Psk.Bone, psk.bones) - _write_section(fp, b'RAWWEIGHTS', Psk.Weight, psk.weights) +from ..builder import build_psk, PskBuildOptions, get_psk_input_objects +from ..writer import write_psk +from ...helpers import populate_bone_group_list def is_bone_filter_mode_item_available(context, identifier): @@ -69,21 +17,6 @@ def is_bone_filter_mode_item_available(context, identifier): return True -class PSK_UL_MaterialList(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - row = layout.row() - row.label(text=str(getattr(item, 'material_name')), icon='MATERIAL') - - -class MaterialListItem(PropertyGroup): - material_name: StringProperty() - index: IntProperty() - - @property - def name(self): - return self.material_name - - def populate_material_list(mesh_objects, material_list): material_list.clear() @@ -102,7 +35,7 @@ def populate_material_list(mesh_objects, material_list): m.index = index -class PskMaterialListItemMoveUp(Operator): +class PSK_OT_material_list_move_up(Operator): bl_idname = 'psk_export.material_list_item_move_up' bl_label = 'Move Up' bl_options = {'INTERNAL'} @@ -120,7 +53,7 @@ class PskMaterialListItemMoveUp(Operator): return {"FINISHED"} -class PskMaterialListItemMoveDown(Operator): +class PSK_OT_material_list_move_down(Operator): bl_idname = 'psk_export.material_list_item_move_down' bl_label = 'Move Down' bl_options = {'INTERNAL'} @@ -138,7 +71,7 @@ class PskMaterialListItemMoveDown(Operator): return {"FINISHED"} -class PskExportOperator(Operator, ExportHelper): +class PSK_OT_export(Operator, ExportHelper): bl_idname = 'export.psk' bl_label = 'Export' bl_options = {'INTERNAL', 'UNDO'} @@ -187,12 +120,16 @@ class PskExportOperator(Operator, ExportHelper): layout = self.layout pg = getattr(context.scene, 'psk_export') - layout.prop(pg, 'use_raw_mesh_data') + # MESH + box = layout.box() + box.label(text='Mesh', icon='MESH_DATA') + box.prop(pg, 'use_raw_mesh_data') # BONES - layout.label(text='Bones', icon='BONE_DATA') + box = layout.box() + box.label(text='Bones', icon='BONE_DATA') bone_filter_mode_items = pg.bl_rna.properties['bone_filter_mode'].enum_items_static - row = layout.row(align=True) + row = box.row(align=True) for item in bone_filter_mode_items: identifier = item.identifier item_layout = row.row(align=True) @@ -200,24 +137,21 @@ class PskExportOperator(Operator, ExportHelper): item_layout.enabled = is_bone_filter_mode_item_available(context, identifier) if pg.bone_filter_mode == 'BONE_GROUPS': - row = layout.row() + row = box.row() rows = max(3, min(len(pg.bone_group_list), 10)) row.template_list('PSX_UL_bone_group_list', '', pg, 'bone_group_list', pg, 'bone_group_list_index', rows=rows) - layout.separator() + box.prop(pg, 'should_enforce_bone_name_restrictions') # MATERIALS - layout.label(text='Materials', icon='MATERIAL') - row = layout.row() + box = layout.box() + box.label(text='Materials', icon='MATERIAL') + row = box.row() rows = max(3, min(len(pg.bone_group_list), 10)) - row.template_list('PSK_UL_MaterialList', '', pg, 'material_list', pg, 'material_list_index', rows=rows) + row.template_list('PSK_UL_materials', '', pg, 'material_list', pg, 'material_list_index', rows=rows) col = row.column(align=True) - col.operator(PskMaterialListItemMoveUp.bl_idname, text='', icon='TRIA_UP') - col.operator(PskMaterialListItemMoveDown.bl_idname, text='', icon='TRIA_DOWN') - - layout.separator() - - layout.prop(pg, 'should_ignore_bone_name_restrictions') + col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP') + col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN') def execute(self, context): pg = context.scene.psk_export @@ -226,11 +160,11 @@ class PskExportOperator(Operator, ExportHelper): options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected] options.use_raw_mesh_data = pg.use_raw_mesh_data options.material_names = [m.material_name for m in pg.material_list] - options.should_ignore_bone_name_restrictions = pg.should_ignore_bone_name_restrictions + options.should_enforce_bone_name_restrictions = pg.should_enforce_bone_name_restrictions try: psk = build_psk(context, options) - export_psk(psk, self.filepath) + write_psk(psk, self.filepath) self.report({'INFO'}, f'PSK export successful') except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) @@ -238,35 +172,8 @@ class PskExportOperator(Operator, ExportHelper): return {'FINISHED'} -class PskExportPropertyGroup(PropertyGroup): - bone_filter_mode: EnumProperty( - name='Bone Filter', - options=set(), - description='', - items=( - ('ALL', 'All', 'All bones will be exported.'), - ('BONE_GROUPS', 'Bone Groups', - 'Only bones belonging to the selected bone groups and their ancestors will be exported.') - ) - ) - bone_group_list: CollectionProperty(type=PSX_PG_bone_group_list_item) - bone_group_list_index: IntProperty(default=0) - use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh') - material_list: CollectionProperty(type=MaterialListItem) - material_list_index: IntProperty(default=0) - should_ignore_bone_name_restrictions: BoolProperty( - default=False, - name='Ignore Bone Name Restrictions', - description='Bone names restrictions will be ignored. Note that bone names without properly formatted names ' - 'cannot be referenced in scripts' - ) - - classes = ( - MaterialListItem, - PSK_UL_MaterialList, - PskMaterialListItemMoveUp, - PskMaterialListItemMoveDown, - PskExportOperator, - PskExportPropertyGroup, + PSK_OT_material_list_move_up, + PSK_OT_material_list_move_down, + PSK_OT_export, ) diff --git a/io_scene_psk_psa/psk/export/properties.py b/io_scene_psk_psa/psk/export/properties.py new file mode 100644 index 0000000..ba5f1f6 --- /dev/null +++ b/io_scene_psk_psa/psk/export/properties.py @@ -0,0 +1,39 @@ +from bpy.props import EnumProperty, CollectionProperty, IntProperty, BoolProperty, StringProperty +from bpy.types import PropertyGroup + +from ...types import PSX_PG_bone_group_list_item + + +class PSK_PG_material_list_item(PropertyGroup): + material_name: StringProperty() + index: IntProperty() + + +class PSK_PG_export(PropertyGroup): + bone_filter_mode: EnumProperty( + name='Bone Filter', + options=set(), + description='', + items=( + ('ALL', 'All', 'All bones will be exported'), + ('BONE_GROUPS', 'Bone Groups', + 'Only bones belonging to the selected bone groups and their ancestors will be exported') + ) + ) + bone_group_list: CollectionProperty(type=PSX_PG_bone_group_list_item) + bone_group_list_index: IntProperty(default=0) + use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh') + material_list: CollectionProperty(type=PSK_PG_material_list_item) + material_list_index: IntProperty(default=0) + should_enforce_bone_name_restrictions: BoolProperty( + default=False, + name='Enforce Bone Name Restrictions', + description='Enforce that bone names must only contain letters, numbers, spaces, hyphens and underscores.\n\n' + 'Depending on the engine, improper bone names might not be referenced correctly by scripts' + ) + + +classes = ( + PSK_PG_material_list_item, + PSK_PG_export, +) diff --git a/io_scene_psk_psa/psk/export/ui.py b/io_scene_psk_psa/psk/export/ui.py new file mode 100644 index 0000000..6a7a056 --- /dev/null +++ b/io_scene_psk_psa/psk/export/ui.py @@ -0,0 +1,12 @@ +from bpy.types import UIList + + +class PSK_UL_materials(UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + row = layout.row() + row.label(text=str(getattr(item, 'material_name')), icon='MATERIAL') + + +classes = ( + PSK_UL_materials, +) diff --git a/io_scene_psk_psa/psk/import_/__init__.py b/io_scene_psk_psa/psk/import_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/io_scene_psk_psa/psk/import_/operators.py b/io_scene_psk_psa/psk/import_/operators.py new file mode 100644 index 0000000..d963b52 --- /dev/null +++ b/io_scene_psk_psa/psk/import_/operators.py @@ -0,0 +1,143 @@ +import os +import sys + +from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty +from bpy.types import Operator +from bpy_extras.io_utils import ImportHelper + +from ..reader import read_psk + +empty_set = set() + + +class PSK_OT_import(Operator, ImportHelper): + bl_idname = 'import_scene.psk' + bl_label = 'Import' + bl_options = {'INTERNAL', 'UNDO', 'PRESET'} + __doc__ = 'Load a PSK file' + filename_ext = '.psk' + filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'}) + filepath: StringProperty( + name='File Path', + description='File path used for exporting the PSK file', + maxlen=1024, + default='') + + should_import_vertex_colors: BoolProperty( + default=True, + options=empty_set, + name='Vertex Colors', + description='Import vertex colors from PSKX files, if available' + ) + vertex_color_space: EnumProperty( + name='Vertex Color Space', + options=empty_set, + description='The source vertex color space', + default='SRGBA', + items=( + ('LINEAR', 'Linear', ''), + ('SRGBA', 'sRGBA', ''), + ) + ) + should_import_vertex_normals: BoolProperty( + default=True, + name='Vertex Normals', + options=empty_set, + description='Import vertex normals, if available' + ) + should_import_extra_uvs: BoolProperty( + default=True, + name='Extra UVs', + options=empty_set, + description='Import extra UV maps, if available' + ) + should_import_mesh: BoolProperty( + default=True, + name='Import Mesh', + options=empty_set, + description='Import mesh' + ) + should_import_materials: BoolProperty( + default=True, + name='Import Materials', + options=empty_set, + ) + should_reuse_materials: BoolProperty( + default=True, + name='Reuse Materials', + options=empty_set, + description='Existing materials with matching names will be reused when available' + ) + should_import_skeleton: BoolProperty( + default=True, + name='Import Skeleton', + options=empty_set, + description='Import skeleton' + ) + bone_length: FloatProperty( + default=1.0, + min=sys.float_info.epsilon, + step=100, + soft_min=1.0, + name='Bone Length', + options=empty_set, + description='Length of the bones' + ) + should_import_shape_keys: BoolProperty( + default=True, + name='Shape Keys', + options=empty_set, + description='Import shape keys, if available' + ) + + def execute(self, context): + psk = read_psk(self.filepath) + + options = PskImportOptions() + options.name = os.path.splitext(os.path.basename(self.filepath))[0] + options.should_import_mesh = self.should_import_mesh + options.should_import_extra_uvs = self.should_import_extra_uvs + options.should_import_vertex_colors = self.should_import_vertex_colors + options.should_import_vertex_normals = self.should_import_vertex_normals + options.vertex_color_space = self.vertex_color_space + options.should_import_skeleton = self.should_import_skeleton + options.bone_length = self.bone_length + options.should_import_materials = self.should_import_materials + options.should_import_shape_keys = self.should_import_shape_keys + + result = import_psk(psk, context, options) + + if len(result.warnings): + message = f'PSK imported with {len(result.warnings)} warning(s)\n' + message += '\n'.join(result.warnings) + self.report({'WARNING'}, message) + else: + self.report({'INFO'}, f'PSK imported') + + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.prop(self, 'should_import_materials') + layout.prop(self, 'should_import_mesh') + row = layout.column() + row.use_property_split = True + row.use_property_decorate = False + if self.should_import_mesh: + row.prop(self, 'should_import_vertex_normals') + row.prop(self, 'should_import_extra_uvs') + row.prop(self, 'should_import_vertex_colors') + if self.should_import_vertex_colors: + row.prop(self, 'vertex_color_space') + row.prop(self, 'should_import_shape_keys') + layout.prop(self, 'should_import_skeleton') + row = layout.column() + row.use_property_split = True + row.use_property_decorate = False + if self.should_import_skeleton: + row.prop(self, 'bone_length') + + +classes = ( + PSK_OT_import, +) diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index 8201d49..2e44dd7 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -1,18 +1,13 @@ -import os -import sys from math import inf from typing import Optional, List import bmesh import bpy import numpy as np -from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty -from bpy.types import Operator, VertexGroup -from bpy_extras.io_utils import ImportHelper +from bpy.types import VertexGroup from mathutils import Quaternion, Vector, Matrix from .data import Psk -from .reader import read_psk from ..helpers import rgb_to_srgb, is_bdk_addon_loaded @@ -277,139 +272,3 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult: pass return result - - -empty_set = set() - - -class PskImportOperator(Operator, ImportHelper): - bl_idname = 'import_scene.psk' - bl_label = 'Import' - bl_options = {'INTERNAL', 'UNDO', 'PRESET'} - __doc__ = 'Load a PSK file' - filename_ext = '.psk' - filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'}) - filepath: StringProperty( - name='File Path', - description='File path used for exporting the PSK file', - maxlen=1024, - default='') - - should_import_vertex_colors: BoolProperty( - default=True, - options=empty_set, - name='Vertex Colors', - description='Import vertex colors from PSKX files, if available' - ) - vertex_color_space: EnumProperty( - name='Vertex Color Space', - options=empty_set, - description='The source vertex color space', - default='SRGBA', - items=( - ('LINEAR', 'Linear', ''), - ('SRGBA', 'sRGBA', ''), - ) - ) - should_import_vertex_normals: BoolProperty( - default=True, - name='Vertex Normals', - options=empty_set, - description='Import vertex normals, if available' - ) - should_import_extra_uvs: BoolProperty( - default=True, - name='Extra UVs', - options=empty_set, - description='Import extra UV maps, if available' - ) - should_import_mesh: BoolProperty( - default=True, - name='Import Mesh', - options=empty_set, - description='Import mesh' - ) - should_import_materials: BoolProperty( - default=True, - name='Import Materials', - options=empty_set, - ) - should_reuse_materials: BoolProperty( - default=True, - name='Reuse Materials', - options=empty_set, - description='Existing materials with matching names will be reused when available' - ) - should_import_skeleton: BoolProperty( - default=True, - name='Import Skeleton', - options=empty_set, - description='Import skeleton' - ) - bone_length: FloatProperty( - default=1.0, - min=sys.float_info.epsilon, - step=100, - soft_min=1.0, - name='Bone Length', - options=empty_set, - description='Length of the bones' - ) - should_import_shape_keys: BoolProperty( - default=True, - name='Shape Keys', - options=empty_set, - description='Import shape keys, if available' - ) - - def execute(self, context): - psk = read_psk(self.filepath) - - options = PskImportOptions() - options.name = os.path.splitext(os.path.basename(self.filepath))[0] - options.should_import_mesh = self.should_import_mesh - options.should_import_extra_uvs = self.should_import_extra_uvs - options.should_import_vertex_colors = self.should_import_vertex_colors - options.should_import_vertex_normals = self.should_import_vertex_normals - options.vertex_color_space = self.vertex_color_space - options.should_import_skeleton = self.should_import_skeleton - options.bone_length = self.bone_length - options.should_import_materials = self.should_import_materials - options.should_import_shape_keys = self.should_import_shape_keys - - result = import_psk(psk, context, options) - - if len(result.warnings): - message = f'PSK imported with {len(result.warnings)} warning(s)\n' - message += '\n'.join(result.warnings) - self.report({'WARNING'}, message) - else: - self.report({'INFO'}, f'PSK imported') - - return {'FINISHED'} - - def draw(self, context): - layout = self.layout - layout.prop(self, 'should_import_materials') - layout.prop(self, 'should_import_mesh') - row = layout.column() - row.use_property_split = True - row.use_property_decorate = False - if self.should_import_mesh: - row.prop(self, 'should_import_vertex_normals') - row.prop(self, 'should_import_extra_uvs') - row.prop(self, 'should_import_vertex_colors') - if self.should_import_vertex_colors: - row.prop(self, 'vertex_color_space') - row.prop(self, 'should_import_shape_keys') - layout.prop(self, 'should_import_skeleton') - row = layout.column() - row.use_property_split = True - row.use_property_decorate = False - if self.should_import_skeleton: - row.prop(self, 'bone_length') - - -classes = ( - PskImportOperator, -) diff --git a/io_scene_psk_psa/psk/writer.py b/io_scene_psk_psa/psk/writer.py new file mode 100644 index 0000000..eec3234 --- /dev/null +++ b/io_scene_psk_psa/psk/writer.py @@ -0,0 +1,54 @@ +from ctypes import Structure, sizeof +from typing import Type + +from .data import Psk +from ..data import Section, Vector3 + +MAX_WEDGE_COUNT = 65536 +MAX_POINT_COUNT = 4294967296 +MAX_BONE_COUNT = 256 +MAX_MATERIAL_COUNT = 256 + + +def _write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None): + section = Section() + section.name = name + if data_type is not None and data is not None: + section.data_size = sizeof(data_type) + section.data_count = len(data) + fp.write(section) + if data is not None: + for datum in data: + fp.write(datum) + + +def write_psk(psk: Psk, path: str): + if len(psk.wedges) > MAX_WEDGE_COUNT: + raise RuntimeError(f'Number of wedges ({len(psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}') + if len(psk.points) > MAX_POINT_COUNT: + raise RuntimeError(f'Numbers of vertices ({len(psk.points)}) exceeds limit of {MAX_POINT_COUNT}') + if len(psk.materials) > MAX_MATERIAL_COUNT: + raise RuntimeError(f'Number of materials ({len(psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}') + if len(psk.bones) > MAX_BONE_COUNT: + raise RuntimeError(f'Number of bones ({len(psk.bones)}) exceeds limit of {MAX_BONE_COUNT}') + elif len(psk.bones) == 0: + raise RuntimeError(f'At least one bone must be marked for export') + + with open(path, 'wb') as fp: + _write_section(fp, b'ACTRHEAD') + _write_section(fp, b'PNTS0000', Vector3, psk.points) + + wedges = [] + for index, w in enumerate(psk.wedges): + 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) + + _write_section(fp, b'VTXW0000', Psk.Wedge16, wedges) + _write_section(fp, b'FACE0000', Psk.Face, psk.faces) + _write_section(fp, b'MATT0000', Psk.Material, psk.materials) + _write_section(fp, b'REFSKELT', Psk.Bone, psk.bones) + _write_section(fp, b'RAWWEIGHTS', Psk.Weight, psk.weights) diff --git a/io_scene_psk_psa/types.py b/io_scene_psk_psa/types.py index e1cac91..26688b0 100644 --- a/io_scene_psk_psa/types.py +++ b/io_scene_psk_psa/types.py @@ -18,13 +18,13 @@ class PSX_PG_bone_group_list_item(PropertyGroup): is_selected: BoolProperty(default=False) -class PSX_PG_ActionExportPropertyGroup(PropertyGroup): +class PSX_PG_action_export(PropertyGroup): compression_ratio: FloatProperty(name='Compression Ratio', default=1.0, min=0.0, max=1.0, subtype='FACTOR', description='The key sampling ratio of the exported sequence.\n\nA compression ratio of 1.0 will export all frames, while a compression ratio of 0.5 will export half of the frames') key_quota: IntProperty(name='Key Quota', default=0, min=1, description='The minimum number of frames to be exported') -class PSX_PT_ActionPropertyPanel(Panel): - bl_idname = 'PSX_PT_ActionPropertyPanel' +class PSX_PT_action(Panel): + bl_idname = 'PSX_PT_action' bl_label = 'PSA Export' bl_space_type = 'DOPESHEET_EDITOR' bl_region_type = 'UI' @@ -43,8 +43,8 @@ class PSX_PT_ActionPropertyPanel(Panel): classes = ( - PSX_PG_ActionExportPropertyGroup, + PSX_PG_action_export, PSX_PG_bone_group_list_item, PSX_UL_bone_group_list, - PSX_PT_ActionPropertyPanel + PSX_PT_action )