Refactoring to reduce pointless class encapsulation when just a function would do.
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
from typing import Dict, Iterable
|
||||
|
||||
from bpy.types import Action
|
||||
from mathutils import Matrix
|
||||
|
||||
from .data import *
|
||||
from ..helpers import *
|
||||
|
||||
|
||||
class PsaBuilderOptions(object):
|
||||
class PsaBuildOptions(object):
|
||||
def __init__(self):
|
||||
self.should_override_animation_data = False
|
||||
self.animation_data_override = None
|
||||
self.fps_source = 'SCENE'
|
||||
self.fps_custom = 30.0
|
||||
self.sequence_source = 'ACTIONS'
|
||||
@@ -23,260 +24,262 @@ class PsaBuilderOptions(object):
|
||||
self.root_motion = False
|
||||
|
||||
|
||||
class PsaBuilder(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_sequence_fps(self, context, options: PsaBuilderOptions, actions: Iterable[Action]) -> float:
|
||||
if options.fps_source == 'SCENE':
|
||||
def get_sequence_fps(context, options: PsaBuildOptions, actions: Iterable[Action]) -> float:
|
||||
if options.fps_source == 'SCENE':
|
||||
return context.scene.render.fps
|
||||
if options.fps_source == 'CUSTOM':
|
||||
return options.fps_custom
|
||||
elif options.fps_source == 'ACTION_METADATA':
|
||||
# Get the minimum value of action metadata FPS values.
|
||||
fps_list = []
|
||||
for action in filter(lambda x: 'psa_sequence_fps' in x, actions):
|
||||
fps = action['psa_sequence_fps']
|
||||
if type(fps) == int or type(fps) == float:
|
||||
fps_list.append(fps)
|
||||
if len(fps_list) > 0:
|
||||
return min(fps_list)
|
||||
else:
|
||||
# No valid action metadata to use, fallback to scene FPS
|
||||
return context.scene.render.fps
|
||||
if options.fps_source == 'CUSTOM':
|
||||
return options.fps_custom
|
||||
elif options.fps_source == 'ACTION_METADATA':
|
||||
# Get the minimum value of action metadata FPS values.
|
||||
fps_list = []
|
||||
for action in filter(lambda x: 'psa_sequence_fps' in x, actions):
|
||||
fps = action['psa_sequence_fps']
|
||||
if type(fps) == int or type(fps) == float:
|
||||
fps_list.append(fps)
|
||||
if len(fps_list) > 0:
|
||||
return min(fps_list)
|
||||
else:
|
||||
# No valid action metadata to use, fallback to scene FPS
|
||||
return context.scene.render.fps
|
||||
else:
|
||||
raise RuntimeError(f'Invalid FPS source "{options.fps_source}"')
|
||||
|
||||
|
||||
def get_timeline_marker_sequence_frame_ranges(animation_data, context, options: PsaBuildOptions) -> 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(animation_data, marker.frame, frame_max)
|
||||
if len(nla_strips) > 0:
|
||||
frame_max = min(frame_max, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips)))
|
||||
frame_min = max(frame_min, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips)))
|
||||
else:
|
||||
# No strips in between this marker and the next, just export this as a one-frame animation.
|
||||
frame_max = frame_min
|
||||
else:
|
||||
raise RuntimeError(f'Invalid FPS source "{options.fps_source}"')
|
||||
|
||||
def build(self, context, options: PsaBuilderOptions) -> Psa:
|
||||
active_object = context.view_layer.objects.active
|
||||
|
||||
if active_object.type != 'ARMATURE':
|
||||
raise RuntimeError('Selected object must be an Armature')
|
||||
|
||||
armature = active_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)
|
||||
|
||||
# The order of the armature bones and the pose bones is not guaranteed to be the same.
|
||||
# As a result, we need to reconstruct the list of pose bones in the same order as the
|
||||
# armature bones.
|
||||
bone_names = [x.name for x in bones]
|
||||
pose_bones = [(bone_names.index(bone.name), bone) for bone in armature.pose.bones]
|
||||
pose_bones.sort(key=lambda x: x[0])
|
||||
pose_bones = [x[1] for x in pose_bones]
|
||||
|
||||
# Get a list of all the bone indices and instigator bones for the bone filter settings.
|
||||
export_bone_names = get_export_bone_names(armature, options.bone_filter_mode, options.bone_group_indices)
|
||||
bone_indices = [bone_names.index(x) for x in export_bone_names]
|
||||
|
||||
# Make the bone lists contain 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]
|
||||
|
||||
# No bones are going to be exported.
|
||||
if len(bones) == 0:
|
||||
raise RuntimeError('No bones available for export')
|
||||
|
||||
# Check that all bone names are valid.
|
||||
check_bone_names(map(lambda bone: bone.name, bones))
|
||||
|
||||
# Build list of PSA bones.
|
||||
for bone in bones:
|
||||
psa_bone = Psa.Bone()
|
||||
psa_bone.name = bytes(bone.name, encoding='windows-1252')
|
||||
|
||||
try:
|
||||
parent_index = bones.index(bone.parent)
|
||||
psa_bone.parent_index = parent_index
|
||||
psa.bones[parent_index].children_count += 1
|
||||
except ValueError:
|
||||
psa_bone.parent_index = -1
|
||||
|
||||
if bone.parent is not None:
|
||||
rotation = bone.matrix.to_quaternion()
|
||||
rotation.x = -rotation.x
|
||||
rotation.y = -rotation.y
|
||||
rotation.z = -rotation.z
|
||||
quat_parent = bone.parent.matrix.to_quaternion().inverted()
|
||||
parent_head = quat_parent @ bone.parent.head
|
||||
parent_tail = quat_parent @ bone.parent.tail
|
||||
location = (parent_tail - parent_head) + bone.head
|
||||
else:
|
||||
location = armature.matrix_local @ bone.head
|
||||
rot_matrix = bone.matrix @ armature.matrix_local.to_3x3()
|
||||
rotation = rot_matrix.to_quaternion()
|
||||
|
||||
psa_bone.location.x = location.x
|
||||
psa_bone.location.y = location.y
|
||||
psa_bone.location.z = location.z
|
||||
|
||||
psa_bone.rotation.x = rotation.x
|
||||
psa_bone.rotation.y = rotation.y
|
||||
psa_bone.rotation.z = rotation.z
|
||||
psa_bone.rotation.w = rotation.w
|
||||
|
||||
psa.bones.append(psa_bone)
|
||||
|
||||
# Populate the export sequence list.
|
||||
class NlaState:
|
||||
def __init__(self):
|
||||
self.frame_min = 0
|
||||
self.frame_max = 0
|
||||
self.action = None
|
||||
|
||||
class ExportSequence:
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.nla_state = NlaState()
|
||||
self.fps = 30.0
|
||||
|
||||
export_sequences = []
|
||||
|
||||
if options.sequence_source == 'ACTIONS':
|
||||
for action in options.actions:
|
||||
if len(action.fcurves) == 0:
|
||||
# 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 animation_data.nla_tracks:
|
||||
if nla_track.mute:
|
||||
continue
|
||||
export_sequence = ExportSequence()
|
||||
export_sequence.nla_state.action = action
|
||||
export_sequence.name = get_psa_sequence_name(action, options.should_use_original_sequence_names)
|
||||
frame_min, frame_max = [int(x) for x in action.frame_range]
|
||||
export_sequence.nla_state.frame_min = frame_min
|
||||
export_sequence.nla_state.frame_max = frame_max
|
||||
export_sequence.fps = self.get_sequence_fps(context, options, [action])
|
||||
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 strip in nla_track.strips:
|
||||
frame_max = max(frame_max, strip.frame_end)
|
||||
|
||||
for name, (frame_min, frame_max) in sequence_frame_ranges.items():
|
||||
export_sequence = ExportSequence()
|
||||
export_sequence.name = name
|
||||
export_sequence.nla_state.action = None
|
||||
export_sequence.nla_state.frame_min = frame_min
|
||||
export_sequence.nla_state.frame_max = frame_max
|
||||
nla_strips_actions = set(
|
||||
map(lambda x: x.action, get_nla_strips_in_timeframe(active_object, frame_min, frame_max)))
|
||||
export_sequence.fps = self.get_sequence_fps(context, options, nla_strips_actions)
|
||||
export_sequences.append(export_sequence)
|
||||
if frame_min > frame_max:
|
||||
continue
|
||||
|
||||
sequence_frame_ranges[marker_name] = int(frame_min), int(frame_max)
|
||||
|
||||
return sequence_frame_ranges
|
||||
|
||||
|
||||
def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
active_object = context.view_layer.objects.active
|
||||
|
||||
if active_object.type != 'ARMATURE':
|
||||
raise RuntimeError('Selected object must be an Armature')
|
||||
|
||||
if options.should_override_animation_data:
|
||||
animation_data_object = options.animation_data_override
|
||||
else:
|
||||
animation_data_object = active_object
|
||||
|
||||
animation_data = animation_data_object.animation_data
|
||||
|
||||
if animation_data is None:
|
||||
raise RuntimeError(f'No animation data for object \'{animation_data_object.name}\'')
|
||||
|
||||
# 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()
|
||||
|
||||
armature = active_object
|
||||
bones = list(armature.data.bones)
|
||||
|
||||
# The order of the armature bones and the pose bones is not guaranteed to be the same.
|
||||
# As a result, we need to reconstruct the list of pose bones in the same order as the
|
||||
# armature bones.
|
||||
bone_names = [x.name for x in bones]
|
||||
pose_bones = [(bone_names.index(bone.name), bone) for bone in armature.pose.bones]
|
||||
pose_bones.sort(key=lambda x: x[0])
|
||||
pose_bones = [x[1] for x in pose_bones]
|
||||
|
||||
# Get a list of all the bone indices and instigator bones for the bone filter settings.
|
||||
export_bone_names = get_export_bone_names(armature, options.bone_filter_mode, options.bone_group_indices)
|
||||
bone_indices = [bone_names.index(x) for x in export_bone_names]
|
||||
|
||||
# Make the bone lists contain 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]
|
||||
|
||||
# No bones are going to be exported.
|
||||
if len(bones) == 0:
|
||||
raise RuntimeError('No bones available for export')
|
||||
|
||||
# Check that all bone names are valid.
|
||||
check_bone_names(map(lambda bone: bone.name, bones))
|
||||
|
||||
# Build list of PSA bones.
|
||||
for bone in bones:
|
||||
psa_bone = Psa.Bone()
|
||||
psa_bone.name = bytes(bone.name, encoding='windows-1252')
|
||||
|
||||
try:
|
||||
parent_index = bones.index(bone.parent)
|
||||
psa_bone.parent_index = parent_index
|
||||
psa.bones[parent_index].children_count += 1
|
||||
except ValueError:
|
||||
psa_bone.parent_index = -1
|
||||
|
||||
if bone.parent is not None:
|
||||
rotation = bone.matrix.to_quaternion()
|
||||
rotation.x = -rotation.x
|
||||
rotation.y = -rotation.y
|
||||
rotation.z = -rotation.z
|
||||
inverse_parent_rotation = bone.parent.matrix.to_quaternion().inverted()
|
||||
parent_head = inverse_parent_rotation @ bone.parent.head
|
||||
parent_tail = inverse_parent_rotation @ bone.parent.tail
|
||||
location = (parent_tail - parent_head) + bone.head
|
||||
else:
|
||||
raise ValueError(f'Unhandled sequence source: {options.sequence_source}')
|
||||
location = armature.matrix_local @ bone.head
|
||||
rot_matrix = bone.matrix @ armature.matrix_local.to_3x3()
|
||||
rotation = rot_matrix.to_quaternion()
|
||||
|
||||
# Add prefixes and suffices to the names of the export sequences and strip whitespace.
|
||||
for export_sequence in export_sequences:
|
||||
export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}'.strip()
|
||||
psa_bone.location.x = location.x
|
||||
psa_bone.location.y = location.y
|
||||
psa_bone.location.z = location.z
|
||||
|
||||
# Now build the PSA sequences.
|
||||
# We actually alter the timeline frame and simply record the resultant pose bone matrices.
|
||||
frame_start_index = 0
|
||||
psa_bone.rotation.x = rotation.x
|
||||
psa_bone.rotation.y = rotation.y
|
||||
psa_bone.rotation.z = rotation.z
|
||||
psa_bone.rotation.w = rotation.w
|
||||
|
||||
for export_sequence in export_sequences:
|
||||
armature.animation_data.action = export_sequence.nla_state.action
|
||||
context.view_layer.update()
|
||||
psa.bones.append(psa_bone)
|
||||
|
||||
psa_sequence = Psa.Sequence()
|
||||
# Populate the export sequence list.
|
||||
class NlaState:
|
||||
def __init__(self):
|
||||
self.frame_min = 0
|
||||
self.frame_max = 0
|
||||
self.action = None
|
||||
|
||||
frame_min = export_sequence.nla_state.frame_min
|
||||
frame_max = export_sequence.nla_state.frame_max
|
||||
frame_count = frame_max - frame_min + 1
|
||||
class ExportSequence:
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.nla_state = NlaState()
|
||||
self.fps = 30.0
|
||||
|
||||
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
|
||||
psa_sequence.frame_count = frame_count
|
||||
psa_sequence.frame_start_index = frame_start_index
|
||||
psa_sequence.fps = export_sequence.fps
|
||||
export_sequences = []
|
||||
|
||||
frame_count = frame_max - frame_min + 1
|
||||
|
||||
for frame in range(frame_count):
|
||||
context.scene.frame_set(frame_min + frame)
|
||||
|
||||
for pose_bone in pose_bones:
|
||||
key = Psa.Key()
|
||||
|
||||
if pose_bone.parent is not None:
|
||||
pose_bone_matrix = pose_bone.matrix
|
||||
pose_bone_parent_matrix = pose_bone.parent.matrix
|
||||
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
|
||||
else:
|
||||
if options.root_motion:
|
||||
# Export root motion
|
||||
pose_bone_matrix = armature.matrix_world @ pose_bone.matrix
|
||||
else:
|
||||
pose_bone_matrix = pose_bone.matrix
|
||||
|
||||
location = pose_bone_matrix.to_translation()
|
||||
rotation = pose_bone_matrix.to_quaternion().normalized()
|
||||
|
||||
if pose_bone.parent is not None:
|
||||
rotation.conjugate()
|
||||
|
||||
key.location.x = location.x
|
||||
key.location.y = location.y
|
||||
key.location.z = location.z
|
||||
key.rotation.x = rotation.x
|
||||
key.rotation.y = rotation.y
|
||||
key.rotation.z = rotation.z
|
||||
key.rotation.w = rotation.w
|
||||
key.time = 1.0 / psa_sequence.fps
|
||||
|
||||
psa.keys.append(key)
|
||||
|
||||
psa_sequence.bone_count = len(pose_bones)
|
||||
psa_sequence.track_time = frame_count
|
||||
|
||||
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)
|
||||
if len(nla_strips) > 0:
|
||||
frame_max = min(frame_max, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips)))
|
||||
frame_min = max(frame_min, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips)))
|
||||
else:
|
||||
# No strips in between this marker and the next, just export this as a one-frame animation.
|
||||
frame_max = frame_min
|
||||
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:
|
||||
if nla_track.mute:
|
||||
continue
|
||||
for strip in nla_track.strips:
|
||||
frame_max = max(frame_max, strip.frame_end)
|
||||
|
||||
if frame_min > frame_max:
|
||||
if options.sequence_source == 'ACTIONS':
|
||||
for action in options.actions:
|
||||
if len(action.fcurves) == 0:
|
||||
continue
|
||||
export_sequence = ExportSequence()
|
||||
export_sequence.nla_state.action = action
|
||||
export_sequence.name = get_psa_sequence_name(action, options.should_use_original_sequence_names)
|
||||
frame_min, frame_max = [int(x) for x in action.frame_range]
|
||||
export_sequence.nla_state.frame_min = frame_min
|
||||
export_sequence.nla_state.frame_max = frame_max
|
||||
export_sequence.fps = get_sequence_fps(context, options, [action])
|
||||
export_sequences.append(export_sequence)
|
||||
pass
|
||||
elif options.sequence_source == 'TIMELINE_MARKERS':
|
||||
sequence_frame_ranges = get_timeline_marker_sequence_frame_ranges(animation_data, context, options)
|
||||
|
||||
sequence_frame_ranges[marker_name] = int(frame_min), int(frame_max)
|
||||
for name, (frame_min, frame_max) in sequence_frame_ranges.items():
|
||||
export_sequence = ExportSequence()
|
||||
export_sequence.name = name
|
||||
export_sequence.nla_state.action = None
|
||||
export_sequence.nla_state.frame_min = frame_min
|
||||
export_sequence.nla_state.frame_max = frame_max
|
||||
nla_strips_actions = set(
|
||||
map(lambda x: x.action, get_nla_strips_in_timeframe(animation_data, frame_min, frame_max)))
|
||||
export_sequence.fps = get_sequence_fps(context, options, nla_strips_actions)
|
||||
export_sequences.append(export_sequence)
|
||||
else:
|
||||
raise ValueError(f'Unhandled sequence source: {options.sequence_source}')
|
||||
|
||||
return sequence_frame_ranges
|
||||
# Add prefixes and suffices to the names of the export sequences and strip whitespace.
|
||||
for export_sequence in export_sequences:
|
||||
export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}'.strip()
|
||||
|
||||
# Now build the PSA sequences.
|
||||
# We actually alter the timeline frame and simply record the resultant pose bone matrices.
|
||||
frame_start_index = 0
|
||||
|
||||
for export_sequence in export_sequences:
|
||||
# Link the action to the animation data and update view layer.
|
||||
animation_data.action = export_sequence.nla_state.action
|
||||
context.view_layer.update()
|
||||
|
||||
frame_min = export_sequence.nla_state.frame_min
|
||||
frame_max = export_sequence.nla_state.frame_max
|
||||
frame_count = frame_max - frame_min + 1
|
||||
|
||||
psa_sequence = Psa.Sequence()
|
||||
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
|
||||
psa_sequence.frame_count = frame_count
|
||||
psa_sequence.frame_start_index = frame_start_index
|
||||
psa_sequence.fps = export_sequence.fps
|
||||
|
||||
for frame in range(frame_count):
|
||||
context.scene.frame_set(frame_min + frame)
|
||||
|
||||
for pose_bone in pose_bones:
|
||||
key = Psa.Key()
|
||||
|
||||
if pose_bone.parent is not None:
|
||||
pose_bone_matrix = pose_bone.matrix
|
||||
pose_bone_parent_matrix = pose_bone.parent.matrix
|
||||
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
|
||||
else:
|
||||
if options.root_motion:
|
||||
# Export root motion
|
||||
pose_bone_matrix = armature.matrix_world @ pose_bone.matrix
|
||||
else:
|
||||
pose_bone_matrix = pose_bone.matrix
|
||||
|
||||
location = pose_bone_matrix.to_translation()
|
||||
rotation = pose_bone_matrix.to_quaternion().normalized()
|
||||
|
||||
if pose_bone.parent is not None:
|
||||
rotation.conjugate()
|
||||
|
||||
key.location.x = location.x
|
||||
key.location.y = location.y
|
||||
key.location.z = location.z
|
||||
key.rotation.x = rotation.x
|
||||
key.rotation.y = rotation.y
|
||||
key.rotation.z = rotation.z
|
||||
key.rotation.w = rotation.w
|
||||
key.time = 1.0 / psa_sequence.fps
|
||||
|
||||
psa.keys.append(key)
|
||||
|
||||
psa_sequence.bone_count = len(pose_bones)
|
||||
psa_sequence.track_time = frame_count
|
||||
|
||||
frame_start_index += frame_count
|
||||
|
||||
psa.sequences[export_sequence.name] = psa_sequence
|
||||
|
||||
return psa
|
||||
|
||||
@@ -10,18 +10,13 @@ from bpy.props import BoolProperty, CollectionProperty, EnumProperty, FloatPrope
|
||||
from bpy.types import Action, Operator, PropertyGroup, UIList
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
from .builder import PsaBuilder, PsaBuilderOptions
|
||||
from .builder import PsaBuildOptions, build_psa
|
||||
from .data import *
|
||||
from ..helpers import *
|
||||
from ..types import BoneGroupListItem
|
||||
|
||||
|
||||
class PsaExporter(object):
|
||||
def __init__(self, psa: Psa):
|
||||
self.psa: Psa = psa
|
||||
|
||||
# This method is shared by both PSA/K file formats, move this?
|
||||
@staticmethod
|
||||
def export_psa(psa: Psa, path: str):
|
||||
def write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None):
|
||||
section = Section()
|
||||
section.name = name
|
||||
@@ -32,13 +27,11 @@ class PsaExporter(object):
|
||||
if data is not None:
|
||||
for datum in data:
|
||||
fp.write(datum)
|
||||
|
||||
def export(self, path: str):
|
||||
with open(path, 'wb') as fp:
|
||||
self.write_section(fp, b'ANIMHEAD')
|
||||
self.write_section(fp, b'BONENAMES', Psa.Bone, self.psa.bones)
|
||||
self.write_section(fp, b'ANIMINFO', Psa.Sequence, list(self.psa.sequences.values()))
|
||||
self.write_section(fp, b'ANIMKEYS', Psa.Key, self.psa.keys)
|
||||
with open(path, 'wb') as fp:
|
||||
write_section(fp, b'ANIMHEAD')
|
||||
write_section(fp, b'BONENAMES', Psa.Bone, psa.bones)
|
||||
write_section(fp, b'ANIMINFO', Psa.Sequence, list(psa.sequences.values()))
|
||||
write_section(fp, b'ANIMKEYS', Psa.Key, psa.keys)
|
||||
|
||||
|
||||
class PsaExportActionListItem(PropertyGroup):
|
||||
@@ -64,6 +57,10 @@ def should_use_original_sequence_names_updated(_, context):
|
||||
update_action_names(context)
|
||||
|
||||
|
||||
def psa_export_property_group_animation_data_override_poll(_context, obj):
|
||||
return obj.animation_data is not None
|
||||
|
||||
|
||||
class PsaExportPropertyGroup(PropertyGroup):
|
||||
root_motion: BoolProperty(
|
||||
name='Root Motion',
|
||||
@@ -71,6 +68,15 @@ class PsaExportPropertyGroup(PropertyGroup):
|
||||
default=False,
|
||||
description='When set, the root bone will be transformed as it appears in the scene',
|
||||
)
|
||||
should_override_animation_data: BoolProperty(
|
||||
name='Override Animation Data',
|
||||
options=set(),
|
||||
default=False
|
||||
)
|
||||
animation_data_override: PointerProperty(
|
||||
type=bpy.types.Object,
|
||||
poll=psa_export_property_group_animation_data_override_poll
|
||||
)
|
||||
sequence_source: EnumProperty(
|
||||
name='Source',
|
||||
options=set(),
|
||||
@@ -154,6 +160,10 @@ def is_bone_filter_mode_item_available(context, identifier):
|
||||
return True
|
||||
|
||||
|
||||
def should_action_be_selected_by_default(action):
|
||||
return action is not None and action.asset_data is None
|
||||
|
||||
|
||||
class PsaExportOperator(Operator, ExportHelper):
|
||||
bl_idname = 'psa_export.operator'
|
||||
bl_label = 'Export'
|
||||
@@ -191,8 +201,11 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
# SOURCE
|
||||
layout.prop(pg, 'sequence_source', text='Source')
|
||||
|
||||
# ROOT MOTION
|
||||
layout.prop(pg, 'root_motion', text='Root Motion')
|
||||
if pg.sequence_source == 'TIMELINE_MARKERS':
|
||||
# ANIMDATA SOURCE
|
||||
layout.prop(pg, 'should_override_animation_data')
|
||||
if pg.should_override_animation_data:
|
||||
layout.prop(pg, 'animation_data_override')
|
||||
|
||||
# SELECT ALL/NONE
|
||||
row = layout.row(align=True)
|
||||
@@ -249,15 +262,17 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
layout.template_list('PSX_UL_BoneGroupList', '', pg, 'bone_group_list', pg, 'bone_group_list_index',
|
||||
rows=rows)
|
||||
|
||||
def should_action_be_selected_by_default(self, action):
|
||||
return action is not None and action.asset_data is None
|
||||
layout.separator()
|
||||
|
||||
# ROOT MOTION
|
||||
layout.prop(pg, 'root_motion', text='Root Motion')
|
||||
|
||||
def is_action_for_armature(self, action):
|
||||
if len(action.fcurves) == 0:
|
||||
return False
|
||||
bone_names = set([x.name for x in self.armature.data.bones])
|
||||
for fcurve in action.fcurves:
|
||||
match = re.match(r'pose\.bones\["(.+)"\].\w+', fcurve.data_path)
|
||||
match = re.match(r'pose\.bones\["(.+)"].\w+', fcurve.data_path)
|
||||
if not match:
|
||||
continue
|
||||
bone_name = match.group(1)
|
||||
@@ -273,7 +288,7 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
if context.view_layer.objects.active.type != 'ARMATURE':
|
||||
raise RuntimeError('The selected object must be an armature')
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context, _event):
|
||||
try:
|
||||
self._check_context(context)
|
||||
except RuntimeError as e:
|
||||
@@ -290,7 +305,7 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
item = pg.action_list.add()
|
||||
item.action = action
|
||||
item.name = action.name
|
||||
item.is_selected = self.should_action_be_selected_by_default(action)
|
||||
item.is_selected = should_action_be_selected_by_default(action)
|
||||
|
||||
update_action_names(context)
|
||||
|
||||
@@ -318,7 +333,9 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
actions = [x.action for x in pg.action_list if x.is_selected]
|
||||
marker_names = [x.name for x in pg.marker_list if x.is_selected]
|
||||
|
||||
options = PsaBuilderOptions()
|
||||
options = PsaBuildOptions()
|
||||
options.should_override_animation_data = pg.should_override_animation_data
|
||||
options.animation_data_override = pg.animation_data_override
|
||||
options.fps_source = pg.fps_source
|
||||
options.fps_custom = pg.fps_custom
|
||||
options.sequence_source = pg.sequence_source
|
||||
@@ -332,16 +349,14 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
options.sequence_name_suffix = pg.sequence_name_suffix
|
||||
options.root_motion = pg.root_motion
|
||||
|
||||
builder = PsaBuilder()
|
||||
|
||||
try:
|
||||
psa = builder.build(context, options)
|
||||
psa = build_psa(context, options)
|
||||
except RuntimeError as e:
|
||||
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
|
||||
exporter = PsaExporter(psa)
|
||||
exporter.export(self.filepath)
|
||||
export_psa(psa, self.filepath)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -368,8 +383,7 @@ def filter_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_c
|
||||
return flt_flags
|
||||
|
||||
|
||||
def get_visible_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[
|
||||
PsaExportActionListItem]:
|
||||
def get_visible_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[PsaExportActionListItem]:
|
||||
visible_sequences = []
|
||||
for i, flag in enumerate(filter_sequences(pg, sequences)):
|
||||
if bool(flag & (1 << 30)):
|
||||
@@ -401,10 +415,9 @@ class PSA_UL_ExportSequenceList(UIList):
|
||||
subrow = row.row(align=True)
|
||||
subrow.prop(pg, 'sequence_filter_asset', icon_only=True, icon='ASSET_MANAGER')
|
||||
|
||||
|
||||
def filter_items(self, context, data, property):
|
||||
def filter_items(self, context, data, prop):
|
||||
pg = context.scene.psa_export
|
||||
actions = getattr(data, property)
|
||||
actions = getattr(data, prop)
|
||||
flt_flags = filter_sequences(pg, actions)
|
||||
flt_neworder = bpy.types.UI_UL_list.sort_items_by_name(actions, 'name')
|
||||
return flt_flags, flt_neworder
|
||||
|
||||
@@ -26,198 +26,195 @@ class PsaImportOptions(object):
|
||||
self.action_name_prefix = ''
|
||||
|
||||
|
||||
class PsaImporter(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
def import_psa(psa_reader: PsaReader, armature_object, options: PsaImportOptions):
|
||||
sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names)
|
||||
armature_data = armature_object.data
|
||||
|
||||
def import_psa(self, psa_reader: PsaReader, armature_object, options: PsaImportOptions):
|
||||
sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names)
|
||||
armature_data = armature_object.data
|
||||
class ImportBone(object):
|
||||
def __init__(self, psa_bone: Psa.Bone):
|
||||
self.psa_bone: Psa.Bone = psa_bone
|
||||
self.parent: Optional[ImportBone] = None
|
||||
self.armature_bone = None
|
||||
self.pose_bone = None
|
||||
self.orig_loc: Vector = Vector()
|
||||
self.orig_quat: Quaternion = Quaternion()
|
||||
self.post_quat: Quaternion = Quaternion()
|
||||
self.fcurves = []
|
||||
|
||||
class ImportBone(object):
|
||||
def __init__(self, psa_bone: Psa.Bone):
|
||||
self.psa_bone: Psa.Bone = psa_bone
|
||||
self.parent: Optional[ImportBone] = None
|
||||
self.armature_bone = None
|
||||
self.pose_bone = None
|
||||
self.orig_loc: Vector = Vector()
|
||||
self.orig_quat: Quaternion = Quaternion()
|
||||
self.post_quat: Quaternion = Quaternion()
|
||||
self.fcurves = []
|
||||
def calculate_fcurve_data(import_bone: ImportBone, key_data: []):
|
||||
# Convert world-space transforms to local-space transforms.
|
||||
key_rotation = Quaternion(key_data[0:4])
|
||||
key_location = Vector(key_data[4:])
|
||||
q = import_bone.post_quat.copy()
|
||||
q.rotate(import_bone.orig_quat)
|
||||
quat = q
|
||||
q = import_bone.post_quat.copy()
|
||||
if import_bone.parent is None:
|
||||
q.rotate(key_rotation.conjugated())
|
||||
else:
|
||||
q.rotate(key_rotation)
|
||||
quat.rotate(q.conjugated())
|
||||
loc = key_location - import_bone.orig_loc
|
||||
loc.rotate(import_bone.post_quat.conjugated())
|
||||
return quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z
|
||||
|
||||
def calculate_fcurve_data(import_bone: ImportBone, key_data: []):
|
||||
# Convert world-space transforms to local-space transforms.
|
||||
key_rotation = Quaternion(key_data[0:4])
|
||||
key_location = Vector(key_data[4:])
|
||||
q = import_bone.post_quat.copy()
|
||||
q.rotate(import_bone.orig_quat)
|
||||
quat = q
|
||||
q = import_bone.post_quat.copy()
|
||||
if import_bone.parent is None:
|
||||
q.rotate(key_rotation.conjugated())
|
||||
# Create an index mapping from bones in the PSA to bones in the target armature.
|
||||
psa_to_armature_bone_indices = {}
|
||||
armature_bone_names = [x.name for x in armature_data.bones]
|
||||
psa_bone_names = []
|
||||
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
||||
psa_bone_name = psa_bone.name.decode('windows-1252')
|
||||
psa_bone_names.append(psa_bone_name)
|
||||
try:
|
||||
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Report if there are missing bones in the target armature.
|
||||
missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names))
|
||||
if len(missing_bone_names) > 0:
|
||||
print(
|
||||
f'The armature object \'{armature_object.name}\' is missing the following bones that exist in the PSA:')
|
||||
print(list(sorted(missing_bone_names)))
|
||||
del armature_bone_names
|
||||
|
||||
# Create intermediate bone data for import operations.
|
||||
import_bones = []
|
||||
import_bones_dict = dict()
|
||||
|
||||
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
||||
bone_name = psa_bone.name.decode('windows-1252')
|
||||
if psa_bone_index not in psa_to_armature_bone_indices: # TODO: replace with bone_name in armature_data.bones
|
||||
# PSA bone does not map to armature bone, skip it and leave an empty bone in its place.
|
||||
import_bones.append(None)
|
||||
continue
|
||||
import_bone = ImportBone(psa_bone)
|
||||
import_bone.armature_bone = armature_data.bones[bone_name]
|
||||
import_bone.pose_bone = armature_object.pose.bones[bone_name]
|
||||
import_bones_dict[bone_name] = import_bone
|
||||
import_bones.append(import_bone)
|
||||
|
||||
for import_bone in filter(lambda x: x is not None, import_bones):
|
||||
armature_bone = import_bone.armature_bone
|
||||
if armature_bone.parent is not None and armature_bone.parent.name in psa_bone_names:
|
||||
import_bone.parent = import_bones_dict[armature_bone.parent.name]
|
||||
# Calculate the original location & rotation of each bone (in world-space maybe?)
|
||||
if armature_bone.get('orig_quat') is not None:
|
||||
# TODO: ideally we don't rely on bone auxiliary data like this, the non-aux data path is incorrect
|
||||
# (animations are flipped 180 around Z)
|
||||
import_bone.orig_quat = Quaternion(armature_bone['orig_quat'])
|
||||
import_bone.orig_loc = Vector(armature_bone['orig_loc'])
|
||||
import_bone.post_quat = Quaternion(armature_bone['post_quat'])
|
||||
else:
|
||||
if import_bone.parent is not None:
|
||||
import_bone.orig_loc = armature_bone.matrix_local.translation - armature_bone.parent.matrix_local.translation
|
||||
import_bone.orig_loc.rotate(armature_bone.parent.matrix_local.to_quaternion().conjugated())
|
||||
import_bone.orig_quat = armature_bone.matrix_local.to_quaternion()
|
||||
import_bone.orig_quat.rotate(armature_bone.parent.matrix_local.to_quaternion().conjugated())
|
||||
import_bone.orig_quat.conjugate()
|
||||
else:
|
||||
q.rotate(key_rotation)
|
||||
quat.rotate(q.conjugated())
|
||||
loc = key_location - import_bone.orig_loc
|
||||
loc.rotate(import_bone.post_quat.conjugated())
|
||||
return quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z
|
||||
import_bone.orig_loc = armature_bone.matrix_local.translation.copy()
|
||||
import_bone.orig_quat = armature_bone.matrix_local.to_quaternion()
|
||||
import_bone.post_quat = import_bone.orig_quat.conjugated()
|
||||
|
||||
# Create an index mapping from bones in the PSA to bones in the target armature.
|
||||
psa_to_armature_bone_indices = {}
|
||||
armature_bone_names = [x.name for x in armature_data.bones]
|
||||
psa_bone_names = []
|
||||
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
||||
psa_bone_name = psa_bone.name.decode('windows-1252')
|
||||
psa_bone_names.append(psa_bone_name)
|
||||
try:
|
||||
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name)
|
||||
except ValueError:
|
||||
pass
|
||||
# Create and populate the data for new sequences.
|
||||
actions = []
|
||||
for sequence in sequences:
|
||||
# Add the action.
|
||||
sequence_name = sequence.name.decode('windows-1252')
|
||||
action_name = options.action_name_prefix + sequence_name
|
||||
|
||||
# Report if there are missing bones in the target armature.
|
||||
missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names))
|
||||
if len(missing_bone_names) > 0:
|
||||
print(
|
||||
f'The armature object \'{armature_object.name}\' is missing the following bones that exist in the PSA:')
|
||||
print(list(sorted(missing_bone_names)))
|
||||
del armature_bone_names
|
||||
if options.should_overwrite and action_name in bpy.data.actions:
|
||||
action = bpy.data.actions[action_name]
|
||||
else:
|
||||
action = bpy.data.actions.new(name=action_name)
|
||||
|
||||
# Create intermediate bone data for import operations.
|
||||
import_bones = []
|
||||
import_bones_dict = dict()
|
||||
if options.should_write_keyframes:
|
||||
# Remove existing f-curves (replace with action.fcurves.clear() in Blender 3.2)
|
||||
while len(action.fcurves) > 0:
|
||||
action.fcurves.remove(action.fcurves[-1])
|
||||
|
||||
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
||||
bone_name = psa_bone.name.decode('windows-1252')
|
||||
if psa_bone_index not in psa_to_armature_bone_indices: # TODO: replace with bone_name in armature_data.bones
|
||||
# PSA bone does not map to armature bone, skip it and leave an empty bone in its place.
|
||||
import_bones.append(None)
|
||||
continue
|
||||
import_bone = ImportBone(psa_bone)
|
||||
import_bone.armature_bone = armature_data.bones[bone_name]
|
||||
import_bone.pose_bone = armature_object.pose.bones[bone_name]
|
||||
import_bones_dict[bone_name] = import_bone
|
||||
import_bones.append(import_bone)
|
||||
# Create f-curves for the rotation and location of each bone.
|
||||
for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items():
|
||||
import_bone = import_bones[psa_bone_index]
|
||||
pose_bone = import_bone.pose_bone
|
||||
rotation_data_path = pose_bone.path_from_id('rotation_quaternion')
|
||||
location_data_path = pose_bone.path_from_id('location')
|
||||
import_bone.fcurves = [
|
||||
action.fcurves.new(rotation_data_path, index=0, action_group=pose_bone.name), # Qw
|
||||
action.fcurves.new(rotation_data_path, index=1, action_group=pose_bone.name), # Qx
|
||||
action.fcurves.new(rotation_data_path, index=2, action_group=pose_bone.name), # Qy
|
||||
action.fcurves.new(rotation_data_path, index=3, action_group=pose_bone.name), # Qz
|
||||
action.fcurves.new(location_data_path, index=0, action_group=pose_bone.name), # Lx
|
||||
action.fcurves.new(location_data_path, index=1, action_group=pose_bone.name), # Ly
|
||||
action.fcurves.new(location_data_path, index=2, action_group=pose_bone.name), # Lz
|
||||
]
|
||||
|
||||
for import_bone in filter(lambda x: x is not None, import_bones):
|
||||
armature_bone = import_bone.armature_bone
|
||||
if armature_bone.parent is not None and armature_bone.parent.name in psa_bone_names:
|
||||
import_bone.parent = import_bones_dict[armature_bone.parent.name]
|
||||
# Calculate the original location & rotation of each bone (in world-space maybe?)
|
||||
if armature_bone.get('orig_quat') is not None:
|
||||
# TODO: ideally we don't rely on bone auxiliary data like this, the non-aux data path is incorrect (animations are flipped 180 around Z)
|
||||
import_bone.orig_quat = Quaternion(armature_bone['orig_quat'])
|
||||
import_bone.orig_loc = Vector(armature_bone['orig_loc'])
|
||||
import_bone.post_quat = Quaternion(armature_bone['post_quat'])
|
||||
else:
|
||||
if import_bone.parent is not None:
|
||||
import_bone.orig_loc = armature_bone.matrix_local.translation - armature_bone.parent.matrix_local.translation
|
||||
import_bone.orig_loc.rotate(armature_bone.parent.matrix_local.to_quaternion().conjugated())
|
||||
import_bone.orig_quat = armature_bone.matrix_local.to_quaternion()
|
||||
import_bone.orig_quat.rotate(armature_bone.parent.matrix_local.to_quaternion().conjugated())
|
||||
import_bone.orig_quat.conjugate()
|
||||
else:
|
||||
import_bone.orig_loc = armature_bone.matrix_local.translation.copy()
|
||||
import_bone.orig_quat = armature_bone.matrix_local.to_quaternion()
|
||||
import_bone.post_quat = import_bone.orig_quat.conjugated()
|
||||
# Read the sequence data matrix from the PSA.
|
||||
sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name)
|
||||
keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8)
|
||||
|
||||
# Create and populate the data for new sequences.
|
||||
actions = []
|
||||
for sequence in sequences:
|
||||
# Add the action.
|
||||
sequence_name = sequence.name.decode('windows-1252')
|
||||
action_name = options.action_name_prefix + sequence_name
|
||||
# Convert the sequence's data from world-space to local-space.
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for frame_index in range(sequence.frame_count):
|
||||
# This bone has writeable keyframes for this frame.
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
# Calculate the local-space key data for the bone.
|
||||
sequence_data_matrix[frame_index, bone_index] = calculate_fcurve_data(import_bone, key_data)
|
||||
|
||||
if options.should_overwrite and action_name in bpy.data.actions:
|
||||
action = bpy.data.actions[action_name]
|
||||
else:
|
||||
action = bpy.data.actions.new(name=action_name)
|
||||
|
||||
if options.should_write_keyframes:
|
||||
# Remove existing f-curves (replace with action.fcurves.clear() in Blender 3.2)
|
||||
while len(action.fcurves) > 0:
|
||||
action.fcurves.remove(action.fcurves[-1])
|
||||
|
||||
# Create f-curves for the rotation and location of each bone.
|
||||
for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items():
|
||||
import_bone = import_bones[psa_bone_index]
|
||||
pose_bone = import_bone.pose_bone
|
||||
rotation_data_path = pose_bone.path_from_id('rotation_quaternion')
|
||||
location_data_path = pose_bone.path_from_id('location')
|
||||
import_bone.fcurves = [
|
||||
action.fcurves.new(rotation_data_path, index=0, action_group=pose_bone.name), # Qw
|
||||
action.fcurves.new(rotation_data_path, index=1, action_group=pose_bone.name), # Qx
|
||||
action.fcurves.new(rotation_data_path, index=2, action_group=pose_bone.name), # Qy
|
||||
action.fcurves.new(rotation_data_path, index=3, action_group=pose_bone.name), # Qz
|
||||
action.fcurves.new(location_data_path, index=0, action_group=pose_bone.name), # Lx
|
||||
action.fcurves.new(location_data_path, index=1, action_group=pose_bone.name), # Ly
|
||||
action.fcurves.new(location_data_path, index=2, action_group=pose_bone.name), # Lz
|
||||
]
|
||||
|
||||
# Read the sequence data matrix from the PSA.
|
||||
sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name)
|
||||
keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8)
|
||||
|
||||
# Convert the sequence's data from world-space to local-space.
|
||||
# Clean the keyframe data. This is accomplished by writing zeroes to the write matrix when there is an
|
||||
# insufficiently large change in the data from the last written frame.
|
||||
if options.should_clean_keys:
|
||||
threshold = 0.001
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for frame_index in range(sequence.frame_count):
|
||||
for fcurve_index in range(len(import_bone.fcurves)):
|
||||
# Get all the keyframe data for the bone's f-curve data from the sequence data matrix.
|
||||
fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index]
|
||||
last_written_datum = 0
|
||||
for frame_index, datum in enumerate(fcurve_frame_data):
|
||||
# If the f-curve data is not different enough to the last written frame, un-mark this data for writing.
|
||||
if frame_index > 0 and abs(datum - last_written_datum) < threshold:
|
||||
keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0
|
||||
else:
|
||||
last_written_datum = datum
|
||||
|
||||
# Write the keyframes out!
|
||||
for frame_index in range(sequence.frame_count):
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
bone_has_writeable_keyframes = any(keyframe_write_matrix[frame_index, bone_index])
|
||||
if bone_has_writeable_keyframes:
|
||||
# This bone has writeable keyframes for this frame.
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
# Calculate the local-space key data for the bone.
|
||||
sequence_data_matrix[frame_index, bone_index] = calculate_fcurve_data(import_bone, key_data)
|
||||
for fcurve, should_write, datum in zip(import_bone.fcurves,
|
||||
keyframe_write_matrix[frame_index, bone_index],
|
||||
key_data):
|
||||
if should_write:
|
||||
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
|
||||
|
||||
# Clean the keyframe data. This is accomplished by writing zeroes to the write matrix when there is an
|
||||
# insufficiently large change in the data from the last written frame.
|
||||
if options.should_clean_keys:
|
||||
threshold = 0.001
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for fcurve_index in range(len(import_bone.fcurves)):
|
||||
# Get all the keyframe data for the bone's f-curve data from the sequence data matrix.
|
||||
fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index]
|
||||
last_written_datum = 0
|
||||
for frame_index, datum in enumerate(fcurve_frame_data):
|
||||
# If the f-curve data is not different enough to the last written frame, un-mark this data for writing.
|
||||
if frame_index > 0 and abs(datum - last_written_datum) < threshold:
|
||||
keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0
|
||||
else:
|
||||
last_written_datum = datum
|
||||
# Write
|
||||
if options.should_write_metadata:
|
||||
action['psa_sequence_name'] = sequence_name
|
||||
action['psa_sequence_fps'] = sequence.fps
|
||||
|
||||
# Write the keyframes out!
|
||||
for frame_index in range(sequence.frame_count):
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
bone_has_writeable_keyframes = any(keyframe_write_matrix[frame_index, bone_index])
|
||||
if bone_has_writeable_keyframes:
|
||||
# This bone has writeable keyframes for this frame.
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
for fcurve, should_write, datum in zip(import_bone.fcurves,
|
||||
keyframe_write_matrix[frame_index, bone_index],
|
||||
key_data):
|
||||
if should_write:
|
||||
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
|
||||
action.use_fake_user = options.should_use_fake_user
|
||||
|
||||
# Write
|
||||
if options.should_write_metadata:
|
||||
action['psa_sequence_name'] = sequence_name
|
||||
action['psa_sequence_fps'] = sequence.fps
|
||||
actions.append(action)
|
||||
|
||||
action.use_fake_user = options.should_use_fake_user
|
||||
|
||||
actions.append(action)
|
||||
|
||||
# If the user specifies, store the new animations as strips on a non-contributing NLA track.
|
||||
if options.should_stash:
|
||||
if armature_object.animation_data is None:
|
||||
armature_object.animation_data_create()
|
||||
for action in actions:
|
||||
nla_track = armature_object.animation_data.nla_tracks.new()
|
||||
nla_track.name = action.name
|
||||
nla_track.mute = True
|
||||
nla_track.strips.new(name=action.name, start=0, action=action)
|
||||
# If the user specifies, store the new animations as strips on a non-contributing NLA track.
|
||||
if options.should_stash:
|
||||
if armature_object.animation_data is None:
|
||||
armature_object.animation_data_create()
|
||||
for action in actions:
|
||||
nla_track = armature_object.animation_data.nla_tracks.new()
|
||||
nla_track.name = action.name
|
||||
nla_track.mute = True
|
||||
nla_track.strips.new(name=action.name, start=0, action=action)
|
||||
|
||||
|
||||
class PsaImportActionListItem(PropertyGroup):
|
||||
@@ -403,7 +400,7 @@ class PsaImportSequencesFromText(Operator):
|
||||
class PsaImportSequencesSelectAll(Operator):
|
||||
bl_idname = 'psa_import.sequences_select_all'
|
||||
bl_label = 'All'
|
||||
bl_description = 'Select all visible sequences'
|
||||
bl_description = 'Select all sequences'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
@@ -589,7 +586,7 @@ class PsaImportOperator(Operator):
|
||||
options.should_write_metadata = pg.should_write_metadata
|
||||
options.should_write_keyframes = pg.should_write_keyframes
|
||||
|
||||
PsaImporter().import_psa(psa_reader, context.view_layer.objects.active, options)
|
||||
import_psa(psa_reader, context.view_layer.objects.active, options)
|
||||
|
||||
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ from .data import *
|
||||
|
||||
class PsaReader(object):
|
||||
"""
|
||||
This class reads the sequences and bone information immediately upon instantiation and hold onto a file handle.
|
||||
The key data is not read into memory upon instantiation due to it's potentially very large size.
|
||||
This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle.
|
||||
The keyframe data is not read into memory upon instantiation due to it's potentially very large size.
|
||||
To read the key data for a particular sequence, call `read_sequence_keys`.
|
||||
"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user