Compare commits

..

5 Commits

Author SHA1 Message Date
Colin Basnett
37f7cc4d9f Increment version to 8.2.4 2025-11-08 18:28:21 -08:00
Colin Basnett
93083f09f8 Fix #135: Extra UV maps have incorrect data
This was caused by a regression caused by 29831d7f09.

The test for importing extra UVs has been updated to check that the data is different between the different UV layers.
2025-11-08 18:27:40 -08:00
Colin Basnett
75660f9dc1 Incremented version to 8.2.3 2025-10-31 12:43:09 -07:00
Colin Basnett
5421ac5151 Removed debugging code 2025-10-31 12:42:14 -07:00
Colin Basnett
9dcbb74058 Fix for missing transform source and broken scale controls on PSK export dialog 2025-10-31 12:41:14 -07:00
11 changed files with 56 additions and 76 deletions

View File

@@ -1,6 +1,6 @@
schema_version = "1.0.0"
id = "io_scene_psk_psa"
version = "8.2.2"
version = "8.2.4"
name = "Unreal PSK/PSA (.psk/.psa)"
tagline = "Import and export PSK and PSA files used in Unreal Engine"
maintainer = "Colin Basnett <cmbasnett@gmail.com>"

View File

@@ -1,10 +1,10 @@
from bpy.types import Action, AnimData, Context, Object, PoseBone
from .data import Psa
from typing import Dict, List, Optional, Tuple, Iterable
from typing import Dict, List, Optional, Tuple
from mathutils import Matrix, Quaternion, Vector
from ..shared.helpers import PsxBoneCollection, create_psx_bones, get_coordinate_system_transform
from ..shared.helpers import create_psx_bones, get_coordinate_system_transform
class PsaBuildSequence:
@@ -14,8 +14,8 @@ class PsaBuildSequence:
self.frame_start: int = 0
self.frame_end: int = 0
def __init__(self, armature_objects: Iterable[Object], anim_data: AnimData):
self.armature_objects = list(armature_objects)
def __init__(self, armature_object: Object, anim_data: AnimData):
self.armature_object = armature_object
self.anim_data = anim_data
self.name: str = ''
self.nla_state: PsaBuildSequence.NlaState = PsaBuildSequence.NlaState()
@@ -27,9 +27,10 @@ class PsaBuildSequence:
class PsaBuildOptions:
def __init__(self):
self.armature_objects: List[Object] = []
self.animation_data: Optional[AnimData] = None
self.sequences: List[PsaBuildSequence] = []
self.bone_filter_mode: str = 'ALL'
self.bone_collection_indices: List[PsxBoneCollection] = []
self.bone_collection_indices: List[PsaBoneCollectionIndex] = []
self.sequence_name_prefix: str = ''
self.sequence_name_suffix: str = ''
self.scale = 1.0
@@ -57,7 +58,7 @@ def _get_pose_bone_location_and_rotation(
if is_false_root_bone:
pose_bone_matrix = coordinate_system_transform
elif pose_bone is not None and pose_bone.parent is not None:
elif pose_bone.parent is not None:
pose_bone_matrix = pose_bone.matrix
pose_bone_parent_matrix = pose_bone.parent.matrix
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
@@ -108,7 +109,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
psa = Psa()
# TODO: move this OUT!
armature_objects_for_bones = options.armature_objects
if options.sequence_source == 'ACTIVE_ACTION' and len(options.armature_objects) >= 2:
# Make sure that the data-block for all the selected armature objects is the same.
@@ -143,7 +143,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
export_sequence.name = export_sequence.name.strip()
# Save each armature object's current action and frame so that we can restore the state once we are done.
saved_armature_object_actions = {o: o.animation_data.action if o.animation_data else None for o in options.armature_objects}
saved_armature_object_actions = {o: o.animation_data.action for o in options.armature_objects}
saved_frame_current = context.scene.frame_current
# Now build the PSA sequences.
@@ -185,9 +185,10 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
frame = float(frame_start)
export_sequence.anim_data.action = export_sequence.nla_state.action
# Link the action to the animation data and update view layer.
for armature_object in options.armature_objects:
armature_object.animation_data.action = export_sequence.nla_state.action
assert context.view_layer
context.view_layer.update()
def add_key(location: Vector, rotation: Quaternion):
@@ -211,7 +212,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
armature_scales: Dict[Object, Vector] = {}
# Extract the scale from the world matrix of the evaluated armature object.
for armature_object in export_sequence.armature_objects:
for armature_object in options.armature_objects:
evaluated_armature_object = armature_object.evaluated_get(context.evaluated_depsgraph_get())
_, _, scale = evaluated_armature_object.matrix_world.decompose()
scale *= options.scale
@@ -222,7 +223,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
# locations.
export_bones: List[PsaExportBone] = []
# TODO: we need different behavior here if it's ACTIVE_ACTION
for psx_bone, armature_object in psx_bone_create_result.bones:
if armature_object is None:
export_bones.append(PsaExportBone(None, None, Vector((1.0, 1.0, 1.0))))

View File

@@ -472,7 +472,7 @@ class PSA_OT_export(Operator, ExportHelper):
for action_item in filter(lambda x: x.is_selected, pg.action_list):
if len(action_item.action.fcurves) == 0:
continue
export_sequence = PsaBuildSequence(self.armature_objects, animation_data)
export_sequence = PsaBuildSequence(context.active_object, animation_data)
export_sequence.name = action_item.name
export_sequence.nla_state.action = action_item.action
export_sequence.nla_state.frame_start = action_item.frame_start
@@ -483,7 +483,7 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences.append(export_sequence)
case 'TIMELINE_MARKERS':
for marker_item in filter(lambda x: x.is_selected, pg.marker_list):
export_sequence = PsaBuildSequence(self.armature_objects, animation_data)
export_sequence = PsaBuildSequence(context.active_object, animation_data)
export_sequence.name = marker_item.name
export_sequence.nla_state.frame_start = marker_item.frame_start
export_sequence.nla_state.frame_end = marker_item.frame_end
@@ -494,7 +494,7 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences.append(export_sequence)
case 'NLA_TRACK_STRIPS':
for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list):
export_sequence = PsaBuildSequence(self.armature_objects, animation_data)
export_sequence = PsaBuildSequence(context.active_object, animation_data)
export_sequence.name = nla_strip_item.name
export_sequence.nla_state.frame_start = nla_strip_item.frame_start
export_sequence.nla_state.frame_end = nla_strip_item.frame_end
@@ -504,7 +504,7 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences.append(export_sequence)
case 'ACTIVE_ACTION':
for active_action_item in filter(lambda x: x.is_selected, pg.active_action_list):
export_sequence = PsaBuildSequence([active_action_item.armature_object], active_action_item.armature_object.animation_data)
export_sequence = PsaBuildSequence(active_action_item.armature_object, active_action_item.armature_object.animation_data)
action = active_action_item.action
export_sequence.name = action.name
export_sequence.nla_state.action = action
@@ -522,6 +522,8 @@ class PSA_OT_export(Operator, ExportHelper):
return {'CANCELLED'}
options = PsaBuildOptions()
options.armature_objects = self.armature_objects
options.animation_data = animation_data
options.sequences = export_sequences
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]

