527 lines
22 KiB
Python
527 lines
22 KiB
Python
# ***** BEGIN GPL LICENSE BLOCK *****
|
|
#
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ***** END GPL LICENCE BLOCK *****
|
|
|
|
import bpy
|
|
import random
|
|
import numpy as np
|
|
from mathutils import Quaternion
|
|
from . import Tools
|
|
|
|
def attr_default(obj, fcu_key):
|
|
#check if the fcurve source belongs to a bone or obj
|
|
if fcu_key[0][:10] == 'pose.bones':
|
|
transform = fcu_key[0].split('.')[-1]
|
|
attr = fcu_key[0].split('"')[-2]
|
|
bone = fcu_key[0].split('"')[1]
|
|
|
|
if bone in obj.pose.bones:
|
|
source = obj.pose.bones[bone]
|
|
#if the bone not found still calculate the default based on the path
|
|
elif '.rotation_quaternion' in fcu_key[0]:
|
|
return [1.0, 0.0, 0.0, 0.0]
|
|
elif '.scale' in fcu_key[0]:
|
|
return [1.0, 1.0, 1.0]
|
|
else:
|
|
return [0]
|
|
|
|
#in case of shapekey animation
|
|
elif fcu_key[0][:10] == 'key_blocks':
|
|
attr = fcu_key[0].split('"')[1]
|
|
if attr not in obj.data.shape_keys.key_blocks:
|
|
return [0]
|
|
shapekey = obj.data.shape_keys.key_blocks[attr]
|
|
return [0] if shapekey.slider_min <= 0 else shapekey.slider_min
|
|
#in case of transforms in object mode
|
|
else:# fcu_key[0] in transform_types:
|
|
source = obj
|
|
transform = fcu_key[0]
|
|
|
|
#check when it's transform property of Blender
|
|
if transform in source.bl_rna.properties.keys():
|
|
if hasattr(source.bl_rna.properties[transform], 'default_array'):
|
|
if len(source.bl_rna.properties[transform].default_array) > fcu_key[1]:
|
|
attrvalue = source.bl_rna.properties[transform].default_array
|
|
return attrvalue
|
|
|
|
#in case of property on object
|
|
elif len(fcu_key[0].split('"')) > 1:
|
|
if fcu_key[0].split('"')[1] in obj.keys():
|
|
attr = fcu_key[0].split('"')[1]
|
|
|
|
if 'attr' not in locals():
|
|
return [0]
|
|
|
|
#since blender 3 access to custom property settings changed
|
|
if attr in source:
|
|
if not isinstance(source[attr], float) and not isinstance(source[attr], int):
|
|
return [0]
|
|
id_attr = source.id_properties_ui(attr).as_dict()
|
|
attrvalue = id_attr['default']
|
|
return [attrvalue]
|
|
|
|
return [0]
|
|
|
|
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 selected_bones_filter(obj, fcu_data_path):
|
|
if not bpy.context.window_manager.atb_ui.multikey.selectedbones:
|
|
#if not obj.als.onlyselected:
|
|
return False
|
|
if obj.mode != 'POSE':
|
|
return True
|
|
transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
|
|
#filter selected bones if option is turned on
|
|
bones = [bone.path_from_id() for bone in bpy.context.selected_pose_bones]
|
|
if fcu_data_path.split('].')[0]+']' not in bones and fcu_data_path not in transform_types:
|
|
return True
|
|
|
|
# def filter_properties(obj, fcu):
|
|
# 'Filter the W X Y Z attributes of the transform properties'
|
|
# 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
|
|
# if not hasattr(bpy.context.scene.multikey, transform):
|
|
# return False
|
|
# attr = getattr(bpy.context.scene.multikey, transform)
|
|
# #print(fcu.data_path, index, transform, attr[index])
|
|
# 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(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 Tools.filter_properties(bpy.context.scene.animtoolbox, 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 = "animtoolbox.multikey_scale_value"
|
|
bl_label = "Scale Values"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def invoke(self, context, event):
|
|
#reset the values for dragging
|
|
self.stop = False
|
|
ui = context.window_manager.atb_ui
|
|
ui['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
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
for fcu in fcurves:
|
|
if obj.mode == 'POSE':
|
|
if selected_bones_filter(obj, fcu.data_path):
|
|
continue
|
|
if Tools.filter_properties(context.scene.animtoolbox, 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:
|
|
ui['is_dragging'] = False
|
|
ui.multikey['scale'] = 1
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
return {'CANCELLED'}
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
|
|
ui = context.window_manager.atb_ui
|
|
scale = ui.multikey.scale
|
|
|
|
#Quit the modal operator when the slider is released
|
|
if self.stop:
|
|
ui['is_dragging'] = False
|
|
ui.multikey['scale'] = 1
|
|
Tools.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
|
|
return {'PASS_THROUGH'}
|
|
|
|
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):
|
|
|
|
ui = context.window_manager.atb_ui
|
|
if ui.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.animtoolbox.multikey_scale_value('INVOKE_DEFAULT')
|
|
|
|
def random_value(self, context):
|
|
|
|
for obj in context.selected_objects:
|
|
if obj.animation_data.action is None:
|
|
continue
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
for fcu in fcurves:
|
|
if obj.mode == 'POSE':
|
|
if selected_bones_filter(obj, fcu.data_path):
|
|
continue
|
|
if Tools.filter_properties(context.scene.animtoolbox, fcu):
|
|
continue
|
|
value_list = []
|
|
threshold = bpy.context.window_manager.atb_ui.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_combine(data_path, added_array, eval_array, array_default, influence):
|
|
|
|
if 'scale' in data_path:
|
|
eval_array = eval_array * (added_array / array_default) ** influence
|
|
elif 'rotation_quaternion' in data_path:
|
|
#multiply first the influence with the w separatly
|
|
added_array[0] = added_array[0] + (1- added_array[0])*(1 - influence)
|
|
added_array[1:] *= influence
|
|
eval_array = np.array(Quaternion(eval_array) @ Quaternion(added_array))# ** influence
|
|
#if it's a custom property
|
|
elif 'rotation_euler' not in data_path and 'location' not in data_path:
|
|
eval_array = eval_array + (added_array - array_default) * influence
|
|
|
|
return eval_array
|
|
|
|
def evaluate_array(fcurves, fcu_path, frame, array_default = [0, 0, 0]):
|
|
'''Create an array from all the indexes'''
|
|
|
|
array_len = len(array_default)
|
|
|
|
#assigning the default array in case
|
|
fcu_array = array_default.copy()
|
|
#get the missing arrays in case quaternion is not complete
|
|
for i in range(array_len):
|
|
fcu = fcurves.find(fcu_path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
fcu_array[i] = fcu.evaluate(frame)
|
|
|
|
if (fcu_array == array_default).all():
|
|
return None
|
|
return np.array(fcu_array)
|
|
|
|
def evaluate_layers(context, obj, anim_data, fcu, array_default):
|
|
'''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
|
|
|
|
eval_array = array_default.copy()
|
|
|
|
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
|
|
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
|
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame_eval, influence, array_default, blend_type, blend_types)
|
|
|
|
#Adding an extra layer from the action outside and on top of the nla
|
|
tweak_mode = anim_data.use_tweak_mode
|
|
if tweak_mode:
|
|
anim_data.use_tweak_mode = False
|
|
|
|
action = anim_data.action
|
|
if action:
|
|
influence = anim_data.action_influence
|
|
blend_type = anim_data.action_blend_type
|
|
|
|
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
|
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence, array_default, blend_type, blend_types)
|
|
anim_data.use_tweak_mode = tweak_mode
|
|
|
|
return eval_array
|
|
|
|
|
|
def evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence,
|
|
array_default, blend_type, blend_types):
|
|
'''Calculate the value based on the blend type'''
|
|
|
|
fcu_array = evaluate_array(fcurves, fcu_path, frame, array_default)
|
|
if fcu_array is None:
|
|
return eval_array
|
|
###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 = 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):
|
|
ui = context.window_manager.atb_ui
|
|
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 ui.multikey.selectedbones else obj.pose.bones
|
|
|
|
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
|
for fcu in fcurves:
|
|
if fcu in fcu_paths:
|
|
continue
|
|
current_value = None
|
|
if Tools.filter_properties(context.scene.animtoolbox, fcu):
|
|
continue
|
|
if obj.mode == 'POSE':
|
|
if 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
|
|
path_split = fcu.data_path.split('"].')
|
|
|
|
if len(path_split) <= 1:
|
|
continue
|
|
else:
|
|
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)
|
|
#In case it was completly filtered out and not current value available
|
|
if not current_value:
|
|
continue
|
|
|
|
array_default = np.array(attr_default(obj, (fcu.data_path, fcu.array_index)))
|
|
eval_array = evaluate_layers(context, obj, anim_data, fcu, array_default)
|
|
if eval_array is None:
|
|
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
|
eval_array = evaluate_array(fcurves, fcu.data_path, context.scene.frame_current, array_default)
|
|
|
|
#calculate the difference between current value and the fcurve value
|
|
add_diff(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 = "animtoolbox.multikey"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@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="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 Factor", description="Scale percentage of the average value", default=1.0, 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)
|
|
|
|
#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 = (ScaleValuesOp, MULTIKEY_OT_Multikey)
|
|
|
|
#register, unregister = bpy.utils.register_classes_factory(classes)
|
|
|
|
def register():
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|
|
# bpy.types.Scene.animtoolbox.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.animtoolbox.multikey |