Added the ability to export sequences using timeline markers (WIP, not thoroughly tested yet!)

A bunch of clean up
This commit is contained in:
Colin Basnett
2022-02-11 15:21:31 -08:00
parent b58b44cafb
commit 7ad8f0238a
8 changed files with 262 additions and 75 deletions

View File

@@ -1,4 +1,6 @@
from bpy.types import NlaStrip
from typing import List from typing import List
from collections import Counter
def rgb_to_srgb(c): def rgb_to_srgb(c):
@@ -8,19 +10,45 @@ def rgb_to_srgb(c):
return 12.92 * c return 12.92 * c
def get_nla_strips_ending_at_frame(object, frame) -> List[NlaStrip]:
if object is None or object.animation_data is None:
return []
strips = []
for nla_track in object.animation_data.nla_tracks:
for strip in nla_track.strips:
if strip.frame_end == frame:
strips.append(strip)
return strips
def get_nla_strips_in_timeframe(object, frame_min, frame_max) -> List[NlaStrip]:
if object is None or object.animation_data is None:
return []
strips = []
for nla_track in object.animation_data.nla_tracks:
for strip in nla_track.strips:
if strip.frame_end >= frame_min and strip.frame_start <= frame_max:
strips.append(strip)
return strips
def populate_bone_group_list(armature_object, bone_group_list): def populate_bone_group_list(armature_object, bone_group_list):
bone_group_list.clear() bone_group_list.clear()
item = bone_group_list.add()
item.name = '(unassigned)'
item.index = -1
item.is_selected = True
if armature_object and armature_object.pose: if armature_object and armature_object.pose:
bone_group_counts = Counter(map(lambda x: x.bone_group, armature_object.pose.bones))
item = bone_group_list.add()
item.name = 'Unassigned'
item.index = -1
item.count = 0 if None not in bone_group_counts else bone_group_counts[None]
item.is_selected = True
for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups): for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups):
item = bone_group_list.add() item = bone_group_list.add()
item.name = bone_group.name item.name = bone_group.name
item.index = bone_group_index item.index = bone_group_index
item.count = 0 if bone_group not in bone_group_counts else bone_group_counts[bone_group]
item.is_selected = True item.is_selected = True

View File

