# 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