Bone group filtering appears to work correctly now

This commit is contained in:
Colin Basnett
2022-01-18 13:21:38 -08:00
parent 78837863e2
commit 7fd0c6de81
7 changed files with 226 additions and 113 deletions

View File

@@ -46,6 +46,7 @@ classes = [
psa_importer.PsaImportOperator, psa_importer.PsaImportOperator,
psa_importer.PsaImportFileSelectOperator, psa_importer.PsaImportFileSelectOperator,
psa_exporter.PSA_UL_ExportActionList, psa_exporter.PSA_UL_ExportActionList,
psa_exporter.PSA_UL_ExportBoneGroupList,
psa_importer.PSA_UL_ImportActionList, psa_importer.PSA_UL_ImportActionList,
psa_importer.PsaImportActionListItem, psa_importer.PsaImportActionListItem,
psa_importer.PsaImportSelectAll, psa_importer.PsaImportSelectAll,
@@ -56,6 +57,7 @@ classes = [
psa_exporter.PsaExportSelectAll, psa_exporter.PsaExportSelectAll,
psa_exporter.PsaExportDeselectAll, psa_exporter.PsaExportDeselectAll,
psa_exporter.PsaExportActionListItem, psa_exporter.PsaExportActionListItem,
psa_exporter.PsaExportBoneGroupListItem,
psa_exporter.PsaExportPropertyGroup, psa_exporter.PsaExportPropertyGroup,
] ]

View File

