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
This commit is contained in:
Colin Basnett
2023-07-29 20:51:23 -07:00
parent 782c210f04
commit afe598f671
16 changed files with 323 additions and 286 deletions

View File

@@ -18,10 +18,14 @@ if 'bpy' in locals():
importlib.reload(psx_types) importlib.reload(psx_types)
importlib.reload(psk_data) importlib.reload(psk_data)
importlib.reload(psk_builder)
importlib.reload(psk_exporter)
importlib.reload(psk_importer)
importlib.reload(psk_reader) 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_data)
importlib.reload(psa_reader) importlib.reload(psa_reader)
@@ -39,10 +43,14 @@ else:
from . import helpers as psx_helpers from . import helpers as psx_helpers
from . import types as psx_types from . import types as psx_types
from .psk import data as psk_data 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 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 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 data as psa_data
from .psa import reader as psa_reader from .psa import reader as psa_reader
@@ -60,8 +68,10 @@ import bpy
from bpy.props import PointerProperty from bpy.props import PointerProperty
classes = psx_types.classes +\ classes = psx_types.classes +\
psk_importer.classes +\ psk_import_operators.classes +\
psk_exporter.classes +\ psk_export_properties.classes +\
psk_export_operators.classes +\
psk_export_ui.classes + \
psa_export_properties.classes +\ psa_export_properties.classes +\
psa_export_operators.classes +\ psa_export_operators.classes +\
psa_export_ui.classes + \ psa_export_ui.classes + \
@@ -71,11 +81,11 @@ classes = psx_types.classes +\
def psk_export_menu_func(self, context): 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): 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): 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.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_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.psa_export = PointerProperty(type=psa_export_properties.PSA_PG_export)
bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup) bpy.types.Scene.psk_export = PointerProperty(type=psk_export_properties.PSK_PG_export)
bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_ActionExportPropertyGroup) bpy.types.Action.psa_export = PointerProperty(type=psx_types.PSX_PG_action_export)
def unregister(): def unregister():

View File

@@ -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] invalid_bone_names = [x for x in bone_names if pattern.match(x) is None]
if len(invalid_bone_names) > 0: if len(invalid_bone_names) > 0:
raise RuntimeError(f'The following bone names are invalid: {invalid_bone_names}.\n' 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]: def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_group_indices: List[int]) -> List[str]:

View File

@@ -27,7 +27,7 @@ class PsaBuildOptions:
self.sequences: List[PsaBuildSequence] = [] self.sequences: List[PsaBuildSequence] = []
self.bone_filter_mode: str = 'ALL' self.bone_filter_mode: str = 'ALL'
self.bone_group_indices: List[int] = [] 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_prefix: str = ''
self.sequence_name_suffix: str = '' self.sequence_name_suffix: str = ''
self.root_motion: bool = False self.root_motion: bool = False

View File

@@ -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', layout.template_list('PSX_UL_bone_group_list', '', pg, 'bone_group_list', pg, 'bone_group_list_index',
rows=rows) rows=rows)
layout.prop(pg, 'should_ignore_bone_name_restrictions') layout.prop(pg, 'should_enforce_bone_name_restrictions')
layout.separator() layout.separator()
@@ -397,7 +397,7 @@ class PSA_OT_export(Operator, ExportHelper):
options.sequences = export_sequences options.sequences = export_sequences
options.bone_filter_mode = pg.bone_filter_mode 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.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_prefix = pg.sequence_name_prefix
options.sequence_name_suffix = pg.sequence_name_suffix options.sequence_name_suffix = pg.sequence_name_suffix
options.root_motion = pg.root_motion options.root_motion = pg.root_motion

View File

@@ -116,10 +116,16 @@ class PSA_PG_export(PropertyGroup):
options=empty_set, options=empty_set,
description='Show actions that belong to an asset library') description='Show actions that belong to an asset library')
sequence_filter_pose_marker: BoolProperty( sequence_filter_pose_marker: BoolProperty(
default=False, default=True,
name='Show pose markers', name='Show pose markers',
options=empty_set) options=empty_set)
sequence_use_filter_sort_reverse: BoolProperty(default=True, 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]: 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: if hasattr(sequence, 'is_pose_marker') and sequence.is_pose_marker:
flt_flags[i] &= ~bitflag_filter_item 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 return flt_flags