@@ -1,13 +1,17 @@
from .data import * from .data import *
from ..helpers import * from ..helpers import *
from typing import Dict
class PsaBuilderOptions(object): class PsaBuilderOptions(object):
def __init__(self): def __init__(self):
self.sequence_source = 'ACTIONS'
self.actions = [] self.actions = []
self.marker_names = []
self.bone_filter_mode = 'ALL' self.bone_filter_mode = 'ALL'
self.bone_group_indices = [] self.bone_group_indices = []
self.should_use_original_sequence_names = False self.should_use_original_sequence_names = False
self.should_trim_timeline_marker_sequences = True
class PsaBuilder(object): class PsaBuilder(object):
@@ -25,6 +29,12 @@ class PsaBuilder(object):
if armature.animation_data is None: if armature.animation_data is None:
raise RuntimeError('No animation data for armature') raise RuntimeError('No animation data for armature')
# Ensure that we actually have items that we are going to be exporting.
if options.sequence_source == 'ACTIONS' and len(options.actions) == 0:
raise RuntimeError('No actions were selected for export')
elif options.sequence_source == 'TIMELINE_MARKERS' and len(options.marker_names) == 0:
raise RuntimeError('No timeline markers were selected for export')
psa = Psa() psa = Psa()
bones = list(armature.data.bones) bones = list(armature.data.bones)
@@ -59,6 +69,7 @@ class PsaBuilder(object):
raise RuntimeError('Exported bone hierarchy must have a single root bone.' raise RuntimeError('Exported bone hierarchy must have a single root bone.'
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}') f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
# Build list of PSA bones.
for pose_bone in bones: for pose_bone in bones:
psa_bone = Psa.Bone() psa_bone = Psa.Bone()
psa_bone.name = bytes(pose_bone.name, encoding='utf-8') psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
@@ -95,28 +106,65 @@ class PsaBuilder(object):
psa.bones.append(psa_bone) psa.bones.append(psa_bone)
# Populate the export sequence list.
class ExportSequence:
def __init__(self):
self.name = ''
self.frame_min = 0
self.frame_max = 0
self.action = None
self.nla_strips_to_be_muted = []
export_sequences = []
if options.sequence_source == 'ACTIONS':
for action in options.actions:
if len(action.fcurves) == 0:
continue
export_sequence = ExportSequence()
export_sequence.action = action
export_sequence.name = get_psa_sequence_name(action, options.should_use_original_sequence_names)
export_sequence.frame_min, export_sequence.frame_max = [int(x) for x in action.frame_range]
export_sequences.append(export_sequence)
pass
elif options.sequence_source == 'TIMELINE_MARKERS':
sequence_frame_ranges = self.get_timeline_marker_sequence_frame_ranges(armature, context, options)
for name, (frame_min, frame_max) in sequence_frame_ranges.items():
export_sequence = ExportSequence()
export_sequence.action = None
export_sequence.name = name
export_sequence.frame_min = frame_min
export_sequence.frame_max = frame_max
export_sequence.nla_strips_to_be_muted = get_nla_strips_ending_at_frame(armature, frame_min)
export_sequences.append(export_sequence)
else:
raise ValueError(f'Unhandled sequence source: {options.sequence_source}')
frame_start_index = 0 frame_start_index = 0
for action in options.actions: # Now build the PSA sequences.
if len(action.fcurves) == 0: # We actually alter the timeline frame and simply record the resultant pose bone matrices.
continue for export_sequence in export_sequences:
armature.animation_data.action = export_sequence.action
armature.animation_data.action = action
context.view_layer.update() context.view_layer.update()
frame_min, frame_max = [int(x) for x in action.frame_range] psa_sequence = Psa.Sequence()
sequence = Psa.Sequence() frame_min = export_sequence.frame_min
frame_max = export_sequence.frame_max
sequence_name = get_psa_sequence_name(action, options.should_use_original_sequence_names) psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
psa_sequence.frame_count = frame_max - frame_min + 1
sequence.name = bytes(sequence_name, encoding='windows-1252') psa_sequence.frame_start_index = frame_start_index
sequence.frame_count = frame_max - frame_min + 1 psa_sequence.fps = context.scene.render.fps
sequence.frame_start_index = frame_start_index
sequence.fps = context.scene.render.fps
frame_count = frame_max - frame_min + 1 frame_count = frame_max - frame_min + 1
# Store the mute state of the NLA strips we need to mute so we can restore the state after we are done.
nla_strip_mute_statuses = {x: x.mute for x in export_sequence.nla_strips_to_be_muted}
for nla_strip in export_sequence.nla_strips_to_be_muted:
nla_strip.mute = True
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)
@@ -143,15 +191,54 @@ class PsaBuilder(object):
key.rotation.y = rotation.y key.rotation.y = rotation.y
key.rotation.z = rotation.z key.rotation.z = rotation.z
key.rotation.w = rotation.w key.rotation.w = rotation.w
key.time = 1.0 / sequence.fps key.time = 1.0 / psa_sequence.fps
psa.keys.append(key) psa.keys.append(key)
frame_start_index += 1 export_sequence.bone_count = len(pose_bones)
export_sequence.track_time = frame_count
sequence.bone_count = len(pose_bones) # Restore the mute state of the NLA strips we muted beforehand.
sequence.track_time = frame_count for nla_strip, mute in nla_strip_mute_statuses.items():
nla_strip.mute = mute
psa.sequences[action.name] = sequence frame_start_index += frame_count
psa.sequences[export_sequence.name] = psa_sequence
return psa return psa
def get_timeline_marker_sequence_frame_ranges(self, object, context, options: PsaBuilderOptions) -> Dict:
# Timeline markers need to be sorted so that we can determine the sequence start and end positions.
sequence_frame_ranges = dict()
sorted_timeline_markers = list(sorted(context.scene.timeline_markers, key=lambda x: x.frame))
sorted_timeline_marker_names = list(map(lambda x: x.name, sorted_timeline_markers))
for marker_name in options.marker_names:
marker = context.scene.timeline_markers[marker_name]
frame_min = marker.frame
# Determine the final frame of the sequence based on the next marker.
# If no subsequent marker exists, use the maximum frame_end from all NLA strips.
marker_index = sorted_timeline_marker_names.index(marker_name)
next_marker_index = marker_index + 1
frame_max = 0
if next_marker_index < len(sorted_timeline_markers):
# There is a next marker. Use that next marker's frame position as the last frame of this sequence.
frame_max = sorted_timeline_markers[next_marker_index].frame
if options.should_trim_timeline_marker_sequences:
nla_strips = get_nla_strips_in_timeframe(object, marker.frame, frame_max)
frame_max = min(frame_max, max(map(lambda x: x.frame_end, nla_strips)))
frame_min = max(frame_min, min(map(lambda x: x.frame_start, nla_strips)))
else:
# There is no next marker.
# Find the final frame of all the NLA strips and use that as the last frame of this sequence.
for nla_track in object.animation_data.nla_tracks:
for strip in nla_track.strips:
frame_max = max(frame_max, strip.frame_end)
if frame_min == frame_max:
continue
sequence_frame_ranges[marker_name] = int(frame_min), int(frame_max)
return sequence_frame_ranges