@@ -4,6 +4,8 @@ from .data import *
class PsaBuilderOptions(object): class PsaBuilderOptions(object):
def __init__(self): def __init__(self):
self.actions = [] self.actions = []
self.bone_filter_mode = 'NONE'
self.bone_group_indices = []
# https://git.cth451.me/cth451/blender-addons/blob/master/io_export_unreal_psk_psa.py # https://git.cth451.me/cth451/blender-addons/blob/master/io_export_unreal_psk_psa.py
@@ -12,7 +14,7 @@ class PsaBuilder(object):
# TODO: add options in here (selected anims, eg.) # TODO: add options in here (selected anims, eg.)
pass pass
def build(self, context, options) -> Psa: def build(self, context, options: PsaBuilderOptions) -> Psa:
object = context.view_layer.objects.active object = context.view_layer.objects.active
if object.type != 'ARMATURE': if object.type != 'ARMATURE':
@@ -35,28 +37,59 @@ class PsaBuilder(object):
pose_bones.sort(key=lambda x: x[0]) pose_bones.sort(key=lambda x: x[0])
pose_bones = [x[1] for x in pose_bones] pose_bones = [x[1] for x in pose_bones]
for bone in bones: bone_indices = list(range(len(bones)))
if options.bone_filter_mode == 'BONE_GROUPS':
# Get a list of the bone indices that are explicitly part of the bone groups we are including.
bone_index_stack = []
for bone_index, pose_bone in enumerate(pose_bones):
if pose_bone.bone_group_index in options.bone_group_indices:
bone_index_stack.append(bone_index)
# For each bone that is explicitly being added, recursively walk up the hierarchy and ensure that all of
# those bone indices are also in the list.
bone_indices = set()
while len(bone_index_stack) > 0:
bone_index = bone_index_stack.pop()
bone = bones[bone_index]
if bone.parent is not None:
parent_bone_index = bone_names.index(bone.parent.name)
if parent_bone_index not in bone_indices:
bone_index_stack.append(parent_bone_index)
bone_indices.add(bone_index)
del bone_names
# Sort out list of bone indices to be exported.
bone_indices = sorted(list(bone_indices))
# The bone lists now contains only the bones that are going to be exported.
bones = [bones[bone_index] for bone_index in bone_indices]
pose_bones = [pose_bones[bone_index] for bone_index in bone_indices]
for pose_bone in bones:
psa_bone = Psa.Bone() psa_bone = Psa.Bone()
psa_bone.name = bytes(bone.name, encoding='utf-8') psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
psa_bone.children_count = len(bone.children)
try: try:
psa_bone.parent_index = bones.index(bone.parent) parent_index = bones.index(pose_bone.parent)
psa_bone.parent_index = parent_index
psa.bones[parent_index].children_count += 1
except ValueError: except ValueError:
psa_bone.parent_index = -1 psa_bone.parent_index = -1
if bone.parent is not None: if pose_bone.parent is not None:
rotation = bone.matrix.to_quaternion() rotation = pose_bone.matrix.to_quaternion()
rotation.x = -rotation.x rotation.x = -rotation.x
rotation.y = -rotation.y rotation.y = -rotation.y
rotation.z = -rotation.z rotation.z = -rotation.z
quat_parent = bone.parent.matrix.to_quaternion().inverted() quat_parent = pose_bone.parent.matrix.to_quaternion().inverted()
parent_head = quat_parent @ bone.parent.head parent_head = quat_parent @ pose_bone.parent.head
parent_tail = quat_parent @ bone.parent.tail parent_tail = quat_parent @ pose_bone.parent.tail
location = (parent_tail - parent_head) + bone.head location = (parent_tail - parent_head) + pose_bone.head
else: else:
location = armature.matrix_local @ bone.head location = armature.matrix_local @ pose_bone.head
rot_matrix = bone.matrix @ armature.matrix_local.to_3x3() rot_matrix = pose_bone.matrix @ armature.matrix_local.to_3x3()
rotation = rot_matrix.to_quaternion() rotation = rot_matrix.to_quaternion()
psa_bone.location.x = location.x psa_bone.location.x = location.x
@@ -92,18 +125,18 @@ class PsaBuilder(object):
for frame in range(frame_count): for frame in range(frame_count):
context.scene.frame_set(frame_min + frame) context.scene.frame_set(frame_min + frame)
for bone in pose_bones: for pose_bone in pose_bones:
key = Psa.Key() key = Psa.Key()
pose_bone_matrix = bone.matrix pose_bone_matrix = pose_bone.matrix
if bone.parent is not None: if pose_bone.parent is not None:
pose_bone_parent_matrix = 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
location = pose_bone_matrix.to_translation() location = pose_bone_matrix.to_translation()
rotation = pose_bone_matrix.to_quaternion().normalized() rotation = pose_bone_matrix.to_quaternion().normalized()
if bone.parent is not None: if pose_bone.parent is not None:
rotation.x = -rotation.x rotation.x = -rotation.x
rotation.y = -rotation.y rotation.y = -rotation.y
rotation.z = -rotation.z rotation.z = -rotation.z

View File