View File

@@ -38,6 +38,7 @@ class PSA_UL_export_sequences(UIList):
subrow = row.row(align=True) subrow = row.row(align=True)
subrow.prop(pg, 'sequence_filter_asset', icon_only=True, icon='ASSET_MANAGER') 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_pose_marker', icon_only=True, icon='PMARKER')
subrow.prop(pg, 'sequence_filter_reversed', text="", icon='FRAME_PREV')
def filter_items(self, context, data, prop): def filter_items(self, context, data, prop):
pg = getattr(context.scene, 'psa_export') pg = getattr(context.scene, 'psa_export')

View File

@@ -18,7 +18,7 @@ class PskBuildOptions(object):
self.bone_group_indices: List[int] = [] self.bone_group_indices: List[int] = []
self.use_raw_mesh_data = True self.use_raw_mesh_data = True
self.material_names: List[str] = [] 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: 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] bones = [armature_data.bones[bone_name] for bone_name in bone_names]
# Check that all bone names are valid. # 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)) check_bone_names(map(lambda x: x.name, bones))
for bone in bones: for bone in bones:

View File

View File

@@ -1,62 +1,10 @@
from typing import Type from bpy.props import StringProperty
from bpy.types import Operator
from bpy.props import BoolProperty, StringProperty, CollectionProperty, IntProperty, EnumProperty
from bpy.types import Operator, PropertyGroup, UIList
from bpy_extras.io_utils import ExportHelper from bpy_extras.io_utils import ExportHelper
from .builder import build_psk, PskBuildOptions, get_psk_input_objects from ..builder import build_psk, PskBuildOptions, get_psk_input_objects
from .data import * from ..writer import write_psk
from ..helpers import populate_bone_group_list 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)
def is_bone_filter_mode_item_available(context, identifier): def is_bone_filter_mode_item_available(context, identifier):
@@ -69,21 +17,6 @@ def is_bone_filter_mode_item_available(context, identifier):
return True 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): def populate_material_list(mesh_objects, material_list):
material_list.clear() material_list.clear()
@@ -102,7 +35,7 @@ def populate_material_list(mesh_objects, material_list):
m.index = index m.index = index
class PskMaterialListItemMoveUp(Operator): class PSK_OT_material_list_move_up(Operator):
bl_idname = 'psk_export.material_list_item_move_up' bl_idname = 'psk_export.material_list_item_move_up'
bl_label = 'Move Up' bl_label = 'Move Up'
bl_options = {'INTERNAL'} bl_options = {'INTERNAL'}
@@ -120,7 +53,7 @@ class PskMaterialListItemMoveUp(Operator):
return {"FINISHED"} return {"FINISHED"}
class PskMaterialListItemMoveDown(Operator): class PSK_OT_material_list_move_down(Operator):
bl_idname = 'psk_export.material_list_item_move_down' bl_idname = 'psk_export.material_list_item_move_down'
bl_label = 'Move Down' bl_label = 'Move Down'
bl_options = {'INTERNAL'} bl_options = {'INTERNAL'}
@@ -138,7 +71,7 @@ class PskMaterialListItemMoveDown(Operator):
return {"FINISHED"} return {"FINISHED"}
class PskExportOperator(Operator, ExportHelper): class PSK_OT_export(Operator, ExportHelper):
bl_idname = 'export.psk' bl_idname = 'export.psk'
bl_label = 'Export' bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'} bl_options = {'INTERNAL', 'UNDO'}
@@ -187,12 +120,16 @@ class PskExportOperator(Operator, ExportHelper):
layout = self.layout layout = self.layout
pg = getattr(context.scene, 'psk_export') 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 # 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 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: for item in bone_filter_mode_items:
identifier = item.identifier identifier = item.identifier
item_layout = row.row(align=True) 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) item_layout.enabled = is_bone_filter_mode_item_available(context, identifier)
if pg.bone_filter_mode == 'BONE_GROUPS': if pg.bone_filter_mode == 'BONE_GROUPS':
row = layout.row() row = box.row()
rows = max(3, min(len(pg.bone_group_list), 10)) 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) 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 # MATERIALS
layout.label(text='Materials', icon='MATERIAL') box = layout.box()
row = layout.row() box.label(text='Materials', icon='MATERIAL')
row = box.row()
rows = max(3, min(len(pg.bone_group_list), 10)) 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 = row.column(align=True)
col.operator(PskMaterialListItemMoveUp.bl_idname, text='', icon='TRIA_UP') col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP')
col.operator(PskMaterialListItemMoveDown.bl_idname, text='', icon='TRIA_DOWN') col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
layout.separator()
layout.prop(pg, 'should_ignore_bone_name_restrictions')
def execute(self, context): def execute(self, context):
pg = context.scene.psk_export 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.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.use_raw_mesh_data = pg.use_raw_mesh_data
options.material_names = [m.material_name for m in pg.material_list] 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: try:
psk = build_psk(context, options) psk = build_psk(context, options)
export_psk(psk, self.filepath) write_psk(psk, self.filepath)
self.report({'INFO'}, f'PSK export successful') self.report({'INFO'}, f'PSK export successful')
except RuntimeError as e: except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e)) self.report({'ERROR_INVALID_CONTEXT'}, str(e))
@@ -238,35 +172,8 @@ class PskExportOperator(Operator, ExportHelper):
return {'FINISHED'} 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 = ( classes = (
MaterialListItem, PSK_OT_material_list_move_up,
PSK_UL_MaterialList, PSK_OT_material_list_move_down,
PskMaterialListItemMoveUp, PSK_OT_export,
PskMaterialListItemMoveDown,
PskExportOperator,
PskExportPropertyGroup,
) )

