Compare commits

..

4 Commits

Author SHA1 Message Date
Colin Basnett
c0ef2f7ce2 Added missing psk_psa_py wheel 2026-02-17 01:02:29 -08:00
Dark Nation
c38773002d Update README.md 2026-02-17 08:56:34 +00:00
Colin Basnett
d178de893f Removed root_bone_name in more places 2026-02-16 19:31:06 -08:00
Colin Basnett
a34570fc1a Removed "Root Bone Name" property since it is no longer used 2026-02-16 19:29:52 -08:00
9 changed files with 27 additions and 68 deletions

View File

@@ -13,37 +13,43 @@ For Blender 4.2 and higher, download the latest version from the [Blender Extens
For Blender 4.1 and lower, see [Legacy Compatibility](#legacy-compatibility). For Blender 4.1 and lower, see [Legacy Compatibility](#legacy-compatibility).
# Features # Features
* Full PSK/PSA import and export capabilities. * [Bone collections](https://docs.blender.org/manual/en/latest/animation/armatures/bones/bone_collections.html#bone-collections) can be excluded from PSK/PSA export (useful for excluding non-contributing bones such as IK controllers).
* Non-standard file section data (.pskx) is supported for import only (vertex normals, extra UV channels, vertex colors, shape keys). * [Collection Exporters](https://docs.blender.org/manual/en/latest/scene_layout/collections/collections.html#exporters) for reliable, repeatable export workflow.
* Non-standard model data (.pskx) is supported for import only (vertex normals, extra UV channels, vertex colors, shape keys).
* Manual re-ordering of material slots on export.
* Non-standard animation data is supported for import only (scale keys).
* Fine-grained PSA sequence importing for efficient workflow when working with large PSA files. * Fine-grained PSA sequence importing for efficient workflow when working with large PSA files.
* PSA sequence metadata (e.g., frame rate) is preserved on import, allowing this data to be reused on export. * PSA sequence metadata (e.g., frame rate) is preserved on import, allowing this data to be reused on export.
* [Bone collections](https://docs.blender.org/manual/en/latest/animation/armatures/bones/bone_collections.html#bone-collections) can be excluded from PSK/PSA export (useful for excluding non-contributing bones such as IK controllers).
* PSA sequences can be exported directly from actions or delineated using a scene's [timeline markers](https://docs.blender.org/manual/en/latest/animation/markers.html), pose markers, or NLA track strips, allowing direct use of the [NLA](https://docs.blender.org/manual/en/latest/editors/nla/index.html) when creating sequences. * PSA sequences can be exported directly from actions or delineated using a scene's [timeline markers](https://docs.blender.org/manual/en/latest/animation/markers.html), pose markers, or NLA track strips, allowing direct use of the [NLA](https://docs.blender.org/manual/en/latest/editors/nla/index.html) when creating sequences.
* Manual re-ordering of material slots. * Compress exported sequences via resampling ratios or frame quotas.
* Multiple armature objects can be exported to a single PSK or PSA file, allowing seamless use of [action slots](https://docs.blender.org/manual/en/latest/animation/actions.html#action-slots).
* Support for exporting instance collections.
# Usage # Usage
## Exporting a PSK
1. Select the mesh objects you wish to export.
2. Navigate to `File` > `Export` > `Unreal PSK (.psk)`.
3. Enter the file name and click `Export`.
## Importing a PSK/PSKX ## Import
### Importing a PSK/PSKX
1. Navigate to `File` > `Import` > `Unreal PSK (.psk/.pskx)`. 1. Navigate to `File` > `Import` > `Unreal PSK (.psk/.pskx)`.
2. Select the PSK file you want to import and click `Import`. 2. Select the PSK file you want to import and click `Import`.
## Exporting a PSA ### Importing a PSA
1. Select the armature objects you wish to export.
2. Navigate to `File` > `Export` > `Unreal PSA (.psa)`.
3. Enter the file name and click `Export`.
## Importing a PSA
1. Select an armature that you want import animations for. 1. Select an armature that you want import animations for.
2. Navigate to `File` > `Import` > `Unreal PSA (.psa)`. 2. Navigate to `File` > `Import` > `Unreal PSA (.psa)`.
3. Select the PSA file you want to import. 3. Select the PSA file you want to import.
4. Select the sequences that you want to import and click `Import`. 4. Select the sequences that you want to import and click `Import`.
## Export
It is highly recommended to use the provided [Collection Exporters](https://docs.blender.org/manual/en/latest/scene_layout/collections/collections.html#exporters) workflow, since it allows for highly reliable, repeatable exports of both PSK and PSA files. However, the traditional export workflow is available as well.
### Exporting a PSK
1. Select the mesh objects you wish to export.
2. Navigate to `File` > `Export` > `Unreal PSK (.psk)`.
3. Enter the file name and click `Export`.
### Exporting a PSA
1. Select the armature objects you wish to export.
2. Navigate to `File` > `Export` > `Unreal PSA (.psa)`.
3. Enter the file name and click `Export`.
> Note that in order to see the imported actions applied to your armature, you must use the [Dope Sheet](https://docs.blender.org/manual/en/latest/editors/dope_sheet/introduction.html) or [Nonlinear Animation](https://docs.blender.org/manual/en/latest/editors/nla/introduction.html) editors. > Note that in order to see the imported actions applied to your armature, you must use the [Dope Sheet](https://docs.blender.org/manual/en/latest/editors/dope_sheet/introduction.html) or [Nonlinear Animation](https://docs.blender.org/manual/en/latest/editors/nla/introduction.html) editors.
# FAQ # FAQ

View File

@@ -39,7 +39,6 @@ class PsaBuildOptions:
self.export_space = 'WORLD' self.export_space = 'WORLD'
self.forward_axis = 'X' self.forward_axis = 'X'
self.up_axis = 'Z' self.up_axis = 'Z'
self.root_bone_name = 'ROOT'
self.sequence_source = 'ACTIONS' # One of ('ACTIONS', 'TIMELINE_MARKERS', 'NLA_STRIPS') self.sequence_source = 'ACTIONS' # One of ('ACTIONS', 'TIMELINE_MARKERS', 'NLA_STRIPS')
@property @property
@@ -162,7 +161,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
psx_bone_create_result = create_psx_bones( psx_bone_create_result = create_psx_bones(
armature_objects=armature_objects_for_bones, armature_objects=armature_objects_for_bones,
export_space=options.export_space, export_space=options.export_space,
root_bone_name=options.root_bone_name,
forward_axis=options.forward_axis, forward_axis=options.forward_axis,
up_axis=options.up_axis, up_axis=options.up_axis,
scale=options.scale, scale=options.scale,

View File

@@ -346,14 +346,6 @@ class PSA_OT_export_collection(Operator, ExportHelper, PsaExportMixin):
op = col.operator(PSK_OT_bone_collection_list_select_all.bl_idname, text='', icon='CHECKBOX_DEHLT') op = col.operator(PSK_OT_bone_collection_list_select_all.bl_idname, text='', icon='CHECKBOX_DEHLT')
op.is_selected = False op.is_selected = False
advanced_bones_header, advanced_bones_panel = bones_panel.panel('Advanced', default_closed=True)
advanced_bones_header.label(text='Advanced')
if advanced_bones_panel:
flow = advanced_bones_panel.grid_flow(row_major=True)
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(self, 'root_bone_name')
# Transform # Transform
transform_header, transform_panel = layout.panel('Transform', default_closed=False) transform_header, transform_panel = layout.panel('Transform', default_closed=False)
transform_header.label(text='Transform', icon='DRIVER_TRANSFORM') transform_header.label(text='Transform', icon='DRIVER_TRANSFORM')
@@ -565,7 +557,6 @@ def create_psa_export_options(context: Context, armature_objects: Sequence[Objec
options.scale = pg.scale options.scale = pg.scale
options.forward_axis = pg.forward_axis options.forward_axis = pg.forward_axis
options.up_axis = pg.up_axis options.up_axis = pg.up_axis
options.root_bone_name = pg.root_bone_name
options.sequence_source = pg.sequence_source options.sequence_source = pg.sequence_source
return options return options
@@ -626,14 +617,6 @@ class PSA_OT_export(Operator, ExportHelper):
rows=rows rows=rows
) )
bones_advanced_header, bones_advanced_panel = bones_panel.panel('Bones Advanced', default_closed=True)
bones_advanced_header.label(text='Advanced')
if bones_advanced_panel:
flow = bones_advanced_panel.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'root_bone_name', text='Root Bone Name')
# TRANSFORM # TRANSFORM
transform_header, transform_panel = layout.panel('Advanced', default_closed=False) transform_header, transform_panel = layout.panel('Advanced', default_closed=False)
transform_header.label(text='Transform', icon='DRIVER_TRANSFORM') transform_header.label(text='Transform', icon='DRIVER_TRANSFORM')

View File

@@ -31,7 +31,6 @@ class PskBuildOptions(object):
self.export_space = 'WORLD' self.export_space = 'WORLD'
self.forward_axis = 'X' self.forward_axis = 'X'
self.up_axis = 'Z' self.up_axis = 'Z'
self.root_bone_name = 'ROOT'
class PskBuildResult(object): class PskBuildResult(object):
@@ -99,7 +98,6 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
forward_axis=options.forward_axis, forward_axis=options.forward_axis,
up_axis=options.up_axis, up_axis=options.up_axis,
scale=options.scale, scale=options.scale,
root_bone_name=options.root_bone_name,
bone_filter_mode=options.bone_filter_mode, bone_filter_mode=options.bone_filter_mode,
bone_collection_indices=options.bone_collection_indices bone_collection_indices=options.bone_collection_indices
) )

View File

@@ -207,7 +207,6 @@ def get_psk_build_options_from_property_group(scene: Scene, pg: PskExportMixin)
options.export_space = pg.export_space options.export_space = pg.export_space
options.bone_filter_mode = pg.bone_filter_mode options.bone_filter_mode = pg.bone_filter_mode
options.bone_collection_indices = [PsxBoneCollection(x.armature_object_name, x.armature_data_name, x.index) for x in pg.bone_collection_list if x.is_selected] options.bone_collection_indices = [PsxBoneCollection(x.armature_object_name, x.armature_data_name, x.index) for x in pg.bone_collection_list if x.is_selected]
options.root_bone_name = pg.root_bone_name
options.material_order_mode = pg.material_order_mode options.material_order_mode = pg.material_order_mode
options.material_name_list = [x.material_name for x in pg.material_name_list] options.material_name_list = [x.material_name for x in pg.material_name_list]
@@ -309,14 +308,6 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
op = col.operator(PSK_OT_bone_collection_list_select_all.bl_idname, text='', icon='CHECKBOX_DEHLT') op = col.operator(PSK_OT_bone_collection_list_select_all.bl_idname, text='', icon='CHECKBOX_DEHLT')
op.is_selected = False op.is_selected = False
advanced_bones_header, advanced_bones_panel = bones_panel.panel('Advanced', default_closed=True)
advanced_bones_header.label(text='Advanced')
if advanced_bones_panel:
flow = advanced_bones_panel.grid_flow(row_major=True)
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(self, 'root_bone_name')
# Materials # Materials
materials_header, materials_panel = layout.panel('Materials', default_closed=False) materials_header, materials_panel = layout.panel('Materials', default_closed=False)
materials_header.label(text='Materials', icon='MATERIAL') materials_header.label(text='Materials', icon='MATERIAL')
@@ -429,13 +420,6 @@ class PSK_OT_export(Operator, ExportHelper):
row = bones_panel.row() row = bones_panel.row()
rows = max(3, min(len(pg.bone_collection_list), 10)) rows = max(3, min(len(pg.bone_collection_list), 10))
row.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', rows=rows) row.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', rows=rows)
bones_advanced_header, bones_advanced_panel = bones_panel.panel('Advanced', default_closed=True)
bones_advanced_header.label(text='Advanced')
if bones_advanced_panel:
flow = bones_advanced_panel.grid_flow(row_major=True)
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'root_bone_name')
# Materials # Materials
materials_header, materials_panel = layout.panel('Materials', default_closed=False) materials_header, materials_panel = layout.panel('Materials', default_closed=False)

View File

@@ -332,7 +332,6 @@ class ObjectTree:
def create_psx_bones( def create_psx_bones(
armature_objects: list[Object], armature_objects: list[Object],
export_space: str = 'WORLD', export_space: str = 'WORLD',
root_bone_name: str = 'ROOT',
forward_axis: str = 'X', forward_axis: str = 'X',
up_axis: str = 'Z', up_axis: str = 'Z',
scale: float = 1.0, scale: float = 1.0,
@@ -517,13 +516,10 @@ def create_psx_bones(
bone_name_counts = Counter(bone.psx_bone.name.decode('windows-1252').upper() for bone in bones) bone_name_counts = Counter(bone.psx_bone.name.decode('windows-1252').upper() for bone in bones)
for bone_name, count in bone_name_counts.items(): for bone_name, count in bone_name_counts.items():
if count > 1: if count > 1:
error_message = f'Found {count} bones with the name "{bone_name}". ' raise RuntimeError(
f'Found {count} bones with the name "{bone_name}". '
f'Bone names must be unique when compared case-insensitively.' f'Bone names must be unique when compared case-insensitively.'
)
if len(armature_objects) > 1 and bone_name == root_bone_name.upper():
error_message += f' This is the name of the automatically generated root bone. Consider changing this '
f''
raise RuntimeError(error_message)
# Apply the scale to the bone locations. # Apply the scale to the bone locations.
for bone in bones: for bone in bones:

View File

@@ -179,11 +179,6 @@ class PsxBoneExportMixin:
) )
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item) bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
bone_collection_list_index: IntProperty(default=0, name='', description='') bone_collection_list_index: IntProperty(default=0, name='', description='')
root_bone_name: StringProperty(
name='Root Bone Name',
description='The name of the root bone when exporting a PSK with either no armature or multiple armatures',
default='ROOT',
)
class PSX_PG_scene_export(PropertyGroup, TransformMixin): class PSX_PG_scene_export(PropertyGroup, TransformMixin):

View File

@@ -54,7 +54,6 @@ class PsxBoneExportMixin:
bone_filter_mode: str bone_filter_mode: str
bone_collection_list: BpyCollectionProperty[PSX_PG_bone_collection_list_item] bone_collection_list: BpyCollectionProperty[PSX_PG_bone_collection_list_item]
bone_collection_list_index: int bone_collection_list_index: int
root_bone_name: str
class PSX_PG_scene_export(TransformSourceMixin): class PSX_PG_scene_export(TransformSourceMixin):