Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'rigify/utils/animation.py')
-rw-r--r--rigify/utils/animation.py818
1 files changed, 806 insertions, 12 deletions
diff --git a/rigify/utils/animation.py b/rigify/utils/animation.py
index ab99282f..62042923 100644
--- a/rigify/utils/animation.py
+++ b/rigify/utils/animation.py
@@ -18,25 +18,28 @@
# <pep8 compliant>
+import bpy
+
+import math
+import json
+
+from mathutils import Matrix, Vector
+
+rig_id = None
#=============================================
# Keyframing functions
#=============================================
-def get_keyed_frames(rig):
- frames = []
- if rig.animation_data:
- if rig.animation_data.action:
- fcus = rig.animation_data.action.fcurves
- for fc in fcus:
- for kp in fc.keyframe_points:
- if kp.co[0] not in frames:
- frames.append(kp.co[0])
+def get_keyed_frames_in_range(context, rig):
+ action = find_action(rig)
+ if action:
+ frame_range = RIGIFY_OT_get_frame_range.get_range(context)
- frames.sort()
-
- return frames
+ return sorted(get_curve_frame_set(action.fcurves, frame_range))
+ else:
+ return []
def bones_in_frame(f, rig, *args):
@@ -82,3 +85,794 @@ def overwrite_prop_animation(rig, bone, prop_name, value, frames):
for kp in curve.keyframe_points:
if kp.co[0] in frames:
kp.co[1] = value
+
+################################################################
+# Utilities for inserting keyframes and/or setting transforms ##
+################################################################
+
+SCRIPT_UTILITIES_KEYING = ['''
+######################
+## Keyframing tools ##
+######################
+
+def get_keying_flags(context):
+ "Retrieve the general keyframing flags from user preferences."
+ prefs = context.preferences
+ ts = context.scene.tool_settings
+ flags = set()
+ # Not adding INSERTKEY_VISUAL
+ if prefs.edit.use_keyframe_insert_needed:
+ flags.add('INSERTKEY_NEEDED')
+ if prefs.edit.use_insertkey_xyz_to_rgb:
+ flags.add('INSERTKEY_XYZ_TO_RGB')
+ if ts.use_keyframe_cycle_aware:
+ flags.add('INSERTKEY_CYCLE_AWARE')
+ return flags
+
+def get_autokey_flags(context, ignore_keyset=False):
+ "Retrieve the Auto Keyframe flags, or None if disabled."
+ ts = context.scene.tool_settings
+ if ts.use_keyframe_insert_auto and (ignore_keyset or not ts.use_keyframe_insert_keyingset):
+ flags = get_keying_flags(context)
+ if context.preferences.edit.use_keyframe_insert_available:
+ flags.add('INSERTKEY_AVAILABLE')
+ if ts.auto_keying_mode == 'REPLACE_KEYS':
+ flags.add('INSERTKEY_REPLACE')
+ return flags
+ else:
+ return None
+
+def add_flags_if_set(base, new_flags):
+ "Add more flags if base is not None."
+ if base is None:
+ return None
+ else:
+ return base | new_flags
+
+def get_4d_rotlock(bone):
+ "Retrieve the lock status for 4D rotation."
+ if bone.lock_rotations_4d:
+ return [bone.lock_rotation_w, *bone.lock_rotation]
+ else:
+ return [all(bone.lock_rotation)] * 4
+
+def keyframe_transform_properties(obj, bone_name, keyflags, *, ignore_locks=False, no_loc=False, no_rot=False, no_scale=False):
+ "Keyframe transformation properties, taking flags and mode into account, and avoiding keying locked channels."
+ bone = obj.pose.bones[bone_name]
+
+ def keyframe_channels(prop, locks):
+ if ignore_locks or not all(locks):
+ if ignore_locks or not any(locks):
+ bone.keyframe_insert(prop, group=bone_name, options=keyflags)
+ else:
+ for i, lock in enumerate(locks):
+ if not lock:
+ bone.keyframe_insert(prop, index=i, group=bone_name, options=keyflags)
+
+ if not (no_loc or bone.bone.use_connect):
+ keyframe_channels('location', bone.lock_location)
+
+ if not no_rot:
+ if bone.rotation_mode == 'QUATERNION':
+ keyframe_channels('rotation_quaternion', get_4d_rotlock(bone))
+ elif bone.rotation_mode == 'AXIS_ANGLE':
+ keyframe_channels('rotation_axis_angle', get_4d_rotlock(bone))
+ else:
+ keyframe_channels('rotation_euler', bone.lock_rotation)
+
+ if not no_scale:
+ keyframe_channels('scale', bone.lock_scale)
+
+######################
+## Constraint tools ##
+######################
+
+def get_constraint_target_matrix(con):
+ target = con.target
+ if target:
+ if target.type == 'ARMATURE' and con.subtarget:
+ if con.subtarget in target.pose.bones:
+ bone = target.pose.bones[con.subtarget]
+ return target.convert_space(pose_bone=bone, matrix=bone.matrix, from_space='POSE', to_space=con.target_space)
+ else:
+ return target.convert_space(matrix=target.matrix_world, from_space='WORLD', to_space=con.target_space)
+ return Matrix.Identity(4)
+
+def undo_copy_scale_with_offset(obj, bone, con, old_matrix):
+ "Undo the effects of Copy Scale with Offset constraint on a bone matrix."
+ inf = con.influence
+
+ if con.mute or inf == 0 or not con.is_valid or not con.use_offset or con.use_add or con.use_make_uniform:
+ return old_matrix
+
+ scale_delta = [
+ 1 / (1 + (math.pow(x, con.power) - 1) * inf)
+ for x in get_constraint_target_matrix(con).to_scale()
+ ]
+
+ for i, use in enumerate([con.use_x, con.use_y, con.use_z]):
+ if not use:
+ scale_delta[i] = 1
+
+ return old_matrix @ Matrix.Diagonal([*scale_delta, 1])
+
+def undo_copy_scale_constraints(obj, bone, matrix):
+ "Undo the effects of all Copy Scale with Offset constraints on a bone matrix."
+ for con in reversed(bone.constraints):
+ if con.type == 'COPY_SCALE':
+ matrix = undo_copy_scale_with_offset(obj, bone, con, matrix)
+ return matrix
+
+###############################
+## Assign and keyframe tools ##
+###############################
+
+def set_custom_property_value(obj, bone_name, prop, value, *, keyflags=None):
+ "Assign the value of a custom property, and optionally keyframe it."
+ from rna_prop_ui import rna_idprop_ui_prop_update
+ bone = obj.pose.bones[bone_name]
+ bone[prop] = value
+ rna_idprop_ui_prop_update(bone, prop)
+ if keyflags is not None:
+ bone.keyframe_insert(rna_idprop_quote_path(prop), group=bone.name, options=keyflags)
+
+def get_transform_matrix(obj, bone_name, *, space='POSE', with_constraints=True):
+ "Retrieve the matrix of the bone before or after constraints in the given space."
+ bone = obj.pose.bones[bone_name]
+ if with_constraints:
+ return obj.convert_space(pose_bone=bone, matrix=bone.matrix, from_space='POSE', to_space=space)
+ else:
+ return obj.convert_space(pose_bone=bone, matrix=bone.matrix_basis, from_space='LOCAL', to_space=space)
+
+def get_chain_transform_matrices(obj, bone_names, **options):
+ return [get_transform_matrix(obj, name, **options) for name in bone_names]
+
+def set_transform_from_matrix(obj, bone_name, matrix, *, space='POSE', undo_copy_scale=False, ignore_locks=False, no_loc=False, no_rot=False, no_scale=False, keyflags=None):
+ "Apply the matrix to the transformation of the bone, taking locked channels, mode and certain constraints into account, and optionally keyframe it."
+ bone = obj.pose.bones[bone_name]
+
+ def restore_channels(prop, old_vec, locks, extra_lock):
+ if extra_lock or (not ignore_locks and all(locks)):
+ setattr(bone, prop, old_vec)
+ else:
+ if not ignore_locks and any(locks):
+ new_vec = Vector(getattr(bone, prop))
+
+ for i, lock in enumerate(locks):
+ if lock:
+ new_vec[i] = old_vec[i]
+
+ setattr(bone, prop, new_vec)
+
+ # Save the old values of the properties
+ old_loc = Vector(bone.location)
+ old_rot_euler = Vector(bone.rotation_euler)
+ old_rot_quat = Vector(bone.rotation_quaternion)
+ old_rot_axis = Vector(bone.rotation_axis_angle)
+ old_scale = Vector(bone.scale)
+
+ # Compute and assign the local matrix
+ if space != 'LOCAL':
+ matrix = obj.convert_space(pose_bone=bone, matrix=matrix, from_space=space, to_space='LOCAL')
+
+ if undo_copy_scale:
+ matrix = undo_copy_scale_constraints(obj, bone, matrix)
+
+ bone.matrix_basis = matrix
+
+ # Restore locked properties
+ restore_channels('location', old_loc, bone.lock_location, no_loc or bone.bone.use_connect)
+
+ if bone.rotation_mode == 'QUATERNION':
+ restore_channels('rotation_quaternion', old_rot_quat, get_4d_rotlock(bone), no_rot)
+ bone.rotation_axis_angle = old_rot_axis
+ bone.rotation_euler = old_rot_euler
+ elif bone.rotation_mode == 'AXIS_ANGLE':
+ bone.rotation_quaternion = old_rot_quat
+ restore_channels('rotation_axis_angle', old_rot_axis, get_4d_rotlock(bone), no_rot)
+ bone.rotation_euler = old_rot_euler
+ else:
+ bone.rotation_quaternion = old_rot_quat
+ bone.rotation_axis_angle = old_rot_axis
+ restore_channels('rotation_euler', old_rot_euler, bone.lock_rotation, no_rot)
+
+ restore_channels('scale', old_scale, bone.lock_scale, no_scale)
+
+ # Keyframe properties
+ if keyflags is not None:
+ keyframe_transform_properties(
+ obj, bone_name, keyflags, ignore_locks=ignore_locks,
+ no_loc=no_loc, no_rot=no_rot, no_scale=no_scale
+ )
+
+def set_chain_transforms_from_matrices(context, obj, bone_names, matrices, **options):
+ for bone, matrix in zip(bone_names, matrices):
+ set_transform_from_matrix(obj, bone, matrix, **options)
+ context.view_layer.update()
+''']
+
+exec(SCRIPT_UTILITIES_KEYING[-1])
+
+############################################
+# Utilities for managing animation curves ##
+############################################
+
+SCRIPT_UTILITIES_CURVES = ['''
+###########################
+## Animation curve tools ##
+###########################
+
+def flatten_curve_set(curves):
+ "Iterate over all FCurves inside a set of nested lists and dictionaries."
+ if curves is None:
+ pass
+ elif isinstance(curves, bpy.types.FCurve):
+ yield curves
+ elif isinstance(curves, dict):
+ for sub in curves.values():
+ yield from flatten_curve_set(sub)
+ else:
+ for sub in curves:
+ yield from flatten_curve_set(sub)
+
+def flatten_curve_key_set(curves, key_range=None):
+ "Iterate over all keys of the given fcurves in the specified range."
+ for curve in flatten_curve_set(curves):
+ for key in curve.keyframe_points:
+ if key_range is None or key_range[0] <= key.co[0] <= key_range[1]:
+ yield key
+
+def get_curve_frame_set(curves, key_range=None):
+ "Compute a set of all time values with existing keys in the given curves and range."
+ return set(key.co[0] for key in flatten_curve_key_set(curves, key_range))
+
+def set_curve_key_interpolation(curves, ipo, key_range=None):
+ "Assign the given interpolation value to all curve keys in range."
+ for key in flatten_curve_key_set(curves, key_range):
+ key.interpolation = ipo
+
+def delete_curve_keys_in_range(curves, key_range=None):
+ "Delete all keys of the given curves within the given range."
+ for curve in flatten_curve_set(curves):
+ points = curve.keyframe_points
+ for i in range(len(points), 0, -1):
+ key = points[i - 1]
+ if key_range is None or key_range[0] <= key.co[0] <= key_range[1]:
+ points.remove(key, fast=True)
+ curve.update()
+
+def nla_tweak_to_scene(anim_data, frames, invert=False):
+ "Convert a frame value or list between scene and tweaked NLA strip time."
+ if frames is None:
+ return None
+ elif anim_data is None or not anim_data.use_tweak_mode:
+ return frames
+ elif isinstance(frames, (int, float)):
+ return anim_data.nla_tweak_strip_time_to_scene(frames, invert=invert)
+ else:
+ return type(frames)(
+ anim_data.nla_tweak_strip_time_to_scene(v, invert=invert) for v in frames
+ )
+
+def find_action(action):
+ if isinstance(action, bpy.types.Object):
+ action = action.animation_data
+ if isinstance(action, bpy.types.AnimData):
+ action = action.action
+ if isinstance(action, bpy.types.Action):
+ return action
+ else:
+ return None
+
+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)
+ action.update_tag()
+
+TRANSFORM_PROPS_LOCATION = frozenset(['location'])
+TRANSFORM_PROPS_ROTATION = frozenset(['rotation_euler', 'rotation_quaternion', 'rotation_axis_angle'])
+TRANSFORM_PROPS_SCALE = frozenset(['scale'])
+TRANSFORM_PROPS_ALL = frozenset(TRANSFORM_PROPS_LOCATION | TRANSFORM_PROPS_ROTATION | TRANSFORM_PROPS_SCALE)
+
+class ActionCurveTable(object):
+ "Table for efficient lookup of FCurves by properties."
+
+ def __init__(self, action):
+ from collections import defaultdict
+ self.action = find_action(action)
+ self.curve_map = defaultdict(dict)
+ self.index_action()
+
+ def index_action(self):
+ if not self.action:
+ return
+
+ for curve in self.action.fcurves:
+ index = curve.array_index
+ if index < 0:
+ index = 0
+ self.curve_map[curve.data_path][index] = curve
+
+ def get_prop_curves(self, ptr, prop_path):
+ "Returns a dictionary from array index to curve for the given property, or Null."
+ return self.curve_map.get(ptr.path_from_id(prop_path))
+
+ def list_all_prop_curves(self, ptr_set, path_set):
+ "Iterates over all FCurves matching the given object(s) and properti(es)."
+ if isinstance(ptr_set, bpy.types.bpy_struct):
+ ptr_set = [ptr_set]
+ for ptr in ptr_set:
+ for path in path_set:
+ curves = self.get_prop_curves(ptr, path)
+ if curves:
+ yield from curves.values()
+
+ def get_custom_prop_curves(self, ptr, prop):
+ return self.get_prop_curves(ptr, rna_idprop_quote_path(prop))
+''']
+
+exec(SCRIPT_UTILITIES_CURVES[-1])
+
+################################################
+# Utilities for operators that bake keyframes ##
+################################################
+
+_SCRIPT_REGISTER_WM_PROPS = '''
+bpy.types.WindowManager.rigify_transfer_use_all_keys = bpy.props.BoolProperty(
+ name="Bake All Keyed Frames", description="Bake on every frame that has a key for any of the bones, as opposed to just the relevant ones", default=False
+)
+bpy.types.WindowManager.rigify_transfer_use_frame_range = bpy.props.BoolProperty(
+ name="Limit Frame Range", description="Only bake keyframes in a certain frame range", default=False
+)
+bpy.types.WindowManager.rigify_transfer_start_frame = bpy.props.IntProperty(
+ name="Start", description="First frame to transfer", default=0, min=0
+)
+bpy.types.WindowManager.rigify_transfer_end_frame = bpy.props.IntProperty(
+ name="End", description="Last frame to transfer", default=0, min=0
+)
+'''
+
+_SCRIPT_UNREGISTER_WM_PROPS = '''
+del bpy.types.WindowManager.rigify_transfer_use_all_keys
+del bpy.types.WindowManager.rigify_transfer_use_frame_range
+del bpy.types.WindowManager.rigify_transfer_start_frame
+del bpy.types.WindowManager.rigify_transfer_end_frame
+'''
+
+_SCRIPT_UTILITIES_BAKE_OPS = '''
+class RIGIFY_OT_get_frame_range(bpy.types.Operator):
+ bl_idname = "rigify.get_frame_range" + ('_'+rig_id if rig_id else '')
+ bl_label = "Get Frame Range"
+ bl_description = "Set start and end frame from scene"
+ bl_options = {'INTERNAL'}
+
+ def execute(self, context):
+ scn = context.scene
+ id_store = context.window_manager
+ id_store.rigify_transfer_start_frame = scn.frame_start
+ id_store.rigify_transfer_end_frame = scn.frame_end
+ return {'FINISHED'}
+
+ @staticmethod
+ def get_range(context):
+ id_store = context.window_manager
+ if not id_store.rigify_transfer_use_frame_range:
+ return None
+ else:
+ return (id_store.rigify_transfer_start_frame, id_store.rigify_transfer_end_frame)
+
+ @classmethod
+ def draw_range_ui(self, context, layout):
+ id_store = context.window_manager
+
+ row = layout.row(align=True)
+ row.prop(id_store, 'rigify_transfer_use_frame_range', icon='PREVIEW_RANGE', text='')
+
+ row = row.row(align=True)
+ row.active = id_store.rigify_transfer_use_frame_range
+ row.prop(id_store, 'rigify_transfer_start_frame')
+ row.prop(id_store, 'rigify_transfer_end_frame')
+ row.operator(self.bl_idname, icon='TIME', text='')
+'''
+
+exec(_SCRIPT_UTILITIES_BAKE_OPS)
+
+################################################
+# Framework for operators that bake keyframes ##
+################################################
+
+SCRIPT_REGISTER_BAKE = ['RIGIFY_OT_get_frame_range']
+
+SCRIPT_UTILITIES_BAKE = SCRIPT_UTILITIES_KEYING + SCRIPT_UTILITIES_CURVES + ['''
+##################################
+# Common bake operator settings ##
+##################################
+''' + _SCRIPT_REGISTER_WM_PROPS + _SCRIPT_UTILITIES_BAKE_OPS + '''
+#######################################
+# Keyframe baking operator framework ##
+#######################################
+
+class RigifyBakeKeyframesMixin:
+ """Basic framework for an operator that updates a set of keyed frames."""
+
+ # Utilities
+ def nla_from_raw(self, frames):
+ "Convert frame(s) from inner action time to scene time."
+ return nla_tweak_to_scene(self.bake_anim, frames)
+
+ def nla_to_raw(self, frames):
+ "Convert frame(s) from scene time to inner action time."
+ return nla_tweak_to_scene(self.bake_anim, frames, invert=True)
+
+ def bake_get_bone(self, bone_name):
+ "Get pose bone by name."
+ return self.bake_rig.pose.bones[bone_name]
+
+ def bake_get_bones(self, bone_names):
+ "Get multiple pose bones by name."
+ if isinstance(bone_names, (list, set)):
+ return [self.bake_get_bone(name) for name in bone_names]
+ else:
+ return self.bake_get_bone(bone_names)
+
+ def bake_get_all_bone_curves(self, bone_names, props):
+ "Get a list of all curves for the specified properties of the specified bones."
+ return list(self.bake_curve_table.list_all_prop_curves(self.bake_get_bones(bone_names), props))
+
+ def bake_get_all_bone_custom_prop_curves(self, bone_names, props):
+ "Get a list of all curves for the specified custom properties of the specified bones."
+ return self.bake_get_all_bone_curves(bone_names, [rna_idprop_quote_path(p) for p in props])
+
+ def bake_get_bone_prop_curves(self, bone_name, prop):
+ "Get an index to curve dict for the specified property of the specified bone."
+ return self.bake_curve_table.get_prop_curves(self.bake_get_bone(bone_name), prop)
+
+ def bake_get_bone_custom_prop_curves(self, bone_name, prop):
+ "Get an index to curve dict for the specified custom property of the specified bone."
+ return self.bake_curve_table.get_custom_prop_curves(self.bake_get_bone(bone_name), prop)
+
+ def bake_add_curve_frames(self, curves):
+ "Register frames keyed in the specified curves for baking."
+ self.bake_frames_raw |= get_curve_frame_set(curves, self.bake_frame_range_raw)
+
+ def bake_add_bone_frames(self, bone_names, props):
+ "Register frames keyed for the specified properties of the specified bones for baking."
+ curves = self.bake_get_all_bone_curves(bone_names, props)
+ self.bake_add_curve_frames(curves)
+ return curves
+
+ def bake_replace_custom_prop_keys_constant(self, bone, prop, new_value):
+ "If the property is keyframed, delete keys in bake range and re-key as Constant."
+ prop_curves = self.bake_get_bone_custom_prop_curves(bone, prop)
+
+ if prop_curves and 0 in prop_curves:
+ range_raw = self.nla_to_raw(self.get_bake_range())
+ delete_curve_keys_in_range(prop_curves, range_raw)
+ set_custom_property_value(self.bake_rig, bone, prop, new_value, keyflags={'INSERTKEY_AVAILABLE'})
+ set_curve_key_interpolation(prop_curves, 'CONSTANT', range_raw)
+
+ # Default behavior implementation
+ def bake_init(self, context):
+ self.bake_rig = context.active_object
+ self.bake_anim = self.bake_rig.animation_data
+ self.bake_frame_range = RIGIFY_OT_get_frame_range.get_range(context)
+ self.bake_frame_range_raw = self.nla_to_raw(self.bake_frame_range)
+ self.bake_curve_table = ActionCurveTable(self.bake_rig)
+ self.bake_current_frame = context.scene.frame_current
+ self.bake_frames_raw = set()
+ self.bake_state = dict()
+
+ self.keyflags = get_keying_flags(context)
+
+ if context.window_manager.rigify_transfer_use_all_keys:
+ self.bake_add_curve_frames(self.bake_curve_table.curve_map)
+
+ def bake_add_frames_done(self):
+ "Computes and sets the final set of frames to bake."
+ frames = self.nla_from_raw(self.bake_frames_raw)
+ self.bake_frames = sorted(set(map(round, frames)))
+
+ def is_bake_empty(self):
+ return len(self.bake_frames_raw) == 0
+
+ def report_bake_empty(self):
+ self.bake_add_frames_done()
+ if self.is_bake_empty():
+ self.report({'WARNING'}, 'No keys to bake.')
+ return True
+ return False
+
+ def get_bake_range(self):
+ "Returns the frame range that is being baked."
+ if self.bake_frame_range:
+ return self.bake_frame_range
+ else:
+ frames = self.bake_frames
+ return (frames[0], frames[-1])
+
+ def get_bake_range_pair(self):
+ "Returns the frame range that is being baked, both in scene and action time."
+ range = self.get_bake_range()
+ return range, self.nla_to_raw(range)
+
+ def bake_save_state(self, context):
+ "Scans frames and collects data for baking before changing anything."
+ rig = self.bake_rig
+ scene = context.scene
+ saved_state = self.bake_state
+
+ for frame in self.bake_frames:
+ scene.frame_set(frame)
+ saved_state[frame] = self.save_frame_state(context, rig)
+
+ def bake_clean_curves_in_range(self, context, curves):
+ "Deletes all keys from the given curves in the bake range."
+ range, range_raw = self.get_bake_range_pair()
+
+ context.scene.frame_set(range[0])
+ delete_curve_keys_in_range(curves, range_raw)
+
+ return range, range_raw
+
+ def bake_apply_state(self, context):
+ "Scans frames and applies the baking operation."
+ rig = self.bake_rig
+ scene = context.scene
+ saved_state = self.bake_state
+
+ for frame in self.bake_frames:
+ scene.frame_set(frame)
+ self.apply_frame_state(context, rig, saved_state.get(frame))
+
+ clean_action_empty_curves(self.bake_rig)
+ scene.frame_set(self.bake_current_frame)
+
+ @staticmethod
+ def draw_common_bake_ui(context, layout):
+ layout.prop(context.window_manager, 'rigify_transfer_use_all_keys')
+
+ RIGIFY_OT_get_frame_range.draw_range_ui(context, layout)
+
+ @classmethod
+ def poll(cls, context):
+ return find_action(context.active_object) is not None
+
+ def execute_scan_curves(self, context, obj):
+ "Override to register frames to be baked, and return curves that should be cleared."
+ raise NotImplementedError()
+
+ def execute_before_apply(self, context, obj, range, range_raw):
+ "Override to execute code one time before the bake apply frame scan."
+ pass
+
+ def init_execute(self, context):
+ "Override to initialize the operator."
+ pass
+
+ def execute(self, context):
+ self.init_execute(context)
+ self.bake_init(context)
+
+ curves = self.execute_scan_curves(context, self.bake_rig)
+
+ if self.report_bake_empty():
+ return {'CANCELLED'}
+
+ self.bake_save_state(context)
+
+ range, range_raw = self.bake_clean_curves_in_range(context, curves)
+
+ self.execute_before_apply(context, self.bake_rig, range, range_raw)
+
+ self.bake_apply_state(context)
+ return {'FINISHED'}
+
+ def init_invoke(self, context):
+ "Override to initialize the operator."
+ pass
+
+ def invoke(self, context, event):
+ self.init_invoke(context)
+
+ if hasattr(self, 'draw'):
+ return context.window_manager.invoke_props_dialog(self)
+ else:
+ return context.window_manager.invoke_confirm(self, event)
+
+
+class RigifySingleUpdateMixin:
+ """Basic framework for an operator that updates only the current frame."""
+
+ def init_execute(self, context):
+ pass
+
+ def execute(self, context):
+ self.init_execute(context)
+ obj = context.active_object
+ self.keyflags = get_autokey_flags(context, ignore_keyset=True)
+ self.keyflags_switch = add_flags_if_set(self.keyflags, {'INSERTKEY_AVAILABLE'})
+ self.apply_frame_state(context, obj, self.save_frame_state(context, obj))
+ return {'FINISHED'}
+
+ def init_invoke(self, context):
+ pass
+
+ def invoke(self, context, event):
+ self.init_invoke(context)
+
+ if hasattr(self, 'draw'):
+ return context.window_manager.invoke_props_popup(self, event)
+ else:
+ return self.execute(context)
+''']
+
+exec(SCRIPT_UTILITIES_BAKE[-1])
+
+#####################################
+# Generic Clear Keyframes operator ##
+#####################################
+
+SCRIPT_REGISTER_OP_CLEAR_KEYS = ['POSE_OT_rigify_clear_keyframes']
+
+SCRIPT_UTILITIES_OP_CLEAR_KEYS = ['''
+#############################
+## Generic Clear Keyframes ##
+#############################
+
+class POSE_OT_rigify_clear_keyframes(bpy.types.Operator):
+ bl_idname = "pose.rigify_clear_keyframes_" + rig_id
+ bl_label = "Clear Keyframes And Transformation"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Remove all keyframes for the relevant bones and reset transformation"
+
+ bones: StringProperty(name="Bone List")
+
+ @classmethod
+ def poll(cls, context):
+ return find_action(context.active_object) is not None
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+ def execute(self, context):
+ obj = context.active_object
+ bone_list = [ obj.pose.bones[name] for name in json.loads(self.bones) ]
+
+ curve_table = ActionCurveTable(context.active_object)
+ curves = list(curve_table.list_all_prop_curves(bone_list, TRANSFORM_PROPS_ALL))
+
+ key_range = RIGIFY_OT_get_frame_range.get_range(context)
+ range_raw = nla_tweak_to_scene(obj.animation_data, key_range, invert=True)
+ delete_curve_keys_in_range(curves, range_raw)
+
+ for bone in bone_list:
+ bone.location = bone.rotation_euler = (0,0,0)
+ bone.rotation_quaternion = (1,0,0,0)
+ bone.rotation_axis_angle = (0,0,1,0)
+ bone.scale = (1,1,1)
+
+ clean_action_empty_curves(obj)
+ obj.update_tag(refresh={'TIME'})
+ return {'FINISHED'}
+''']
+
+def add_clear_keyframes_button(panel, *, bones=[], label='', text=''):
+ panel.use_bake_settings()
+ panel.script.add_utilities(SCRIPT_UTILITIES_OP_CLEAR_KEYS)
+ panel.script.register_classes(SCRIPT_REGISTER_OP_CLEAR_KEYS)
+
+ op_props = { 'bones': json.dumps(bones) }
+
+ panel.operator('pose.rigify_clear_keyframes_{rig_id}', text=text, icon='CANCEL', properties=op_props)
+
+
+###################################
+# Generic Snap FK to IK operator ##
+###################################
+
+SCRIPT_REGISTER_OP_SNAP_FK_IK = ['POSE_OT_rigify_generic_fk2ik', 'POSE_OT_rigify_generic_fk2ik_bake']
+
+SCRIPT_UTILITIES_OP_SNAP_FK_IK = ['''
+###########################
+## Generic Snap FK to IK ##
+###########################
+
+class RigifyGenericFk2IkBase:
+ fk_bones: StringProperty(name="FK Bone Chain")
+ ik_bones: StringProperty(name="IK Result Bone Chain")
+ ctrl_bones: StringProperty(name="IK Controls")
+
+ undo_copy_scale: bpy.props.BoolProperty(name="Undo Copy Scale", default=False)
+
+ keyflags = None
+
+ def init_execute(self, context):
+ self.fk_bone_list = json.loads(self.fk_bones)
+ self.ik_bone_list = json.loads(self.ik_bones)
+ self.ctrl_bone_list = json.loads(self.ctrl_bones)
+
+ def save_frame_state(self, context, obj):
+ return get_chain_transform_matrices(obj, self.ik_bone_list)
+
+ def apply_frame_state(self, context, obj, matrices):
+ set_chain_transforms_from_matrices(
+ context, obj, self.fk_bone_list, matrices,
+ undo_copy_scale=self.undo_copy_scale, keyflags=self.keyflags
+ )
+
+class POSE_OT_rigify_generic_fk2ik(RigifyGenericFk2IkBase, RigifySingleUpdateMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_generic_fk2ik_" + rig_id
+ bl_label = "Snap FK->IK"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Snap the FK chain to IK result"
+
+class POSE_OT_rigify_generic_fk2ik_bake(RigifyGenericFk2IkBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_generic_fk2ik_bake_" + rig_id
+ bl_label = "Apply Snap FK->IK To Keyframes"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Snap the FK chain keyframes to IK result"
+
+ def execute_scan_curves(self, context, obj):
+ self.bake_add_bone_frames(self.ctrl_bone_list, TRANSFORM_PROPS_ALL)
+ return self.bake_get_all_bone_curves(self.fk_bone_list, TRANSFORM_PROPS_ALL)
+''']
+
+def add_fk_ik_snap_buttons(panel, op_single, op_bake, *, label=None, rig_name='', properties=None, clear_bones=None, compact=None):
+ assert label and properties
+
+ if rig_name:
+ label += ' (%s)' % (rig_name)
+
+ if compact or not clear_bones:
+ row = panel.row(align=True)
+ row.operator(op_single, text=label, icon='SNAP_ON', properties=properties)
+ row.operator(op_bake, text='', icon='ACTION_TWEAK', properties=properties)
+
+ if clear_bones:
+ add_clear_keyframes_button(row, bones=clear_bones)
+ else:
+ col = panel.column(align=True)
+ col.operator(op_single, text=label, icon='SNAP_ON', properties=properties)
+ row = col.row(align=True)
+ row.operator(op_bake, text='Action', icon='ACTION_TWEAK', properties=properties)
+ add_clear_keyframes_button(row, bones=clear_bones, text='Clear')
+
+def add_generic_snap_fk_to_ik(panel, *, fk_bones=[], ik_bones=[], ik_ctrl_bones=[], label='FK->IK', rig_name='', undo_copy_scale=False, compact=None, clear=True):
+ panel.use_bake_settings()
+ panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP_FK_IK)
+ panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP_FK_IK)
+
+ op_props = {
+ 'fk_bones': json.dumps(fk_bones),
+ 'ik_bones': json.dumps(ik_bones),
+ 'ctrl_bones': json.dumps(ik_ctrl_bones),
+ 'undo_copy_scale': undo_copy_scale,
+ }
+
+ clear_bones = fk_bones if clear else None
+
+ add_fk_ik_snap_buttons(
+ panel, 'pose.rigify_generic_fk2ik_{rig_id}', 'pose.rigify_generic_fk2ik_bake_{rig_id}',
+ label=label, rig_name=rig_name, properties=op_props, clear_bones=clear_bones, compact=compact,
+ )
+
+###############################
+# Module register/unregister ##
+###############################
+
+def register():
+ from bpy.utils import register_class
+
+ exec(_SCRIPT_REGISTER_WM_PROPS)
+
+ register_class(RIGIFY_OT_get_frame_range)
+
+def unregister():
+ from bpy.utils import unregister_class
+
+ exec(_SCRIPT_UNREGISTER_WM_PROPS)
+
+ unregister_class(RIGIFY_OT_get_frame_range)