Initial commit for work to fix the multi-armature workflow

This commit is contained in:
Colin Basnett
2025-10-28 23:13:34 -07:00
parent 8ed985263c
commit e704069763
7 changed files with 68 additions and 26 deletions

View File

@@ -1,10 +1,10 @@
from bpy.types import Action, AnimData, Context, Object, PoseBone from bpy.types import Action, AnimData, Context, Object, PoseBone
from .data import Psa from .data import Psa
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple, Iterable
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:
@@ -14,8 +14,8 @@ class PsaBuildSequence:
self.frame_start: int = 0 self.frame_start: int = 0
self.frame_end: int = 0 self.frame_end: int = 0
def __init__(self, armature_object: Object, anim_data: AnimData): def __init__(self, armature_objects: Iterable[Object], anim_data: AnimData):
self.armature_object = armature_object self.armature_objects = list(armature_objects)
self.anim_data = anim_data self.anim_data = anim_data
self.name: str = '' self.name: str = ''
self.nla_state: PsaBuildSequence.NlaState = PsaBuildSequence.NlaState() self.nla_state: PsaBuildSequence.NlaState = PsaBuildSequence.NlaState()
@@ -27,10 +27,9 @@ class PsaBuildSequence:
class PsaBuildOptions: class PsaBuildOptions:
def __init__(self): def __init__(self):
self.armature_objects: List[Object] = [] self.armature_objects: List[Object] = []
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
@@ -58,7 +57,7 @@ def _get_pose_bone_location_and_rotation(
if is_false_root_bone: if is_false_root_bone:
pose_bone_matrix = coordinate_system_transform pose_bone_matrix = coordinate_system_transform
elif pose_bone.parent is not None: elif pose_bone is not None and pose_bone.parent is not None:
pose_bone_matrix = pose_bone.matrix pose_bone_matrix = pose_bone.matrix
pose_bone_parent_matrix = pose_bone.parent.matrix pose_bone_parent_matrix = pose_bone.parent.matrix
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
@@ -109,6 +108,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
psa = Psa() psa = Psa()
# TODO: move this OUT!
armature_objects_for_bones = options.armature_objects armature_objects_for_bones = options.armature_objects
if options.sequence_source == 'ACTIVE_ACTION' and len(options.armature_objects) >= 2: 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. # 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() 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. # 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 for o in options.armature_objects} saved_armature_object_actions = {o: o.animation_data.action if o.animation_data else None for o in options.armature_objects}
saved_frame_current = context.scene.frame_current saved_frame_current = context.scene.frame_current
# Now build the PSA sequences. # Now build the PSA sequences.
@@ -184,11 +184,10 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
psa_sequence.key_reduction = 1.0 psa_sequence.key_reduction = 1.0
frame = float(frame_start) 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. assert context.view_layer
for armature_object in options.armature_objects:
armature_object.animation_data.action = export_sequence.nla_state.action
context.view_layer.update() context.view_layer.update()
def add_key(location: Vector, rotation: Quaternion): def add_key(location: Vector, rotation: Quaternion):
@@ -212,7 +211,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
armature_scales: Dict[Object, Vector] = {} armature_scales: Dict[Object, Vector] = {}
# Extract the scale from the world matrix of the evaluated armature object. # Extract the scale from the world matrix of the evaluated armature object.
for armature_object in options.armature_objects: for armature_object in export_sequence.armature_objects:
evaluated_armature_object = armature_object.evaluated_get(context.evaluated_depsgraph_get()) evaluated_armature_object = armature_object.evaluated_get(context.evaluated_depsgraph_get())
_, _, scale = evaluated_armature_object.matrix_world.decompose() _, _, scale = evaluated_armature_object.matrix_world.decompose()
scale *= options.scale scale *= options.scale
@@ -223,6 +222,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
# locations. # locations.
export_bones: List[PsaExportBone] = [] 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: for psx_bone, armature_object in psx_bone_create_result.bones:
if armature_object is None: if armature_object is None:
export_bones.append(PsaExportBone(None, None, Vector((1.0, 1.0, 1.0)))) 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): for action_item in filter(lambda x: x.is_selected, pg.action_list):
if len(action_item.action.fcurves) == 0: if len(action_item.action.fcurves) == 0:
continue continue
export_sequence = PsaBuildSequence(context.active_object, animation_data) export_sequence = PsaBuildSequence(self.armature_objects, animation_data)
export_sequence.name = action_item.name export_sequence.name = action_item.name
export_sequence.nla_state.action = action_item.action export_sequence.nla_state.action = action_item.action
export_sequence.nla_state.frame_start = action_item.frame_start 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) export_sequences.append(export_sequence)
case 'TIMELINE_MARKERS': case 'TIMELINE_MARKERS':
for marker_item in filter(lambda x: x.is_selected, pg.marker_list): for marker_item in filter(lambda x: x.is_selected, pg.marker_list):
export_sequence = PsaBuildSequence(context.active_object, animation_data) export_sequence = PsaBuildSequence(self.armature_objects, animation_data)
export_sequence.name = marker_item.name export_sequence.name = marker_item.name
export_sequence.nla_state.frame_start = marker_item.frame_start export_sequence.nla_state.frame_start = marker_item.frame_start
export_sequence.nla_state.frame_end = marker_item.frame_end 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) export_sequences.append(export_sequence)
case 'NLA_TRACK_STRIPS': case 'NLA_TRACK_STRIPS':
for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list): for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list):
export_sequence = PsaBuildSequence(context.active_object, animation_data) export_sequence = PsaBuildSequence(self.armature_objects, animation_data)
export_sequence.name = nla_strip_item.name export_sequence.name = nla_strip_item.name
export_sequence.nla_state.frame_start = nla_strip_item.frame_start export_sequence.nla_state.frame_start = nla_strip_item.frame_start
export_sequence.nla_state.frame_end = nla_strip_item.frame_end 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) export_sequences.append(export_sequence)
case 'ACTIVE_ACTION': case 'ACTIVE_ACTION':
for active_action_item in filter(lambda x: x.is_selected, pg.active_action_list): 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 action = active_action_item.action
export_sequence.name = action.name export_sequence.name = action.name
export_sequence.nla_state.action = action export_sequence.nla_state.action = action
@@ -522,8 +522,6 @@ class PSA_OT_export(Operator, ExportHelper):
return {'CANCELLED'} return {'CANCELLED'}
options = PsaBuildOptions() options = PsaBuildOptions()
options.armature_objects = self.armature_objects
options.animation_data = animation_data
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_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]

