diff --git a/scripts/addons_core/rigify/rigs/limbs/spline_tentacle.py b/scripts/addons_core/rigify/rigs/limbs/spline_tentacle.py index 0099c02fb2a..329c2221b75 100644 --- a/scripts/addons_core/rigify/rigs/limbs/spline_tentacle.py +++ b/scripts/addons_core/rigify/rigs/limbs/spline_tentacle.py @@ -1161,20 +1161,28 @@ class RigifySplineTentacleToggleControlBase: obj.pose.bones[self.prop_bone][self.prop_name] = value def keyframe_increment(self, context, obj, delta): - action = find_action(obj) + assert isinstance(obj, bpy.types.Object), "Expected {} to be an object".format(obj) + + action = obj.animation_data.action + action_slot = obj.animation_data.action_slot + + assert action, "Expected {} to have an Action assigned".format(obj.name) + assert action_slot, "Expected {} to have an Action slot assigned".format(obj.name) + bone = obj.pose.bones[self.prop_bone] prop_quoted = rna_idprop_quote_path(self.prop_name) # Find the F-Curve data_path = bone.path_from_id(prop_quoted) - fcurve = action.fcurves.find(data_path) or action.fcurves.new(data_path, action_group=self.prop_bone) + channelbag = anim_utils.action_ensure_channelbag_for_slot(action, action_slot) + fcurve = channelbag.fcurves.ensure(data_path, group_name=self.prop_bone) # Ensure the current value is keyed at the start of the animation keyflags = get_keying_flags(context) frame = context.scene.frame_current if len(fcurve.keyframe_points) == 0: - min_x = min(fcu.keyframe_points[0].co[0] for fcu in action.fcurves if len(fcu.keyframe_points) > 0) + min_x = min(fcu.keyframe_points[0].co[0] for fcu in channelbag.fcurves if len(fcu.keyframe_points) > 0) min_frame = nla_tweak_to_scene(obj.animation_data, min_x) if min_frame < frame: bone.keyframe_insert(prop_quoted, frame=min_frame, options=keyflags) diff --git a/scripts/addons_core/rigify/rot_mode.py b/scripts/addons_core/rigify/rot_mode.py index b155a8020e7..9b2945fdd56 100644 --- a/scripts/addons_core/rigify/rot_mode.py +++ b/scripts/addons_core/rigify/rot_mode.py @@ -14,6 +14,7 @@ This script/addon: - Converts multiple Actions TO-DO: + - Properly support slotted Actions. - To convert object's rotation mode (already done in Mutant Bob script, but not done in this one.) - To understand "EnumProperty" and write it well. @@ -47,87 +48,90 @@ from bpy.props import ( EnumProperty, StringProperty, ) +from bpy.types import ( + ActionChannelbag, +) + +from bpy_extras import anim_utils -def get_or_create_fcurve(action, data_path, array_index=-1, group=None): - for fc in action.fcurves: - if fc.data_path == data_path and (array_index < 0 or fc.array_index == array_index): - return fc - - fc = action.fcurves.new(data_path, index=array_index) - fc.group = group - return fc - - -def add_keyframe_quat(action, quat, frame, bone_prefix, group): +def add_keyframe_quat( + channelbag: ActionChannelbag, + quat: list[float], + frame: float, + bone_prefix: str, + group_name: str) -> None: for i in range(len(quat)): - fc = get_or_create_fcurve(action, bone_prefix + "rotation_quaternion", i, group) + fc = channelbag.fcurves.ensure(bone_prefix + "rotation_quaternion", index=i, group_name=group_name) pos = len(fc.keyframe_points) fc.keyframe_points.add(1) fc.keyframe_points[pos].co = [frame, quat[i]] fc.update() -def add_keyframe_euler(action, euler, frame, bone_prefix, group): +def add_keyframe_euler( + channelbag: ActionChannelbag, + euler: list[float], + frame: float, + bone_prefix: str, + group_name: str) -> None: for i in range(len(euler)): - fc = get_or_create_fcurve(action, bone_prefix + "rotation_euler", i, group) + fc = channelbag.fcurves.ensure(bone_prefix + "rotation_euler", index=i, group_name=group_name) pos = len(fc.keyframe_points) fc.keyframe_points.add(1) fc.keyframe_points[pos].co = [frame, euler[i]] fc.update() -def frames_matching(action, data_path): +def frames_matching(channelbag, data_path): frames = set() - for fc in action.fcurves: + for fc in channelbag.fcurves: if fc.data_path == data_path: fri = [kp.co[0] for kp in fc.keyframe_points] frames.update(fri) return frames -def group_qe(_obj, action, bone, bone_prefix, order): - """Converts only one group/bone in one action - Quaternion to euler.""" +def group_qe(_obj, channelbag, bone, bone_prefix, order): + """Converts only one group/bone in one channelbag - Quaternion to euler.""" # pose_bone = bone data_path = bone_prefix + "rotation_quaternion" - frames = frames_matching(action, data_path) - group = action.groups[bone.name] + frames = frames_matching(channelbag, data_path) for fr in frames: quat = bone.rotation_quaternion.copy() - for fc in action.fcurves: + for fc in channelbag.fcurves: if fc.data_path == data_path: quat[fc.array_index] = fc.evaluate(fr) euler = quat.to_euler(order) - add_keyframe_euler(action, euler, fr, bone_prefix, group) + add_keyframe_euler(channelbag, euler, fr, bone_prefix, bone.name) bone.rotation_mode = order -def group_eq(_obj, action, bone, bone_prefix, order): - """Converts only one group/bone in one action - Euler to Quaternion.""" +def group_eq(obj, channelbag, bone, bone_prefix, order): + """Converts only one group/bone in one channelbag - Euler to Quaternion.""" # pose_bone = bone data_path = bone_prefix + "rotation_euler" - frames = frames_matching(action, data_path) - group = action.groups[bone.name] + frames = frames_matching(channelbag, data_path) for fr in frames: euler = bone.rotation_euler.copy() - for fc in action.fcurves: + for fc in channelbag.fcurves: if fc.data_path == data_path: euler[fc.array_index] = fc.evaluate(fr) quat = euler.to_quaternion() - add_keyframe_quat(action, quat, fr, bone_prefix, group) + add_keyframe_quat(channelbag, quat, fr, bone_prefix, bone.name) bone.rotation_mode = order -def convert_curves_of_bone_in_action(obj, action, bone, order): - """Convert given bone's curves in given action to given rotation order.""" +def convert_curves_of_bone(obj, channelbag, bone, order): + """Convert given bone's curves in given channelbag to given rotation order.""" to_euler = False bone_prefix = '' - for fcurve in action.fcurves: + for fcurve in channelbag.fcurves: if fcurve.group.name == bone.name: # If To-Euler conversion @@ -149,25 +153,25 @@ def convert_curves_of_bone_in_action(obj, action, bone, order): # If To-Euler conversion if to_euler and order != 'QUATERNION': # Converts the group/bone from Quaternion to Euler - group_qe(obj, action, bone, bone_prefix, order) + group_qe(obj, channelbag, bone, bone_prefix, order) # Removes quaternion fcurves - for key in action.fcurves: + for key in channelbag.fcurves: if key.data_path == 'pose.bones["' + bone.name + '"].rotation_quaternion': fcurves_to_remove.append(key) # If To-Quaternion conversion elif to_euler: # Converts the group/bone from Euler to Quaternion - group_eq(obj, action, bone, bone_prefix, order) + group_eq(obj, channelbag, bone, bone_prefix, order) # Removes euler fcurves - for key in action.fcurves: + for key in channelbag.fcurves: if key.data_path == 'pose.bones["' + bone.name + '"].rotation_euler': fcurves_to_remove.append(key) for fcurve in fcurves_to_remove: - action.fcurves.remove(fcurve) + channelbag.fcurves.remove(fcurve) # Changes rotation mode to new one bone.rotation_mode = order @@ -239,16 +243,40 @@ class POSE_OT_convert_rotation(bpy.types.Operator): def execute(self, context): obj = context.active_object - actions = [bpy.data.actions.get(self.selected_action)] - pose_bones = context.selected_pose_bones + assigned_action = obj.animation_data and obj.animation_data.action + assigned_slot = obj.animation_data and obj.animation_data.action_slot + if self.affected_bones == 'ALL': pose_bones = obj.pose.bones + else: + pose_bones = context.selected_pose_bones + if self.affected_actions == 'ALL': actions = bpy.data.actions + else: + actions = [bpy.data.actions.get(self.selected_action)] for action in actions: + if action == assigned_action: + # On the assigned Action, use the assigned slot. + action_slot = assigned_slot + else: + # Otherwise find a suitable slot to process. + # + # NOTE: this may not pick the right slot if they are not + # consistently named. Also, if there are multiple suitable + # slots, only the first one is converted. + if assigned_slot.identifier in action.slots: + action_slot = action.slots[assigned_slot.identifier] + else: + action_slot = anim_utils.action_get_first_suitable_slot(action, 'OBJECT') + + channelbag = anim_utils.action_get_channelbag_for_slot(action, action_slot) + if not channelbag: + continue + for pb in pose_bones: - convert_curves_of_bone_in_action(obj, action, pb, self.target_rotation_mode) + convert_curves_of_bone(obj, channelbag, pb, self.target_rotation_mode) return {'FINISHED'} diff --git a/scripts/addons_core/rigify/ui.py b/scripts/addons_core/rigify/ui.py index 040d586765b..23341441814 100644 --- a/scripts/addons_core/rigify/ui.py +++ b/scripts/addons_core/rigify/ui.py @@ -16,6 +16,7 @@ from bpy.app.translations import ( pgettext_rpt as rpt_, contexts as i18n_contexts, ) +from bpy_extras import anim_utils from collections import defaultdict from typing import TYPE_CHECKING, Callable, Any @@ -1469,7 +1470,7 @@ def ik_to_fk(rig: ArmatureObject, window='ALL'): break -def clear_animation(act, anim_type, names): +def clear_animation(channelbag, anim_type, names): bones = [] for group in names: if names[group]['limb_type'] == 'arm': @@ -1485,7 +1486,7 @@ def clear_animation(act, anim_type, names): bones.extend([names[group]['controls'][1], names[group]['controls'][2], names[group]['controls'][3], names[group]['controls'][4]]) f_curves = [] - for fcu in act.fcurves: + for fcu in channelbag.fcurves: words = fcu.data_path.split('"') if words[0] == "pose.bones[" and words[1] in bones: f_curves.append(fcu) @@ -1494,7 +1495,7 @@ def clear_animation(act, anim_type, names): return for fcu in f_curves: - act.fcurves.remove(fcu) + channelbag.fcurves.remove(fcu) # Put cleared bones back to rest pose bpy.ops.pose.loc_clear() @@ -1673,7 +1674,7 @@ class OBJECT_OT_ClearAnimation(bpy.types.Operator): bl_idname = "rigify.clear_animation" bl_label = "Clear Animation" bl_description = "Clear animation for FK or IK bones" - bl_options = {'INTERNAL'} + bl_options = {'INTERNAL', 'UNDO'} anim_type: StringProperty() @@ -1681,13 +1682,14 @@ class OBJECT_OT_ClearAnimation(bpy.types.Operator): rig = verify_armature_obj(context.object) if not rig.animation_data: - return {'FINISHED'} + return {'CANCELLED'} - act = rig.animation_data.action - if not act: - return {'FINISHED'} + channelbag = anim_utils.action_get_channelbag_for_slot( + rig.animation_data.action, rig.animation_data.action_slot) + if not channelbag: + return {'CANCELLED'} - clear_animation(act, self.anim_type, names=get_limb_generated_names(rig)) + clear_animation(channelbag, self.anim_type, names=get_limb_generated_names(rig)) return {'FINISHED'} diff --git a/scripts/addons_core/rigify/utils/action_layers.py b/scripts/addons_core/rigify/utils/action_layers.py index 5e7caccd71c..cb6c42e7032 100644 --- a/scripts/addons_core/rigify/utils/action_layers.py +++ b/scripts/addons_core/rigify/utils/action_layers.py @@ -5,6 +5,7 @@ from typing import Optional, List, Dict, Tuple, TYPE_CHECKING from bpy.types import Action, Mesh, Armature from bl_math import clamp +from bpy_extras import anim_utils from .errors import MetarigError from .misc import MeshObject, IdPropSequence, verify_mesh_obj @@ -48,7 +49,14 @@ class ActionSlotBase: """Return a list of bone names that have keyframes in the Action of this Slot.""" keyed_bones = [] - for fc in self.action.fcurves: + # This will get updated to actually support slots properly in #146182. For + # now, just pick the first suitable action slot. + action_slot = anim_utils.action_get_first_suitable_slot(self.action, 'OBJECT') + channelbag = anim_utils.action_get_channelbag_for_slot(self.action, action_slot) + if not channelbag: + return [] + + for fc in channelbag.fcurves: # Extracting bone name from fcurve data path if fc.data_path.startswith('pose.bones["'): bone_name = fc.data_path[12:].split('"]')[0] diff --git a/scripts/addons_core/rigify/utils/animation.py b/scripts/addons_core/rigify/utils/animation.py index 21659582518..de53da9e662 100644 --- a/scripts/addons_core/rigify/utils/animation.py +++ b/scripts/addons_core/rigify/utils/animation.py @@ -8,6 +8,7 @@ from mathutils import Matrix, Vector # noqa from typing import TYPE_CHECKING, Callable, Any, Collection, Iterator, Optional, Sequence from bpy.types import Action, bpy_struct, FCurve +from bpy_extras import anim_utils import json @@ -22,15 +23,24 @@ rig_id = None # Keyframing functions ############################################## -def get_keyed_frames_in_range(context, rig): - action = find_action(rig) - if action: - frame_range = RIGIFY_OT_get_frame_range.get_range(context) +def _get_channelbag_for_rig(rig: bpy.types.Object) -> bpy.types.ActionChannelbag | None: + assert isinstance(rig, bpy.types.Object) + if not rig.animation_data: + return None - return sorted(get_curve_frame_set(action.fcurves, frame_range)) - else: + action = rig.animation_data.action + action_slot = rig.animation_data.action_slot + return anim_utils.action_get_channelbag_for_slot(action, action_slot) + + +def get_keyed_frames_in_range(context: bpy.types.Context, rig: bpy.types.Object) -> list[float]: + channelbag = _get_channelbag_for_rig(rig) + if not channelbag: return [] + frame_range = RIGIFY_OT_get_frame_range.get_range(context) + return sorted(get_curve_frame_set(channelbag.fcurves, frame_range)) + def bones_in_frame(f, rig, *args): """ @@ -41,12 +51,11 @@ def bones_in_frame(f, rig, *args): :return: """ - if rig.animation_data and rig.animation_data.action: - fcurves = rig.animation_data.action.fcurves - else: + channelbag = _get_channelbag_for_rig(rig) + if not channelbag: return False - for fc in fcurves: + for fc in channelbag.fcurves: animated_frames = [kp.co[0] for kp in fc.keyframe_points] for bone in args: if bone in fc.data_path.split('"') and f in animated_frames: @@ -56,14 +65,14 @@ def bones_in_frame(f, rig, *args): def overwrite_prop_animation(rig, bone, prop_name, value, frames): - act = rig.animation_data.action - if not act: + channelbag = _get_channelbag_for_rig(rig) + if not channelbag: return bone_name = bone.name curve = None - for fcu in act.fcurves: + for fcu in channelbag.fcurves: words = fcu.data_path.split('"') if words[0] == "pose.bones[" and words[1] == bone_name and words[-2] == prop_name: curve = fcu @@ -310,6 +319,8 @@ SCRIPT_UTILITIES_CURVES = [''' ## Animation curve tools ## ########################### +from bpy_extras import anim_utils + def flatten_curve_set(curves): "Iterate over all FCurves inside a set of nested lists and dictionaries." if curves is None: @@ -375,9 +386,12 @@ def find_action(action): def clean_action_empty_curves(action): "Delete completely empty curves from the given action." action = find_action(action) - for curve in list(action.fcurves): - if curve.is_empty: - action.fcurves.remove(curve) + for layer in action.layers: + for strip in layer.strips: + for channelbag in strip.channelbags: + for curve in channelbag.fcurves[:]: + if curve.is_empty: + channelbag.fcurves.remove(curve) action.update_tag() TRANSFORM_PROPS_LOCATION = frozenset(['location']) @@ -428,11 +442,19 @@ class FCurveTable(object): class ActionCurveTable(FCurveTable): "Table for efficient lookup of Action FCurves by properties." - def __init__(self, action): + def __init__(self, rig: bpy.types.Object) -> None: super().__init__() - self.action = find_action(action) - if self.action: - self.index_curves(self.action.fcurves) + assert isinstance(rig, bpy.types.Object) + + if not rig.animation_data: + return + action = rig.animation_data.action + action_slot = rig.animation_data.action_slot + + channelbag = anim_utils.action_get_channelbag_for_slot(action, action_slot) + if not channelbag: + return + self.index_curves(channelbag.fcurves) class DriverCurveTable(FCurveTable): "Table for efficient lookup of Driver FCurves by properties." diff --git a/scripts/modules/bpy_extras/anim_utils.py b/scripts/modules/bpy_extras/anim_utils.py index 17e51b8bd4d..9807074630f 100644 --- a/scripts/modules/bpy_extras/anim_utils.py +++ b/scripts/modules/bpy_extras/anim_utils.py @@ -96,6 +96,23 @@ def action_get_channelbag_for_slot(action: Action | None, slot: ActionSlot | Non return None +def action_get_first_suitable_slot(action: Action | None, target_id_type: str) -> ActionSlot | None: + """Return the first Slot of the given Action that's suitable for the given ID type. + + Typically you should not need this function; when an Action is assigned to a + data-block, just use the slot that was assigned along with it. + """ + + if not action: + return None + + slot_types = ('UNSPECIFIED', target_id_type) + for slot in action.slots: + if slot.target_id_type in slot_types: + return slot + return None + + def action_ensure_channelbag_for_slot(action: Action, slot: ActionSlot) -> ActionChannelbag: """Ensure a layer and a keyframe strip exists, then ensure that strip has a channelbag for the slot."""