Refactor: convert Rigify from legacy Action API to the current API
Minimal changes to make Rigify use the current Action API (introduced in Blender 4.4) instead of the legacy API (removed in 5.0). Most of the refactoring consists of: - Find the right `Channelbag` - Replace operations on `Action` with operations on that `Channelbag`. I didn't manage to test all code, because some code paths are very hard to follow, and others seem to only be available for legacy rigs. This is part of #146586 Pull Request: https://projects.blender.org/blender/blender/pulls/147060
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'}
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user