blender-addons/rigify/utils/animation.py
Campbell Barton e8da6131fd License headers: use SPDX-FileCopyrightText for all addons
Move copyright text to SPDX-FileCopyrightText or set to the
Blender Foundation so "make check_licenses" now runs without warnings.
2023-06-15 16:54:05 +10:00

1005 lines
36 KiB
Python

# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy # noqa
import math # noqa
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
import json
if TYPE_CHECKING:
from ..rig_ui_template import PanelLayout
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)
return sorted(get_curve_frame_set(action.fcurves, frame_range))
else:
return []
def bones_in_frame(f, rig, *args):
"""
True if one of the bones listed in args is animated at frame f
:param f: the frame
:param rig: the rig
:param args: bone names
:return:
"""
if rig.animation_data and rig.animation_data.action:
fcurves = rig.animation_data.action.fcurves
else:
return False
for fc in 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:
return True
return False
def overwrite_prop_animation(rig, bone, prop_name, value, frames):
act = rig.animation_data.action
if not act:
return
bone_name = bone.name
curve = None
for fcu in act.fcurves:
words = fcu.data_path.split('"')
if words[0] == "pose.bones[" and words[1] == bone_name and words[-2] == prop_name:
curve = fcu
break
if not curve:
return
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_keyingset=False):
"Retrieve the Auto Keyframe flags, or None if disabled."
ts = context.scene.tool_settings
if ts.use_keyframe_insert_auto and (ignore_keyingset 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_rot_lock(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_rot_lock(bone))
elif bone.rotation_mode == 'AXIS_ANGLE':
keyframe_channels('rotation_axis_angle', get_4d_rot_lock(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:
return old_matrix
tgt_matrix = get_constraint_target_matrix(con)
tgt_scale = tgt_matrix.to_scale()
use = [con.use_x, con.use_y, con.use_z]
if con.use_make_uniform:
if con.use_x and con.use_y and con.use_z:
total = tgt_matrix.determinant()
else:
total = 1
for i, use in enumerate(use):
if use:
total *= tgt_scale[i]
tgt_scale = [abs(total)**(1./3.)]*3
else:
for i, use in enumerate(use):
if not use:
tgt_scale[i] = 1
scale_delta = [
1 / (1 + (math.pow(x, con.power) - 1) * inf)
for x in tgt_scale
]
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_rot_lock(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_rot_lock(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)
def transform_props_with_locks(lock_location, lock_rotation, lock_scale):
props = set()
if not lock_location:
props |= TRANSFORM_PROPS_LOCATION
if not lock_rotation:
props |= TRANSFORM_PROPS_ROTATION
if not lock_scale:
props |= TRANSFORM_PROPS_SCALE
return props
class FCurveTable(object):
"Table for efficient lookup of FCurves by properties."
def __init__(self):
self.curve_map = collections.defaultdict(dict)
def index_curves(self, curves):
for curve in curves:
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 properties."
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))
class ActionCurveTable(FCurveTable):
"Table for efficient lookup of Action FCurves by properties."
def __init__(self, action):
super().__init__()
self.action = find_action(action)
if self.action:
self.index_curves(self.action.fcurves)
class DriverCurveTable(FCurveTable):
"Table for efficient lookup of Driver FCurves by properties."
def __init__(self, object):
super().__init__()
self.anim_data = object.animation_data
if self.anim_data:
self.index_curves(self.anim_data.drivers)
''']
AnyCurveSet = None | FCurve | dict | Collection
flatten_curve_set: Callable[[AnyCurveSet], Iterator[FCurve]]
flatten_curve_key_set: Callable[..., set[float]]
get_curve_frame_set: Callable[..., set[float]]
set_curve_key_interpolation: Callable[..., None]
delete_curve_keys_in_range: Callable[..., None]
nla_tweak_to_scene: Callable
find_action: Callable[[bpy_struct], Action]
clean_action_empty_curves: Callable[[bpy_struct], None]
TRANSFORM_PROPS_LOCATION: frozenset[str]
TRANSFORM_PROPS_ROTATION = frozenset[str]
TRANSFORM_PROPS_SCALE = frozenset[str]
TRANSFORM_PROPS_ALL = frozenset[str]
transform_props_with_locks: Callable[[bool, bool, bool], set[str]]
FCurveTable: Any
ActionCurveTable: Any
DriverCurveTable: Any
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='')
'''
RIGIFY_OT_get_frame_range: Any
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 RigifyOperatorMixinBase:
bl_options = {'UNDO', 'INTERNAL'}
def init_invoke(self, context):
"Override to initialize the operator before invoke."
def init_execute(self, context):
"Override to initialize the operator before execute."
def before_save_state(self, context, rig):
"Override to prepare for saving state."
def after_save_state(self, context, rig):
"Override to undo before_save_state."
class RigifyBakeKeyframesMixin(RigifyOperatorMixinBase):
"""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)
self.keyflags_switch = None
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
try:
self.before_save_state(context, rig)
for frame in self.bake_frames:
scene.frame_set(frame)
saved_state[frame] = self.save_frame_state(context, rig)
finally:
self.after_save_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 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'}
try:
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)
except Exception as e:
traceback.print_exc()
self.report({'ERROR'}, 'Exception: ' + str(e))
return {'FINISHED'}
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(RigifyOperatorMixinBase):
"""Basic framework for an operator that updates only the current frame."""
def execute(self, context):
self.init_execute(context)
obj = context.active_object
self.keyflags = get_autokey_flags(context, ignore_keyingset=True)
self.keyflags_switch = add_flags_if_set(self.keyflags, {'INSERTKEY_AVAILABLE'})
try:
try:
self.before_save_state(context, obj)
state = self.save_frame_state(context, obj)
finally:
self.after_save_state(context, obj)
self.apply_frame_state(context, obj, state)
except Exception as e:
traceback.print_exc()
self.report({'ERROR'}, 'Exception: ' + str(e))
return {'FINISHED'}
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)
''']
RigifyOperatorMixinBase: Any
RigifyBakeKeyframesMixin: Any
RigifySingleUpdateMixin: Any
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: 'PanelLayout', *,
bones: Sequence[str] = (), 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 = ['POSE_OT_rigify_generic_snap', 'POSE_OT_rigify_generic_snap_bake']
SCRIPT_UTILITIES_OP_SNAP = ['''
#############################
## Generic Snap (FK to IK) ##
#############################
class RigifyGenericSnapBase:
input_bones: StringProperty(name="Input Chain")
output_bones: StringProperty(name="Output Chain")
ctrl_bones: StringProperty(name="Input Controls")
tooltip: StringProperty(name="Tooltip", default="FK to IK")
locks: bpy.props.BoolVectorProperty(name="Locked", size=3, default=[False,False,False])
undo_copy_scale: bpy.props.BoolProperty(name="Undo Copy Scale", default=False)
def init_execute(self, context):
self.input_bone_list = json.loads(self.input_bones)
self.output_bone_list = json.loads(self.output_bones)
self.ctrl_bone_list = json.loads(self.ctrl_bones)
def save_frame_state(self, context, obj):
return get_chain_transform_matrices(obj, self.input_bone_list)
def apply_frame_state(self, context, obj, matrices):
set_chain_transforms_from_matrices(
context, obj, self.output_bone_list, matrices,
undo_copy_scale=self.undo_copy_scale, keyflags=self.keyflags,
no_loc=self.locks[0], no_rot=self.locks[1], no_scale=self.locks[2],
)
class POSE_OT_rigify_generic_snap(RigifyGenericSnapBase, RigifySingleUpdateMixin, bpy.types.Operator):
bl_idname = "pose.rigify_generic_snap_" + rig_id
bl_label = "Snap Bones"
bl_description = "Snap on the current frame"
@classmethod
def description(cls, context, props):
return "Snap " + props.tooltip + " on the current frame"
class POSE_OT_rigify_generic_snap_bake(RigifyGenericSnapBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
bl_idname = "pose.rigify_generic_snap_bake_" + rig_id
bl_label = "Apply Snap To Keyframes"
bl_description = "Apply snap to keyframes"
@classmethod
def description(cls, context, props):
return "Apply snap " + props.tooltip + " to keyframes"
def execute_scan_curves(self, context, obj):
props = transform_props_with_locks(*self.locks)
self.bake_add_bone_frames(self.ctrl_bone_list, TRANSFORM_PROPS_ALL)
return self.bake_get_all_bone_curves(self.output_bone_list, props)
''']
def add_fk_ik_snap_buttons(panel: 'PanelLayout', op_single: str, op_bake: str, *,
label, rig_name='', properties: dict[str, Any],
clear_bones: Optional[list[str]] = None,
compact: Optional[bool] = 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(panel: 'PanelLayout', *,
output_bones: Sequence[str] = (), input_bones: Sequence[str] = (),
input_ctrl_bones: Sequence[str] = (), label='Snap',
rig_name='', undo_copy_scale=False, compact: Optional[bool] = None,
clear=True, locks: Optional[Sequence[bool]] = None,
tooltip: Optional[str] = None):
panel.use_bake_settings()
panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP)
panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP)
op_props = {
'output_bones': json.dumps(output_bones),
'input_bones': json.dumps(input_bones),
'ctrl_bones': json.dumps(input_ctrl_bones or input_bones),
}
if undo_copy_scale:
op_props['undo_copy_scale'] = undo_copy_scale
if locks is not None:
op_props['locks'] = tuple(locks[0:3])
if tooltip is not None:
op_props['tooltip'] = tooltip
clear_bones = output_bones if clear else None
add_fk_ik_snap_buttons(
panel, 'pose.rigify_generic_snap_{rig_id}', 'pose.rigify_generic_snap_bake_{rig_id}',
label=label, rig_name=rig_name, properties=op_props, clear_bones=clear_bones, compact=compact,
)
def add_generic_snap_fk_to_ik(panel: 'PanelLayout', *,
fk_bones: Sequence[str] = (), ik_bones: Sequence[str] = (),
ik_ctrl_bones: Sequence[str] = (), label='FK->IK',
rig_name='', undo_copy_scale=False,
compact: Optional[bool] = None, clear=True):
add_generic_snap(
panel, output_bones=fk_bones, input_bones=ik_bones, input_ctrl_bones=ik_ctrl_bones,
label=label, rig_name=rig_name, undo_copy_scale=undo_copy_scale, compact=compact, clear=clear
)
###############################
# 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)