@@ -1,6 +1,6 @@
import bpy import bpy
from bpy.types import Operator, PropertyGroup, Action, UIList from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty
from bpy_extras.io_utils import ExportHelper from bpy_extras.io_utils import ExportHelper
from typing import Type from typing import Type
from .builder import PsaBuilder, PsaBuilderOptions from .builder import PsaBuilder, PsaBuilderOptions
@@ -43,11 +43,28 @@ class PsaExportActionListItem(PropertyGroup):
return self.action.name return self.action.name
class PsaExportPropertyGroup(bpy.types.PropertyGroup): class PsaExportBoneGroupListItem(PropertyGroup):
name: StringProperty()
index: IntProperty()
is_selected: BoolProperty(default=False)
@property
def name(self):
return self.bone_group.name
class PsaExportPropertyGroup(PropertyGroup):
action_list: CollectionProperty(type=PsaExportActionListItem) action_list: CollectionProperty(type=PsaExportActionListItem)
import_action_list: CollectionProperty(type=PsaExportActionListItem) action_list_index: IntProperty(default=0)
action_list_index: IntProperty(name='index for list??', default=0) bone_filter_mode: EnumProperty(
import_action_list_index: IntProperty(name='index for list??', default=0) name='Bone Filter',
items={
('NONE', 'None', 'All bones will be exported.'),
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups will be exported.'),
}
)
bone_group_list: CollectionProperty(type=PsaExportBoneGroupListItem)
bone_group_list_index: IntProperty(default=0)
class PsaExportOperator(Operator, ExportHelper): class PsaExportOperator(Operator, ExportHelper):
@@ -68,14 +85,29 @@ class PsaExportOperator(Operator, ExportHelper):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
scene = context.scene scene = context.scene
box = layout.box() box = layout.box()
box.label(text='Actions', icon='ACTION') box.label(text='Actions', icon='ACTION')
row = box.row() row = box.row()
row.template_list('PSA_UL_ExportActionList', 'asd', scene.psa_export, 'action_list', scene.psa_export, 'action_list_index', rows=10) row.template_list('PSA_UL_ExportActionList', 'asd', scene.psa_export, 'action_list', scene.psa_export, 'action_list_index', rows=10)
row = box.row() row = box.row()
row.operator('psa_export.actions_select_all', text='Select All') row.operator('psa_export.actions_select_all', text='Select All')
row.operator('psa_export.actions_deselect_all', text='Deselect All') row.operator('psa_export.actions_deselect_all', text='Deselect All')
box = layout.box()
box.label(text='Bone Filter', icon='FILTER')
row = box.row()
row.alignment = 'EXPAND'
row.prop(scene.psa_export, 'bone_filter_mode', expand=True, text='Bone Filter')
if scene.psa_export.bone_filter_mode == 'BONE_GROUPS':
row = box.row()
rows = max(3, min(len(scene.psa_export.bone_group_list), 10))
row.template_list('PSA_UL_ExportBoneGroupList', 'asd', scene.psa_export, 'bone_group_list', scene.psa_export, 'bone_group_list_index', rows=rows)
def is_action_for_armature(self, action): def is_action_for_armature(self, action):
if len(action.fcurves) == 0: if len(action.fcurves) == 0:
return False return False
@@ -90,12 +122,17 @@ class PsaExportOperator(Operator, ExportHelper):
return False return False
def invoke(self, context, event): def invoke(self, context, event):
if context.view_layer.objects.active is None:
self.report({'ERROR_INVALID_CONTEXT'}, 'An armature must be selected')
return {'CANCELLED'}
if context.view_layer.objects.active.type != 'ARMATURE': if context.view_layer.objects.active.type != 'ARMATURE':
self.report({'ERROR_INVALID_CONTEXT'}, 'The selected object must be an armature.') self.report({'ERROR_INVALID_CONTEXT'}, 'The selected object must be an armature.')
return {'CANCELLED'} return {'CANCELLED'}
self.armature = context.view_layer.objects.active self.armature = context.view_layer.objects.active
# Populate actions list.
context.scene.psa_export.action_list.clear() context.scene.psa_export.action_list.clear()
for action in bpy.data.actions: for action in bpy.data.actions:
item = context.scene.psa_export.action_list.add() item = context.scene.psa_export.action_list.add()
@@ -105,10 +142,20 @@ class PsaExportOperator(Operator, ExportHelper):
item.is_selected = True item.is_selected = True
if len(context.scene.psa_export.action_list) == 0: if len(context.scene.psa_export.action_list) == 0:
# If there are no actions at all, we have nothing to export, so just cancel the operation.
self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.') self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.')
return {'CANCELLED'} return {'CANCELLED'}
# Populate bone groups list.
context.scene.psa_export.bone_group_list.clear()
for bone_group_index, bone_group in enumerate(self.armature.pose.bone_groups):
item = context.scene.psa_export.bone_group_list.add()
item.name = bone_group.name
item.index = bone_group_index
item.is_selected = False
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def execute(self, context): def execute(self, context):
@@ -120,6 +167,8 @@ class PsaExportOperator(Operator, ExportHelper):
options = PsaBuilderOptions() options = PsaBuilderOptions()
options.actions = actions options.actions = actions
options.bone_filter_mode = context.scene.psa_export.bone_filter_mode
options.bone_group_indices = [x.index for x in context.scene.psa_export.bone_group_list if x.is_selected]
builder = PsaBuilder() builder = PsaBuilder()
psa = builder.build(context, options) psa = builder.build(context, options)
exporter = PsaExporter(psa) exporter = PsaExporter(psa)
@@ -127,6 +176,13 @@ class PsaExportOperator(Operator, ExportHelper):
return {'FINISHED'} return {'FINISHED'}
class PSA_UL_ExportBoneGroupList(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.alignment = 'LEFT'
layout.prop(item, 'is_selected', icon_only=True)
layout.label(text=item.name, icon='GROUP_BONE')
class PSA_UL_ExportActionList(UIList): class PSA_UL_ExportActionList(UIList):
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_propname, index):
layout.alignment = 'LEFT' layout.alignment = 'LEFT'
@@ -134,7 +190,6 @@ class PSA_UL_ExportActionList(UIList):
layout.label(text=item.action_name) layout.label(text=item.action_name)
def filter_items(self, context, data, property): def filter_items(self, context, data, property):
# TODO: returns two lists, apparently
actions = getattr(data, property) actions = getattr(data, property)
flt_flags = [] flt_flags = []
flt_neworder = [] flt_neworder = []
@@ -153,6 +208,12 @@ class PsaExportSelectAll(bpy.types.Operator):
bl_idname = 'psa_export.actions_select_all' bl_idname = 'psa_export.actions_select_all'
bl_label = 'Select All' bl_label = 'Select All'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_export.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list))
return len(action_list) > 0 and has_unselected_actions
def execute(self, context): def execute(self, context):
for action in context.scene.psa_export.action_list: for action in context.scene.psa_export.action_list:
action.is_selected = True action.is_selected = True
@@ -163,6 +224,12 @@ class PsaExportDeselectAll(bpy.types.Operator):
bl_idname = 'psa_export.actions_deselect_all' bl_idname = 'psa_export.actions_deselect_all'
bl_label = 'Deselect All' bl_label = 'Deselect All'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_export.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
return len(action_list) > 0 and has_selected_actions
def execute(self, context): def execute(self, context):
for action in context.scene.psa_export.action_list: for action in context.scene.psa_export.action_list:
action.is_selected = False action.is_selected = False

