Fix #139: PSA import does not work on Blender 5.0

This commit is contained in:
Colin Basnett
2025-11-27 12:52:40 -08:00
parent 3153be3cdf
commit 1bac8b2b30
6 changed files with 44 additions and 18 deletions

View File

@@ -1,7 +1,5 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
ARG BLENDER_VERSION=4.4
RUN apt-get update -y && \ RUN apt-get update -y && \
apt-get install -y libxxf86vm-dev libxfixes3 libxi-dev libxkbcommon-x11-0 libgl1 libglx-mesa0 python3 python3-pip \ apt-get install -y libxxf86vm-dev libxfixes3 libxi-dev libxkbcommon-x11-0 libgl1 libglx-mesa0 python3 python3-pip \
libxrender1 libsm6 libxrender1 libsm6
@@ -10,6 +8,8 @@ RUN pip install --upgrade pip
RUN pip install pytest-blender RUN pip install pytest-blender
RUN pip install blender-downloader RUN pip install blender-downloader
ARG BLENDER_VERSION=5.0
# Set BLENDER_EXECUTABLE and BLENDER_PYTHON as environment variables # Set BLENDER_EXECUTABLE and BLENDER_PYTHON as environment variables
RUN BLENDER_EXECUTABLE=$(blender-downloader $BLENDER_VERSION --extract --remove-compressed --print-blender-executable) && \ RUN BLENDER_EXECUTABLE=$(blender-downloader $BLENDER_VERSION --extract --remove-compressed --print-blender-executable) && \
BLENDER_PYTHON=$(pytest-blender --blender-executable "${BLENDER_EXECUTABLE}") && \ BLENDER_PYTHON=$(pytest-blender --blender-executable "${BLENDER_EXECUTABLE}") && \

View File