View File

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

@@ -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 # 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). # no shared frames between sequences (all exporters that I know of do this, so it's a safe assumption).
frame_start_index = 0 frame_start_index = 0
for i, sequence in enumerate(sequences): for sequence in sequences:
sequence.frame_start_index = frame_start_index sequence.frame_start_index = frame_start_index
frame_start_index += sequence.frame_count frame_start_index += sequence.frame_count
return True return True

View File

@@ -1,10 +1,14 @@
import os import os
from pathlib import Path from pathlib import Path
from bpy.props import CollectionProperty, StringProperty from typing import cast as typing_cast
from bpy.types import Context, FileHandler, Operator, OperatorFileListElement, UILayout from bpy.props import CollectionProperty, StringProperty, FloatProperty, EnumProperty
from bpy.types import Armature, Context, FileHandler, Operator, OperatorFileListElement, UILayout
from bpy_extras.io_utils import ImportHelper 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 ..importer import PskImportOptions, import_psk
from ..properties import PskImportMixin from ..properties import PskImportMixin
from ..reader import read_psk from ..reader import read_psk
@@ -162,6 +166,46 @@ class PSK_OT_import_drag_and_drop(Operator, PskImportMixin):
return {'FINISHED'} 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 # TODO: move to another file
class PSK_FH_import(FileHandler): class PSK_FH_import(FileHandler):
bl_idname = 'PSK_FH_import' bl_idname = 'PSK_FH_import'

View File

@@ -53,7 +53,7 @@ class PskImportResult:
self.mesh_object: Optional[Object] = None self.mesh_object: Optional[Object] = None
@property @property
def root_object(self) -> Object: def root_object(self) -> Optional[Object]:
return self.armature_object if self.armature_object is not None else self.mesh_object return self.armature_object if self.armature_object is not None else self.mesh_object