View File

@@ -4,10 +4,11 @@ from mathutils import Vector, Quaternion, Matrix
from .data import Psa from .data import Psa
from typing import List, AnyStr, Optional from typing import List, AnyStr, Optional
import bpy import bpy
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
from .reader import PsaReader from .reader import PsaReader
import numpy as np
class PsaImporter(object): class PsaImporter(object):
@@ -30,14 +31,7 @@ class PsaImporter(object):
self.orig_loc: Vector = Vector() self.orig_loc: Vector = Vector()
self.orig_quat: Quaternion = Quaternion() self.orig_quat: Quaternion = Quaternion()
self.post_quat: Quaternion = Quaternion() self.post_quat: Quaternion = Quaternion()
# TODO: this is UGLY, come up with a way to just map indices for these self.fcurves = []
self.fcurve_quat_w = None
self.fcurve_quat_x = None
self.fcurve_quat_y = None
self.fcurve_quat_z = None
self.fcurve_location_x = None
self.fcurve_location_y = None
self.fcurve_location_z = None
# create an index mapping from bones in the PSA to bones in the target armature. # create an index mapping from bones in the PSA to bones in the target armature.
psa_to_armature_bone_indices = {} psa_to_armature_bone_indices = {}
@@ -103,90 +97,105 @@ class PsaImporter(object):
import_bone = import_bones[psa_bone_index] import_bone = import_bones[psa_bone_index]
pose_bone = import_bone.pose_bone pose_bone = import_bone.pose_bone
# rotation # create fcurves from rotation and location data
rotation_data_path = pose_bone.path_from_id('rotation_quaternion') rotation_data_path = pose_bone.path_from_id('rotation_quaternion')
import_bone.fcurve_quat_w = action.fcurves.new(rotation_data_path, index=0)
import_bone.fcurve_quat_x = action.fcurves.new(rotation_data_path, index=1)
import_bone.fcurve_quat_y = action.fcurves.new(rotation_data_path, index=2)
import_bone.fcurve_quat_z = action.fcurves.new(rotation_data_path, index=3)
# location
location_data_path = pose_bone.path_from_id('location') location_data_path = pose_bone.path_from_id('location')
import_bone.fcurve_location_x = action.fcurves.new(location_data_path, index=0) import_bone.fcurves.extend([
import_bone.fcurve_location_y = action.fcurves.new(location_data_path, index=1) action.fcurves.new(rotation_data_path, index=0), # Qw
import_bone.fcurve_location_z = action.fcurves.new(location_data_path, index=2) action.fcurves.new(rotation_data_path, index=1), # Qx
action.fcurves.new(rotation_data_path, index=2), # Qy
action.fcurves.new(rotation_data_path, index=3), # Qz
action.fcurves.new(location_data_path, index=0), # Lx
action.fcurves.new(location_data_path, index=1), # Ly
action.fcurves.new(location_data_path, index=2), # Lz
])
# add keyframes
import_bone.fcurve_quat_w.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_quat_x.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_quat_y.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_quat_z.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_location_x.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_location_y.keyframe_points.add(sequence.frame_count)
import_bone.fcurve_location_z.keyframe_points.add(sequence.frame_count)
should_invert_root = False
key_index = 0 key_index = 0
# Read the sequence keys from the PSA file.
sequence_name = sequence.name.decode('windows-1252') sequence_name = sequence.name.decode('windows-1252')
sequence_keys = psa_reader.get_sequence_keys(sequence_name) sequence_keys = psa_reader.read_sequence_keys(sequence_name)
for frame_index in range(sequence.frame_count): for frame_index in range(sequence.frame_count):
for import_bone in import_bones: for bone_index, import_bone in enumerate(import_bones):
if import_bone is None: if import_bone is None:
# bone does not exist in the armature, skip it # bone does not exist in the armature, skip it
key_index += 1 key_index += 1
continue continue
key_location = Vector(tuple(sequence_keys[key_index].location)) # Convert world-space transforms to local-space transforms.
key_rotation = Quaternion(tuple(sequence_keys[key_index].rotation)) key_rotation = Quaternion(tuple(sequence_keys[key_index].rotation))
q = import_bone.post_quat.copy() q = import_bone.post_quat.copy()
q.rotate(import_bone.orig_quat) q.rotate(import_bone.orig_quat)
quat = q quat = q
q = import_bone.post_quat.copy() q = import_bone.post_quat.copy()
if import_bone.parent is None and not should_invert_root: if import_bone.parent is None:
q.rotate(key_rotation.conjugated()) q.rotate(key_rotation.conjugated())
else: else:
q.rotate(key_rotation) q.rotate(key_rotation)
quat.rotate(q.conjugated()) quat.rotate(q.conjugated())
key_location = Vector(tuple(sequence_keys[key_index].location))
loc = key_location - import_bone.orig_loc loc = key_location - import_bone.orig_loc
loc.rotate(import_bone.post_quat.conjugated()) loc.rotate(import_bone.post_quat.conjugated())
import_bone.fcurve_quat_w.keyframe_points[frame_index].co = frame_index, quat.w bone_fcurve_data = quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z
import_bone.fcurve_quat_x.keyframe_points[frame_index].co = frame_index, quat.x for fcurve, datum in zip(import_bone.fcurves, bone_fcurve_data):
import_bone.fcurve_quat_y.keyframe_points[frame_index].co = frame_index, quat.y fcurve.keyframe_points.insert(frame_index, datum)
import_bone.fcurve_quat_z.keyframe_points[frame_index].co = frame_index, quat.z
import_bone.fcurve_location_x.keyframe_points[frame_index].co = frame_index, loc.x
import_bone.fcurve_location_y.keyframe_points[frame_index].co = frame_index, loc.y
import_bone.fcurve_location_z.keyframe_points[frame_index].co = frame_index, loc.z
key_index += 1 key_index += 1
class PsaImportActionListItem(PropertyGroup): class PsaImportActionListItem(PropertyGroup):
action_name: StringProperty() action_name: StringProperty()
is_selected: BoolProperty(default=True) frame_count: IntProperty()
is_selected: BoolProperty(default=False)
@property @property
def name(self): def name(self):
return self.action_name return self.action_name
def on_psa_filepath_updated(property, context):
context.scene.psa_import.action_list.clear()
try:
# Read the file and populate the action list.
psa = PsaReader(context.scene.psa_import.psa_filepath).psa
for sequence in psa.sequences.values():
item = context.scene.psa_import.action_list.add()
item.action_name = sequence.name.decode('windows-1252')
item.frame_count = sequence.frame_count
item.is_selected = True
except IOError:
# TODO: set an error somewhere so the user knows the PSA could not be read.
pass
class PsaImportPropertyGroup(bpy.types.PropertyGroup): class PsaImportPropertyGroup(bpy.types.PropertyGroup):
cool_filepath: StringProperty(default='') psa_filepath: StringProperty(default='', subtype='FILE_PATH', update=on_psa_filepath_updated)
armature_object: PointerProperty(type=bpy.types.Object) # TODO: figure out how to filter this to only objects of a specific type armature_object: PointerProperty(name='Armature', type=bpy.types.Object)
action_list: CollectionProperty(type=PsaImportActionListItem) action_list: CollectionProperty(type=PsaImportActionListItem)
import_action_list: CollectionProperty(type=PsaImportActionListItem) action_list_index: IntProperty(name='', default=0)
action_list_index: IntProperty(name='index for list??', default=0) action_filter_name: StringProperty(default='')
import_action_list_index: IntProperty(name='index for list??', default=0)
class PSA_UL_ImportActionList(UIList): class PSA_UL_ImportActionList(UIList):
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_propname, index):
layout.alignment = 'LEFT' row = layout.row(align=True)
layout.prop(item, 'is_selected', icon_only=True) split = row.split(align=True, factor=0.75)
layout.label(text=item.action_name) action_col = split.row(align=True)
action_col.alignment = 'LEFT'
action_col.prop(item, 'is_selected', icon_only=True)
action_col.label(text=item.action_name)
def draw_filter(self, context, layout):
row = layout.row()
subrow = row.row(align=True)
subrow.prop(self, 'filter_name', text="")
subrow.prop(self, 'use_filter_invert', text="", icon='ARROW_LEFTRIGHT')
subrow = row.row(align=True)
subrow.prop(self, 'use_filter_sort_reverse', text='', icon='SORT_ASC')
def filter_items(self, context, data, property): def filter_items(self, context, data, property):
actions = getattr(data, property) actions = getattr(data, property)
@@ -205,7 +214,13 @@ class PSA_UL_ImportActionList(UIList):
class PsaImportSelectAll(bpy.types.Operator): class PsaImportSelectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_select_all' bl_idname = 'psa_import.actions_select_all'
bl_label = 'Select All' bl_label = 'All'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list))
return len(action_list) > 0 and has_unselected_actions
def execute(self, context): def execute(self, context):
for action in context.scene.psa_import.action_list: for action in context.scene.psa_import.action_list:
@@ -215,7 +230,13 @@ class PsaImportSelectAll(bpy.types.Operator):
class PsaImportDeselectAll(bpy.types.Operator): class PsaImportDeselectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_deselect_all' bl_idname = 'psa_import.actions_deselect_all'
bl_label = 'Deselect All' bl_label = 'None'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
return len(action_list) > 0 and has_selected_actions
def execute(self, context): def execute(self, context):
for action in context.scene.psa_import.action_list: for action in context.scene.psa_import.action_list:
@@ -234,25 +255,34 @@ class PSA_PT_ImportPanel(Panel):
layout = self.layout layout = self.layout
scene = context.scene scene = context.scene
row = layout.row() row = layout.row()
row.operator('psa_import.file_select', icon='FILE_FOLDER', text='') row.prop(scene.psa_import, 'psa_filepath', text='PSA File')
row.label(text=scene.psa_import.cool_filepath) row = layout.row()
row.prop_search(scene.psa_import, 'armature_object', bpy.data, 'objects')
box = layout.box() box = layout.box()
box.label(text='Actions', icon='ACTION') box.label(text=f'Actions ({len(scene.psa_import.action_list)})', icon='ACTION')
row = box.row() row = box.row()
row.template_list('PSA_UL_ImportActionList', 'asd', scene.psa_import, 'action_list', scene.psa_import, 'action_list_index', rows=10) rows = max(3, min(len(scene.psa_import.action_list), 10))
row = box.row() row.template_list('PSA_UL_ImportActionList', 'asd', scene.psa_import, 'action_list', scene.psa_import, 'action_list_index', rows=rows)
row.operator('psa_import.actions_select_all', text='Select All') row = box.row(align=True)
row.operator('psa_import.actions_deselect_all', text='Deselect All') row.label(text='Select')
layout.prop(scene.psa_import, 'armature_object', icon_only=True) row.operator('psa_import.actions_select_all', text='All')
layout.operator('psa_import.import', text='Import') row.operator('psa_import.actions_deselect_all', text='None')
layout.operator('psa_import.import', text=f'Import')
class PsaImportOperator(Operator): class PsaImportOperator(Operator):
bl_idname = 'psa_import.import' bl_idname = 'psa_import.import'
bl_label = 'Import' bl_label = 'Import'
@classmethod
def poll(cls, context):
action_list = context.scene.psa_import.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
armature_object = context.scene.psa_import.armature_object
return has_selected_actions and armature_object is not None
def execute(self, context): def execute(self, context):
psa_reader = PsaReader(context.scene.psa_import.cool_filepath) psa_reader = PsaReader(context.scene.psa_import.psa_filepath)
sequence_names = [x.action_name for x in context.scene.psa_import.action_list if x.is_selected] sequence_names = [x.action_name for x in context.scene.psa_import.action_list if x.is_selected]
PsaImporter().import_psa(psa_reader, sequence_names, context) PsaImporter().import_psa(psa_reader, sequence_names, context)
return {'FINISHED'} return {'FINISHED'}
@@ -274,16 +304,6 @@ class PsaImportFileSelectOperator(Operator, ImportHelper):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def execute(self, context): def execute(self, context):
context.scene.psa_import.cool_filepath = self.filepath context.scene.psa_import.psa_filepath = self.filepath
# Load the sequence names from the selected file # Load the sequence names from the selected file
sequence_names = []
try:
sequence_names = PsaReader.scan_sequence_names(self.filepath)
except IOError:
pass
context.scene.psa_import.action_list.clear()
for sequence_name in sequence_names:
item = context.scene.psa_import.action_list.add()
item.action_name = sequence_name.decode('windows-1252')
item.is_selected = True
return {'FINISHED'} return {'FINISHED'}

