Added NLA track sequence source option (reimplementation of feature-nla-track-sequence-source branch).

This just needs a bit of time in the oven to test it out.
This commit is contained in:
Colin Basnett
2023-08-11 02:09:16 -07:00
parent d0d6deb63c
commit b20d19d072
4 changed files with 118 additions and 44 deletions

View File

@@ -34,7 +34,7 @@ def rgb_to_srgb(c: float):
return 12.92 * c return 12.92 * c
def get_nla_strips_in_timeframe(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]: def get_nla_strips_in_frame_range(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]:
if animation_data is None: if animation_data is None:
return [] return []
strips = [] strips = []

View File

@@ -8,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke
from bpy_extras.io_utils import ExportHelper from bpy_extras.io_utils import ExportHelper
from bpy_types import Operator from bpy_types import Operator
from io_scene_psk_psa.helpers import populate_bone_group_list, get_nla_strips_in_timeframe from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
from io_scene_psk_psa.psa.builder import build_psa, PsaBuildSequence, PsaBuildOptions from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
from io_scene_psk_psa.psa.export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences from ..writer import write_psa
from io_scene_psk_psa.psa.writer import write_psa from ...helpers import populate_bone_group_list, get_nla_strips_in_frame_range
def is_action_for_armature(armature: Armature, action: Action): def is_action_for_armature(armature: Armature, action: Action):
@@ -150,7 +150,7 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
if next_marker_index < len(sorted_timeline_markers): 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. # There is a next marker. Use that next marker's frame position as the last frame of this sequence.
frame_end = sorted_timeline_markers[next_marker_index].frame frame_end = sorted_timeline_markers[next_marker_index].frame
nla_strips = get_nla_strips_in_timeframe(animation_data, marker.frame, frame_end) nla_strips = get_nla_strips_in_frame_range(animation_data, marker.frame, frame_end)
if len(nla_strips) > 0: if len(nla_strips) > 0:
frame_end = min(frame_end, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips))) frame_end = min(frame_end, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips)))
frame_start = max(frame_start, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips))) frame_start = max(frame_start, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips)))
@@ -254,12 +254,18 @@ class PSA_OT_export(Operator, ExportHelper):
# SOURCE # SOURCE
layout.prop(pg, 'sequence_source', text='Source') layout.prop(pg, 'sequence_source', text='Source')
if pg.sequence_source == 'TIMELINE_MARKERS': if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}:
# ANIMDATA SOURCE # ANIMDATA SOURCE
layout.prop(pg, 'should_override_animation_data') layout.prop(pg, 'should_override_animation_data')
if pg.should_override_animation_data: if pg.should_override_animation_data:
layout.prop(pg, 'animation_data_override', text='') layout.prop(pg, 'animation_data_override', text='')
if pg.sequence_source == 'NLA_TRACK_STRIPS':
flow = layout.grid_flow()
flow.use_property_split = True
flow.use_property_decorate = False
flow.prop(pg, 'nla_track')
# SELECT ALL/NONE # SELECT ALL/NONE
row = layout.row(align=True) row = layout.row(align=True)
row.label(text='Select') row.label(text='Select')
@@ -269,25 +275,19 @@ class PSA_OT_export(Operator, ExportHelper):
# ACTIONS # ACTIONS
if pg.sequence_source == 'ACTIONS': if pg.sequence_source == 'ACTIONS':
rows = max(3, min(len(pg.action_list), 10)) rows = max(3, min(len(pg.action_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows) layout.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix')
elif pg.sequence_source == 'TIMELINE_MARKERS': elif pg.sequence_source == 'TIMELINE_MARKERS':
rows = max(3, min(len(pg.marker_list), 10)) rows = max(3, min(len(pg.marker_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
rows=rows) elif pg.sequence_source == 'NLA_TRACK_STRIPS':
rows = max(3, min(len(pg.nla_strip_list), 10))
layout.template_list('PSA_UL_export_sequences', '', pg, 'nla_strip_list', pg, 'nla_strip_list_index', rows=rows)
col = layout.column() col = layout.column()
col.use_property_split = True col.use_property_split = True
col.use_property_decorate = False col.use_property_decorate = False
col.prop(pg, 'sequence_name_prefix') col.prop(pg, 'sequence_name_prefix')
col.prop(pg, 'sequence_name_suffix') col.prop(pg, 'sequence_name_suffix')
# 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_items = [x for x in pg.action_list if x.is_selected] selected_items = [x for x in pg.action_list if x.is_selected]
@@ -360,6 +360,8 @@ class PSA_OT_export(Operator, ExportHelper):
raise RuntimeError('No actions were selected for export') raise RuntimeError('No actions were selected for export')
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_names) == 0: elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_names) == 0:
raise RuntimeError('No timeline markers were selected for export') raise RuntimeError('No timeline markers were selected for export')
elif pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0:
raise RuntimeError('No NLA track strips were selected for export')
# Populate the export sequence list. # Populate the export sequence list.
animation_data_object = get_animation_data_object(context) animation_data_object = get_animation_data_object(context)
@@ -371,29 +373,38 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences: List[PsaBuildSequence] = [] export_sequences: List[PsaBuildSequence] = []
if pg.sequence_source == 'ACTIONS': if pg.sequence_source == 'ACTIONS':
for action 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.action.fcurves) == 0: if len(action_item.action.fcurves) == 0:
continue continue
export_sequence = PsaBuildSequence() export_sequence = PsaBuildSequence()
export_sequence.nla_state.action = action.action export_sequence.nla_state.action = action_item.action
export_sequence.name = action.name export_sequence.name = action_item.name
export_sequence.nla_state.frame_start = action.frame_start export_sequence.nla_state.frame_start = action_item.frame_start
export_sequence.nla_state.frame_end = action.frame_end export_sequence.nla_state.frame_end = action_item.frame_end
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action.action]) export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action_item.action])
export_sequence.compression_ratio = action.action.psa_export.compression_ratio export_sequence.compression_ratio = action_item.action.psa_export.compression_ratio
export_sequence.key_quota = action.action.psa_export.key_quota export_sequence.key_quota = action_item.action.psa_export.key_quota
export_sequences.append(export_sequence) export_sequences.append(export_sequence)
elif pg.sequence_source == 'TIMELINE_MARKERS': elif pg.sequence_source == 'TIMELINE_MARKERS':
for marker in pg.marker_list: for marker_item in filter(lambda x: x.is_selected, pg.marker_list):
export_sequence = PsaBuildSequence() export_sequence = PsaBuildSequence()
export_sequence.name = marker.name export_sequence.name = marker_item.name
export_sequence.nla_state.action = None export_sequence.nla_state.action = None
export_sequence.nla_state.frame_start = marker.frame_start export_sequence.nla_state.frame_start = marker_item.frame_start
export_sequence.nla_state.frame_end = marker.frame_end export_sequence.nla_state.frame_end = marker_item.frame_end
nla_strips_actions = set( nla_strips_actions = set(
map(lambda x: x.action, get_nla_strips_in_timeframe(animation_data, marker.frame_start, marker.frame_end))) map(lambda x: x.action, get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.frame_end)))
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, nla_strips_actions) export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, nla_strips_actions)
export_sequences.append(export_sequence) export_sequences.append(export_sequence)
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list):
export_sequence = PsaBuildSequence()
export_sequence.name = nla_strip_item.name
export_sequence.nla_state.action = nla_strip_item.action
export_sequence.nla_state.frame_start = nla_strip_item.frame_start
export_sequence.nla_state.frame_end = nla_strip_item.frame_end
export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action])
export_sequences.append(export_sequence)
else: else:
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}') raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
@@ -432,6 +443,8 @@ class PSA_OT_export_actions_select_all(Operator):
return pg.action_list return pg.action_list
elif pg.sequence_source == 'TIMELINE_MARKERS': elif pg.sequence_source == 'TIMELINE_MARKERS':
return pg.marker_list return pg.marker_list
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
return pg.nla_strip_list
return None return None
@classmethod @classmethod
@@ -463,6 +476,8 @@ class PSA_OT_export_actions_deselect_all(Operator):
return pg.action_list return pg.action_list
elif pg.sequence_source == 'TIMELINE_MARKERS': elif pg.sequence_source == 'TIMELINE_MARKERS':
return pg.marker_list return pg.marker_list
elif pg.sequence_source == 'NLA_TRACK_STRIPS':
return pg.nla_strip_list
return None return None
@classmethod @classmethod

