Files
blender-portable-repo/scripts/addons/Animation_Layers/multikey.py
T
2026-03-17 14:30:01 -06:00

410 lines
17 KiB
Python

# if "bpy" in locals():
# import importlib
# if "multikey" in locals():
# importlib.reload()
import bpy
import random
import numpy as np
from mathutils import Quaternion
from . import bake_ops
from . import anim_layers
def store_handles(key):
#storing the distance between the handles bezier to the key value
handle_r = key.handle_right[1] - key.co[1]
handle_l = key.handle_left[1] - key.co[1]
return handle_r, handle_l
def apply_handles(key, handle_r, handle_l):
key.handle_right[1] = key.co[1] + handle_r
key.handle_left[1] = key.co[1] + handle_l
def filter_properties(obj, fcu):
'Filter the W X Y Z attributes of the transform properties'
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
#check if the fcurve data path ends with any of the transformations
if not any(fcu.data_path.endswith(transform) for transform in transformations):
return True
transform = fcu.data_path.split('"].')[1] if obj.mode == 'POSE' else fcu.data_path
index = fcu.array_index
if 'rotation' in transform:
if transform == 'rotation_euler':
index -= 1
transform = 'rotation'
transform = 'filter_' + transform
#in case of channels like bbone_scalein that are no included then return
if not hasattr(bpy.context.scene.multikey, transform):
return True
attr = getattr(bpy.context.scene.multikey, transform)
return True if attr[index] else False
def add_value(key, value):
if key.select_control_point:
#store handle values in relative to the keyframe value
handle_r, handle_l = store_handles(key)
key.co[1] += value
apply_handles(key, handle_r, handle_l)
#calculate the difference between current value and the fcurve value
def add_diff(obj, fcurves, path, current_value, eval_array):
array_value = current_value - eval_array
if not any(array_value):
return
for i, value in enumerate(array_value):
fcu = fcurves.find(path, index = i)
if fcu is None or not filter_properties(obj, fcu):
continue
for key in fcu.keyframe_points:
add_value(key, value)
fcu.update()
class ScaleValuesOp(bpy.types.Operator):
"""Modal operator used while scale value is running before release"""
bl_idname = "anim.multikey_scale_value"
bl_label = "Scale Values"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
#reset the values for dragging
self.stop = False
scene = context.scene
scene.multikey['is_dragging'] = True
self.avg_value = dict()
#dictionary of the keyframes and their original INITIAL values
self.keyframes_values = dict()
self.keyframes_handle_right = dict()
self.keyframes_handle_left = dict()
#the average value for each fcurve
self.keyframes_avg_value = dict()
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
action = obj.animation_data.action
for fcu in action.fcurves:
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
continue
if not filter_properties(obj, fcu):
continue
#avg and value list per fcurve
avg_value = []
value_list = []
for key in fcu.keyframe_points:
if key.select_control_point:
value_list.append(key.co[1])
self.keyframes_values.update({key : key.co[1]})
self.keyframes_handle_right.update({key : key.handle_right[1]})
self.keyframes_handle_left.update({key : key.handle_left[1]})
if len(value_list)>1:
#the average value with the scale property added to it
avg_value = sum(value_list) / len(value_list)
for key in fcu.keyframe_points:
if key.select_control_point:
self.keyframes_avg_value.update({key : avg_value})
if not self.keyframes_avg_value:
return('CANCELLED')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
scene = context.scene
scale = scene.multikey.scale
#Quit the modal operator when the slider is released
if self.stop:
scene.multikey['is_dragging'] = False
scene.multikey['scale'] = 1
anim_layers.redraw_areas(['VIEW_3D'])
#modal is being cancelled because of undo issue with the modal running through the property
return {'FINISHED'}
if event.value == 'RELEASE': # Stop the modal on next frame. Don't block the event since we want to exit the field dragging
self.stop = True
for key, key_value in self.keyframes_values.items():
if not key.select_control_point:
continue
if key not in self.keyframes_avg_value:
continue
avg_value = self.keyframes_avg_value[key]
handle_right_value = self.keyframes_handle_right[key]
handle_left_value = self.keyframes_handle_left[key]
#add the value of the distance from the average * scale factor
key.co[1] = avg_value + ((key_value - avg_value)*scale)
key.handle_right[1] = avg_value + ((handle_right_value - avg_value)*scale)
key.handle_left[1] = avg_value + ((handle_left_value - avg_value)*scale)
return {'PASS_THROUGH'}
def scale_value(self, context):
scene = context.scene
if scene.multikey.is_dragging:
return
obj = context.object
if obj is None:
self['scale'] = 1
return
action = obj.animation_data.action
if action is None:
self['scale'] = 1
return
if context.mode == 'POSE' and not context.selected_pose_bones:
self['scale'] = 1
return
bpy.ops.anim.multikey_scale_value('INVOKE_DEFAULT')
def random_value(self, context):
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
action = obj.animation_data.action
for fcu in action.fcurves:
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
continue
if not filter_properties(obj, fcu):
continue
value_list = []
threshold = bpy.context.scene.multikey.randomness
for key in fcu.keyframe_points:
if key.select_control_point == True:
value_list.append(key.co[1])
if len(value_list) > 0:
value = max(value_list)- min(value_list)
for key in fcu.keyframe_points:
add_value(key, value * random.uniform(-threshold, threshold))
fcu.update()
self['randomness'] = 0.1
def evaluate_array(action, fcu_path, frame, array_len):
'''Create an array from all the indexes'''
fcu_array = []
for i in range(array_len):
fcu = action.fcurves.find(fcu_path, index = i)
if fcu is None:
continue
fcu_array.append(fcu.evaluate(frame))
if not len(fcu_array):
return None
return np.array(fcu_array)
def evaluate_layers(context, obj, anim_data, fcu, array_len):
'''Calculate the evaluation of all the layers when using the nla'''
if not hasattr(anim_data, 'nla_tracks') or not anim_data.use_nla:
return None
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):
return None
frame = context.scene.frame_current
#blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_path = fcu.data_path
#array_default = np.array([bake_ops.attr_default(obj, (fcu_path, i)) for i in range(4) if anim_data.action.fcurves.find(fcu_path, index = i) is not None])
array_default = np.array(bake_ops.attr_default(obj, (fcu_path, fcu.array_index)))
eval_array = array_default
for track in nla_tracks:
if track.mute:
continue
if not len(track.strips):
continue
for strip in track.strips:
if not strip.frame_start < frame < strip.frame_end:
continue
action = strip.action
if action is None:
continue
blend_type = strip.blend_type
#get the influence value either from the attribute or the fcurve. function coming from bake
influence = strip.influence
if len(strip.fcurves):
if not strip.fcurves[0].mute and len(strip.fcurves[0].keyframe_points):
influence = strip.fcurves[0].evaluate(frame)
#evaluate the frame according to the strip settings
frame_eval = frame
#change the frame if the strip is on hold
if frame < strip.frame_start:
if strip.extrapolation == 'HOLD':
frame_eval = strip.frame_start
elif frame >= strip.frame_end:
if strip.extrapolation == 'HOLD' or strip.extrapolation == 'HOLD_FORWARD':
frame_eval = strip.frame_end
last_frame = strip.frame_start + (strip.frame_end - strip.frame_start) / strip.repeat
if strip.repeat > 1 and (frame) >= last_frame:
action_range = (strip.action_frame_end * strip.scale - strip.action_frame_start * strip.scale)
frame_eval = (((frame_eval - strip.frame_start) % (action_range)) + strip.frame_start)
if strip.use_reverse:
frame_eval = last_frame - (frame_eval - strip.frame_start)
offset = (strip.frame_start * 1/strip.scale - strip.action_frame_start) * strip.scale
frame_eval = strip.frame_start * 1/strip.scale + (frame_eval - strip.frame_start) * 1/strip.scale - offset * 1/strip.scale
fcu_array = evaluate_array(action, fcu_path, frame_eval, array_len)
if fcu_array is None:
continue
###EVALUATION###
eval_array = evaluation(blend_type, fcu_path, fcu_array, eval_array, array_default, influence)
#If there is an action on top of the nla tracks (not using anim layers) add it to the evaluation
if anim_data.action is not None and not anim_data.use_tweak_mode:
fcu_array = evaluate_array(anim_data.action, fcu_path, frame, array_len)
if fcu_array is not None:
eval_array = evaluation(anim_data.action_blend_type, fcu_path, fcu_array, eval_array, array_default, anim_data.action_influence)
return eval_array
def evaluation(blend_type, fcu_path, fcu_array, eval_array, array_default, influence):
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
# fcu_array = evaluate_array(action, fcu_path, frame_eval, array_len)
# if fcu_array is None:
# continue
###EVALUATION###
if blend_type =='COMBINE':
if 'location' in fcu_path or 'rotation_euler' in fcu_path:
blend_type = 'ADD'
if blend_type =='REPLACE':
eval_array = eval_array * (1 - influence) + fcu_array * influence
elif blend_type =='COMBINE':
eval_array = bake_ops.evaluate_combine(fcu_path, fcu_array, eval_array, array_default, influence)
else:
eval_array = eval('eval_array' + blend_types[blend_type] + 'fcu_array' + '*' + str(influence))
return eval_array
def evaluate_value(self, context):
for obj in context.selected_objects:
anim_data = obj.animation_data
if anim_data is None:
return
if anim_data.action is None:
return
action = obj.animation_data.action
fcu_paths = []
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
if obj.mode == 'POSE':
bonelist = context.selected_pose_bones if obj.als.onlyselected else obj.pose.bones
for fcu in action.fcurves:
if fcu in fcu_paths:
continue
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
continue
for bone in bonelist:
#find the fcurve of the bone
if fcu.data_path.rfind(bone.name) != 12 or fcu.data_path[12 + len(bone.name)] != '"':
continue
# transform = fcu.data_path[15 + len(bone.name):]
transform = fcu.data_path.split('"].')[1]
if transform not in transformations:
continue
current_value = getattr(obj.pose.bones[bone.name], transform)
else:
transform = fcu.data_path
current_value = getattr(obj, transform)
eval_array = evaluate_layers(context, obj, anim_data, fcu, len(current_value))
if eval_array is None:
eval_array = evaluate_array(action, fcu.data_path, context.scene.frame_current, len(current_value))
#calculate the difference between current value and the fcurve value
add_diff(obj, action.fcurves, fcu.data_path, np.array(current_value), eval_array)
class MULTIKEY_OT_Multikey(bpy.types.Operator):
"""Edit all selected keyframes"""
bl_label = "Edit Selected Keyframes"
bl_idname = "fcurves.multikey"
bl_options = {'REGISTER', 'UNDO'}
# bl_description = ('Select keyframes, move your bone or objecet and press the operator. Does not work with Autokey')
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.animation_data and bpy.context.scene.tool_settings.use_keyframe_insert_auto == False
def execute(self, context):
evaluate_value(self, context)
return {'FINISHED'}
class MultikeyProperties(bpy.types.PropertyGroup):
#selectedbones: bpy.props.BoolProperty(name="Affect only selected bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
#handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
scale: bpy.props.FloatProperty(name="Scale Values Factor", description="Scale percentage from the average value", default=1.0, soft_max = 10, soft_min = -10, step=0.1, precision = 3, update = scale_value)
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = random_value)
is_dragging: bpy.props.BoolProperty(default = False)
#filters
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(True, True, True), size = 3, options={'HIDDEN'})
filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(True, True, True, True), size = 4, options={'HIDDEN'})
filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(True, True, True), size = 3, options={'HIDDEN'})
class FilterProperties(bpy.types.Operator):
"""Filter Location Rotation and Scale Properties"""
bl_idname = "fcurves.filter"
bl_label = "Filter Properties W X Y Z"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self, width = 200)
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text = 'Location')
row.prop(context.scene.multikey, 'filter_location', text = '')
row = layout.row()
row.label(text = 'Rotation')
row.prop(context.scene.multikey, 'filter_rotation', text = '')
row = layout.row()
row.label(text = 'Scale')
row.prop(context.scene.multikey, 'filter_scale', text = '')
def execute(self, context):
return {'CANCELLED'}
classes = (MultikeyProperties, FilterProperties, MULTIKEY_OT_Multikey, ScaleValuesOp)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.Scene.multikey = bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
del bpy.types.Scene.multikey