Files
blender-portable-repo/scripts/addons/Animation_Layers/bake_ops.py
T

1350 lines
56 KiB
Python

import bpy
from . import anim_layers
from . import subscriptions
from mathutils import Vector, Quaternion
import numpy as np
# import time
def frame_start_end(scene):
if scene.use_preview_range:
frame_start = scene.frame_preview_start
frame_end = scene.frame_preview_end
else:
frame_start = scene.frame_start
frame_end = scene.frame_end
return float(frame_start), float(frame_end)
def smart_start_end(smartkeys, frame_start, frame_end):
'''add the first and last frame of the scene if necessery'''
frame_start = float(frame_start)
frame_end = float(frame_end)
if not len(smartkeys):
return smartkeys
frames = [key.frame for key in smartkeys]
#if min(frames) < frame_start:
if frame_start not in frames:
keystart = smartkey()
keystart.startendkeys(frame_start)
smartkeys.append(keystart)
else:
for keystart in smartkeys:
if keystart.frame == frame_start:
keystart.startendkeys(frame_start)
break
#if max(frames) > frame_end:
if frame_end not in frames:
keyend = smartkey()
keyend.startendkeys(frame_end)
smartkeys.append(keyend)
else:
for keyend in reversed(smartkeys):
if keyend.frame == frame_end:
keyend.startendkeys(frame_end)
break
smartkeys.sort()
return smartkeys
def remove_outofrange_keys(smartkeys, frame_start, frame_end):
'''remove smartkeys that are outside of the timeline'''
i = 0
while i < len(smartkeys):
if smartkeys[i].frame < frame_start or smartkeys[i].frame > frame_end:
smartkeys.pop(i)
else:
i += 1
return smartkeys
def smart_repeat(smartkeys, fcu, strip):
'''duplicates the smartkeys strip repeat'''
strip_end = strip.action_frame_end * strip.scale
#last_frame = (strip.frame_end - strip.frame_start) / strip.repeat + strip.frame_start
# fcu_range = (strip_end - fcu.range()[0])
fcu_range = (strip_end - strip.action_frame_start * strip.scale)
#duplicate the keys on the cycle after
keyframes_dup = []
for i in range(1, int(strip.repeat)):
for key in smartkeys[1:]:
keydup = smartkey(key)
keydup.frame += round(fcu_range*(i), 2)
#duplicate the tangents tuple values
if hasattr(key, 'handle_left'):
keydup.handle_left = Vector([key.handle_left[0] + fcu_range*(i+1), key.handle_left[1]])
#if it's the last keyframe then the right handle get the value from the first keyframes
if hasattr(key, 'handle_right'):
keydup.handle_right = Vector([key.handle_right[0] + fcu_range*(i+1), key.handle_right[1]])
keyframes_dup.append(keydup)
#merge the keyframes from the cycle with the original keyframes
smartkeys.extend(keyframes_dup)
smartkeys.sort()
return smartkeys
def smart_cycle(smartkeys, fcu, frame_start, frame_end):
'''duplicates the smartkeys cycle'''
for mod in fcu.modifiers:
if mod.type != 'CYCLES' or mod.mute is True:
continue
fcu_range = round(fcu.range()[1] - fcu.range()[0])
if not fcu_range:
return smartkeys
if not mod.cycles_after and mod.mode_after != 'None':
#if it's an iternal cycle then duplicate the smartkeys until the scene frame end
cycle_end_dup = int((frame_end - fcu.range()[1])/fcu_range)+2
if mod.use_restricted_range and mod.frame_end < frame_end:
cycle_end_dup = int((mod.frame_end - fcu.range()[1])/fcu_range)+2
elif mod.mode_after != 'None':
cycle_end_dup = mod.cycles_after
if mod.use_restricted_range and mod.frame_end < (fcu.range()[1] + fcu_range * cycle_end_dup):
cycle_end_dup = int((mod.frame_end - fcu.range()[1])/fcu_range)+2
#copy the the right handle of the first keyframe to the last, and the left handle from the last keyframe to the first
smartkeys[-1].handle_right_type = smartkeys[0].handle_left_type
smartkeys[0].handle_left_type = smartkeys[-1].handle_right_type
# if smartkeys[-1].interpolation == 'BEZIER':
if hasattr(smartkeys[-1], 'handle_right'):
smartkeys[-1].handle_right = [smartkeys[0].handle_right[0] + fcu_range, smartkeys[-1].handle_right[1]]
# if smartkeys[0].interpolation == 'BEZIER':
if hasattr(smartkeys[0], 'handle_left'):
smartkeys[0].handle_left = [smartkeys[-1].handle_left[0] - fcu_range, smartkeys[0].handle_left[1]]
#duplicate the keys on the cycle after
keyframes_dup = []
for key in smartkeys[1:]:
for i in range(cycle_end_dup):
keydup = smartkey(key)
keydup.frame += float(round(fcu_range, 2)*(i+1))
if hasattr(keydup, 'handle_left'):
#duplicate the tangents tuple values
keydup.handle_left = Vector([key.handle_left[0] + fcu_range*(i+1), key.handle_left[1]])
if hasattr(keydup, 'handle_right'):
#if it's the last keyframe then the right handle get the value from the first keyframes
keydup.handle_right = Vector([key.handle_right[0] + fcu_range*(i+1), key.handle_right[1]])
if keydup not in keyframes_dup:
keydup.frame = round(keydup.frame, 2)
keyframes_dup.append(keydup)
#if it's an iternal cycle then duplicate the keyframes before the cycle keyframes
if not mod.cycles_before and mod.mode_before != 'None':
cycle_start_dup = int((fcu.range()[0] - frame_start) /fcu_range)+2
if mod.use_restricted_range and mod.frame_start > frame_start:
cycle_start_dup = int((fcu.range()[0]-mod.frame_start)/fcu_range)+2
elif mod.mode_before != 'None':
cycle_start_dup = mod.cycles_before
if mod.use_restricted_range and mod.frame_start > (fcu.range()[0] + fcu_range * cycle_start_dup):
cycle_start_dup = int((fcu.range()[0]-mod.frame_start)/fcu_range)+2
#duplicate the keys on the cycle before
for key in smartkeys[:-1]:
for i in range(cycle_start_dup):
keydup = smartkey(key)
keydup.frame -= fcu_range*(i+1)
if hasattr(keydup, 'handle_left'):
#duplicate the tangents
keydup.handle_left = [key.handle_left[0] - fcu_range*(i+1), key.handle_left[1]]
if hasattr(keydup, 'handle_right'):
keydup.handle_right = [key.handle_right[0] - fcu_range*(i+1), key.handle_right[1]]
if keydup not in keyframes_dup:
keydup.frame = round(keydup.frame, 2)
keyframes_dup.append(keydup)
#merge the keyframes from the cycle with the or iginal keyframes
smartkeys.extend(keyframes_dup)
smartkeys = list(set(smartkeys))
smartkeys.sort()
if mod.use_restricted_range:
smartkeys = smart_start_end(smartkeys, mod.frame_start, mod.frame_end)
smartkeys = smart_start_end(smartkeys, mod.frame_start+1, mod.frame_end-1)
return smartkeys
def smart_bake(context):
#record all the keyframes into smartkeys
obj = context.object
frame_start, frame_end = context.scene.als.bake_range
fcu_smartkeys = {}
anim_data = anim_layers.anim_data_type(obj)
# Initialize the progress bar
wm = context.window_manager
total_iterations = 0
for track in anim_data.nla_tracks:
if track.mute:
continue
if len(track.strips) != 1 or track.strips[0].action is None:
continue
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
total_iterations += len(fcurves)
wm.progress_begin(0, total_iterations)
processed = 0
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
if track.mute:
continue
if len(track.strips) != 1 or track.strips[0].action is None:
continue
strip = track.strips[0]
strip_start = strip.action_frame_start
strip_end = strip.action_frame_end
# include influence keyframes in the smartbake
for strip_fcu in strip.fcurves:
strip_keyframes = [keyframe for keyframe in strip_fcu.keyframe_points if len(strip_fcu.keyframe_points) and not strip_fcu.mute]
fcurves = anim_layers.get_fcurves(obj, strip.action)
for fcu in fcurves:
if not fcu.is_valid or fcu.mute or selected_bones_filter(obj, fcu.data_path):
continue
smartkeys = []
#duplicate all the keyframes into a new keyframes class duplicate
for key in list(fcu.keyframe_points) + strip_keyframes:
keyframe = smartkey(key)
#add the handle values only if it's in combine or replace layers
if strip.blend_type in {'COMBINE', 'REPLACE'}:
keyframe.handle_left = Vector(key.handle_left)
keyframe.handle_right = Vector(key.handle_right)
keyframe.handle_left[0] = strip_start * layer.speed + (keyframe.handle_left[0] - strip_start) * layer.speed + layer.offset
keyframe.handle_right[0] = strip_start * layer.speed + (keyframe.handle_right[0] - strip_start) * layer.speed + layer.offset
#keyframe.frame += layer.offset
keyframe.frame = strip_start * layer.speed + (keyframe.frame - strip_start) * layer.speed + layer.offset# * layer.speed
keyframe.frame = round(keyframe.frame, 2)
if keyframe not in smartkeys:
smartkeys.append(keyframe)
if not smartkeys:
continue
#remove duplicate or any subframe from first and end frames for clean cyclic
if len(fcu.modifiers) and obj.als.mergefcurves:
smartkeys[0].frame = float(round(smartkeys[0].frame))
smartkeys[-1].frame = float(round(smartkeys[-1].frame))
#remove extra keyframes
smartkeys = list(set(smartkeys))
smartkeys.sort()
if len(fcu.modifiers) and obj.als.mergefcurves:
smartkeys = smart_cycle(smartkeys, fcu, frame_start, frame_end)
#apply strip action settings
last_frame = (strip.frame_end - strip.frame_start) / strip.repeat + strip.frame_start
if strip.use_reverse:
for key in smartkeys:
key.frame = last_frame - (key.frame - strip.frame_start)
if strip.repeat > 1:
smartkeys = smart_start_end(smartkeys, last_frame , last_frame+1) #strip.frame_start
smartkeys = remove_outofrange_keys(smartkeys, strip.frame_start, last_frame+1) #+ layer.offset
smartkeys = smart_repeat(smartkeys, fcu, strip)
if layer.custom_frame_range:
smartkeys = smart_start_end(smartkeys, strip.frame_start, strip.frame_end)
smartkeys = remove_outofrange_keys(smartkeys, strip.frame_start, strip.frame_end)
#if the strip is cutting with a different strip, then add keyframes in the cut
for layercut in obj.Anim_Layers:
if layercut.mute or not layercut.custom_frame_range or layercut == layer:
continue
if strip_start < layercut.frame_start < strip_end:
smartkeys = smart_start_end(smartkeys, (layercut.frame_start-1), strip.frame_end-1)
if strip_start < layercut.frame_end < strip_end:
smartkeys = smart_start_end(smartkeys, (layercut.frame_end+1), strip.frame_end-1)
#if the list of keyframes exists in a different track list then add them
if (fcu.data_path, fcu.array_index) in fcu_smartkeys:
smartkeys = list(set(fcu_smartkeys[(fcu.data_path, fcu.array_index)]+smartkeys))
#Merge all duplicated keyframes
smartkeys = list(set(smartkeys))
smartkeys.sort()
fcu_smartkeys.update({(fcu.data_path, fcu.array_index):smartkeys})
processed += 1
wm.progress_update(processed)
wm.progress_end()
#add inbetweens
for fcu, smartkeys in fcu_smartkeys.items():
if not smartkeys:
continue
if min(smartkeys).frame < frame_start or max(smartkeys).frame > frame_end:
smartkeys = smart_start_end(smartkeys, frame_start, frame_end)
smartkeys = remove_outofrange_keys(smartkeys, frame_start, frame_end)
if context.scene.als.handles_type not in {'ALIGNED', 'VECTOR', 'AUTO', 'AUTO_CLAMPED'}:
smartkeys = add_inbetween(smartkeys)
return fcu_smartkeys
def add_inbetween(smartkeys):
i = 0
while i < len(smartkeys)-1:
if smartkeys[i].inbetween:
i += 1
continue
if (smartkeys[i+1].frame - smartkeys[i].frame) <= 1:
i += 1
continue
key1 = smartkey()
key1.frame = smartkeys[i].frame + (smartkeys[i+1].frame - smartkeys[i].frame)*1/3
key1.inbetween = True
key2 = smartkey()
key2.frame = smartkeys[i].frame + (smartkeys[i+1].frame - smartkeys[i].frame)*2/3
key1.frame = round(key1.frame, 2)
key2.frame = round(key2.frame, 2)
key2.inbetween = True
smartkeys.insert(i+1, key1)
smartkeys.insert(i+2, key2)
i += 3
return smartkeys
class smartkey:
def __init__(self, key = None):
if not key:
return
if hasattr(key, 'co'):
self.frame = round(float(key.co[0]), 1)
elif hasattr(key, 'frame'):
self.frame = round(float(key.frame), 1)
self.interpolation = key.interpolation
self.handle_left_type = key.handle_left_type
self.handle_right_type = key.handle_right_type
# if hasattr(key, 'handle_left'):
# self.handle_left = Vector(key.handle_left)
# if hasattr(key, 'handle_right'):
# self.handle_right = Vector(key.handle_right)
self.easing = key.easing
self.inbetween = False
def startendkeys(self, frame):
self.frame = round(float(frame), 1)
self.interpolation = 'BEZIER'
self.handle_left_type = 'VECTOR'
self.handle_right_type = 'VECTOR'
self.easing = 'AUTO'
self.inbetween = False
def __lt__(self, other):
return self.frame < other.frame
def __hash__(self):
return hash(self.frame)
def __eq__(self, other):
if not isinstance(other, type(self)): return NotImplemented
return self.frame == other.frame
def __str__(self):
return str(self.frame)
def mute_unbaked_layers(layer_index, nla_tracks, additive):
obj = bpy.context.object
#a list to record which layers that are not merged were muted
mute_rec = []
#mute the layers that are not going to be baked
if obj.als.direction == 'ALL':
return mute_rec
for index, track in enumerate(nla_tracks):
#if running into a replace layer during additive bake then exclude the rest of the layers from the bake
if track.mute:
mute_rec.append(track)
continue
if additive and track.strips[0].blend_type == 'REPLACE' and index >= layer_index:
layer_index = len(nla_tracks)
#layer_index = len(nla_tracks)-1
if obj.als.direction == 'DOWN' and index > layer_index:
track.mute = True
track.select = False
if obj.als.direction == 'UP' and index < layer_index:
track.mute = True
track.select = False
return mute_rec
def mute_modifiers(obj, nla_tracks):
#disable modifiers if merge fcurves is false
modifier_rec = []
extrapolations = []
for track in nla_tracks:
if len(track.strips) != 1 or track.strips[0].action is None:
continue
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
for fcu in fcurves:
if selected_bones_filter(obj, fcu.data_path):
continue
if fcu.extrapolation == 'LINEAR':
extrapolations.append((fcu.data_path, fcu.array_index))
if fcu.lock:
fcu.lock = False
if fcu.group is not None:
if fcu.group.lock:
fcu.group.lock = False
if not fcu.is_valid:
continue
if len(fcu.modifiers) and not obj.als.mergefcurves:
for mod in fcu.modifiers:
if mod.mute == False:
modifier_rec.append(mod)
mod.mute = True
return modifier_rec, extrapolations
def unmute_modifiers(obj, nla_tracks, modifier_rec):
#Turn on fcurve modifiers if merge fcurves is false
for track in nla_tracks:
if track.strips[0].action is None:
continue
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
for fcu in fcurves:
if not fcu.is_valid or selected_bones_filter(obj, fcu.data_path):
continue
if not len(fcu.modifiers):
continue
for mod in fcu.modifiers:
if mod in modifier_rec:
mod.mute = False
elif obj.als.mergefcurves and track == nla_tracks[obj.als.layer_index]:
mod.mute = True
def invisible_layers(b_layers):
#Store the current invisible layer bones and make them visible for baking
layers_rec = []
for i in range(len(b_layers)):
if b_layers[i] == False:
layers_rec.append(i)
b_layers[i] = True
return layers_rec
def select_keyframed_bones(self, context, obj):
#Select all keyframed bones in layers if not only selected
if obj.als.onlyselected:
return
if obj.mode != 'POSE':
bpy.ops.object.posemode_toggle()
bpy.ops.pose.select_all(action='DESELECT')
for i in range(0, obj.als.layer_index+1):
obj.als['layer_index'] = i
anim_layers.select_layer_bones(self, context)
def mute_constraints(obj):
#Mute constraints if are not cleared during bake
constraint_rec = []
if obj.als.clearconstraints:
return constraint_rec
for bone in bpy.context.selected_pose_bones:
for constraint in bone.constraints:
if constraint.mute == False:
constraint_rec.append(constraint)
constraint.mute = True
return constraint_rec
def smartbake_apply(obj, nla_tracks, fcu_keys, extrapolations):
#apply smartbake for blenders bake
#smart bake - delete unnecessery keyframes:
# transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
strip = nla_tracks[obj.als.layer_index].strips[0]
# if strip.action is None:
# return
# action_range = strip.frame_end - strip.frame_start
fcurves = anim_layers.get_fcurves(obj, strip.action)
for fcu in fcurves:
if not fcu.is_valid:
continue
if selected_bones_filter(obj, fcu.data_path):
continue
fcu_key = (fcu.data_path, fcu.array_index)
if fcu_key not in fcu_keys.keys():
fcurves.remove(fcu)
continue
#get all the frames of the smart keys
smartkeys = fcu_keys[fcu_key]
#Get all the inbetween values
for smartkey in smartkeys:
if smartkey.inbetween:
smartkey.value = round(fcu.evaluate(smartkey.frame), 2)
smart_frames = [key.frame for key in smartkeys if not key.inbetween]
#get all the frames of the baked keyframes
key_frames = [key.co[0] for key in fcu.keyframe_points]
for smart_key in smartkeys:
if smart_key.frame > strip.action.frame_range[1]:
break
if smart_key.inbetween:
continue
#add keyframes that are missing from the bake but included in the smart bake
if smart_key.frame not in key_frames:
value = fcu.evaluate(smart_key.frame)
fcu.keyframe_points.add(1)
fcu.keyframe_points[-1].co = (smart_key.frame, value)
fcu.update()
i = 0
while i < len(fcu.keyframe_points):
key = fcu.keyframe_points[i]
if key.co[0] not in smart_frames:
fcu.keyframe_points.remove(key)
else:
key.co[1] = round(key.co[1], 3)
i += 1
fcu.update()
#add_interpolations(fcu, smartkeys)
if (fcu.data_path, fcu.array_index) in extrapolations:
fcu.extrapolation = 'LINEAR'
fcu.update()
def armature_restore(obj, b_layers, layers_rec, constraint_rec):
if obj.type != 'ARMATURE':
return
#Turn off previous invisible bone layers
if bpy.app.version < (4, 0, 0) :
for i in range(len(b_layers)):
if i in layers_rec:
b_layers[i] = False
else:
for key, value in layers_rec.items():
key.is_visible = value
#Turn on constraints
if not obj.als.clearconstraints:
for constraint in constraint_rec:
constraint.mute = False
def attr_default(obj, fcu_key):
'''Returns the default value or default array value in a list'''
#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#[fcu_key[1]]
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():
# print(fcu_key[0], 'has no attributes returning 0')
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 selected_bones_filter(obj, fcu_data_path):
'''using obj.als.onlyselected property for the bake'''
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 evaluate_combine(data_path, added_array, eval_array, array_default, influence):
if data_path.endswith('scale'):
eval_array = eval_array * (added_array / array_default) ** influence
elif data_path.endswith('rotation_quaternion'):
#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 not data_path.endswith('rotation_euler') and not data_path.endswith('location'):
eval_array = eval_array + (added_array - array_default) * influence
return eval_array
def frame_evaluation(frame, strip):
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
offset = strip.frame_start - strip.action_frame_start * strip.scale
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)
frame_eval = strip.frame_start * 1/strip.scale + (frame_eval - strip.frame_start) * 1/strip.scale - offset * 1/strip.scale
return frame_eval
def clean_no_user_slots(action):
'''Remove all the slots that are connected to the action and object but not part of the action slot'''
if not hasattr(action, 'slots'):
return
for slot in action.slots:
if slot is None:
continue
if len(slot.users()):
continue
action.slots.remove(slot)
def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, actioncopy, baked_layer = None):
obj = bpy.context.object
if obj is None:
return
anim_data = anim_layers.anim_data_type(obj)
# baked_action = anim_data.action
track = nla_tracks[obj.als.layer_index]
baked_action = track.strips[0].action
clean_no_user_slots(baked_action)
#create the baked fcurve
# baked_channelbag = anim_layers.get_channelbag(obj, baked_action, obj.als.data_type)
baked_channelbag = anim_layers.get_channelbag(obj, baked_action)
baked_fcurves = baked_channelbag.fcurves
if not obj.als.smartbake:
interpolation_types = ('CONSTANT', 'LINEAR', 'BEZIER', 'SINE', 'QUAD', 'CUBIC', 'QUART', 'QUINT', 'EXPO', 'CIRC', 'BACK', 'BOUNCE', 'ELASTIC')
handle_types = ('FREE', 'ALIGNED', 'VECTOR', 'AUTO', 'AUTO_CLAMPED')
# Get the index position to assign with foreach_set
interpolation = interpolation_types.index(bpy.context.preferences.edit.keyframe_new_interpolation_type)
handle_type = handle_types.index(bpy.context.preferences.edit.keyframe_new_handle_type)
if obj.als.operator == 'MERGE':# and not additive: # and anim_data.action is not None: #and obj.als.onlyselected
#overwrite action
anim_data.use_tweak_mode = False
#create a duplicate of the action on the merged layer to have a clean action in order to not write over the calculation
action_copy = bpy.data.actions[baked_action.name].copy()
track.strips[0].action = action_copy
if hasattr(baked_action, 'id_root'):
baked_action.id_root = obj.als.data_type
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_paths = []
# Initialize the progress bar
wm = bpy.context.window_manager
fcu_set = {fcu_key[0] for fcu_key in fcu_keys.keys()}
total_iterations = len(fcu_set)
wm.progress_begin(0, total_iterations) # (start, end range)
processed = 0
for fcu_key in fcu_keys.keys():
if fcu_key[0] in fcu_paths:
continue
else:
fcu_paths.append(fcu_key[0])
if selected_bones_filter(obj, fcu_key[0]):
continue
extrapolation = False
#adding default value to the array
array_default = np.array(attr_default(obj, fcu_key))
array_length = array_default.shape[0]
if isinstance(array_default[0], str):
continue
#find or create the fcurve in the new action
baked_fcus = []
smart_frames = {}
mod_copy = []
mod_list = {}
#Create smart frames and new fcurves for all layers, apply their groups and modifiers
for i in range(array_length):
if (fcu_key[0], i) not in fcu_keys:
continue
smart_frames.update({i : [key.frame for key in fcu_keys[fcu_key[0], i]]})
if not smart_frames[i]:
continue
#find the groups and modifiers
for track in nla_tracks:
if track.mute:
continue
if not len(track.strips):
continue
if track.strips[0].action is None:
continue
# if track == baked_layer:
# continue
#finding the channel group of array
channelbag = anim_layers.get_channelbag(obj, track.strips[0].action)
if channelbag is None:
continue
fcurves = channelbag.fcurves
fcu = fcurves.find(fcu_key[0], index = i)
# print(f'track {track.name} fcurves {len(fcurves)}')
if fcu is None:
# print('fcu is none', fcu_key[0], track.name)
continue
group = fcu.group if fcu.group is not None else None
if group is not None:
if group.name in baked_channelbag.groups:
group = baked_channelbag.groups[group.name]
else:
group = baked_channelbag.groups.new(group.name)
#copy and append Modifiers into mod_list. Mute them if turned on
if len(fcu.modifiers) and not obj.als.mergefcurves:
for mod in fcu.modifiers:
mod_copy = anim_layers.copy_modifiers(mod, mod_copy)
mod_list.update({i: mod_copy})
#turn off modifier after copying it and append it
if mod.mute == False:
# modifier_rec.append(mod)
mod.mute = True
extrapolation = True if fcu.extrapolation == 'LINEAR' else False
#### Creating or overwritting (during merge) the new baked fcurves####
baked_fcu = ensure_fcurves_bversion(baked_fcurves, fcu_key[0], i)
baked_fcu.color_mode = 'AUTO_RGB'
if group is not None:
if baked_fcu.group != group:
baked_fcu.group = group
if extrapolation:
baked_fcu.extrapolation = 'LINEAR'
baked_fcus.append(baked_fcu)
#select smart bake frame range or every frame in the range
if obj.als.smartbake:
#merge all the smartframe arrays
frame_range = set([item for sf in smart_frames.values() for item in sf])
else:
frame_range = range(frame_start, frame_end+1, step)
#Evaluate the value of the current frame from all the unmuted tracks
for frame in frame_range:
eval_array = np.zeros(array_length) if additive else array_default
layers_count = 0
#ITERATE through all the layers to evaluate
for track in nla_tracks:
if not len(track.strips):
continue
if track.mute or track == baked_layer or track.strips[0].action is None:
continue
strip = track.strips[0]
fcurves = anim_layers.get_fcurves(obj, strip.action)
if not len(fcurves):
continue
if (frame < strip.frame_start or frame > strip.frame_end) and strip.extrapolation == 'NOTHING':
layers_count += 1
continue
#get the influence value either from the attribute or the fcurve
if not strip.fcurves[0].mute and len(strip.fcurves[0].keyframe_points):
influence = strip.fcurves[0].evaluate(frame)
#if there are influence keyframes then make sure layers_count is not 1 to recalculate the handles
layers_count = 2
else:
influence = strip.influence
#evaluate the frame according to the strip settings
frame_eval = frame_evaluation(frame, strip)
blend_type = strip.blend_type
#Create the array of the current layer if a value is missing then add the default value
added_array = []
missing = 0
for i in range(array_length):
fcu = fcurves.find(fcu_key[0], index = i)
#if the fcurve is not found then get the default value
if fcu is None:
missing += 1
#getting the previous value if the fcurve is missing instead of just default
#the other option would be to use an array for the influence as well
value = eval_array[i] if blend_type in ('REPLACE', 'COMBINE') else 0
else:
value = fcu.evaluate(frame_eval)
added_array.append(value)
if missing == array_length:
continue
#convert the array to a numpy array
added_array = np.array(added_array)
###EVALUATION###
if blend_type =='COMBINE':
if 'location' in fcu_key[0] or 'rotation_euler' in fcu_key[0]:
blend_type = 'ADD'
if blend_type =='REPLACE':
eval_array = eval_array * (1 - influence) + added_array * influence
elif blend_type =='COMBINE':
eval_array = evaluate_combine(fcu_key[0], added_array, eval_array, array_default, influence)
else:
eval_array = eval('eval_array' + blend_types[blend_type] +' added_array' + '*' + str(influence))
layers_count += 1
if not eval_array.size:
continue
#WRITING the keyframes:
for baked_fcu in baked_fcus:
i = baked_fcu.array_index
if obj.als.smartbake:
#find the smartkey in the fcu_keys -smartkeys and array of the fcurves
if frame not in smart_frames[i]:
continue
smartkey = fcu_keys[(fcu_key[0], i)][smart_frames[i].index(frame)]
smartkey.value = eval_array[i]
if smartkey.inbetween:
continue
baked_fcu.keyframe_points.add(1)
keyframe = baked_fcu.keyframe_points[-1]
keyframe.co = (frame, eval_array[i])
for baked_fcu in baked_fcus:
if not len(baked_fcu.keyframe_points):
continue
baked_fcu.update()
i = baked_fcu.array_index
if (fcu_key[0], i) not in fcu_keys:
continue
if obj.als.smartbake:
add_interpolations(baked_fcu, fcu_keys[(fcu_key[0], i)], layers_count)
else:
# Add the interpolation and handle types from the preferences defaults
set_all_handles(baked_fcu, interpolation, handle_type)
baked_fcu.update()
#paste the modifiers to the new baked fcurve
if i in mod_list and not len(baked_fcu.modifiers):
anim_layers.paste_modifiers(baked_fcu, mod_list[i])
processed += 1
wm.progress_update(processed)
wm.progress_end()
if not actioncopy and obj.als.operator == 'MERGE':
bpy.data.actions.remove(action_copy)
return baked_action
def set_all_handles(baked_fcu, interpolation, handle_type):
'''Set all the handle types and interpolations in an fcurve using foreach_set and numpy
should use numerical value instead of a string'''
keyframes_len = len(baked_fcu.keyframe_points)
interpolations = np.full(keyframes_len, interpolation)
handles = np.full(keyframes_len, handle_type)
kp = baked_fcu.keyframe_points
kp.foreach_set('interpolation', interpolations)
kp.foreach_set('handle_left_type', handles)
kp.foreach_set('handle_right_type', handles)
def ensure_fcurves_bversion(fcurves, data_path, i):
'''Either use ensure in Blender 5.0 and above or use find and new in older version'''
if hasattr(fcurves, 'ensure'):
baked_fcu = fcurves.ensure(data_path, index = i)
else:
baked_fcu = fcurves.find(data_path, index = i)
if baked_fcu is None:
baked_fcu = fcurves.new(data_path, index = i)
if len(baked_fcu.keyframe_points):
baked_fcu.keyframe_points.clear()
return baked_fcu
def non_recalc_handle_type(baked_keys):
handles_type = bpy.context.scene.als.handles_type
if handles_type not in {'ALIGNED', 'VECTOR', 'AUTO', 'AUTO_CLAMPED'}:
return False
for key in baked_keys:
key.handle_left_type = handles_type
key.handle_right_type = handles_type
return True
def add_interpolations(baked_fcu, smartkeys, layers_count = 0):
'''Add the interpolation or control points between every two smartkeys'''
baked_keys = baked_fcu.keyframe_points
keys = [key for key in smartkeys if not key.inbetween]
if non_recalc_handle_type(baked_keys):
return
#the index for the inbetweens
P1index = 1
P2index = 2
if len(baked_keys) != len(keys):
print('unequal length of keys ',baked_fcu.data_path, len(baked_keys), len(keys))
print('keys ', [key.frame for key in keys])
print('baked keys ', [round(key.co[0], 2) for key in baked_keys])
print('set difference ', set([key.frame for key in keys]).difference(set([key.co[0] for key in baked_keys])))
baked_keys[0].handle_left_type = 'VECTOR'
baked_keys[-1].handle_right_type = 'VECTOR'
#keys are smartkeys without the inbetween keyframes
for i, key in enumerate(keys[:-1]):
skip = False
#if the fcurve was counted only once, then just copy the old handle values instead of recalculating
if layers_count == 1:# and key != keys[0] and i < len(keys) - 3:
apply_handle_types(baked_keys, keys, i)
if hasattr(key, 'handle_right'):
baked_keys[i].handle_right = key.handle_right
if hasattr(keys[i+1], 'handle_left'):
baked_keys[i+1].handle_left = keys[i+1].handle_left
if hasattr(key, 'handle_right') and hasattr(keys[i+1], 'handle_left'):
skip = True
if key.interpolation != 'BEZIER':
baked_keys[i].interpolation = key.interpolation
baked_keys[i].easing = key.easing
if keys[-1] == key:
baked_keys[i+1].interpolation = keys[i+1].interpolation
baked_keys[i+1].easing = keys[i+1].easing
skip = True
if not smartkeys[P1index].inbetween :
P1index += 1
P2index += 1
continue
if skip:
P1index += 3
P2index += 3
continue
#skip if value not found in smartkey (bug that need to be solved)
if not hasattr(smartkeys[P1index], 'value') or not hasattr(smartkeys[P2index], 'value'):
apply_handle_types(baked_keys, keys, i)
print(baked_fcu.data_path, 'missing smartkey value on frame', baked_keys[i].co[0])
P1index += 3
P2index += 3
continue
P0 = baked_keys[i].co[1]
P3 = baked_keys[i+1].co[1]
P1 = smartkeys[P1index].value
P2 = smartkeys[P2index].value
cp1 = (1/6)*( -5*P0 + 18*P1 - 9*P2 + 2*P3)
cp2 = (1/6)*( 2*P0 - 9*P1 +18*P2 - 5*P3)
apply_handle_types(baked_keys, keys, i)
baked_keys[i].handle_right = [smartkeys[P1index].frame, cp1]
baked_keys[i+1].handle_left = [smartkeys[P2index].frame, cp2]
#iterate through the inbetween smartkeys
P1index += 3
P2index += 3
#add in-betweener
def apply_handle_types(baked_keys, smartkeys, i):
handles_type = bpy.context.scene.als.handles_type
if handles_type == 'PRESERVE':
if hasattr(smartkeys[i], 'handle_right_type'):
baked_keys[i].handle_right_type = smartkeys[i].handle_right_type
if hasattr(smartkeys[i+1], 'handle_left_type'):
baked_keys[i+1].handle_left_type = smartkeys[i+1].handle_left_type
else:
baked_keys[i].handle_right_type = 'FREE'
baked_keys[i+1].handle_left_type = 'FREE'
else:
baked_keys[i].handle_right_type = handles_type
baked_keys[i+1].handle_left_type = handles_type
def bake_range_type(self, context):
if self.bake_range_type == 'SCENE':
frame_start, frame_end = frame_start_end(context.scene)
self.bake_range = (int(frame_start), int(frame_end))
if self.bake_range_type == 'KEYFRAMES':
obj = context.object
anim_data = anim_layers.anim_data_type(obj)
frame_end = []
frame_start = []
posebones = context.selected_pose_bones
#if baking only selected bones then find the longest fcurves for the range
if obj.als.onlyselected and posebones:
bonespath = [bone.path_from_id() for bone in posebones]
#get the frame range from
if not bonespath:
return
for track in anim_data.nla_tracks:
if not len(track.strips):
continue
action = track.strips[0].action
if obj.als.onlyselected and posebones:
# Get fcurve range from the selected objects
fcurves = anim_layers.get_fcurves(obj, action)
for fcu in fcurves:
#check if the fcurve is in the selected bones
if any(path in fcu.data_path for path in bonespath):
frame_start.append(fcu.range()[0])
frame_end.append(fcu.range()[1])
else:
# Get the action range
action = track.strips[0].action
frame_start.append(action.frame_range[0])
frame_end.append(action.frame_range[1])
# Checking for the longest action in all the actions
if frame_start:
self.bake_range = (int(min(frame_start)), int(max(frame_end)))
class MergeAnimLayerDown(bpy.types.Operator):
"""Merge and bake the layers from the current selected layer down to the base"""
bl_idname = "anim.layers_merge_down"
bl_label = "Merge_Layers_Down"
bl_options = {'REGISTER', 'UNDO'}
step: bpy.props.IntProperty(name='Step', description='Bake every number of frame steps', default=1)
actioncopy: bpy.props.BoolProperty(name='Copy original merged action', description='Create a copy of the original action that is being overwritten', default = False)
def invoke(self, context, event):
obj = context.object
bake_range_type(context.scene.als, context)
if obj.mode != 'POSE':
obj.als.onlyselected = False
wm = context.window_manager
return wm.invoke_props_dialog(self, width = 200)
def draw(self, context):
obj = context.object
layout = self.layout
box = layout.box()
if obj.als.data_type == 'OBJECT':
split = box.split(factor=0.5, align = True)
split.label(text = 'Bake Type :')
split.prop(obj.als, 'baketype')
split = box.split(factor=0.5, align = True)
split.label(text = 'Bake Operator :')
split.prop(obj.als, 'operator')
split = box.split(factor=0.5, align = True)
split.label(text = 'Bake Direction :')
split.prop(obj.als, 'direction')
box.prop(obj.als, 'mergefcurves')
split = box.split(factor=0.5, align = True)
if obj.als.baketype == 'AL':
split.prop(obj.als, 'smartbake')
if obj.als.smartbake:
split.prop_menu_enum(context.scene.als, 'handles_type')
else:
split.prop(self, 'step')
else:
split.prop(self, 'step')
if obj.mode == 'POSE':
box.prop(obj.als, 'onlyselected')
if obj.als.baketype == 'NLA':
# split.prop(self, 'direction')
box.prop(obj.als, 'clearconstraints')
if obj.als.operator == 'MERGE':
box.prop(self, 'actioncopy')
split = box.split(factor=0.5, align = True)
split.label(text = 'Frame Range :')
split.prop(context.scene.als, 'bake_range_type', text = '')
#split = box.split(factor=0.2, align = True)
if context.scene.als.bake_range_type == 'CUSTOM':
row = box.row()
row.prop(context.scene.als, 'bake_range', text = '')
def execute(self, context):
obj = bpy.context.object
if obj is None:
return {'CANCELLED'}
anim_data = anim_layers.anim_data_type(obj)
nla_tracks = anim_data.nla_tracks
if obj.als.direction == 'DOWN' and not obj.als.layer_index:
return {'CANCELLED'}
#disable baking up from Blender's bake
if obj.als.direction == 'UP' and obj.als.layer_index == len(nla_tracks)-1:
return {'CANCELLED'}
if obj.als.baketype == 'NLA':
obj.als.smartbake = False
subscriptions.subscriptions_remove()
# start = time.time()
#define the start and end frame of the bake, according to scene or preview length
frame_start, frame_end = context.scene.als.bake_range
# Incase the strips are shorter then the keyframe range (because scene is shorter)
# Then updating the strips length
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
if layer.custom_frame_range:
continue
if len(track.strips) != 1:
continue
strip = track.strips[0]
strip.frame_start =frame_start
anim_layers.update_action_frame_range(frame_start, frame_end, layer, strip)
strip.scale = layer.speed
strip.frame_end = frame_end
obj.als.view_all_keyframes = False
if context.scene.frame_current > frame_end:
context.scene.frame_current = frame_end
layer_index = obj.als.layer_index
blendings = [track.strips[0].blend_type for track in nla_tracks[layer_index:] if len(track.strips) == 1]
#define if the new baked layer is going to be additive or replace
additive = False
if obj.als.direction == 'UP' and 'REPLACE' not in blendings and obj.als.baketype == 'AL' and layer_index:
if 'COMBINE' in blendings:
blend = 'COMBINE'
else:
blend = 'ADD'
additive = True
else:
blend = 'REPLACE'
mute_rec = mute_unbaked_layers(layer_index, nla_tracks, additive)
fcu_keys = smart_bake(context)
if obj.als.operator == 'MERGE':
if obj.als.direction == 'DOWN':
obj.als.layer_index = 0
baked_layer = None
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
action = strip.action
if hasattr(strip, 'action_slot'):
action_slot = strip.action_slot
if action is not None: action_name = action.name
#if baking to a new layer then setup the new index and layer
elif obj.als.operator == 'NEW':
self.actioncopy = False
if obj.als.direction == 'UP' and additive and 'REPLACE' in blendings:
obj.als.layer_index = layer_index + blendings.index('REPLACE') - 1
elif obj.als.direction == 'UP' or obj.als.direction == 'ALL':
obj.als.layer_index = len(obj.Anim_Layers)-1
layer_names = [layer.name for layer in obj.Anim_Layers]
baked_layer = anim_layers.add_animlayer(layer_name = anim_layers.unique_name(layer_names, 'Baked_Layer') , duplicate = False, index = obj.als.layer_index, blend_type = blend)
anim_layers.register_layers(obj, nla_tracks)
obj.als.layer_index += 1
#remove subsciption again after adding a layer there was new subsciption applied
subscriptions.subscriptions_remove()
track = nla_tracks[obj.als.layer_index]
#use internal bake
if obj.als.baketype =='NLA':
modifier_rec, extrapolations = mute_modifiers(obj, nla_tracks)
if obj.als.smartbake and not obj.als.mergefcurves:
#apply the last frame of the smart bake instead of the whole action when it has a smaller value
smart_end = max(([value.frame for fcu_values in fcu_keys.values() for value in fcu_values]))
if smart_end < frame_end : frame_end = smart_end
if obj.type == 'ARMATURE':
if bpy.app.version < (4, 0, 0) :
b_layers = obj.data.layers
layers_rec = invisible_layers(b_layers)
else:
b_layers = None
layers_rec = dict()
for col in obj.data.collections:
layers_rec.update({col : col.is_visible})
col.is_visible = True
# Select the bones from all the layers
self.shift = True
select_keyframed_bones(self, context, obj)
constraint_rec = mute_constraints(obj)
if obj.als.onlyselected:
bake_type = {'POSE'}
else:
bake_type = {'OBJECT', 'POSE'}
if obj.als.smartbake:
self.step = 1
if not obj.select_get():
obj.select_set(True)
bpy.ops.nla.bake(frame_start = frame_start, frame_end = frame_end, only_selected = True, visual_keying=True, clear_constraints=obj.als.clearconstraints, bake_types = bake_type, step = self.step)
# anim_data.action.fcurves.update()
strip = track.strips[0]
old_action = strip.action
action = anim_data.action
action_name = old_action.name
#reset the strip settings, remove old and create new
track.strips.remove(strip)
strip = track.strips.new(track.name, 0, action)
anim_layers.tweak_mode_upper_stack(context, obj, anim_data, enter = False)
if obj.als.smartbake:
smartbake_apply(obj, nla_tracks, fcu_keys, extrapolations)
if obj.type == 'ARMATURE':
armature_restore(obj, b_layers, layers_rec, constraint_rec)
unmute_modifiers(obj, nla_tracks, modifier_rec)
# anim_data.action = None
#bpy.data.actions.remove(old_action)
if self.actioncopy:
old_action.name = action_name + '_old'
else:
bpy.data.actions.remove(old_action)
# strip.action.name = action_name
if blendings.count('COMBINE') == len(blendings) and len(blendings) and obj.als.direction == 'UP':
track.strips[0].blend_type = 'COMBINE'
else: #use anim layers bake
action = AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, self.step, self.actioncopy, baked_layer)
anim_layers.tweak_mode_upper_stack(context, obj, anim_data, enter = False)
track.strips.remove(track.strips[0])
strip = track.strips.new(track.name, 0, action)
strip.blend_type = blend
#removing layers after merge
if obj.als.operator == 'MERGE':
if hasattr(strip, 'action_slot'):
if action_slot in strip.action.slots.values():
strip.action_slot = action_slot
else:
# During NLA Bake slot doesn't exist need to find a new on
strip.action_slot = anim_layers.get_obj_slot(obj, action)
#reset layer settings
baked_layer = obj.Anim_Layers[obj.als.layer_index]
baked_layer.repeat, baked_layer.speed, baked_layer.offset = 1, 1, 0
strip.use_sync_length = False
if baked_layer.custom_frame_range:
baked_layer.custom_frame_range = False
baked_layer.frame_start = frame_start
baked_layer.frame_end = frame_end
if obj.als.baketype == 'AL':
#Rename the old action with a number
bpy.data.actions[action_name].use_fake_user = False
bpy.data.actions[action_name].name = anim_layers.unique_name(bpy.data.actions, action_name)
#Rename the current action to the old action
action.name = action_name
#delete the baked layers except for the base layer
if obj.als.direction == 'DOWN':
while layer_index > 0:
nla_tracks.remove(nla_tracks[layer_index])
layer_index -= 1
if obj.als.direction == 'UP':
layer_index += 1
while layer_index < len(nla_tracks):
if additive and nla_tracks[layer_index].strips[0].blend_type == 'REPLACE':
break
nla_tracks.remove(nla_tracks[layer_index])
if obj.als.direction == 'ALL':
obj.als.layer_index = 0
index = 0
merged_track = nla_tracks[layer_index]
while len(nla_tracks) > 1:
if nla_tracks[index] != merged_track:
nla_tracks.remove(nla_tracks[index])
else:
index += 1
#turn the tracks back on if necessery
if obj.als.direction != 'ALL':
for track in nla_tracks:
if track in mute_rec:
track.mute = True
else:
track.mute = False
action.use_fake_user = True
anim_layers.register_layers(obj, nla_tracks)
#refresh tweak mode for all objects
for al in context.scene.AL_objects:
obj = al.object
obj.als.layer_index = obj.als.layer_index
subscriptions.frameend_update_callback()
subscriptions.subscriptions_add(context.scene)
# end = time.time()
# print("time ", end-start)
return {'FINISHED'}
def register():
bpy.utils.register_class(MergeAnimLayerDown)
def unregister():
bpy.utils.unregister_class(MergeAnimLayerDown)