View File

@@ -13,7 +13,7 @@ class PSA_UL_export_sequences(UIList):
# Show the filtering options by default.
self.use_filter_show = True
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index, flt_flag):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
item = typing_cast(PSA_PG_export_action_list_item, item)
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
@@ -44,7 +44,7 @@ class PSA_UL_export_sequences(UIList):
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, property):
def filter_items(self, context, data, prop):
pg = getattr(context.scene, 'psa_export')
actions = getattr(data, prop)
flt_flags = filter_sequences(pg, actions)

View File

@@ -375,7 +375,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
if animation_data is None:
animation_data = armature_object.animation_data_create()
for action in actions:
nla_track = animation_data.nla_tracks.new()
nla_track = armature_object.animation_data.nla_tracks.new()
nla_track.name = action.name
nla_track.mute = True
nla_track.strips.new(name=action.name, start=0, action=action)

View File

@@ -17,7 +17,7 @@ def _try_fix_cue4parse_issue_103(sequences) -> bool:
# Manually set the frame_start_index for each sequence. This assumes that the sequences are in order with
# no shared frames between sequences (all exporters that I know of do this, so it's a safe assumption).
frame_start_index = 0
for sequence in sequences:
for i, sequence in enumerate(sequences):
sequence.frame_start_index = frame_start_index
frame_start_index += sequence.frame_count
return True

View File

@@ -241,7 +241,7 @@ def get_psk_build_options_from_property_group(scene: Scene, pg: PskExportMixin)
match pg.transform_source:
case 'SCENE':
transform_source = getattr(scene, 'psx_export')
case 'SELF':
case 'CUSTOM':
transform_source = pg
case _:
assert False, f'Invalid transform source: {pg.transform_source}'
@@ -486,9 +486,24 @@ class PSK_OT_export(Operator, ExportHelper):
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'export_space')
flow.prop(pg, 'scale')
flow.prop(pg, 'forward_axis')
flow.prop(pg, 'up_axis')
flow.prop(pg, 'transform_source')
flow = transform_panel.grid_flow(row_major=True)
flow.use_property_split = True
flow.use_property_decorate = False
match pg.transform_source:
case 'SCENE':
transform_source = getattr(context.scene, 'psx_export')
flow.enabled = False
case 'CUSTOM':
transform_source = pg
case _:
assert False, f'Invalid transform source: {pg.transform_source}'
flow.prop(transform_source, 'scale')
flow.prop(transform_source, 'forward_axis')
flow.prop(transform_source, 'up_axis')
# Extended Format
extended_format_header, extended_format_panel = layout.panel('Extended Format', default_closed=False)

