From 7ad8f0238a3a70e0fbea8939c152d408bdae6e47 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 11 Feb 2022 15:21:31 -0800 Subject: [PATCH] Added the ability to export sequences using timeline markers (WIP, not thoroughly tested yet!) A bunch of clean up --- io_scene_psk_psa/helpers.py | 38 +++++++-- io_scene_psk_psa/psa/builder.py | 123 ++++++++++++++++++++++++----- io_scene_psk_psa/psa/exporter.py | 128 +++++++++++++++++++++++-------- io_scene_psk_psa/psa/importer.py | 22 ++++-- io_scene_psk_psa/psk/builder.py | 16 ++-- io_scene_psk_psa/psk/exporter.py | 1 + io_scene_psk_psa/psk/importer.py | 2 +- io_scene_psk_psa/types.py | 7 +- 8 files changed, 262 insertions(+), 75 deletions(-) diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index 8bf856a..6164a30 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -1,4 +1,6 @@ +from bpy.types import NlaStrip from typing import List +from collections import Counter def rgb_to_srgb(c): @@ -8,19 +10,45 @@ def rgb_to_srgb(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): 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: + 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): item = bone_group_list.add() item.name = bone_group.name 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 diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index a359096..74da670 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -1,13 +1,17 @@ from .data import * from ..helpers import * +from typing import Dict class PsaBuilderOptions(object): def __init__(self): + self.sequence_source = 'ACTIONS' self.actions = [] + self.marker_names = [] self.bone_filter_mode = 'ALL' self.bone_group_indices = [] self.should_use_original_sequence_names = False + self.should_trim_timeline_marker_sequences = True class PsaBuilder(object): @@ -25,6 +29,12 @@ class PsaBuilder(object): if armature.animation_data is None: 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() bones = list(armature.data.bones) @@ -59,6 +69,7 @@ class PsaBuilder(object): 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}') + # Build list of PSA bones. for pose_bone in bones: psa_bone = Psa.Bone() psa_bone.name = bytes(pose_bone.name, encoding='utf-8') @@ -95,28 +106,65 @@ class PsaBuilder(object): 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 - for action in options.actions: - if len(action.fcurves) == 0: - continue - - armature.animation_data.action = action + # Now build the PSA sequences. + # We actually alter the timeline frame and simply record the resultant pose bone matrices. + for export_sequence in export_sequences: + armature.animation_data.action = export_sequence.action 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) - - sequence.name = bytes(sequence_name, encoding='windows-1252') - sequence.frame_count = frame_max - frame_min + 1 - sequence.frame_start_index = frame_start_index - sequence.fps = context.scene.render.fps + psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252') + psa_sequence.frame_count = frame_max - frame_min + 1 + psa_sequence.frame_start_index = frame_start_index + psa_sequence.fps = context.scene.render.fps 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): context.scene.frame_set(frame_min + frame) @@ -143,15 +191,54 @@ class PsaBuilder(object): key.rotation.y = rotation.y key.rotation.z = rotation.z key.rotation.w = rotation.w - key.time = 1.0 / sequence.fps + key.time = 1.0 / psa_sequence.fps 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) - sequence.track_time = frame_count + # Restore the mute state of the NLA strips we muted beforehand. + 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 + + 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 diff --git a/io_scene_psk_psa/psa/exporter.py b/io_scene_psk_psa/psa/exporter.py index 470b8c9..770df29 100644 --- a/io_scene_psk_psa/psa/exporter.py +++ b/io_scene_psk_psa/psa/exporter.py @@ -1,5 +1,5 @@ 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_extras.io_utils import ExportHelper from typing import Type @@ -46,6 +46,16 @@ class PsaExportActionListItem(PropertyGroup): 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): pg = context.scene.psa_export for item in pg.action_list: @@ -58,18 +68,28 @@ def should_use_original_sequence_names_updated(property, context): 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_index: IntProperty(default=0) + marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem) + marker_list_index: IntProperty(default=0) bone_filter_mode: EnumProperty( name='Bone Filter', description='', items=( ('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_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) @@ -84,6 +104,7 @@ def is_bone_filter_mode_item_available(context, identifier): class PsaExportOperator(Operator, ExportHelper): bl_idname = 'psa_export.operator' bl_label = 'Export' + bl_options = {'INTERNAL', 'UNDO'} __doc__ = 'Export actions to PSA' filename_ext = '.psa' filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) @@ -100,24 +121,34 @@ class PsaExportOperator(Operator, ExportHelper): layout = self.layout pg = context.scene.psa_export - # ACTIONS - layout.label(text='Actions', icon='ACTION') - 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) + # SOURCE + layout.prop(pg, 'sequence_source', text='Source') - col = layout.column(heading="Options") - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'should_use_original_sequence_names') + # ACTIONS + if pg.sequence_source == 'ACTIONS': + layout.label(text='Actions', icon='ACTION') + 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. - selected_actions = [x for x in pg.action_list if x.is_selected] - action_names = [x.action_name for x in selected_actions] + selected_items = [x for x in pg.action_list if x.is_selected] + action_names = [x.action_name for x in selected_items] action_name_counts = Counter(action_names) for action_name, count in action_name_counts.items(): if count > 1: @@ -180,9 +211,15 @@ class PsaExportOperator(Operator, ExportHelper): 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. - 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'} # Populate bone groups list. @@ -194,28 +231,51 @@ class PsaExportOperator(Operator, ExportHelper): def execute(self, context): pg = context.scene.psa_export - actions = [x.action for x in pg.action_list if x.is_selected] - if len(actions) == 0: - self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.') - return {'CANCELLED'} + actions = [x.action for x in pg.action_list if x.is_selected] + marker_names = [x.marker_name for x in pg.marker_list if x.is_selected] options = PsaBuilderOptions() + options.sequence_source = pg.sequence_source options.actions = actions + options.marker_names = marker_names 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.should_use_original_sequence_names = pg.should_use_original_sequence_names builder = PsaBuilder() + try: psa = builder.build(context, options) except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} + exporter = PsaExporter(psa) exporter.export(self.filepath) 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): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.alignment = 'LEFT' @@ -237,17 +297,18 @@ class PSA_UL_ExportActionList(UIList): return flt_flags, flt_neworder -class PsaExportSelectAll(bpy.types.Operator): +class PsaExportSelectAll(Operator): bl_idname = 'psa_export.actions_select_all' bl_label = 'Select All' bl_description = 'Select all actions' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): pg = context.scene.psa_export - action_list = pg.action_list - has_unselected_actions = any(map(lambda action: not action.is_selected, action_list)) - return len(action_list) > 0 and has_unselected_actions + item_list = pg.action_list + has_unselected_actions = any(map(lambda action: not action.is_selected, item_list)) + return len(item_list) > 0 and has_unselected_actions def execute(self, context): pg = context.scene.psa_export @@ -256,17 +317,18 @@ class PsaExportSelectAll(bpy.types.Operator): return {'FINISHED'} -class PsaExportDeselectAll(bpy.types.Operator): +class PsaExportDeselectAll(Operator): bl_idname = 'psa_export.actions_deselect_all' bl_label = 'Deselect All' bl_description = 'Deselect all actions' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): pg = context.scene.psa_export - action_list = pg.action_list - has_selected_actions = any(map(lambda action: action.is_selected, action_list)) - return len(action_list) > 0 and has_selected_actions + item_list = pg.action_list + has_selected_actions = any(map(lambda action: action.is_selected, item_list)) + return len(item_list) > 0 and has_selected_actions def execute(self, context): pg = context.scene.psa_export @@ -277,9 +339,11 @@ class PsaExportDeselectAll(bpy.types.Operator): classes = ( PsaExportActionListItem, + PsaExportTimelineMarkerListItem, PsaExportPropertyGroup, PsaExportOperator, PSA_UL_ExportActionList, + PSA_UL_ExportTimelineMarkerList, PsaExportSelectAll, PsaExportDeselectAll, ) diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index 4be3729..871a22a 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -293,10 +293,11 @@ class PSA_UL_ImportActionList(PSA_UL_SequenceList, UIList): pass -class PsaImportSequencesSelectAll(bpy.types.Operator): +class PsaImportSequencesSelectAll(Operator): bl_idname = 'psa_import.sequences_select_all' bl_label = 'All' bl_description = 'Select all sequences' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): @@ -312,10 +313,11 @@ class PsaImportSequencesSelectAll(bpy.types.Operator): return {'FINISHED'} -class PsaImportActionsSelectAll(bpy.types.Operator): +class PsaImportActionsSelectAll(Operator): bl_idname = 'psa_import.actions_select_all' bl_label = 'All' bl_description = 'Select all actions' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): @@ -331,10 +333,11 @@ class PsaImportActionsSelectAll(bpy.types.Operator): return {'FINISHED'} -class PsaImportSequencesDeselectAll(bpy.types.Operator): +class PsaImportSequencesDeselectAll(Operator): bl_idname = 'psa_import.sequences_deselect_all' bl_label = 'None' bl_description = 'Deselect all sequences' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): @@ -350,10 +353,11 @@ class PsaImportSequencesDeselectAll(bpy.types.Operator): return {'FINISHED'} -class PsaImportActionsDeselectAll(bpy.types.Operator): +class PsaImportActionsDeselectAll(Operator): bl_idname = 'psa_import.actions_deselect_all' bl_label = 'None' bl_description = 'Deselect all actions' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): @@ -406,7 +410,7 @@ class PSA_PT_ImportPanel_PsaData(Panel): pg = context.scene.psa_import.psa 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): @@ -469,7 +473,7 @@ class PSA_PT_ImportPanel(Panel): class PsaImportFileReload(Operator): bl_idname = 'psa_import.file_reload' bl_label = 'Refresh' - bl_options = {'REGISTER'} + bl_options = {'INTERNAL'} bl_description = 'Refresh the PSA file' def execute(self, context): @@ -480,7 +484,7 @@ class PsaImportFileReload(Operator): class PsaImportSelectFile(Operator): bl_idname = 'psa_import.select_file' bl_label = 'Select' - bl_options = {'REGISTER', 'UNDO'} + bl_options = {'INTERNAL'} bl_description = 'Select a PSA file from which to import animations' filepath: bpy.props.StringProperty(subtype='FILE_PATH') filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'}) @@ -498,6 +502,7 @@ class PsaImportOperator(Operator): bl_idname = 'psa_import.import' bl_label = 'Import' bl_description = 'Import the selected animations into the scene as actions' + bl_options = {'INTERNAL', 'UNDO'} @classmethod def poll(cls, context): @@ -524,6 +529,7 @@ class PsaImportOperator(Operator): class PsaImportPushToActions(Operator): bl_idname = 'psa_import.push_to_actions' bl_label = 'Push to Actions' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): @@ -547,6 +553,7 @@ class PsaImportPushToActions(Operator): class PsaImportPopFromActions(Operator): bl_idname = 'psa_import.pop_from_actions' bl_label = 'Pop From Actions' + bl_options = {'INTERNAL'} @classmethod def poll(cls, context): @@ -570,6 +577,7 @@ class PsaImportPopFromActions(Operator): class PsaImportFileSelectOperator(Operator, ImportHelper): bl_idname = 'psa_import.file_select' bl_label = 'File Select' + bl_options = {'INTERNAL'} filename_ext = '.psa' filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) filepath: StringProperty( diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 44425cc..5b8362c 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -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 # that a PSK file must have at least one 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.children_count = 0 psk_bone.parent_index = 0 @@ -88,8 +88,6 @@ class PskBuilder(object): # Ensure that the exported hierarchy has a single root bone. root_bones = [x for x in bones if x.parent is None] - print('root bones') - print(root_bones) if len(root_bones) > 1: root_bone_names = [x.name for x in root_bones] raise RuntimeError('Exported bone hierarchy must have a single root bone.' @@ -97,7 +95,7 @@ class PskBuilder(object): for bone in bones: 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.children_count = 0 @@ -133,9 +131,9 @@ class PskBuilder(object): psk.bones.append(psk_bone) - vertex_offset = 0 - for object in input_objects.mesh_objects: + vertex_offset = len(psk.points) + # VERTICES for vertex in object.data.vertices: point = Vector3() @@ -153,8 +151,10 @@ class PskBuilder(object): if m is None: raise RuntimeError('Material cannot be empty (index ' + str(i) + ')') if m.name in materials: + # Material already evaluated, just get its index. material_index = list(materials.keys()).index(m.name) else: + # New material. material = Psk.Material() material.name = bytes(m.name, encoding='utf-8') material.texture_index = len(psk.materials) @@ -230,9 +230,9 @@ class PskBuilder(object): bone = bone.parent for vertex_group_index, vertex_group in enumerate(object.vertex_groups): if vertex_group_index not in vertex_group_bone_indices: + # Vertex group has no associated bone, skip it. continue 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)): try: weight = vertex_group.weight(vertex_index) @@ -246,6 +246,4 @@ class PskBuilder(object): w.weight = weight psk.weights.append(w) - vertex_offset = len(psk.points) - return psk diff --git a/io_scene_psk_psa/psk/exporter.py b/io_scene_psk_psa/psk/exporter.py index 504f29d..893559d 100644 --- a/io_scene_psk_psa/psk/exporter.py +++ b/io_scene_psk_psa/psk/exporter.py @@ -73,6 +73,7 @@ def is_bone_filter_mode_item_available(context, identifier): class PskExportOperator(Operator, ExportHelper): bl_idname = 'export.psk' bl_label = 'Export' + bl_options = {'INTERNAL', 'UNDO'} __doc__ = 'Export mesh and armature to PSK' filename_ext = '.psk' filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index c47d183..cd0f90c 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -245,6 +245,7 @@ class PskImportPropertyGroup(PropertyGroup): class PskImportOperator(Operator, ImportHelper): bl_idname = 'import.psk' bl_label = 'Export' + bl_options = {'INTERNAL', 'UNDO'} __doc__ = 'Load a PSK file' filename_ext = '.psk' filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'}) @@ -276,7 +277,6 @@ class PskImportOperator(Operator, ImportHelper): layout.prop(pg, 'vertex_color_space') - classes = ( PskImportOperator, PskImportPropertyGroup, diff --git a/io_scene_psk_psa/types.py b/io_scene_psk_psa/types.py index 89b6b26..142f4fb 100644 --- a/io_scene_psk_psa/types.py +++ b/io_scene_psk_psa/types.py @@ -4,14 +4,15 @@ from bpy.props import StringProperty, IntProperty, BoolProperty class PSX_UL_BoneGroupList(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' if item.index >= 0 else 'NONE') + row = layout.row() + row.prop(item, 'is_selected', text=item.name) + row.label(text=str(item.count), icon='BONE_DATA') class BoneGroupListItem(PropertyGroup): name: StringProperty() index: IntProperty() + count: IntProperty() is_selected: BoolProperty(default=False) @property