@@ -1,13 +1,13 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "io_scene_psk_psa" id = "io_scene_psk_psa"
version = "8.2.4" version = "9.0.0"
name = "Unreal PSK/PSA (.psk/.psa)" name = "Unreal PSK/PSA (.psk/.psa)"
tagline = "Import and export PSK and PSA files used in Unreal Engine" tagline = "Import and export PSK and PSA files used in Unreal Engine"
maintainer = "Colin Basnett <cmbasnett@gmail.com>" maintainer = "Colin Basnett <cmbasnett@gmail.com>"
type = "add-on" type = "add-on"
website = "https://github.com/DarklightGames/io_scene_psk_psa/" website = "https://github.com/DarklightGames/io_scene_psk_psa/"
tags = ["Game Engine", "Import-Export"] tags = ["Game Engine", "Import-Export"]
blender_version_min = "4.4.0" blender_version_min = "5.0.0"
# Optional: maximum supported Blender version # Optional: maximum supported Blender version
# blender_version_max = "5.1.0" # blender_version_max = "5.1.0"
license = [ license = [

View File

@@ -4,7 +4,7 @@ from .data import Psa
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from mathutils import Matrix, Quaternion, Vector from mathutils import Matrix, Quaternion, Vector
from ..shared.helpers import create_psx_bones, get_coordinate_system_transform from ..shared.helpers import PsxBoneCollection, create_psx_bones, get_coordinate_system_transform
class PsaBuildSequence: class PsaBuildSequence:
@@ -30,7 +30,7 @@ class PsaBuildOptions:
self.animation_data: Optional[AnimData] = None self.animation_data: Optional[AnimData] = None
self.sequences: List[PsaBuildSequence] = [] self.sequences: List[PsaBuildSequence] = []
self.bone_filter_mode: str = 'ALL' self.bone_filter_mode: str = 'ALL'
self.bone_collection_indices: List[PsaBoneCollectionIndex] = [] self.bone_collection_indices: List[PsxBoneCollection] = []
self.sequence_name_prefix: str = '' self.sequence_name_prefix: str = ''
self.sequence_name_suffix: str = '' self.sequence_name_suffix: str = ''
self.scale = 1.0 self.scale = 1.0

View File

@@ -18,6 +18,7 @@ from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
from ..writer import write_psa from ..writer import write_psa
from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range, PsxBoneCollection from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range, PsxBoneCollection
from ...shared.ui import draw_bone_filter_mode from ...shared.ui import draw_bone_filter_mode
from ...shared.types import PSX_PG_action_export, PSX_PG_scene_export
def get_sequences_propnames_from_source(sequence_source: str) -> Tuple[str, str]: def get_sequences_propnames_from_source(sequence_source: str) -> Tuple[str, str]:
@@ -453,7 +454,7 @@ class PSA_OT_export(Operator, ExportHelper):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def execute(self, context): def execute(self, context):
pg = getattr(context.scene, 'psa_export') pg = typing_cast(PSA_PG_export, getattr(context.scene, 'psa_export'))
# Populate the export sequence list. # Populate the export sequence list.
animation_data_object = get_animation_data_object(context) animation_data_object = get_animation_data_object(context)

View File

@@ -214,6 +214,8 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
) )
del armature_bone_names del armature_bone_names
assert armature_object.pose
# Create intermediate bone data for import operations. # Create intermediate bone data for import operations.
import_bones = [] import_bones = []
psa_bone_names_to_import_bones = dict() psa_bone_names_to_import_bones = dict()
@@ -277,7 +279,12 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
action = bpy.data.actions[action_name] action = bpy.data.actions[action_name]
else: else:
action = bpy.data.actions.new(name=action_name) action = bpy.data.actions.new(name=action_name)
action.slots.new('OBJECT', armature_object.name) action_slot = action.slots.new('OBJECT', armature_object.name)
# TODO: Wish there was a better way to do this.
action_slot = action.slots[f'OB{armature_object.name}']
assert action_slot
# Calculate the target FPS. # Calculate the target FPS.
match options.fps_source: match options.fps_source:
@@ -293,7 +300,22 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
if options.should_write_keyframes: if options.should_write_keyframes:
# Remove existing f-curves. # Remove existing f-curves.
action.fcurves.clear() if len(action.layers) == 0:
layer = action.layers.new(armature_object.name)
else:
layer = action.layers[0]
if len(layer.strips) == 0:
action_strip = layer.strips.new()
else:
action_strip = layer.strips[0]
if len(action_strip.channelbags) == 0:
channelbag = action_strip.channelbags.new(action_slot)
else:
channelbag = action_strip.channelbags[0]
channelbag.fcurves.clear()
# Create f-curves for the rotation and location of each bone. # Create f-curves for the rotation and location of each bone.
for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items(): for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items():
@@ -305,13 +327,13 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
add_rotation_fcurves = (bone_track_flags & REMOVE_TRACK_ROTATION) == 0 add_rotation_fcurves = (bone_track_flags & REMOVE_TRACK_ROTATION) == 0
add_location_fcurves = (bone_track_flags & REMOVE_TRACK_LOCATION) == 0 add_location_fcurves = (bone_track_flags & REMOVE_TRACK_LOCATION) == 0
import_bone.fcurves = [ import_bone.fcurves = [
action.fcurves.new(rotation_data_path, index=0, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qw channelbag.fcurves.new(rotation_data_path, index=0, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qw
action.fcurves.new(rotation_data_path, index=1, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qx channelbag.fcurves.new(rotation_data_path, index=1, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qx
action.fcurves.new(rotation_data_path, index=2, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qy channelbag.fcurves.new(rotation_data_path, index=2, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qy
action.fcurves.new(rotation_data_path, index=3, action_group=pose_bone.name) if add_rotation_fcurves else None, # Qz channelbag.fcurves.new(rotation_data_path, index=3, group_name=pose_bone.name) if add_rotation_fcurves else None, # Qz
action.fcurves.new(location_data_path, index=0, action_group=pose_bone.name) if add_location_fcurves else None, # Lx channelbag.fcurves.new(location_data_path, index=0, group_name=pose_bone.name) if add_location_fcurves else None, # Lx
action.fcurves.new(location_data_path, index=1, action_group=pose_bone.name) if add_location_fcurves else None, # Ly channelbag.fcurves.new(location_data_path, index=1, group_name=pose_bone.name) if add_location_fcurves else None, # Ly
action.fcurves.new(location_data_path, index=2, action_group=pose_bone.name) if add_location_fcurves else None, # Lz channelbag.fcurves.new(location_data_path, index=2, group_name=pose_bone.name) if add_location_fcurves else None, # Lz
] ]
# Read the sequence data matrix from the PSA. # Read the sequence data matrix from the PSA.
@@ -375,7 +397,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
if animation_data is None: if animation_data is None:
animation_data = armature_object.animation_data_create() animation_data = armature_object.animation_data_create()
for action in actions: for action in actions:
nla_track = armature_object.animation_data.nla_tracks.new() nla_track = animation_data.nla_tracks.new()
nla_track.name = action.name nla_track.name = action.name
nla_track.mute = True nla_track.mute = True
nla_track.strips.new(name=action.name, start=0, action=action) nla_track.strips.new(name=action.name, start=0, action=action)

View File

@@ -345,7 +345,10 @@ def create_psx_bones(
coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis) coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis)
coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion() coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion()
total_bone_count = sum(len(armature_object.data.bones) for armature_object in armature_objects) total_bone_count = 0
for armature_object in filter(lambda x: x.data is not None, armature_objects):
armature_data = typing_cast(Armature, armature_object.data)
total_bone_count += len(armature_data.bones)
# Store the bone names to be exported for each armature object. # Store the bone names to be exported for each armature object.
armature_object_bone_names: Dict[Object, List[str]] = dict() armature_object_bone_names: Dict[Object, List[str]] = dict()