View File

@@ -1,5 +1,5 @@
import bpy import bpy
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel, TimelineMarker
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty 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
@@ -46,6 +46,16 @@ class PsaExportActionListItem(PropertyGroup):
return self.action.name return self.action.name
class PsaExportTimelineMarkerListItem(PropertyGroup):
marker_index: IntProperty()
marker_name: StringProperty()
is_selected: BoolProperty(default=True)
@property
def name(self):
return self.marker_name
def update_action_names(context): def update_action_names(context):
pg = context.scene.psa_export pg = context.scene.psa_export
for item in pg.action_list: for item in pg.action_list:
@@ -58,18 +68,28 @@ def should_use_original_sequence_names_updated(property, context):
class PsaExportPropertyGroup(PropertyGroup): class PsaExportPropertyGroup(PropertyGroup):
sequence_source: EnumProperty(
name='Source',
description='',
items=(
('ACTIONS', 'Actions', 'Sequences will be exported using actions'),
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers'),
)
)
action_list: CollectionProperty(type=PsaExportActionListItem) action_list: CollectionProperty(type=PsaExportActionListItem)
action_list_index: IntProperty(default=0) action_list_index: IntProperty(default=0)
marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem)
marker_list_index: IntProperty(default=0)
bone_filter_mode: EnumProperty( bone_filter_mode: EnumProperty(
name='Bone Filter', name='Bone Filter',
description='', description='',
items=( items=(
('ALL', 'All', 'All bones will be exported.'), ('ALL', 'All', 'All bones will be exported.'),
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.') ('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.'),
) )
) )
bone_group_list: CollectionProperty(type=BoneGroupListItem) bone_group_list: CollectionProperty(type=BoneGroupListItem)
bone_group_list_index: IntProperty(default=0) bone_group_list_index: IntProperty(default=0, name='', description='')
should_use_original_sequence_names: BoolProperty(default=False, name='Original Names', description='If the action was imported from the PSA Import panel, the original name of the sequence will be used instead of the Blender action name', update=should_use_original_sequence_names_updated) should_use_original_sequence_names: BoolProperty(default=False, name='Original Names', description='If the action was imported from the PSA Import panel, the original name of the sequence will be used instead of the Blender action name', update=should_use_original_sequence_names_updated)
@@ -84,6 +104,7 @@ def is_bone_filter_mode_item_available(context, identifier):
class PsaExportOperator(Operator, ExportHelper): class PsaExportOperator(Operator, ExportHelper):
bl_idname = 'psa_export.operator' bl_idname = 'psa_export.operator'
bl_label = 'Export' bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
__doc__ = 'Export actions to PSA' __doc__ = 'Export actions to PSA'
filename_ext = '.psa' filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
@@ -100,24 +121,34 @@ class PsaExportOperator(Operator, ExportHelper):
layout = self.layout layout = self.layout
pg = context.scene.psa_export pg = context.scene.psa_export
# ACTIONS # SOURCE
layout.label(text='Actions', icon='ACTION') layout.prop(pg, 'sequence_source', text='Source')
row = layout.row(align=True)
row.label(text='Select')
row.operator(PsaExportSelectAll.bl_idname, text='All')
row.operator(PsaExportDeselectAll.bl_idname, text='None')
row = layout.row()
rows = max(3, min(len(pg.action_list), 10))
row.template_list('PSA_UL_ExportActionList', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
col = layout.column(heading="Options") # ACTIONS
col.use_property_split = True if pg.sequence_source == 'ACTIONS':
col.use_property_decorate = False layout.label(text='Actions', icon='ACTION')
col.prop(pg, 'should_use_original_sequence_names') row = layout.row(align=True)
row.label(text='Select')
row.operator(PsaExportSelectAll.bl_idname, text='All')
row.operator(PsaExportDeselectAll.bl_idname, text='None')
row = layout.row()
rows = max(3, min(len(pg.action_list), 10))
row.template_list('PSA_UL_ExportActionList', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_use_original_sequence_names')
elif pg.sequence_source == 'TIMELINE_MARKERS':
layout.label(text='Markers', icon='MARKER')
row = layout.row()
rows = max(3, min(len(pg.marker_list), 10))
row.template_list('PSA_UL_ExportTimelineMarkerList', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
# Determine if there is going to be a naming conflict and display an error, if so. # Determine if there is going to be a naming conflict and display an error, if so.
selected_actions = [x for x in pg.action_list if x.is_selected] selected_items = [x for x in pg.action_list if x.is_selected]
action_names = [x.action_name for x in selected_actions] action_names = [x.action_name for x in selected_items]
action_name_counts = Counter(action_names) action_name_counts = Counter(action_names)
for action_name, count in action_name_counts.items(): for action_name, count in action_name_counts.items():
if count > 1: if count > 1:
@@ -180,9 +211,15 @@ class PsaExportOperator(Operator, ExportHelper):
update_action_names(context) update_action_names(context)
if len(pg.action_list) == 0: # Populate timeline markers list.
pg.marker_list.clear()
for marker in context.scene.timeline_markers:
item = pg.marker_list.add()
item.marker_name = marker.name
if len(pg.action_list) == 0 and len(pg.marker_names) == 0:
# If there are no actions at all, we have nothing to export, so just cancel the operation. # 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 or timeline markers to export.')
return {'CANCELLED'} return {'CANCELLED'}
# Populate bone groups list. # Populate bone groups list.
@@ -194,28 +231,51 @@ class PsaExportOperator(Operator, ExportHelper):
def execute(self, context): def execute(self, context):
pg = context.scene.psa_export pg = context.scene.psa_export
actions = [x.action for x in pg.action_list if x.is_selected]
if len(actions) == 0: actions = [x.action for x in pg.action_list if x.is_selected]
self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.') marker_names = [x.marker_name for x in pg.marker_list if x.is_selected]
return {'CANCELLED'}
options = PsaBuilderOptions() options = PsaBuilderOptions()
options.sequence_source = pg.sequence_source
options.actions = actions options.actions = actions
options.marker_names = marker_names
options.bone_filter_mode = pg.bone_filter_mode options.bone_filter_mode = pg.bone_filter_mode
options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected] options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected]
options.should_use_original_sequence_names = pg.should_use_original_sequence_names options.should_use_original_sequence_names = pg.should_use_original_sequence_names
builder = PsaBuilder() builder = PsaBuilder()
try: try:
psa = builder.build(context, options) psa = builder.build(context, options)
except RuntimeError as e: except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e)) self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'} return {'CANCELLED'}
exporter = PsaExporter(psa) exporter = PsaExporter(psa)
exporter.export(self.filepath) exporter.export(self.filepath)
return {'FINISHED'} return {'FINISHED'}
class PSA_UL_ExportTimelineMarkerList(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.marker_name)
def filter_items(self, context, data, property):
actions = getattr(data, property)
flt_flags = []
flt_neworder = []
if self.filter_name:
flt_flags = bpy.types.UI_UL_list.filter_items_by_name(
self.filter_name,
self.bitflag_filter_item,
actions,
'marker_name',
reverse=self.use_filter_invert
)
return flt_flags, flt_neworder
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'
@@ -237,17 +297,18 @@ class PSA_UL_ExportActionList(UIList):
return flt_flags, flt_neworder return flt_flags, flt_neworder
class PsaExportSelectAll(bpy.types.Operator): class PsaExportSelectAll(Operator):
bl_idname = 'psa_export.actions_select_all' bl_idname = 'psa_export.actions_select_all'
bl_label = 'Select All' bl_label = 'Select All'
bl_description = 'Select all actions' bl_description = 'Select all actions'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
pg = context.scene.psa_export pg = context.scene.psa_export
action_list = pg.action_list item_list = pg.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list)) has_unselected_actions = any(map(lambda action: not action.is_selected, item_list))
return len(action_list) > 0 and has_unselected_actions return len(item_list) > 0 and has_unselected_actions
def execute(self, context): def execute(self, context):
pg = context.scene.psa_export pg = context.scene.psa_export
@@ -256,17 +317,18 @@ class PsaExportSelectAll(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class PsaExportDeselectAll(bpy.types.Operator): class PsaExportDeselectAll(Operator):
bl_idname = 'psa_export.actions_deselect_all' bl_idname = 'psa_export.actions_deselect_all'
bl_label = 'Deselect All' bl_label = 'Deselect All'
bl_description = 'Deselect all actions' bl_description = 'Deselect all actions'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
pg = context.scene.psa_export pg = context.scene.psa_export
action_list = pg.action_list item_list = pg.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list)) has_selected_actions = any(map(lambda action: action.is_selected, item_list))
return len(action_list) > 0 and has_selected_actions return len(item_list) > 0 and has_selected_actions
def execute(self, context): def execute(self, context):
pg = context.scene.psa_export pg = context.scene.psa_export
@@ -277,9 +339,11 @@ class PsaExportDeselectAll(bpy.types.Operator):
classes = ( classes = (
PsaExportActionListItem, PsaExportActionListItem,
PsaExportTimelineMarkerListItem,
PsaExportPropertyGroup, PsaExportPropertyGroup,
PsaExportOperator, PsaExportOperator,
PSA_UL_ExportActionList, PSA_UL_ExportActionList,
PSA_UL_ExportTimelineMarkerList,
PsaExportSelectAll, PsaExportSelectAll,
PsaExportDeselectAll, PsaExportDeselectAll,
) )