View File

@@ -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,
)

View File

@@ -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,
)

View File

View File

@@ -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,
)

View File

@@ -1,18 +1,13 @@
import os
import sys
from math import inf from math import inf
from typing import Optional, List from typing import Optional, List
import bmesh import bmesh
import bpy import bpy
import numpy as np import numpy as np
from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty from bpy.types import VertexGroup
from bpy.types import Operator, VertexGroup
from bpy_extras.io_utils import ImportHelper
from mathutils import Quaternion, Vector, Matrix from mathutils import Quaternion, Vector, Matrix
from .data import Psk from .data import Psk
from .reader import read_psk
from ..helpers import rgb_to_srgb, is_bdk_addon_loaded from ..helpers import rgb_to_srgb, is_bdk_addon_loaded
@@ -277,139 +272,3 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
pass pass
return result 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,
)

View File

@@ -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)

View File

@@ -18,13 +18,13 @@ class PSX_PG_bone_group_list_item(PropertyGroup):
is_selected: BoolProperty(default=False) 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') 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') key_quota: IntProperty(name='Key Quota', default=0, min=1, description='The minimum number of frames to be exported')
class PSX_PT_ActionPropertyPanel(Panel): class PSX_PT_action(Panel):
bl_idname = 'PSX_PT_ActionPropertyPanel' bl_idname = 'PSX_PT_action'
bl_label = 'PSA Export' bl_label = 'PSA Export'
bl_space_type = 'DOPESHEET_EDITOR' bl_space_type = 'DOPESHEET_EDITOR'
bl_region_type = 'UI' bl_region_type = 'UI'
@@ -43,8 +43,8 @@ class PSX_PT_ActionPropertyPanel(Panel):
classes = ( classes = (
PSX_PG_ActionExportPropertyGroup, PSX_PG_action_export,
PSX_PG_bone_group_list_item, PSX_PG_bone_group_list_item,
PSX_UL_bone_group_list, PSX_UL_bone_group_list,
PSX_PT_ActionPropertyPanel PSX_PT_action
) )