View File

@@ -34,19 +34,13 @@ class PsaReader(object):
fp.seek(section.data_size * section.data_count, 1) fp.seek(section.data_size * section.data_count, 1)
return [] return []
def get_sequence_keys(self, sequence_name) -> List[Psa.Key]: def read_sequence_keys(self, sequence_name) -> List[Psa.Key]:
# Set the file reader to the beginning of the keys data # Set the file reader to the beginning of the keys data
sequence = self.psa.sequences[sequence_name] sequence = self.psa.sequences[sequence_name]
data_size = sizeof(Psa.Key) data_size = sizeof(Psa.Key)
bone_count = len(self.psa.bones) bone_count = len(self.psa.bones)
buffer_length = data_size * bone_count * sequence.frame_count buffer_length = data_size * bone_count * sequence.frame_count
print(f'data_size: {data_size}')
print(f'buffer_length: {buffer_length}')
print(f'bone_count: {bone_count}')
print(f'sequence.frame_count: {sequence.frame_count}')
print(f'self.keys_data_offset: {self.keys_data_offset}')
sequence_keys_offset = self.keys_data_offset + (sequence.frame_start_index * bone_count * data_size) sequence_keys_offset = self.keys_data_offset + (sequence.frame_start_index * bone_count * data_size)
print(f'sequence_keys_offset: {sequence_keys_offset}')
self.fp.seek(sequence_keys_offset, 0) self.fp.seek(sequence_keys_offset, 0)
buffer = self.fp.read(buffer_length) buffer = self.fp.read(buffer_length)
offset = 0 offset = 0

