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,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