View File

@@ -293,10 +293,11 @@ class PSA_UL_ImportActionList(PSA_UL_SequenceList, UIList):
pass pass
class PsaImportSequencesSelectAll(bpy.types.Operator): class PsaImportSequencesSelectAll(Operator):
bl_idname = 'psa_import.sequences_select_all' bl_idname = 'psa_import.sequences_select_all'
bl_label = 'All' bl_label = 'All'
bl_description = 'Select all sequences' bl_description = 'Select all sequences'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -312,10 +313,11 @@ class PsaImportSequencesSelectAll(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class PsaImportActionsSelectAll(bpy.types.Operator): class PsaImportActionsSelectAll(Operator):
bl_idname = 'psa_import.actions_select_all' bl_idname = 'psa_import.actions_select_all'
bl_label = 'All' bl_label = 'All'
bl_description = 'Select all actions' bl_description = 'Select all actions'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -331,10 +333,11 @@ class PsaImportActionsSelectAll(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class PsaImportSequencesDeselectAll(bpy.types.Operator): class PsaImportSequencesDeselectAll(Operator):
bl_idname = 'psa_import.sequences_deselect_all' bl_idname = 'psa_import.sequences_deselect_all'
bl_label = 'None' bl_label = 'None'
bl_description = 'Deselect all sequences' bl_description = 'Deselect all sequences'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -350,10 +353,11 @@ class PsaImportSequencesDeselectAll(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class PsaImportActionsDeselectAll(bpy.types.Operator): class PsaImportActionsDeselectAll(Operator):
bl_idname = 'psa_import.actions_deselect_all' bl_idname = 'psa_import.actions_deselect_all'
bl_label = 'None' bl_label = 'None'
bl_description = 'Deselect all actions' bl_description = 'Deselect all actions'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -406,7 +410,7 @@ class PSA_PT_ImportPanel_PsaData(Panel):
pg = context.scene.psa_import.psa pg = context.scene.psa_import.psa
layout.label(text=f'{len(pg.bones)} Bones', icon='BONE_DATA') layout.label(text=f'{len(pg.bones)} Bones', icon='BONE_DATA')
layout.label(text=f'{len(pg.sequence_count)} Sequences', icon='SEQUENCE') layout.label(text=f'{pg.sequence_count} Sequences', icon='SEQUENCE')
class PSA_PT_ImportPanel(Panel): class PSA_PT_ImportPanel(Panel):
@@ -469,7 +473,7 @@ class PSA_PT_ImportPanel(Panel):
class PsaImportFileReload(Operator): class PsaImportFileReload(Operator):
bl_idname = 'psa_import.file_reload' bl_idname = 'psa_import.file_reload'
bl_label = 'Refresh' bl_label = 'Refresh'
bl_options = {'REGISTER'} bl_options = {'INTERNAL'}
bl_description = 'Refresh the PSA file' bl_description = 'Refresh the PSA file'
def execute(self, context): def execute(self, context):
@@ -480,7 +484,7 @@ class PsaImportFileReload(Operator):
class PsaImportSelectFile(Operator): class PsaImportSelectFile(Operator):
bl_idname = 'psa_import.select_file' bl_idname = 'psa_import.select_file'
bl_label = 'Select' bl_label = 'Select'
bl_options = {'REGISTER', 'UNDO'} bl_options = {'INTERNAL'}
bl_description = 'Select a PSA file from which to import animations' bl_description = 'Select a PSA file from which to import animations'
filepath: bpy.props.StringProperty(subtype='FILE_PATH') filepath: bpy.props.StringProperty(subtype='FILE_PATH')
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'}) filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
@@ -498,6 +502,7 @@ class PsaImportOperator(Operator):
bl_idname = 'psa_import.import' bl_idname = 'psa_import.import'
bl_label = 'Import' bl_label = 'Import'
bl_description = 'Import the selected animations into the scene as actions' bl_description = 'Import the selected animations into the scene as actions'
bl_options = {'INTERNAL', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -524,6 +529,7 @@ class PsaImportOperator(Operator):
class PsaImportPushToActions(Operator): class PsaImportPushToActions(Operator):
bl_idname = 'psa_import.push_to_actions' bl_idname = 'psa_import.push_to_actions'
bl_label = 'Push to Actions' bl_label = 'Push to Actions'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -547,6 +553,7 @@ class PsaImportPushToActions(Operator):
class PsaImportPopFromActions(Operator): class PsaImportPopFromActions(Operator):
bl_idname = 'psa_import.pop_from_actions' bl_idname = 'psa_import.pop_from_actions'
bl_label = 'Pop From Actions' bl_label = 'Pop From Actions'
bl_options = {'INTERNAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -570,6 +577,7 @@ class PsaImportPopFromActions(Operator):
class PsaImportFileSelectOperator(Operator, ImportHelper): class PsaImportFileSelectOperator(Operator, ImportHelper):
bl_idname = 'psa_import.file_select' bl_idname = 'psa_import.file_select'
bl_label = 'File Select' bl_label = 'File Select'
bl_options = {'INTERNAL'}
filename_ext = '.psa' filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
filepath: StringProperty( filepath: StringProperty(

View File

@@ -70,7 +70,7 @@ class PskBuilder(object):
# If the mesh has no armature object, simply assign it a dummy bone at the root to satisfy the requirement # If the mesh has no armature object, simply assign it a dummy bone at the root to satisfy the requirement
# that a PSK file must have at least one bone. # that a PSK file must have at least one bone.
psk_bone = Psk.Bone() psk_bone = Psk.Bone()
psk_bone.name = bytes('static', encoding='utf-8') psk_bone.name = bytes('static', encoding='windows-1252')
psk_bone.flags = 0 psk_bone.flags = 0
psk_bone.children_count = 0 psk_bone.children_count = 0
psk_bone.parent_index = 0 psk_bone.parent_index = 0
@@ -88,8 +88,6 @@ class PskBuilder(object):
# Ensure that the exported hierarchy has a single root bone. # Ensure that the exported hierarchy has a single root bone.
root_bones = [x for x in bones if x.parent is None] root_bones = [x for x in bones if x.parent is None]
print('root bones')
print(root_bones)
if len(root_bones) > 1: if len(root_bones) > 1:
root_bone_names = [x.name for x in root_bones] root_bone_names = [x.name for x in root_bones]
raise RuntimeError('Exported bone hierarchy must have a single root bone.' raise RuntimeError('Exported bone hierarchy must have a single root bone.'
@@ -97,7 +95,7 @@ class PskBuilder(object):
for bone in bones: for bone in bones:
psk_bone = Psk.Bone() psk_bone = Psk.Bone()
psk_bone.name = bytes(bone.name, encoding='utf-8') psk_bone.name = bytes(bone.name, encoding='windows-1252')
psk_bone.flags = 0 psk_bone.flags = 0
psk_bone.children_count = 0 psk_bone.children_count = 0
@@ -133,9 +131,9 @@ class PskBuilder(object):
psk.bones.append(psk_bone) psk.bones.append(psk_bone)
vertex_offset = 0
for object in input_objects.mesh_objects: for object in input_objects.mesh_objects:
vertex_offset = len(psk.points)
# VERTICES # VERTICES
for vertex in object.data.vertices: for vertex in object.data.vertices:
point = Vector3() point = Vector3()
@@ -153,8 +151,10 @@ class PskBuilder(object):
if m is None: if m is None:
raise RuntimeError('Material cannot be empty (index ' + str(i) + ')') raise RuntimeError('Material cannot be empty (index ' + str(i) + ')')
if m.name in materials: if m.name in materials:
# Material already evaluated, just get its index.
material_index = list(materials.keys()).index(m.name) material_index = list(materials.keys()).index(m.name)
else: else:
# New material.
material = Psk.Material() material = Psk.Material()
material.name = bytes(m.name, encoding='utf-8') material.name = bytes(m.name, encoding='utf-8')
material.texture_index = len(psk.materials) material.texture_index = len(psk.materials)
@@ -230,9 +230,9 @@ class PskBuilder(object):
bone = bone.parent bone = bone.parent
for vertex_group_index, vertex_group in enumerate(object.vertex_groups): for vertex_group_index, vertex_group in enumerate(object.vertex_groups):
if vertex_group_index not in vertex_group_bone_indices: if vertex_group_index not in vertex_group_bone_indices:
# Vertex group has no associated bone, skip it.
continue continue
bone_index = vertex_group_bone_indices[vertex_group_index] bone_index = vertex_group_bone_indices[vertex_group_index]
# TODO: exclude vertex group if it doesn't match to a bone we are exporting
for vertex_index in range(len(object.data.vertices)): for vertex_index in range(len(object.data.vertices)):
try: try:
weight = vertex_group.weight(vertex_index) weight = vertex_group.weight(vertex_index)
@@ -246,6 +246,4 @@ class PskBuilder(object):
w.weight = weight w.weight = weight
psk.weights.append(w) psk.weights.append(w)
vertex_offset = len(psk.points)
return psk return psk

View File

@@ -73,6 +73,7 @@ def is_bone_filter_mode_item_available(context, identifier):
class PskExportOperator(Operator, ExportHelper): class PskExportOperator(Operator, ExportHelper):
bl_idname = 'export.psk' bl_idname = 'export.psk'
bl_label = 'Export' bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
__doc__ = 'Export mesh and armature to PSK' __doc__ = 'Export mesh and armature to PSK'
filename_ext = '.psk' filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})

View File

@@ -245,6 +245,7 @@ class PskImportPropertyGroup(PropertyGroup):
class PskImportOperator(Operator, ImportHelper): class PskImportOperator(Operator, ImportHelper):
bl_idname = 'import.psk' bl_idname = 'import.psk'
bl_label = 'Export' bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
__doc__ = 'Load a PSK file' __doc__ = 'Load a PSK file'
filename_ext = '.psk' filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'}) filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
@@ -276,7 +277,6 @@ class PskImportOperator(Operator, ImportHelper):
layout.prop(pg, 'vertex_color_space') layout.prop(pg, 'vertex_color_space')
classes = ( classes = (
PskImportOperator, PskImportOperator,
PskImportPropertyGroup, PskImportPropertyGroup,

View File

@@ -4,14 +4,15 @@ from bpy.props import StringProperty, IntProperty, BoolProperty
class PSX_UL_BoneGroupList(UIList): class PSX_UL_BoneGroupList(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()
layout.prop(item, 'is_selected', icon_only=True) row.prop(item, 'is_selected', text=item.name)
layout.label(text=item.name, icon='GROUP_BONE' if item.index >= 0 else 'NONE') row.label(text=str(item.count), icon='BONE_DATA')
class BoneGroupListItem(PropertyGroup): class BoneGroupListItem(PropertyGroup):
name: StringProperty() name: StringProperty()
index: IntProperty() index: IntProperty()
count: IntProperty()
is_selected: BoolProperty(default=False) is_selected: BoolProperty(default=False)
@property @property