View File

@@ -40,7 +40,7 @@ class PskBuilder(object):
modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE'] modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE']
if len(modifiers) == 0: if len(modifiers) == 0:
continue continue
elif len(modifiers) == 2: elif len(modifiers) > 1:
raise RuntimeError(f'Mesh "{obj.name}" must have only one armature modifier') raise RuntimeError(f'Mesh "{obj.name}" must have only one armature modifier')
armature_modifier_objects.add(modifiers[0].object) armature_modifier_objects.add(modifiers[0].object)

View File

@@ -48,7 +48,6 @@ class PskImporter(object):
self.post_quat: Quaternion = Quaternion() self.post_quat: Quaternion = Quaternion()
import_bones = [] import_bones = []
should_invert_root = False
new_bone_size = 8.0 new_bone_size = 8.0
for bone_index, psk_bone in enumerate(psk.bones): for bone_index, psk_bone in enumerate(psk.bones):
@@ -57,10 +56,7 @@ class PskImporter(object):
import_bone.local_rotation = Quaternion(tuple(psk_bone.rotation)) import_bone.local_rotation = Quaternion(tuple(psk_bone.rotation))
import_bone.local_translation = Vector(tuple(psk_bone.location)) import_bone.local_translation = Vector(tuple(psk_bone.location))
if psk_bone.parent_index == 0 and bone_index == 0: if psk_bone.parent_index == 0 and bone_index == 0:
if should_invert_root: import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix()
import_bone.world_rotation_matrix = import_bone.local_rotation.conjugated().to_matrix()
else:
import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix()
import_bone.world_matrix = Matrix.Translation(import_bone.local_translation) import_bone.world_matrix = Matrix.Translation(import_bone.local_translation)
import_bones.append(import_bone) import_bones.append(import_bone)
@@ -82,7 +78,7 @@ class PskImporter(object):
if import_bone.parent is not None: if import_bone.parent is not None:
edit_bone.parent = armature_data.edit_bones[import_bone.psk_bone.parent_index] edit_bone.parent = armature_data.edit_bones[import_bone.psk_bone.parent_index]
elif not should_invert_root: else:
import_bone.local_rotation.conjugate() import_bone.local_rotation.conjugate()
edit_bone.tail = Vector((0.0, new_bone_size, 0.0)) edit_bone.tail = Vector((0.0, new_bone_size, 0.0))
@@ -126,7 +122,8 @@ class PskImporter(object):
degenerate_face_indices.add(face_index) degenerate_face_indices.add(face_index)
pass pass
print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).') if len(degenerate_face_indices) > 0:
print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).')
bm.to_mesh(mesh_data) bm.to_mesh(mesh_data)