View File

@@ -1,14 +1,10 @@
import os
from pathlib import Path
from typing import cast as typing_cast
from bpy.props import CollectionProperty, StringProperty, FloatProperty, EnumProperty
from bpy.types import Armature, Context, FileHandler, Operator, OperatorFileListElement, UILayout
from bpy.props import CollectionProperty, StringProperty
from bpy.types import Context, FileHandler, Operator, OperatorFileListElement, UILayout
from bpy_extras.io_utils import ImportHelper
from ...shared.helpers import get_coordinate_system_transform
from ...shared.types import AxisMixin
from ..importer import PskImportOptions, import_psk
from ..properties import PskImportMixin
from ..reader import read_psk
@@ -166,46 +162,6 @@ class PSK_OT_import_drag_and_drop(Operator, PskImportMixin):
return {'FINISHED'}
class PSK_OT_create_bones_from_selected_objects(Operator, AxisMixin):
bl_idname = 'psk.create_bones_from_selected_objects'
bl_label = 'Create Bones from Selected Objects'
bl_options = {'UNDO'}
length: FloatProperty(name='Length', subtype='DISTANCE', default=0.01)
@classmethod
def poll(cls, context: Context) -> bool:
return context.active_object is not None and context.active_object.type == 'ARMATURE'
def invoke(self, context, event):
assert context.window_manager
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context):
armature_object = context.active_object
assert armature_object
armature_data = typing_cast(Armature, armature_object.data)
axis_transform = get_coordinate_system_transform(self.forward_axis, self.up_axis)
import bpy
bpy.ops.object.mode_set(mode='EDIT')
for index, obj in enumerate(context.selected_objects):
if obj == armature_object:
continue
edit_bone_matrix = armature_object.matrix_world.inverted() @ obj.matrix_world
edit_bone = armature_data.edit_bones.new(f'{obj.name}_{index}')
# translation, rotation, _ = edit_bone_matrix.decompose()
edit_bone.length = self.length
edit_bone.matrix = edit_bone_matrix @ axis_transform
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
# TODO: move to another file
class PSK_FH_import(FileHandler):
bl_idname = 'PSK_FH_import'

View File

@@ -53,7 +53,7 @@ class PskImportResult:
self.mesh_object: Optional[Object] = None
@property
def root_object(self) -> Optional[Object]:
def root_object(self) -> Object:
return self.armature_object if self.armature_object is not None else self.mesh_object
@@ -210,8 +210,9 @@ def import_psk(psk: Psk, context: Context, name: str, options: PskImportOptions)
for face_index, face in enumerate(psk.faces):
if face_index in invalid_face_indices:
continue
for wedge in map(lambda i: psk.wedges[i], reversed(face.wedge_indices)):
uv_layer_data[uv_layer_data_index] = wedge.u, 1.0 - wedge.v
for wedge_index in reversed(face.wedge_indices):
u, v = psk.extra_uvs[wedge_index_offset + wedge_index]
uv_layer_data[uv_layer_data_index] = u, 1.0 - v
uv_layer_data_index += 1
wedge_index_offset += len(psk.wedges)
uv_layer = mesh_data.uv_layers.new(name=f'EXTRAUV{extra_uv_index}')

View File

@@ -67,7 +67,7 @@ def read_psk(path: str) -> Psk:
case b'MRPHDATA':
_read_types(fp, Psk.MorphData, section, psk.morph_data)
case _:
if section.name.startswith(b'EXTRAUVS'):
if section.name.startswith(b'EXTRAUV'):
_read_types(fp, Vector2, section, psk.extra_uvs)
else:
# Section is not handled, skip it.

View File

@@ -220,6 +220,12 @@ def test_psk_import_extra_uvs():
assert mesh_data.uv_layers[0].name == 'UVMap', "First UV layer should be named 'UVMap'"
assert mesh_data.uv_layers[1].name == 'EXTRAUV0', "Second UV layer should be named 'EXTRAUV0'"
# Verify that the data is actually different
assert mesh_data.uv_layers[0].uv[0].vector.x == 0.92480468750
assert mesh_data.uv_layers[0].uv[0].vector.y == 0.90533447265625
assert mesh_data.uv_layers[1].uv[0].vector.x == 3.0517578125e-05
assert mesh_data.uv_layers[1].uv[0].vector.y == 0.999969482421875
def test_psk_import_materials():
assert bpy.ops.psk.import_file(