2025-12-01
This commit is contained in:
@@ -10,6 +10,46 @@ from mathutils import Quaternion
|
||||
from . import bake_ops
|
||||
from . import anim_layers
|
||||
|
||||
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]
|
||||
source = obj.pose.bones[bone]
|
||||
|
||||
#in case of shapekey animation
|
||||
elif fcu_key[0][:10] == 'key_blocks':
|
||||
attr = fcu_key[0].split('"')[1]
|
||||
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[fcu_key[1]]
|
||||
return attrvalue
|
||||
|
||||
#in case of property on object
|
||||
elif 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:
|
||||
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]
|
||||
@@ -50,10 +90,17 @@ def add_value(key, 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):
|
||||
def add_diff(obj, fcurves, path, current_value, eval_array):
|
||||
'''Get the difference value and add it to all selected keyframes'''
|
||||
|
||||
if eval_array is None:
|
||||
return
|
||||
|
||||
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):
|
||||
@@ -72,7 +119,9 @@ class ScaleValuesOp(bpy.types.Operator):
|
||||
#reset the values for dragging
|
||||
self.stop = False
|
||||
scene = context.scene
|
||||
scene.multikey['is_dragging'] = True
|
||||
global is_dragging
|
||||
is_dragging = True
|
||||
|
||||
self.avg_value = dict()
|
||||
#dictionary of the keyframes and their original INITIAL values
|
||||
self.keyframes_values = dict()
|
||||
@@ -81,15 +130,16 @@ class ScaleValuesOp(bpy.types.Operator):
|
||||
|
||||
#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
|
||||
|
||||
fcurves = anim_layers.get_fcurves(obj, action)
|
||||
for fcu in fcurves:
|
||||
if anim_layers.selected_bones_filter(obj, fcu):
|
||||
continue
|
||||
if not filter_properties(obj, fcu):
|
||||
continue
|
||||
|
||||
@@ -112,6 +162,10 @@ class ScaleValuesOp(bpy.types.Operator):
|
||||
self.keyframes_avg_value.update({key : avg_value})
|
||||
|
||||
if not self.keyframes_avg_value:
|
||||
if 'is_dragging' in globals():
|
||||
del is_dragging
|
||||
scene.multikey['scale'] = 1
|
||||
anim_layers.redraw_areas(['VIEW_3D'])
|
||||
return('CANCELLED')
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
@@ -119,41 +173,52 @@ class ScaleValuesOp(bpy.types.Operator):
|
||||
|
||||
def modal(self, context, event):
|
||||
|
||||
scene = context.scene
|
||||
scale = scene.multikey.scale
|
||||
global is_dragging
|
||||
|
||||
try:
|
||||
scene = context.scene
|
||||
scale = scene.multikey.scale
|
||||
#Quit the modal operator when the slider is released
|
||||
if self.stop:
|
||||
del is_dragging
|
||||
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
|
||||
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'}
|
||||
|
||||
#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
|
||||
except Exception as e:
|
||||
# Log the error
|
||||
print("Error:", e)
|
||||
self['scale'] = 1
|
||||
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'}
|
||||
del is_dragging
|
||||
return {'CANCELLED'}
|
||||
|
||||
def scale_value(self, context):
|
||||
|
||||
if 'is_dragging' in globals():
|
||||
if is_dragging:
|
||||
return
|
||||
|
||||
scene = context.scene
|
||||
if scene.multikey.is_dragging:
|
||||
return
|
||||
obj = context.object
|
||||
|
||||
if obj is None:
|
||||
@@ -168,7 +233,6 @@ def scale_value(self, context):
|
||||
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):
|
||||
@@ -177,10 +241,11 @@ def random_value(self, context):
|
||||
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
|
||||
fcurves = anim_layers.get_fcurves(obj, action)
|
||||
for fcu in fcurves:
|
||||
# if obj.mode == 'POSE':
|
||||
if anim_layers.selected_bones_filter(obj, fcu):
|
||||
continue
|
||||
if not filter_properties(obj, fcu):
|
||||
continue
|
||||
value_list = []
|
||||
@@ -197,20 +262,42 @@ def random_value(self, context):
|
||||
|
||||
self['randomness'] = 0.1
|
||||
|
||||
def evaluate_array(action, fcu_path, frame, array_len):
|
||||
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'''
|
||||
|
||||
fcu_array = []
|
||||
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 = action.fcurves.find(fcu_path, index = i)
|
||||
fcu = fcurves.find(fcu_path, index = i)
|
||||
if fcu is None:
|
||||
continue
|
||||
fcu_array.append(fcu.evaluate(frame))
|
||||
if not len(fcu_array):
|
||||
return None
|
||||
fcu_array[i] = fcu.evaluate(frame)
|
||||
|
||||
# if (fcu_array == array_default).all():
|
||||
# # print('295 return none')
|
||||
# return None
|
||||
|
||||
return np.array(fcu_array)
|
||||
|
||||
def evaluate_layers(context, obj, anim_data, fcu, array_len):
|
||||
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:
|
||||
@@ -219,12 +306,11 @@ def evaluate_layers(context, obj, anim_data, fcu, array_len):
|
||||
if not len(nla_tracks):
|
||||
return None
|
||||
frame = context.scene.frame_current
|
||||
#blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
|
||||
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
|
||||
eval_array = array_default.copy()
|
||||
|
||||
for track in nla_tracks:
|
||||
if track.mute:
|
||||
continue
|
||||
@@ -264,43 +350,49 @@ def evaluate_layers(context, obj, anim_data, fcu, array_len):
|
||||
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)
|
||||
fcurves = anim_layers.get_fcurves(obj, action)
|
||||
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame_eval, influence, array_default, blend_type, blend_types)
|
||||
|
||||
#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
|
||||
#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
|
||||
|
||||
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
|
||||
action = anim_data.action
|
||||
if action:
|
||||
influence = anim_data.action_influence
|
||||
blend_type = anim_data.action_blend_type
|
||||
|
||||
fcurves = anim_layers.get_fcurves(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 = bake_ops.evaluate_combine(fcu_path, fcu_array, eval_array, array_default, influence)
|
||||
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):
|
||||
|
||||
for obj in context.selected_objects:
|
||||
|
||||
anim_data = obj.animation_data
|
||||
@@ -310,36 +402,51 @@ def evaluate_value(self, context):
|
||||
return
|
||||
|
||||
action = obj.animation_data.action
|
||||
fcu_paths = []
|
||||
# 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:
|
||||
|
||||
fcurves = anim_layers.get_fcurves(obj, action)
|
||||
for fcu in fcurves:
|
||||
# if fcu in fcu_paths:
|
||||
# continue
|
||||
current_value = None
|
||||
if not filter_properties(obj, fcu):
|
||||
continue
|
||||
if obj.mode == 'POSE':
|
||||
if bake_ops.selected_bones_filter(obj, fcu.data_path):
|
||||
if anim_layers.selected_bones_filter(obj, fcu):
|
||||
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]
|
||||
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)
|
||||
|
||||
eval_array = evaluate_layers(context, obj, anim_data, fcu, len(current_value))
|
||||
#In case it was completly filtered out and not current value available
|
||||
if not current_value:
|
||||
continue
|
||||
|
||||
array_default = np.array(bake_ops.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:
|
||||
eval_array = evaluate_array(action, fcu.data_path, context.scene.frame_current, len(current_value))
|
||||
fcurves = anim_layers.get_fcurves(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(obj, action.fcurves, fcu.data_path, np.array(current_value), eval_array)
|
||||
add_diff(obj, fcurves, fcu.data_path, np.array(current_value), eval_array)
|
||||
|
||||
class MULTIKEY_OT_Multikey(bpy.types.Operator):
|
||||
"""Edit all selected keyframes"""
|
||||
@@ -362,7 +469,7 @@ class MultikeyProperties(bpy.types.PropertyGroup):
|
||||
#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)
|
||||
# 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'})
|
||||
|
||||
Reference in New Issue
Block a user