View File

@@ -1,10 +1,11 @@
import re
import sys import sys
from fnmatch import fnmatch from fnmatch import fnmatch
from typing import List from typing import List, Optional
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \ from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
StringProperty StringProperty
from bpy.types import PropertyGroup, Object, Action from bpy.types import PropertyGroup, Object, Action, AnimData, Context
from ...types import PSX_PG_bone_group_list_item from ...types import PSX_PG_bone_group_list_item
@@ -25,7 +26,7 @@ class PSA_PG_export_action_list_item(PropertyGroup):
is_pose_marker: BoolProperty(options={'HIDDEN'}) is_pose_marker: BoolProperty(options={'HIDDEN'})
class PSA_PG_export_timeline_markers(PropertyGroup): class PSA_PG_export_timeline_markers(PropertyGroup): # TODO: rename this to singular
marker_index: IntProperty() marker_index: IntProperty()
name: StringProperty() name: StringProperty()
is_selected: BoolProperty(default=True) is_selected: BoolProperty(default=True)
@@ -33,6 +34,51 @@ class PSA_PG_export_timeline_markers(PropertyGroup):
frame_end: IntProperty(options={'HIDDEN'}) frame_end: IntProperty(options={'HIDDEN'})
class PSA_PG_export_nla_strip_list_item(PropertyGroup):
name: StringProperty()
action: PointerProperty(type=Action)
frame_start: FloatProperty()
frame_end: FloatProperty()
is_selected: BoolProperty(default=True)
def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None:
self.nla_strip_list.clear()
if context.object is None or context.object.animation_data is None:
return
match = re.match(r'^(\d+).+$', self.nla_track)
self.nla_track_index = int(match.group(1)) if match else -1
if self.nla_track_index >= 0:
nla_track = context.object.animation_data.nla_tracks[self.nla_track_index]
for nla_strip in nla_track.strips:
strip: PSA_PG_export_nla_strip_list_item = self.nla_strip_list.add()
strip.action = nla_strip.action
strip.name = nla_strip.name
strip.frame_start = nla_strip.frame_start
strip.frame_end = nla_strip.frame_end
def get_animation_data(pg: 'PSA_PG_export', context: Context) -> Optional[AnimData]:
animation_data_object = context.object
if pg.should_override_animation_data:
animation_data_object = pg.animation_data_override
return animation_data_object.animation_data if animation_data_object else None
def nla_track_search_cb(self, context: Context, edit_text: str):
pg = getattr(context.scene, 'psa_export')
animation_data = get_animation_data(pg, context)
if animation_data is None:
return
for index, nla_track in enumerate(animation_data.nla_tracks):
yield f'{index} - {nla_track.name}'
def animation_data_override_update_cb(self: 'PSA_PG_export', context: Context):
# Reset NLA track selection
self.nla_track = ''
class PSA_PG_export(PropertyGroup): class PSA_PG_export(PropertyGroup):
root_motion: BoolProperty( root_motion: BoolProperty(
name='Root Motion', name='Root Motion',
@@ -46,10 +92,12 @@ class PSA_PG_export(PropertyGroup):
name='Override Animation Data', name='Override Animation Data',
options=empty_set, options=empty_set,
default=False, default=False,
description='Use the animation data from a different object instead of the selected object' description='Use the animation data from a different object instead of the selected object',
update=animation_data_override_update_cb,
) )
animation_data_override: PointerProperty( animation_data_override: PointerProperty(
type=Object, type=Object,
update=animation_data_override_update_cb,
poll=psa_export_property_group_animation_data_override_poll poll=psa_export_property_group_animation_data_override_poll
) )
sequence_source: EnumProperty( sequence_source: EnumProperty(
@@ -58,10 +106,18 @@ class PSA_PG_export(PropertyGroup):
description='', description='',
items=( items=(
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0), ('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0),
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers', 'MARKER_HLT', ('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences are delineated by scene timeline markers', 'MARKER_HLT', 1),
1), ('NLA_TRACK_STRIPS', 'NLA Track Strips', 'Sequences are delineated by the start & end times of strips on the selected NLA track', 'NLA', 2)
) )
) )
nla_track: StringProperty(
name='NLA Track',
options=empty_set,
description='',
search=nla_track_search_cb,
update=nla_track_update_cb
)
nla_track_index: IntProperty(name='NLA Track Index', default=-1)
fps_source: EnumProperty( fps_source: EnumProperty(
name='FPS Source', name='FPS Source',
options=empty_set, options=empty_set,
@@ -80,6 +136,8 @@ class PSA_PG_export(PropertyGroup):
action_list_index: IntProperty(default=0) action_list_index: IntProperty(default=0)
marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers) marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers)
marker_list_index: IntProperty(default=0) marker_list_index: IntProperty(default=0)
nla_strip_list: CollectionProperty(type=PSA_PG_export_nla_strip_list_item)
nla_strip_list_index: IntProperty(default=0)
bone_filter_mode: EnumProperty( bone_filter_mode: EnumProperty(
name='Bone Filter', name='Bone Filter',
options=empty_set, options=empty_set,
@@ -145,7 +203,7 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
if not pg.sequence_filter_asset: if not pg.sequence_filter_asset:
for i, sequence in enumerate(sequences): for i, sequence in enumerate(sequences):
if hasattr(sequence, 'action') and sequence.action.asset_data is not None: if hasattr(sequence, 'action') and sequence.action is not None and sequence.action.asset_data is not None:
flt_flags[i] &= ~bitflag_filter_item flt_flags[i] &= ~bitflag_filter_item
if not pg.sequence_filter_pose_marker: if not pg.sequence_filter_pose_marker:
@@ -164,5 +222,6 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
classes = ( classes = (
PSA_PG_export_action_list_item, PSA_PG_export_action_list_item,
PSA_PG_export_timeline_markers, PSA_PG_export_timeline_markers,
PSA_PG_export_nla_strip_list_item,
PSA_PG_export, PSA_PG_export,
) )

View File

@@ -16,7 +16,7 @@ class PSA_UL_export_sequences(UIList):
item = cast(PSA_PG_export_action_list_item, item) item = 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
layout.prop(item, 'is_selected', icon_only=True, text=item.name) layout.prop(item, 'is_selected', icon_only=True, text=item.name)
if hasattr(item, 'action') and item.action.asset_data is not None: if hasattr(item, 'action') and item.action is not None and item.action.asset_data is not None:
layout.label(text='', icon='ASSET_MANAGER') layout.label(text='', icon='ASSET_MANAGER')
row = layout.row(align=True) row = layout.row(align=True)