Compare commits
16 Commits
psa-collec
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925fc01a01 | ||
|
|
3bb9e7d178 | ||
|
|
d5386cd870 | ||
|
|
65a8b62825 | ||
|
|
6c917faad1 | ||
|
|
ee05baf508 | ||
|
|
f613e50f3a | ||
|
|
be920d1c91 | ||
|
|
be45611657 | ||
|
|
9b0805279d | ||
|
|
2e217e2902 | ||
|
|
0cf6c8a36f | ||
|
|
c0ef2f7ce2 | ||
|
|
c38773002d | ||
|
|
d178de893f | ||
|
|
a34570fc1a |
16
.github/workflows/main.yml
vendored
16
.github/workflows/main.yml
vendored
@@ -16,11 +16,16 @@ jobs:
|
||||
env:
|
||||
ADDON_NAME: io_scene_psk_psa
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
persist-credentials: true
|
||||
- name: Checkout LFS objects
|
||||
run: git lfs checkout
|
||||
run: |
|
||||
git lfs install --local
|
||||
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
|
||||
git config --local --unset http.${{ github.server_url }}/.extraheader
|
||||
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
|
||||
git lfs pull
|
||||
- uses: SebRollen/toml-action@v1.2.0
|
||||
id: read_manifest
|
||||
with:
|
||||
@@ -38,8 +43,7 @@ jobs:
|
||||
sudo apt-get install python3 -y
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install virtualenv
|
||||
sudo apt-get install python3-virtualenv -y
|
||||
python3 -m virtualenv venv
|
||||
source venv/bin/activate
|
||||
pip install pytest-blender
|
||||
@@ -69,7 +73,7 @@ jobs:
|
||||
source venv/bin/activate
|
||||
pytest -svv tests --blender-addons-dirs .
|
||||
- name: Archive addon
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: https://github.com/christopherHX/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ADDON_NAME }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
path: |
|
||||
|
||||
12
AGENTS.md
12
AGENTS.md
@@ -1,12 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
This is an Blender addon for importing and exporting Unreal Engine PSK (skeletal mesh) and PSX (animation) files.
|
||||
|
||||
# PSK/PSA File Format Notes
|
||||
* PSK and PSA bone hierarchies must have a single root bone. The root bone's `parent_index` is always `0`.
|
||||
* All indices in PSK/PSX files are zero-based.
|
||||
* All string fields in PSK/PSX files use Windows-1252 encoding and are null-terminated if they do not use the full length of the field.
|
||||
* Bone transforms are in parent bone space, except for root bones, which are in world space.
|
||||
|
||||
# Naming Conventions
|
||||
* The `PSX` prefix is used when refer to concepts that are shared between PSK and PSX files.
|
||||
40
README.md
40
README.md
@@ -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).
|
||||
|
||||
# Features
|
||||
* Full PSK/PSA import and export capabilities.
|
||||
* Non-standard file section data (.pskx) is supported for import only (vertex normals, extra UV channels, vertex colors, shape keys).
|
||||
* [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).
|
||||
* [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.
|
||||
* 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.
|
||||
* Manual re-ordering of material slots.
|
||||
* 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.
|
||||
* Compress exported sequences via resampling ratios or frame quotas.
|
||||
|
||||
# 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)`.
|
||||
2. Select the PSK file you want to import and click `Import`.
|
||||
|
||||
## 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`.
|
||||
|
||||
## Importing a PSA
|
||||
### Importing a PSA
|
||||
1. Select an armature that you want import animations for.
|
||||
2. Navigate to `File` > `Import` > `Unreal PSA (.psa)`.
|
||||
3. Select the PSA file you want to 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.
|
||||
|
||||
# FAQ
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from .shared import types as shared_types, helpers as shared_helpers
|
||||
from .shared import dfs as shared_dfs, ui as shared_ui
|
||||
from .shared import operators as shared_operators
|
||||
from .shared import (
|
||||
types as shared_types,
|
||||
helpers as shared_helpers,
|
||||
dfs as shared_dfs,
|
||||
ui as shared_ui,
|
||||
operators as shared_operators,
|
||||
)
|
||||
from .psk import (
|
||||
builder as psk_builder,
|
||||
importer as psk_importer,
|
||||
properties as psk_properties,
|
||||
ui as psk_ui,
|
||||
)
|
||||
from .psk import ui as psk_ui
|
||||
from .psk.export import (
|
||||
operators as psk_export_operators,
|
||||
properties as psk_export_properties,
|
||||
ui as psk_export_ui,
|
||||
)
|
||||
from .psk.import_ import operators as psk_import_operators
|
||||
|
||||
from .psk.import_ import (
|
||||
operators as psk_import_operators,
|
||||
)
|
||||
from .psa import (
|
||||
config as psa_config,
|
||||
builder as psa_builder,
|
||||
importer as psa_importer,
|
||||
file_handlers as psa_file_handlers,
|
||||
)
|
||||
from .psa.export import (
|
||||
properties as psa_export_properties,
|
||||
ui as psa_export_ui,
|
||||
operators as psa_export_operators,
|
||||
)
|
||||
from .psa.import_ import operators as psa_import_operators
|
||||
from .psa.import_ import ui as psa_import_ui, properties as psa_import_properties
|
||||
|
||||
from .psa import file_handlers as psa_file_handlers
|
||||
from .psa.import_ import (
|
||||
operators as psa_import_operators,
|
||||
ui as psa_import_ui,
|
||||
properties as psa_import_properties,
|
||||
)
|
||||
|
||||
_needs_reload = 'bpy' in locals()
|
||||
|
||||
@@ -108,7 +113,6 @@ 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)
|
||||
|
||||
setattr(bpy.types.Material, 'psk', PointerProperty(type=psk_properties.PSX_PG_material, options={'HIDDEN'}))
|
||||
setattr(bpy.types.Scene, 'psx_export', PointerProperty(type=shared_types.PSX_PG_scene_export, options={'HIDDEN'}))
|
||||
setattr(bpy.types.Scene, 'psa_import', PointerProperty(type=psa_import_properties.PSA_PG_import, options={'HIDDEN'}))
|
||||
@@ -124,7 +128,6 @@ def unregister():
|
||||
delattr(bpy.types.Scene, 'psa_export')
|
||||
delattr(bpy.types.Scene, 'psk_export')
|
||||
delattr(bpy.types.Action, 'psa_export')
|
||||
|
||||
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)
|
||||
@@ -135,16 +138,3 @@ def unregister():
|
||||
|
||||
if __name__ == '__main__':
|
||||
register()
|
||||
|
||||
|
||||
@persistent
|
||||
def load_handler(dummy):
|
||||
# Convert old `psa_sequence_fps` property to new `psa_export.fps` property.
|
||||
# This is only needed for backwards compatibility with files that may have used older versions of the addon.
|
||||
for action in bpy.data.actions:
|
||||
if 'psa_sequence_fps' in action:
|
||||
action.psa_export.fps = action['psa_sequence_fps']
|
||||
del action['psa_sequence_fps']
|
||||
|
||||
|
||||
bpy.app.handlers.load_post.append(load_handler)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "io_scene_psk_psa"
|
||||
version = "9.1.0"
|
||||
version = "9.1.1"
|
||||
name = "Unreal PSK/PSA (.psk/.psa)"
|
||||
tagline = "Import and export PSK and PSA files used in Unreal Engine"
|
||||
maintainer = "Colin Basnett <cmbasnett@gmail.com>"
|
||||
|
||||
@@ -39,7 +39,6 @@ class PsaBuildOptions:
|
||||
self.export_space = 'WORLD'
|
||||
self.forward_axis = 'X'
|
||||
self.up_axis = 'Z'
|
||||
self.root_bone_name = 'ROOT'
|
||||
self.sequence_source = 'ACTIONS' # One of ('ACTIONS', 'TIMELINE_MARKERS', 'NLA_STRIPS')
|
||||
|
||||
@property
|
||||
@@ -162,7 +161,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
|
||||
psx_bone_create_result = create_psx_bones(
|
||||
armature_objects=armature_objects_for_bones,
|
||||
export_space=options.export_space,
|
||||
root_bone_name=options.root_bone_name,
|
||||
forward_axis=options.forward_axis,
|
||||
up_axis=options.up_axis,
|
||||
scale=options.scale,
|
||||
|
||||
@@ -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.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_header, transform_panel = layout.panel('Transform', default_closed=False)
|
||||
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.forward_axis = pg.forward_axis
|
||||
options.up_axis = pg.up_axis
|
||||
options.root_bone_name = pg.root_bone_name
|
||||
options.sequence_source = pg.sequence_source
|
||||
|
||||
return options
|
||||
@@ -626,14 +617,6 @@ class PSA_OT_export(Operator, ExportHelper):
|
||||
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_header, transform_panel = layout.panel('Advanced', default_closed=False)
|
||||
transform_header.label(text='Transform', icon='DRIVER_TRANSFORM')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.types import Armature, Context, Object, Mesh, Material
|
||||
from mathutils import Matrix
|
||||
from typing import Iterable, Sequence, cast as typing_cast
|
||||
from psk_psa_py.shared.data import Vector3
|
||||
from bpy.types import Armature, Context, Object, Mesh
|
||||
from mathutils import Matrix, Quaternion
|
||||
from typing import Iterable, cast as typing_cast
|
||||
from psk_psa_py.shared.data import PsxBone, Vector3
|
||||
from psk_psa_py.psk.data import Psk
|
||||
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
||||
from ..shared.helpers import (
|
||||
@@ -12,6 +12,7 @@ from ..shared.helpers import (
|
||||
ObjectTree,
|
||||
PskInputObjects,
|
||||
PsxBoneCollection,
|
||||
convert_bpy_quaternion_to_psx_quaternion,
|
||||
convert_string_to_cp1252_bytes,
|
||||
create_psx_bones,
|
||||
get_armature_for_mesh_object,
|
||||
@@ -31,7 +32,6 @@ class PskBuildOptions(object):
|
||||
self.export_space = 'WORLD'
|
||||
self.forward_axis = 'X'
|
||||
self.up_axis = 'Z'
|
||||
self.root_bone_name = 'ROOT'
|
||||
|
||||
|
||||
class PskBuildResult(object):
|
||||
@@ -99,13 +99,20 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
||||
forward_axis=options.forward_axis,
|
||||
up_axis=options.up_axis,
|
||||
scale=options.scale,
|
||||
root_bone_name=options.root_bone_name,
|
||||
bone_filter_mode=options.bone_filter_mode,
|
||||
bone_collection_indices=options.bone_collection_indices
|
||||
)
|
||||
|
||||
psk.bones = [bone.psx_bone for bone in psx_bone_create_result.bones]
|
||||
|
||||
if len(psk.bones) == 0:
|
||||
# Add a default root bone if there are no bones to export.
|
||||
# This is necessary because Unreal Engine requires at least one bone in the PSK file.
|
||||
psx_bone = PsxBone()
|
||||
psx_bone.name = b'ROOT'
|
||||
psx_bone.rotation = convert_bpy_quaternion_to_psx_quaternion(Quaternion())
|
||||
psk.bones.append(psx_bone)
|
||||
|
||||
# Materials
|
||||
mesh_objects = [dfs_object.obj for dfs_object in input_objects.mesh_dfs_objects]
|
||||
|
||||
@@ -311,18 +318,17 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
||||
poly_groups, groups = mesh_data.calc_smooth_groups(use_bitflags=True)
|
||||
psk_face_start_index = len(psk.faces)
|
||||
for f in mesh_data.loop_triangles:
|
||||
face = Psk.Face()
|
||||
face.material_index = material_indices[f.material_index]
|
||||
face.wedge_indices[0] = loop_wedge_indices[f.loops[2]]
|
||||
face.wedge_indices[1] = loop_wedge_indices[f.loops[1]]
|
||||
face.wedge_indices[2] = loop_wedge_indices[f.loops[0]]
|
||||
face.smoothing_groups = poly_groups[f.polygon_index]
|
||||
face = Psk.Face(
|
||||
wedge_indices=(loop_wedge_indices[f.loops[2]], loop_wedge_indices[f.loops[1]], loop_wedge_indices[f.loops[0]]),
|
||||
material_index=material_indices[f.material_index],
|
||||
smoothing_groups=poly_groups[f.polygon_index],
|
||||
)
|
||||
psk.faces.append(face)
|
||||
|
||||
if should_flip_normals:
|
||||
# Invert the normals of the faces.
|
||||
for face in psk.faces[psk_face_start_index:]:
|
||||
face.wedge_indices[0], face.wedge_indices[2] = face.wedge_indices[2], face.wedge_indices[0]
|
||||
face.wedge_indices = (face.wedge_indices[2], face.wedge_indices[1], face.wedge_indices[0])
|
||||
|
||||
# Weights
|
||||
if armature_object is not None:
|
||||
|
||||
@@ -207,7 +207,6 @@ def get_psk_build_options_from_property_group(scene: Scene, pg: PskExportMixin)
|
||||
options.export_space = pg.export_space
|
||||
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.root_bone_name = pg.root_bone_name
|
||||
options.material_order_mode = pg.material_order_mode
|
||||
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.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_header, materials_panel = layout.panel('Materials', default_closed=False)
|
||||
materials_header.label(text='Materials', icon='MATERIAL')
|
||||
@@ -429,13 +420,6 @@ class PSK_OT_export(Operator, ExportHelper):
|
||||
row = bones_panel.row()
|
||||
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)
|
||||
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_header, materials_panel = layout.panel('Materials', default_closed=False)
|
||||
|
||||
@@ -3,10 +3,9 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
PointerProperty,
|
||||
StringProperty,
|
||||
)
|
||||
from bpy.types import Material, PropertyGroup
|
||||
from bpy.types import PropertyGroup
|
||||
|
||||
from ...shared.types import ExportSpaceMixin, TransformMixin, PsxBoneExportMixin, TransformSourceMixin
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from bpy.types import Material
|
||||
|
||||
from ...shared.types import BpyCollectionProperty, ExportSpaceMixin, TransformMixin, PsxBoneExportMixin, TransformSourceMixin
|
||||
|
||||
|
||||
|
||||
@@ -332,7 +332,6 @@ class ObjectTree:
|
||||
def create_psx_bones(
|
||||
armature_objects: list[Object],
|
||||
export_space: str = 'WORLD',
|
||||
root_bone_name: str = 'ROOT',
|
||||
forward_axis: str = 'X',
|
||||
up_axis: str = 'Z',
|
||||
scale: float = 1.0,
|
||||
@@ -351,9 +350,12 @@ def create_psx_bones(
|
||||
armature_tree = ObjectTree(armature_objects)
|
||||
|
||||
if len(armature_tree.root_nodes) >= 2:
|
||||
root_bone_names = []
|
||||
for root_node in armature_tree.root_nodes:
|
||||
root_bone_names.append(root_node.object.name)
|
||||
raise RuntimeError(
|
||||
'Multiple root armature objects were found. '
|
||||
'Only one root armature object is allowed. '
|
||||
f'Multiple root armature objects were found: {root_bone_names}.\n'
|
||||
'Only one root armature object is allowed.\n'
|
||||
'To use multiple armature objects, parent them to one another in a hierarchy using Bone parenting.'
|
||||
)
|
||||
|
||||
@@ -453,6 +455,14 @@ def create_psx_bones(
|
||||
|
||||
bones.extend(PsxBoneResult(psx_bone, armature_object) for psx_bone in armature_psx_bones)
|
||||
|
||||
# Check that we have any bones to export at this point. If not, we can skip the rest of the processing.
|
||||
if len(bones) == 0:
|
||||
return PsxBoneCreateResult(
|
||||
bones=[],
|
||||
armature_object_root_bone_indices=armature_object_root_bone_indices,
|
||||
armature_object_bone_names=armature_object_bone_names,
|
||||
)
|
||||
|
||||
# Check if any of the armatures are parented to one another.
|
||||
# If so, adjust the hierarchy as though they are part of the same armature object.
|
||||
# This will let us re-use rig components without destructively joining them.
|
||||
@@ -517,13 +527,10 @@ def create_psx_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():
|
||||
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.'
|
||||
|
||||
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.
|
||||
for bone in bones:
|
||||
|
||||
@@ -179,11 +179,6 @@ class PsxBoneExportMixin:
|
||||
)
|
||||
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
||||
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):
|
||||
|
||||
@@ -54,7 +54,6 @@ class PsxBoneExportMixin:
|
||||
bone_filter_mode: str
|
||||
bone_collection_list: BpyCollectionProperty[PSX_PG_bone_collection_list_item]
|
||||
bone_collection_list_index: int
|
||||
root_bone_name: str
|
||||
|
||||
|
||||
class PSX_PG_scene_export(TransformSourceMixin):
|
||||
|
||||
BIN
io_scene_psk_psa/wheels/psk_psa_py-0.0.4-py3-none-any.whl
Normal file
BIN
io_scene_psk_psa/wheels/psk_psa_py-0.0.4-py3-none-any.whl
Normal file
Binary file not shown.
BIN
tests/data/psk_export_tests.blend
LFS
Normal file
BIN
tests/data/psk_export_tests.blend
LFS
Normal file
Binary file not shown.
62
tests/psk_export_test.py
Normal file
62
tests/psk_export_test.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import bpy
|
||||
import pytest
|
||||
import tempfile
|
||||
from psk_psa_py.psk.reader import read_psk_from_file
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def run_before_and_after_tests(tmpdir):
|
||||
# Setup: Run before the tests
|
||||
bpy.ops.wm.read_homefile(filepath='tests/data/psk_export_tests.blend')
|
||||
|
||||
yield
|
||||
# Teardown: Run after the tests
|
||||
pass
|
||||
|
||||
|
||||
def export_psk_and_read_back(collection_name: str):
|
||||
collection = bpy.data.collections.get(collection_name, None)
|
||||
assert collection is not None, f"Collection {collection_name} not found in the scene."
|
||||
|
||||
# Select the collection to make it the active collection.
|
||||
view_layer = bpy.context.view_layer
|
||||
assert view_layer is not None, "No active view layer found."
|
||||
view_layer.active_layer_collection = view_layer.layer_collection.children[collection_name]
|
||||
|
||||
filepath = str(tempfile.gettempdir() + f'/{collection_name}.psk')
|
||||
|
||||
collection.exporters[0].filepath = filepath
|
||||
|
||||
assert bpy.ops.collection.exporter_export() == {'FINISHED'}, "PSK export failed."
|
||||
|
||||
# Now load the exported PSK file and return its contents.
|
||||
psk = read_psk_from_file(filepath)
|
||||
return psk
|
||||
|
||||
|
||||
def test_psk_export_cube_no_bones():
|
||||
psk = export_psk_and_read_back('cube_no_bones')
|
||||
|
||||
# There should be one bone when no armature is present, this is added automatically to serve as the root bone for the mesh.
|
||||
assert len(psk.bones) == 1, f"Expected 1 bone, but found {len(psk.bones)}."
|
||||
assert len(psk.points) == 8, f"Expected 8 points, but found {len(psk.points)}."
|
||||
assert len(psk.faces) == 12, f"Expected 12 faces, but found {len(psk.faces)}."
|
||||
assert len(psk.materials) == 1, f"Expected 1 material, but found {len(psk.materials)}."
|
||||
|
||||
|
||||
def test_cube_edge_split():
|
||||
# The cube has all the edges set to split with a modifier.
|
||||
psk = export_psk_and_read_back('cube_edge_split')
|
||||
|
||||
assert len(psk.bones) == 1, f"Expected 1 bone, but found {len(psk.bones)}."
|
||||
assert len(psk.points) == 24, f"Expected 24 points, but found {len(psk.points)}."
|
||||
assert len(psk.faces) == 12, f"Expected 12 faces, but found {len(psk.faces)}."
|
||||
assert len(psk.materials) == 1, f"Expected 1 material, but found {len(psk.materials)}."
|
||||
|
||||
|
||||
def test_cube_with_simple_armature():
|
||||
# The cube has all the edges set to split with a modifier.
|
||||
psk = export_psk_and_read_back('cube_with_simple_armature')
|
||||
|
||||
assert len(psk.bones) == 1, f"Expected 1 bone, but found {len(psk.bones)}."
|
||||
assert psk.bones[0].name == b'ROOT', f"Expected bone name 'ROOT', but found {psk.bones[0].name}."
|
||||
Reference in New Issue
Block a user