import bpy import blf import numpy as np import mathutils import bisect from mathutils import Matrix from mathutils import Vector # import math from math import pi from . import emp # import time from bpy.app.handlers import persistent from bpy_extras import anim_utils # import gpu from gpu_extras.batch import batch_for_shader # import bpy_extras #from bpy_extras.view3d_utils import location_3d_to_region_2d def sort_selection(new_selection, selected_items, active_item): if not selected_items: return new_selection if len(new_selection) < len(selected_items): new_bones = set(selected_items) - set(new_selection) new_selection += list(new_bones) elif len(new_selection) > len(selected_items): new_bones = set(new_selection) - set(selected_items) for bone in new_bones: new_selection.remove(bone) elif set(new_selection) != set(selected_items): new_selection = selected_items if active_item in new_selection[:-1]: new_selection.remove(active_item) new_selection.append(active_item) return new_selection @persistent def selection_order(self, context): global bone_selection, obj_selection if 'bone_selection' not in globals(): bone_selection = [] if 'obj_selection' not in globals(): obj_selection = [] selected_pose_bones = bpy.context.selected_pose_bones selected_objects = bpy.context.selected_objects bone_selection = sort_selection(bone_selection, selected_pose_bones, bpy.context.active_pose_bone) obj_selection = sort_selection(obj_selection, selected_objects, bpy.context.active_object) def asset_shelf_height(context): '''Get the height of the asset shelf to always draw notification on top of it''' if context.area.type != 'VIEW_3D': return 0 if context.area.regions[2].type != 'ASSET_SHELF': return 0 as_height = context.area.regions[2].height return as_height def draw_text_callback_px(self, context): '''Write a text notification during modal operators''' font_id = 0 # XXX, need to find out how best to get this. #fade in factor = 0.5 if not self.fade_out_start: alpha = (self.timer.time_duration / self.fade_duration) * factor if not self.no_timer else factor else: timer = self.timer.time_duration - self.fade_out_start alpha = (1 * factor) - ((timer / self.fade_duration ) * factor) as_height = asset_shelf_height(context) # draw some text blf.position(font_id, 70, 45 + as_height, 0) blf.size(font_id, self.size) blf.color(0, 1, 1, 0, alpha) blf.draw(font_id, self.text) class Markers_Retimer(bpy.types.Operator): """Use Markers to retime your keyframes""" bl_idname = "anim.markers_retimer" bl_label = "Markers_Retimer" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return len(context.scene.timeline_markers) > 1 def store_markers(self, scene): self.marker_frames = {} for marker in scene.timeline_markers: self.marker_frames.update({marker : marker.frame}) #create a list of the frames sorted self.sorted_frames = sorted(self.marker_frames.values()) def invoke(self, context, event): #If the modal is already running, then don't run it the second time scene = context.scene ui = context.window_manager.atb_ui if ui.markers_retimer: ui.markers_retimer = False return {'CANCELLED'} self.store_markers(scene) ui.markers_retimer = True if context.area.type == 'VIEW_3D': # the arguments we pass the the callback args = (self, context) # Add the region OpenGL drawing callback # draw in view space with 'POST_VIEW' and 'PRE_VIEW' wm = context.window_manager self.timer = wm.event_timer_add(0.1, window=context.window) self.no_timer = False self.fade_out_start = False self.fade_duration = 3.0 self.text = 'Markers Retimer is On' self.size = 20 self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(draw_text_callback_px, args, 'WINDOW', 'POST_PIXEL') bpy.app.driver_namespace['markers_retimer_dh'] = self.draw_handle context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): try: scene = context.scene ui = context.window_manager.atb_ui if event.type in {'ESC'}: ui.markers_retimer = False if not ui.markers_retimer: bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW') if 'markers_retimer_dh' in bpy.app.driver_namespace: bpy.app.driver_namespace.pop('markers_retimer_dh') redraw_areas(['VIEW_3D']) self.report({'INFO'},'Quitting RetimerMarkers') context.window_manager.event_timer_remove(self.timer) return {'FINISHED'} if event.type == 'TIMER': #check if the fade in finished and remove the timer if self.timer.time_duration >= self.fade_duration and not self.no_timer: # context.window_manager.event_timer_remove(self.timer) self.no_timer = True else: redraw_areas(['VIEW_3D']) #continue if nothing changed marker_frames = np.array(list(self.marker_frames.values())) markers_array = np.zeros(len(scene.timeline_markers)) scene.timeline_markers.foreach_get('frame', markers_array) if np.array_equal(marker_frames, markers_array): return{'PASS_THROUGH'} # context.area.tag_redraw() #if markers were added or removed then restore them if len(scene.timeline_markers) != len(self.marker_frames): self.store_markers(scene) return{'PASS_THROUGH'} #If markers are the same then skip marker_frames = np.array(list(self.marker_frames.values())) markers_array = np.zeros(len(scene.timeline_markers)) scene.timeline_markers.foreach_get('frame', markers_array) if np.array_equal(marker_frames, markers_array): return{'PASS_THROUGH'} # actions = set() all_fcurves = set() #get all the actions that are going to be influenced from all objects and layers for obj in context.selected_objects: if obj.animation_data is None: continue if obj.animation_data.use_nla: for track in obj.animation_data.nla_tracks: if track.mute: continue for strip in track.strips: if strip.mute: continue # actions.add(strip.action) all_fcurves.add(get_fcurves_channelbag(obj, strip.action)) fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) if fcurves not in all_fcurves: all_fcurves.add(fcurves) # actions.add(obj.animation_data.action) prev_frames = {} next_frames = {} changed_markers = {} prev_dist_dict = {} next_dist_dict = {} #check which marker was changed for marker in scene.timeline_markers: #If markers are not stored anymore in case of undo, store them again and start again if marker not in self.marker_frames: self.store_markers(scene) return {'PASS_THROUGH'} #check if the marker frame was changed and if yes then calculate the percentage of the scale if marker.frame != self.marker_frames[marker]: #start_time = time.time() old_frame = self.marker_frames[marker] frame_index = self.sorted_frames.index(self.marker_frames[marker]) #get the previous frame if it's not the first marker frame if frame_index: previous_frame = self.sorted_frames[frame_index-1] old_prev_dist = old_frame - previous_frame new_prev_dist = marker.frame - previous_frame prev_dist_perc = (new_prev_dist - old_prev_dist) / old_prev_dist prev_frames.update({old_frame : previous_frame}) prev_dist_dict.update({old_frame : prev_dist_perc}) #get the next frame if it's not the last marker frame if max(self.sorted_frames) != old_frame: next_frame = self.sorted_frames[frame_index+1] old_next_dist = next_frame - old_frame new_next_dist = next_frame - marker.frame next_dist_perc = (new_next_dist - old_next_dist) / old_next_dist next_frames.update({old_frame : next_frame}) next_dist_dict.update({old_frame : next_dist_perc}) changed_markers.update({old_frame : marker}) #adding the distance calculation to the keyframes for fcurves in all_fcurves: for fcu in fcurves: if filter_properties(context.scene.animtoolbox, fcu): continue keyframes = np.zeros([len(fcu.keyframe_points)*2]) updated_left_handle = np.zeros([len(fcu.keyframe_points)*2]) updated_right_handle = np.zeros([len(fcu.keyframe_points)*2]) fcu.keyframe_points.foreach_get('co', keyframes) fcu.keyframe_points.foreach_get('handle_left', updated_left_handle) fcu.keyframe_points.foreach_get('handle_right', updated_right_handle) #initializing a new array to add added_frames = np.zeros([len(fcu.keyframe_points)*2]) if bpy.context.scene.animtoolbox.filter_keyframes: select_control_points = np.zeros([len(fcu.keyframe_points)]) fcu.keyframe_points.foreach_get('select_control_point', select_control_points) else: select_control_points = 1 #find the begining of the array to start iterating start_index = 0 end_index = len(keyframes) #get the start frame, the one before the first changed marker if prev_frames and len(prev_frames) == len(changed_markers): previous_frame = prev_frames[min(changed_markers.keys())] indices = np.where(keyframes[::2] > previous_frame)[0] * 2 if len(indices): start_index = indices[0] #get the last frame, the one before the first changed marker if next_frames and len(next_frames) == len(changed_markers): next_frame = next_frames[max(changed_markers.keys())] indices = np.where(keyframes[::2] > next_frame)[0] * 2 if len(indices): end_index = indices[0] for i in range(start_index, end_index, 2): #get the keyframe position along all the markers key_in_markers = list(marker_frames) if keyframes[i] not in changed_markers: key_in_markers.append(keyframes[i]) key_in_markers.sort() else: #in case the keyframe is exactly on the frame old_frame = keyframes[i] marker = changed_markers[keyframes[i]] added_frames[i]= marker.frame - old_frame continue marker_i = key_in_markers.index(keyframes[i]) prev_marker = None if marker_i - 1 < 0 else key_in_markers[marker_i - 1] next_marker = None if marker_i + 1 > len(key_in_markers)-1 else key_in_markers[marker_i + 1] #markers don't affect this frame if prev_marker not in changed_markers and next_marker not in changed_markers: continue #add from both directions if (next_marker in changed_markers and prev_marker in changed_markers) or (next_marker in changed_markers and prev_marker is None): # if both are selected or it the next is the first marker old_frame = next_marker marker = changed_markers[old_frame] added_frames[i]= marker.frame - old_frame continue if prev_marker in changed_markers: if next_marker is None: old_frame = prev_marker marker = changed_markers[old_frame] added_frames[i]= marker.frame - old_frame else: frame_dist = keyframes[i] - next_marker next_dist_perc = next_dist_dict[prev_marker] added_frames[i]= (frame_dist * next_dist_perc) elif next_marker in changed_markers: #I needed to flip here next marker with prev distance frame_dist = keyframes[i] - prev_marker prev_dist_perc = prev_dist_dict[next_marker] added_frames[i]= (frame_dist * prev_dist_perc) #add only to selected keyframes added_frames[::2] *= select_control_points #add the whole array to the keyframes keyframes += added_frames updated_left_handle += added_frames updated_right_handle += added_frames fcu.keyframe_points.foreach_set('co', keyframes) fcu.keyframe_points.foreach_set('handle_left', updated_left_handle) fcu.keyframe_points.foreach_set('handle_right', updated_right_handle) fcu.update() # self.sorted_frames[frame_index] = marker.frame # self.marker_frames[marker] = marker.frame #end_time= time.time() # break redraw_areas(['DOPESHEET_EDITOR', 'GRAPH_EDITOR']) self.store_markers(scene) return {'PASS_THROUGH'} except Exception as e: # Log the error bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW') if 'markers_retimer_dh' in bpy.app.driver_namespace: bpy.app.driver_namespace.pop('markers_retimer_dh') redraw_areas(['VIEW_3D']) context.window_manager.event_timer_remove(self.timer) print("Error:", e) self.report({'ERROR'}, str(e) + '. Quitting RetimerMarkers') return {'CANCELLED'} def init_frame_start_marker(self, scene, name = "Frame_Start"): '''Create the start frame marker''' self.frame_start_marker = scene.timeline_markers.new(name, frame = self.frame_start) self.frame_start_marker[ "Frame_Start"] = True self.frame_start_marker.select = False self.frame_start_marker_name = name def init_frame_end_marker(self, scene, name = "Frame_End"): '''Create the start frame marker''' self.frame_end_marker = scene.timeline_markers.new(name, frame = self.frame_end) self.frame_end_marker["Frame_End"] = True self.frame_end_marker.select = False self.frame_end_marker_name = name def check_removed_markers(self, scene): #check if markers are gone in case of undo or if removed markers = [marker for marker in scene.timeline_markers] if self.frame_end_marker not in markers or self.frame_start_marker not in markers: start_marker = next((marker for marker in scene.timeline_markers if marker.name == self.frame_start_marker_name), None) #and 'Frame_Start' in marker end_marker = next((marker for marker in scene.timeline_markers if marker.name == self.frame_end_marker_name), None) # and 'Frame_End' in marker if start_marker is not None: self.frame_start_marker = start_marker else: init_frame_start_marker(self, scene, self.frame_start_marker_name) if end_marker is not None: self.frame_end_marker = end_marker else: init_frame_end_marker(self, scene, self.frame_end_marker_name) return True else: return False def update_frame_range_markers(self, scene, frame_start, frame_end): #update the frame range using marker only if it was changed both from the scene frame range and the operator frame range properties if getattr(scene, frame_start) != self.frame_start_marker.frame and self.frame_start_marker.frame != self.frame_start: if self.frame_start_marker.frame < 0: self.frame_start_marker.frame = 0 setattr(scene, frame_start, self.frame_start_marker.frame) self.frame_start = self.frame_start_marker.frame redraw_areas(['VIEW_3D']) return True elif getattr(scene, frame_start) != self.frame_start: self.frame_start_marker.frame = self.frame_start = getattr(scene, frame_start) return True if getattr(scene, frame_end) != self.frame_end_marker.frame and self.frame_end_marker.frame != self.frame_end: setattr(scene, frame_end, self.frame_end_marker.frame) self.frame_end = self.frame_end_marker.frame redraw_areas(['VIEW_3D']) return True elif getattr(scene, frame_end) != self.frame_end: self.frame_end_marker.frame = self.frame_end = getattr(scene, frame_end) return False class Markers_FrameRange(bpy.types.Operator): """Create Markers for an interactive frame range""" bl_idname = "anim.markers_framerange" bl_label = "Markers_Framerange" bl_options = {'REGISTER', 'UNDO'} frame_start: bpy.props.IntProperty(name = 'Frame Start', description="Storing the frame start to check if the property or marker were changed") frame_end: bpy.props.IntProperty(name = 'Frame End', description="Storing the frame end to check if the property or marker were changed") def execute(self, context): context.scene.animtoolbox.marker_frame_range = False context.scene.timeline_markers.remove(self.frame_start_marker) context.scene.timeline_markers.remove(self.frame_end_marker) return {'FINISHED'} def invoke(self, context, event): #If the modal is already running, then don't run it the second time scene = context.scene atb = scene.animtoolbox if atb.marker_frame_range: atb.marker_frame_range = False return {'CANCELLED'} scene.animtoolbox.marker_frame_range = True self.frame_start = scene.frame_preview_start if scene.use_preview_range else scene.frame_start self.frame_end = scene.frame_preview_end if scene.use_preview_range else scene.frame_end init_frame_start_marker(self, scene) init_frame_end_marker(self, scene) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): scene = context.scene if check_removed_markers(self, scene): return {'PASS_THROUGH'} # #If one of the markers were removed then remove the rest and quit the modal operator # if self.frame_start_marker.name not in scene.timeline_markers: # if self.frame_end_marker.name in scene.timeline_markers: # scene.timeline_markers.remove(self.frame_end_marker) # context.scene.marker_frame_range = False # return {'CANCELLED'} # if self.frame_end_marker.name not in scene.timeline_markers: # if self.frame_start_marker.name in scene.timeline_markers: # scene.timeline_markers.remove(self.frame_start_marker) # context.scene.marker_frame_range = False # return {'CANCELLED'} #If the modal was running again or changed then exit if not scene.animtoolbox.marker_frame_range: self.execute(context) return {'FINISHED'} #get the attribute depending if using preview range or normal frame range if scene.use_preview_range: frame_start = 'frame_preview_start' frame_end = 'frame_preview_end' else: frame_start = 'frame_start' frame_end = 'frame_end' # frame_start = getattr(scene, frame_start_attr) # frame_end = getattr(scene, frame_end_attr) if update_frame_range_markers(self, scene, frame_start, frame_end): return {'RUNNING_MODAL'} if event.type in {'ESC'}: # Cancel self.execute(context) return {'FINISHED'} return {'PASS_THROUGH'} class Markers_BakeRange(bpy.types.Operator): """Create Markers for an interactive bake frame range""" bl_idname = "anim.markers_bakerange" bl_label = "Markers_Bakerange" bl_options = {'REGISTER', 'UNDO'} frame_start: bpy.props.IntProperty(name = 'Frame Start', description="Storing the frame start to check if the property or marker were changed") frame_end: bpy.props.IntProperty(name = 'Frame End', description="Storing the frame end to check if the property or marker were changed") def execute(self, context): context.scene.animtoolbox.bake_frame_range = False context.scene.timeline_markers.remove(self.frame_start_marker) context.scene.timeline_markers.remove(self.frame_end_marker) return {'FINISHED'} def invoke(self, context, event): #If the modal is already running, then don't run it the second time scene = context.scene atb = scene.animtoolbox if atb.bake_frame_range: atb.bake_frame_range = False return {'CANCELLED'} scene.animtoolbox.bake_frame_range = True self.frame_start = atb.bake_frame_start self.frame_end = atb.bake_frame_end #init_frame_range_markers(self, scene) init_frame_start_marker(self, scene, name = 'Bake Start') init_frame_end_marker(self, scene, name = 'Bake End') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): scene = context.scene if check_removed_markers(self, scene): return {'PASS_THROUGH'} #If the modal was running again or changed then exit if not scene.animtoolbox.bake_frame_range: self.execute(context) return {'FINISHED'} #get the attribute depending if using preview range or normal frame range frame_start = 'bake_frame_start' frame_end = 'bake_frame_end' if update_frame_range_markers(self, scene.animtoolbox, frame_start, frame_end): return {'RUNNING_MODAL'} if event.type in {'ESC'}: # Cancel self.execute(context) return {'FINISHED'} return {'PASS_THROUGH'} def relative_cursor_frame(self, context): '''updating the cursor matrix after every frame change''' context = bpy.context obj = context.object if context.selected_pose_bones: selected_items = context.selected_pose_bones attr = 'matrix' else: selected_items = context.selected_objects attr = 'matrix_world' update_cursor_matrix(context) global last_matrices last_matrices = [getattr(item, attr).copy() for item in selected_items] def cursor_change_post(self, context): '''Depsgraph Handler to update cursor matrix if it was moved''' global cursor_matrix, last_matrices context = bpy.context if context.selected_pose_bones: new_matrices = [bone.matrix.copy() for bone in context.selected_pose_bones] elif context.selected_objects: new_matrices = [obj.matrix_world.copy() for obj in context.selected_objects] #checking if the selection was moving and have different matrix if last_matrices != new_matrices: update_cursor_matrix(context) last_matrices = new_matrices #check if the cursor was moved if 'cursor_matrix' not in globals(): cursor_matrix = context.scene.cursor.matrix if context.scene.cursor.matrix == cursor_matrix: return assign_relative_cursor_prop(context) def cursor_change_pre(self, context): '''Depsgraph Handler to update cursor matrix if it was changed''' context = bpy.context global cursor_matrix, bone_selection, obj_selection, last_matrices #check if selection was changed if context.selected_pose_bones: selected_items = context.selected_pose_bones item_selection = bone_selection else: selected_items = context.selected_objects item_selection = obj_selection if set(selected_items) != set(item_selection): if len(selected_items) > 1 or len(selected_items) < len(item_selection): assign_relative_cursor_prop(context) return item = selected_items[0] if 'relative_cursor' in item: update_cursor_matrix(context) else: assign_relative_cursor_prop(context) return def get_matrix_avg(selected_items): '''get the average between the matrices of all the selected items''' if hasattr(selected_items[0], 'matrix'): locs, rots, scales = zip(*(bone.matrix.decompose() for bone in selected_items)) else: locs, rots, scales = zip(*(obj.matrix_world.decompose() for obj in selected_items)) translations = np.array(locs) rotations = np.array(rots) loc_avg = translations.mean(axis = 0) rot_avg = rotations.mean(axis = 0) rot_avg = mathutils.Quaternion(rot_avg) return Matrix.LocRotScale(loc_avg, rot_avg, scales[0]) def update_cursor_matrix(context): '''updating the new position of the Cursor''' global cursor_matrix selected_bones = context.selected_pose_bones cursor = context.scene.cursor if selected_bones: item = selected_bones[0] selected_items = selected_bones else: item = context.object selected_items = context.selected_objects # for bone in selected_bones: if len(selected_items) > 1: #multiple selection cursor is relative to the center of all the selections item_matrix = get_matrix_avg(selected_items) else: if hasattr(item, 'matrix'): item_matrix = item.id_data.matrix_world @ item.matrix else: item_matrix = item.matrix_world #apply the new cursor position based on previous cursor relative_matrix = Matrix(item['relative_cursor']) cursor.matrix = item_matrix @ relative_matrix.inverted() cursor_matrix = cursor.matrix def assign_relative_matrix(context, items): '''get the cursor relative matrices either from object or bones''' cursor_matrix = context.scene.cursor.matrix matrix_avg = get_matrix_avg(items) for item in items: item_matrix = item.id_data.matrix_world @ item.matrix if hasattr(item, 'matrix') else item.matrix_world #check if it's a bone or object if hasattr(item, 'matrix'): item_matrix = item.id_data.matrix_world @ item.matrix last_matrices.append(item.matrix.copy()) else: item_matrix = item.matrix_world last_matrices.append(item.matrix_world.copy()) item_matrix = matrix_avg if len(items) > 1 else item_matrix matrix_dist = cursor_matrix.inverted() @ item_matrix item['relative_cursor'] = matrix_dist return last_matrices def assign_relative_cursor_prop(context): '''Updating the matrix property of the cursor distance on the bone''' global last_matrices, bone_selection, obj_selection last_matrices = [] if context.selected_pose_bones: selected_items = context.selected_pose_bones item_selection = bone_selection else: selected_items = context.selected_objects item_selection = obj_selection # cursor_matrix = context.scene.cursor.matrix #Assigning the Matrix property if selected_items: assign_relative_matrix(context, selected_items) if len(selected_items) >= len(item_selection): return #In case of Multiple controls, if controls are deselected, then apply the relative matrix also the previous selected controls prev_items = set(item_selection) - set(selected_items) assign_relative_matrix(context, list(prev_items)) class RelativeCursor(bpy.types.Operator): """Cursor moves relative to the selection to use as a Temp Pivot""" bl_idname = "anim.relative_cursor" bl_label = "Relative Cursor" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): # atb = context.scene.animtoolbox ui = context.window_manager.atb_ui # relative_cursor = ui.relative_cursor # obj = context.object # selected_bones = context.selected_pose_bones global transform_pivot_point, cursor_matrix #Turn off, remove the handlers if ui.relative_cursor: if relative_cursor_frame in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.remove(relative_cursor_frame) if cursor_change_pre in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.remove(cursor_change_pre) if cursor_change_post in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(cursor_change_post) #quit the operator if 'transform_pivot_point' in globals(): context.scene.tool_settings.transform_pivot_point = transform_pivot_point del transform_pivot_point del cursor_matrix ui.relative_cursor = False return {'FINISHED'} #Turning on ui.relative_cursor = True transform_pivot_point = context.scene.tool_settings.transform_pivot_point cursor_matrix = context.scene.cursor.matrix context.scene.tool_settings.transform_pivot_point = 'CURSOR' items = context.selected_pose_bones if context.selected_pose_bones else context.selected_objects if items: #if it's a single selection and already has a cursor assigned then don't re-assign if len(items) != 1 or 'relative_cursor' not in items[0]: assign_relative_cursor_prop(context) #appending all the handlers if relative_cursor_frame not in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.append(relative_cursor_frame) if cursor_change_pre not in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.append(cursor_change_pre) if cursor_change_post not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(cursor_change_post) return {'FINISHED'} class Keyframe_Offset(bpy.types.Operator): """Tool in the toolbar for interactive offsets keyframes""" bl_idname = "anim.keyframe_offset" bl_label = "Keyframe Offset" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): # scene = context.scene # scene.animtoolbox.keyframes_offset +=0.1 return {'FINISHED'} def invoke(self, context, event): self.mouse_x = event.mouse_x # self.mouse_y = event.mouse_y context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): # print('inside modal', event.type, event.value) scene = context.scene value = 0 if event.mouse_x > self.mouse_x: value = event.mouse_x - self.mouse_x self.mouse_x = event.mouse_x elif event.mouse_x < self.mouse_x: value = event.mouse_x - self.mouse_x self.mouse_x = event.mouse_x scene.animtoolbox.keyframes_offset += value*0.01 if event.type in {'ESC'} or event.value == 'RELEASE': # Cancel self.execute(context) return {'FINISHED'} return {'RUNNING_MODAL'} def keyframes_offset_slider(self, context): '''interactive slider offsets keyframes and adds a property''' if context.selected_objects is None: return global bone_selection, obj_selection frame_offset = init_frame_offset = self.keyframes_offset # for obj in context.selected_objects: for obj in obj_selection: if obj is None: continue # if obj.mode == 'POSE': # continue if obj.animation_data is None: continue if obj.animation_data.action is None: continue # if obj.mode =='OBJECT': add_offset(obj, frame_offset, obj.animtoolbox.keyframes_offset) obj.animtoolbox.keyframes_offset = frame_offset frame_offset += init_frame_offset if obj.mode != 'POSE': continue frame_offset_bone = init_frame_offset for bone in bone_selection: #context.selected_pose_bones if bone.name not in obj.pose.bones: continue #add the offset property to the bone if 'keyframes_offset' not in bone: bone['keyframes_offset'] = 0.0 bone_id = bone.path_from_id() add_offset(obj, frame_offset_bone, bone['keyframes_offset'], bone_id) bone['keyframes_offset'] = frame_offset_bone frame_offset_bone += init_frame_offset #if init_frame_offset is 0 then remove the keyframes offset properties if not init_frame_offset: remove_offset_property(context) def remove_offset_property(context): 'removes the keyframes offset properties from the bones and objects' for obj in context.selected_objects: if obj.mode == 'POSE': for bone in context.selected_pose_bones: if 'keyframes_offset' in bone: del bone['keyframes_offset'] obj.animtoolbox.keyframes_offset = 0 def add_offset(obj, offset, prev_offset, bone_id = None): fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) for fcu in fcurves: if bone_id is not None: if bone_id not in fcu.data_path: continue else: #if it's object animation, then skip bone fcurves if len(fcu.data_path.split(".")) != 1: continue if filter_properties(bpy.context.scene.animtoolbox, fcu): continue #create a sequence with all the keyframes keyframes = np.zeros([len(fcu.keyframe_points)*2]) updated_left_handle = np.zeros([len(fcu.keyframe_points)*2]) updated_right_handle = np.zeros([len(fcu.keyframe_points)*2]) fcu.keyframe_points.foreach_get('co', keyframes) fcu.keyframe_points.foreach_get('handle_left', updated_left_handle) fcu.keyframe_points.foreach_get('handle_right', updated_right_handle) if bpy.context.scene.animtoolbox.filter_keyframes: select_control_points = np.zeros([len(fcu.keyframe_points)]) fcu.keyframe_points.foreach_get('select_control_point', select_control_points) else: select_control_points = 1 #add the new frame values only to the frames, every second item keyframes[::2] += (offset - prev_offset) * select_control_points updated_left_handle[::2] += (offset - prev_offset) * select_control_points updated_right_handle[::2] += (offset - prev_offset) * select_control_points #add the new values to all the keyframes altogether fcu.keyframe_points.foreach_set('co', keyframes) fcu.keyframe_points.foreach_set('handle_left', updated_left_handle) fcu.keyframe_points.foreach_set('handle_right', updated_right_handle) fcu.update() def add_keyframe(bone): obj = bone.id_data if bone.rotation_mode == 'QUATERNION': rotation = 'rotation_quaternion' elif bone.rotation_mode == 'AXIS_ANGLE': rotation = 'rotation_axis_angle' else: rotation = 'rotation_euler' # rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler' transforms = ['location', rotation, 'scale'] path = bone.path_from_id() if obj.mode == 'POSE' else '' groups = dict() #find fcurves that are locked to exclude them excluded_fcus = {} if obj.animation_data: if obj.animation_data.action: fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) for fcu in fcurves: if path not in fcu.data_path or not fcu.lock: continue groups.update({(fcu.data_path, fcu.array_index ): fcu.group.name}) if fcu.data_path in excluded_fcus: excluded_fcus[fcu.data_path].append(fcu.array_index) else: excluded_fcus.update({fcu.data_path : [fcu.array_index]}) #insert keyframe only to channels that are not excluded for transform in transforms: #path = bone.path_from_id() + '.' + transform path = bone.path_from_id() + '.' + transform if obj.mode == 'POSE' else transform # if path not in excluded_fcus: # bone.keyframe_insert(transform) # continue length = len(getattr(bone, transform)) for i in range(length): if path in excluded_fcus: if i in excluded_fcus[path]: continue if (path, i) in groups: group_name = groups[(path, i)] else: group_name = bone.name bone.keyframe_insert(transform, index = i, group = group_name, frame = bpy.context.scene.frame_current_final) def get_fcu_inbetweens(bone, frame, fcu_inbetweens): obj = bone.id_data if bone.rotation_mode == 'QUATERNION': rotation = 'rotation_quaternion' elif bone.rotation_mode == 'AXIS_ANGLE': rotation = 'rotation_axis_angle' else: rotation = 'rotation_euler' # rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler' transforms = ['location', rotation, 'scale'] # fcurves = obj.animation_data.action.fcurves fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) #insert keyframe only to channels that are not excluded location, rotation, scale = bone.matrix_basis.decompose() for transform in transforms: path = bone.path_from_id() + '.' + transform if obj.mode == 'POSE' else transform length = len(getattr(bone, transform)) for i in range(length): fcu = fcurves.find(data_path = path, index = i) if not fcu: continue if fcu.lock: continue if 'rotation' in transform: if transform == 'rotation_euler': coords = bone.matrix_basis.to_euler(bone.rotation_mode) else: coords = rotation else: coords = locals()[transform] if fcu in fcu_inbetweens: fcu_inbetweens[fcu].update({frame : coords[i]}) else: fcu_inbetweens.update({fcu : {frame : coords[i]}}) return fcu_inbetweens def is_writable(drv, attr): try: setattr(drv, attr, getattr(drv, attr)) return True except: return False def add_inbetween_key(self, context): '''Adding a Breakdown keyframe that works also in Layers''' obj = context.object #anim_data = anim_data_type(obj) anim_data = obj.animation_data if anim_data is None: return if anim_data.action is None: return #strip = anim_data.nla_tracks[obj.als.layer_index].strips[0] #frame = round(bake_ops.frame_evaluation(context.scene.frame_current, strip), 3) frame = context.scene.frame_current paths = [bone.path_from_id() for bone in context.selected_pose_bones] fcurves = get_fcurves_channelbag(obj, anim_data.action) for fcu in fcurves: #filter selected bones if obj.mode == 'POSE': #apply only to selected bones #if obj.als.only_selected_bones: if fcu.data_path.split('].')[0]+']' not in paths: continue if filter_properties(context.scene.animtoolbox, fcu): continue #get the last previous key for keyframe in fcu.keyframe_points: if round(keyframe.co[0], 3) > frame: key_after = keyframe break elif round(keyframe.co[0], 3) < frame: key_before = keyframe else: key_added = keyframe if 'key_after' not in locals() or 'key_before' not in locals(): continue if 'key_added' not in locals(): fcu.keyframe_points.add(1) key_added = fcu.keyframe_points[-1] value = key_before.co[1] + (key_after.co[1] - key_before.co[1]) * (self.inbetweener + 1)*0.5 key_added.co = (frame, value) fcu.update() del key_after del key_before del key_added self['inbetweener'] = 0.0 def find_bone_in_datapath(datapath, context): #iterating over the bones path from id for bone in context.selected_pose_bones: if bone.path_from_id() in datapath: return bone def get_matrix(obj, bone = None): #get either matrix from bone or matrix world from object if obj.mode == 'POSE': matrix = bone.matrix.copy() else: matrix = obj.matrix_world.copy() return matrix def set_inbetween_matrix(self, source, matrix, matrix_other): if self.inbetween_worldmatrix > 0: matrix = matrix.copy().lerp(matrix_other, self.inbetween_worldmatrix) elif self.inbetween_worldmatrix < 0: matrix = matrix.copy().lerp(matrix_other, self.inbetween_worldmatrix*-1) matrix = reverse_childof_constraint(source, matrix) return matrix def add_inbetween_worldmatrix(self, context): '''Adding a Breakdown keyframe that works also in Layers''' obj = context.object if context.window_manager.atb_ui.is_dragging: return if obj is None: self['inbetween_worldmatrix'] = 0.0 return action = obj.animation_data.action if action is None: self['inbetween_worldmatrix'] = 0.0 return # frame = round(context.scene.frame_current, 2) if not self.inbetween_worldmatrix: # scene.frame_set(frame) return if obj.mode == 'POSE' and not context.selected_pose_bones: self['inbetween_worldmatrix'] = 0.0 return bpy.ops.anim.inbetween_world_matrix('INVOKE_DEFAULT') def get_matrix_other(self, context): '''Get the matrix from the other frame, either previous or the next''' scene = context.scene for item, frames in self.items_frames.items(): #get the next and previous frames next_frames = [frame for frame in frames if frame > self.frame] if next_frames: next_frame = next_frames[0] prev_frames = [frame for frame in frames if frame < self.frame] if prev_frames: prev_frame = prev_frames[-1] if not prev_frame and not next_frame: continue #get all the list of the frames from the different channels for each bone #rest of the code is only for posebones #creating a new dict to avoid frame_set at the same frame multiple times. It will get the matrix of multiple bones from each frame #This is done just to avoid extra scene evaluation using frame_set #adding the bone to the new flipped dict key using the frame number prev_frame_bones = dict() if prev_frame in prev_frame_bones: prev_frame_bones[prev_frame].append(item) else: prev_frame_bones.update({prev_frame : [item]}) #the bones that are on the next frame next_frame_bones = dict() if next_frame in next_frame_bones: next_frame_bones[next_frame].append(item) else: next_frame_bones.update({next_frame : [item]}) #if item.id_data.mode == 'POSE': #Iterating the flipped dict with the frame as the key for prev_frame, bones in prev_frame_bones.items(): # go to the frame scene.frame_set(int(prev_frame), subframe = prev_frame % 1) for bone in bones: matrix_prev = get_matrix(bone.id_data, bone) self.items_matrix_prev.update({bone : matrix_prev}) for next_frame, bones in next_frame_bones.items(): # go to the frame scene.frame_set(int(next_frame), subframe = next_frame % 1) for bone in bones: matrix_next = get_matrix(bone.id_data, bone) self.items_matrix_next.update({bone : matrix_next}) #going back to the original current frame scene.frame_set(self.frame) class InbetweenWorldMatrix(bpy.types.Operator): """Modal operator used while inbetween world matrix is running before release""" bl_idname = "anim.inbetween_world_matrix" bl_label = "Inbetween World Matrix" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): #reset the values for dragging self.stop = False context.window_manager.atb_ui['is_dragging'] = True if context.selected_pose_bones: paths = [bone.path_from_id() for bone in context.selected_pose_bones] self.atb = context.window_manager.atb_ui self.frame = round(context.scene.frame_current, 2) self.inbetween_worldmatrix = self.atb.inbetween_worldmatrix #naming them as items because it is being used both for bones and objs self.items_matrix_prev = dict() self.items_matrix_next = dict() self.items_matrix_org = dict() # self.bones_other_frame = dict() self.items_frames = dict() for obj in context.selected_objects: action = obj.animation_data.action bone = None fcurves = get_fcurves_channelbag(obj, action) for fcu in fcurves: #filter selected bones if obj.mode == 'POSE': #apply only to selected bones if fcu.data_path.split('].')[0]+']' not in paths: continue else: #get the related bone bone = find_bone_in_datapath(fcu.data_path, context) bonename = fcu.data_path.split('"')[1] if bonename in obj.pose.bones: bone = obj.pose.bones[bonename] if bone not in self.items_matrix_org: self.items_matrix_org.update({bone : bone.matrix.copy()}) else: if obj not in self.items_matrix_org: self.items_matrix_org.update({obj : obj.matrix_world.copy()}) bone = obj #get all the frames from the fcurve length = len(fcu.keyframe_points) * 2 keyframes = np.zeros(length) fcu.keyframe_points.foreach_get('co', keyframes) frames = sorted([round(frame, 2) for frame in keyframes[::2]]) #record all the frames if bone in self.items_frames: self.items_frames[bone] += frames self.items_frames[bone] = sorted(set(self.items_frames[bone])) else: self.items_frames.update({bone : frames}) get_matrix_other(self, context) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): scene = context.scene if self.stop: context.window_manager.atb_ui['is_dragging'] = False selected_items = context.selected_pose_bones if context.selected_pose_bones else context.selected_objects for item in selected_items: if scene.tool_settings.use_keyframe_insert_auto: add_keyframe(item) self.atb['inbetween_worldmatrix'] = 0 redraw_areas(['PROPERTIES']) #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 if self.inbetween_worldmatrix != self.atb.inbetween_worldmatrix: self.inbetween_worldmatrix = self.atb.inbetween_worldmatrix #Add the inbetween value that was calculated during the invoke if self.inbetween_worldmatrix > 0: items_matrix_other = self.items_matrix_next elif self.inbetween_worldmatrix < 0: items_matrix_other = self.items_matrix_prev else: return {'PASS_THROUGH'} for bone, matrix_other in items_matrix_other.items(): matrix = set_inbetween_matrix(self, bone, self.items_matrix_org[bone], matrix_other) matrix = filter_matrix_properties(context, self.items_matrix_org[bone], matrix) if bone.id_data.mode == 'POSE': bone.matrix = matrix else: bone.matrix_world = matrix if scene.tool_settings.use_keyframe_insert_auto: add_keyframe(bone) return {'PASS_THROUGH'} def find_mirror_bone(bone): digit = '' name = bone.name #find if there is a digit number in the end of the name and separate it if name.split('.')[-1].isdigit(): digit = '.' + name.split('.')[-1] name = ''.join(name.split('.')[:-1]) #All the conventions and separators mirror_conventions = {'L' : 'R', 'R' : 'L', 'l' : 'r', 'r' : 'l', 'Right':'Left', 'Left':'Right', 'right' : 'left', 'left' : 'right', 'LEFT' : 'RIGHT', 'RIGHT' : 'LEFT'} indexes = (0, -1) separators = ('.', '_', '-', ' ') rig = bone.id_data for separator in separators: for i in indexes: if name.split(separator)[i] in mirror_conventions: side = name.split(separator)[i] mirror_side = mirror_conventions[side] mirror_name = name.replace(side, mirror_side) if mirror_name + digit in rig.pose.bones: return rig.pose.bones[mirror_name + digit] startend = {'Right':'Left', 'Left':'Right', 'right' : 'left', 'left' : 'right', 'LEFT' : 'RIGHT', 'RIGHT' : 'LEFT'} for side, mirror_side in startend.items(): if name.endswith(side) or name.startswith(side): mirror_name = name.replace(side, mirror_side) if mirror_name + digit in rig.pose.bones: return rig.pose.bones[mirror_name + digit] return None def blend_to_mirror(self, context): '''blend to mirrored pose''' if context.window_manager.atb_ui.is_dragging: return else: bpy.ops.anim.blend_to_mirror('INVOKE_DEFAULT') class BlendToMirroModal(bpy.types.Operator): """Modal operator used while blend to mirror is running before release""" bl_idname = "anim.blend_to_mirror" bl_label = "Blend_To_Mirror" bl_options = {'REGISTER', 'UNDO'} #stop: bpy.props.BoolProperty() # This is used so we don't end up in an infinite loop because we blocked the release event def invoke(self, context, event): self.stop = False self.ui = context.window_manager.atb_ui context.window_manager.atb_ui['is_dragging'] = True #assign the initial matrix to each bone for bone in context.selected_pose_bones: bone['matrix'] = bone.matrix_basis context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): scene = context.scene #finishing after release if self.stop: context.window_manager.atb_ui['is_dragging'] = False for bone in context.selected_pose_bones: if scene.tool_settings.use_keyframe_insert_auto: add_keyframe(bone) if 'matrix' in bone: del bone['matrix'] self.ui['blend_mirror'] = 0 redraw_areas(['PROPERTIES']) #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 bone in context.selected_pose_bones: mirror_bone = find_mirror_bone(bone) if mirror_bone is None: continue mirror_bone_matrix = mirror_bone.matrix_basis.copy() mirror_plane = Matrix.Scale(-1, 4, (1, 0, 0)) mirrored_matrix = mirror_plane @ mirror_bone_matrix @ mirror_plane matrix = Matrix(bone['matrix']).lerp(mirrored_matrix, self.ui.blend_mirror) matrix = filter_matrix_properties(context, bone.matrix_basis, matrix) bone.matrix_basis = matrix #self['blend_mirror'] = 0 return {'PASS_THROUGH'} class ApplyKeyframesOffset(bpy.types.Operator): """Offset keyframes for every selected object""" bl_idname = "anim.apply_keyframes_offset" bl_label = "Apply the keyframe offset" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return len(context.selected_objects) def execute(self, context): remove_offset_property(context) context.scene.animtoolbox.keyframes_offset = 0 return {'FINISHED'} class SelectKeyframesOffset(bpy.types.Operator): """Offset keyframes for every selected object""" bl_idname = "anim.select_keyframes_offset" bl_label = "Select_keyframes_offset" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): # if not hasattr(bpy.types.Object, 'keyframes_offset') and not hasattr(bpy.types.PoseBone, 'keyframes_offset'): # return {'CANCELLED'} for obj in bpy.data.objects: if obj.animtoolbox.keyframes_offset: obj.select_set(True) if obj.type == 'ARMATURE': posemode = False for bone in obj.pose.bones: if 'keyframes_offset' in bone: #check if there are bones with an offset posemode = True obj.data.bones[bone.name].select = True if posemode and obj.mode != 'POSE': context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode = 'POSE') return {'FINISHED'} def get_frame_range(context, obj): #get the frame range frame_range = [] inbetweens = [] atb = context.scene.animtoolbox if atb.range_type == 'CURRENT': frame_range = [context.scene.frame_current] elif atb.range_type == 'RANGE': #make sure that the first frame is before the second if atb.bake_frame_start > atb.bake_frame_end: atb.bake_frame_start = atb.bake_frame_end frame_range = list(range(atb.bake_frame_start, atb.bake_frame_end+1)) elif atb.range_type == 'SELECTED': if obj.mode == 'POSE': bones_paths = [bone.path_from_id() for bone in context.selected_pose_bones] if obj.animation_data is None: return frame_range if obj.animation_data.action is None: return frame_range smartframes = set() fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) for fcu in fcurves: path = fcu.data_path.split('"].')[0] + '"]' if obj.mode == 'POSE' and path not in bones_paths: continue for keyframe in fcu.keyframe_points: smartframes.add(round(keyframe.co[0], 2)) if keyframe.select_control_point: frame_range.append(round(keyframe.co[0], 2)) smartframes = sorted(smartframes) frame_range = sorted(set(frame_range)) #getting the inbetweens smartframes += add_inbetweens(list(smartframes)) smartframes = sorted(smartframes) #getting the inbetweens within the frame range inbetweens = [] for frame in frame_range: index = smartframes.index(frame) if frame == smartframes[-1]: continue if smartframes[index+1] in frame_range or smartframes[index+2] in frame_range: continue inbetweens.append(smartframes[index+1]) inbetweens.append(smartframes[index+2]) #merge inbetweens into the frame range # frame_range = sorted(frame_range + inbetweens) return frame_range, inbetweens def notification_invoke(self, context, text = 'Copy Matrix', size = 20.0): '''get ready and lauch the notification''' wm = context.window_manager self.timer = wm.event_timer_add(0.1, window=context.window) self.no_timer = False self.fade_duration = 1.0 self.max_duration = 3 self.text = text self.size = size self.fade_out_start = 0 self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(draw_text_callback_px, (self, context), 'WINDOW', 'POST_PIXEL') redraw_areas(['VIEW_3D']) def update_notification(self, context, event, esc = False, fade_out = True): '''notify with a modal operator that the matrix was being copied''' if not hasattr(self, 'draw_handle'): return {'FINISHED'} if event.type in {'ESC'} or esc: bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW') context.area.tag_redraw() context.window_manager.event_timer_remove(self.timer) del self.draw_handle return {'FINISHED'} if event.type == 'TIMER': # print('timer running in the bg', self.scale, self.rotate) #check if the fade in finished and remove the timer if self.timer.time_duration >= self.fade_duration: self.no_timer = True redraw_areas(['VIEW_3D']) if fade_out: if self.timer.time_duration > self.max_duration and fade_out: bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW') context.window_manager.event_timer_remove(self.timer) return {'FINISHED'} # else: # redraw_areas(['VIEW_3D']) if (self.max_duration - self.timer.time_duration) <= self.fade_duration and not self.fade_out_start and fade_out: self.fade_out_start = self.timer.time_duration + 0.001 # if self.timer.time_duration >= self.fade_duration and not self.fade_out_start: # self.fade_out_start = self.timer.time_duration + 0.001 return {'PASS_THROUGH'} class CopyMatrix(bpy.types.Operator): """Copy the matrix of the selection and store it""" bl_idname = "anim.copy_matrix" bl_label = "Copy" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object is not None def invoke(self, context, event): notification_invoke(self, context) context.window_manager.modal_handler_add(self) self.execute(context) return {'RUNNING_MODAL'} def modal(self, context, event): return_modal = update_notification(self, context, event) return return_modal def execute(self, context): obj = context.object global matrix, objs_matrix if 'matrix' in globals(): del matrix if 'objs_matrix' in globals(): del objs_matrix objs_matrix = dict() for obj in context.selected_objects: if obj.mode == 'POSE': if not context.selected_pose_bones: return {'CANCELLED'} for bone in context.selected_pose_bones: if bone.id_data != obj: continue bone_matrix = obj.matrix_world @ bone.matrix.copy() if obj.name in objs_matrix: objs_matrix[obj.name].update({bone.name : bone_matrix}) else: objs_matrix.update({obj.name : {bone.name : bone_matrix}}) if bone == context.active_pose_bone: matrix = bone_matrix else: matrix = obj.matrix_world.copy() objs_matrix.update({obj.name : matrix}) return {'FINISHED'} class PasteMatrix(bpy.types.Operator): """paste the matrix of the selection""" bl_idname = "anim.paste_matrix" bl_label = "Paste" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object is not None def execute(self, context): #obj = context.object global matrix, objs_matrix if 'matrix' not in globals(): return {'CANCELLED'} scene = context.scene frame_current_final = scene.frame_current_final bones_matrices = dict() constrained = set() #constraint evaluation is # con.target.matrix_world.copy() @ con.inverse_matrix @ basis #to flip it I use # con.inverse_matrix.inverted() @ con.target.matrix_world.copy().inverted() @ matrix # to find the basis (Matrix.Identity(4) + con.target.matrix_world @ con.inverse_matrix).inverted() @ (2 * matrix) #finding the influence # using for location and rotation obj.matrix_basis = (Matrix.Identity(4) + 0.3 * (con.target.matrix_world @ con.inverse_matrix - Matrix.Identity(4))).inverted() @ matrix # using for scale obj.matrix_basis = (Matrix.Identity(4).lerp(offset, 0.7)).inverted() @ matrix #formula for the rest of the constraints # diff = basis.inverted() @ obj.matrix_parent_inverse.inverted() @ obj.parent.matrix_world.inverted() @ matrix_world # obj.matrix_basis = obj.matrix_parent_inverse.inverted() @ obj.parent.matrix_world.inverted() @ matrix @ diff.inverted() #apply the new matrix with the difference for obj in context.selected_objects: frame_range, inbetweens = get_frame_range(context, obj) fcu_inbetweens = dict() for frame in sorted(frame_range + inbetweens): scene.frame_set(int(frame), subframe = frame % 1) if obj.mode == 'POSE': #first get all the bones and matrices before writing back for bone in context.selected_pose_bones: matrix_copied = matrix if bone.id_data.name in objs_matrix: if bone.name in objs_matrix[bone.id_data.name]: matrix_copied = objs_matrix[bone.id_data.name][bone.name] matrix_copied = obj.matrix_world.inverted() @ matrix_copied #Store the matrices for each bone that will use it matrix_copied = reverse_childof_constraint(bone, matrix_copied, constrained) #use matrix for bones without constraints bones_matrices.update({bone : matrix_copied}) #Reordering the bones, so that we apply first the matrix offset to the constrained bones bones_matrices = reorder_bones_matrices(bones_matrices, constrained) paste_bones_matrices(bones_matrices, constrained) #adding the keyframes for bone in bones_matrices.keys(): paste_keyframes_get_inbetweens(scene, bone, inbetweens, frame, frame_range, fcu_inbetweens) else: target = obj #drivers, constraints = rec_drivers_constraints(target) matrix_copied = reverse_childof_constraint(target, matrix, constrained) if target not in constrained: matrix_copied = filter_matrix_properties(context, target.matrix_world, matrix_copied) target.matrix_world = matrix_copied if target in constrained: context.view_layer.update() matrix_copied = reverse_constraint_offset(target.matrix_world, matrix_copied) target.matrix_world = filter_matrix_properties(context, target.matrix_world, matrix_copied) paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens) if inbetweens and len(frame_range) > 1: add_interpolations(fcu_inbetweens.keys(), fcu_inbetweens, frames = frame_range[:-1]) if context.scene.frame_current_final != frame_current_final: scene.frame_current = int(frame_current_final) scene.frame_subframe = frame_current_final % 1 #Turn off markers frame range if it' activated if context.scene.animtoolbox.bake_frame_range: context.scene.animtoolbox.bake_frame_range = False return {'FINISHED'} class CopyRelativeMatrix(bpy.types.Operator): """Copy the relative distance matrix from active between two objects or bones""" bl_idname = "anim.copy_relative_matrix" bl_label = "Copy_Relative_Matrix" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object is not None def invoke(self, context, event): obj = context.object if context.selected_pose_bones: if len(context.selected_pose_bones) < 2 and len(context.selected_objects) < 2: return {'CANCELLED'} else: if len(context.selected_objects) < 2: return {'CANCELLED'} selection = context.active_pose_bone.name if context.active_pose_bone and obj.mode == 'POSE' else context.active_object.name notification_invoke(self, context, text = 'Copy Matrix relative to ' + selection) context.window_manager.modal_handler_add(self) self.execute(context) return {'RUNNING_MODAL'} def modal(self, context, event): return_modal = update_notification(self, context, event) return return_modal def execute(self, context): # obj = context.object global objs_matrix_dist, matrix_dist, source_active_name, source_rig_name if 'matrix_dist' in globals(): del matrix_dist if 'source_active_name' in globals(): del source_active_name if 'source_rig_name' in globals(): del source_rig_name if 'objs_matrix_dist' in globals(): del objs_matrix_dist #create a dictionary for all the realtive distance of the bones and objects objs_matrix_dist = dict() #get the source bone or object if context.active_pose_bone: source_active = context.active_pose_bone source_rig_name = source_active.id_data.name source_matrix = source_active.matrix else: source_active = context.active_object source_matrix = source_active.matrix_world source_active_name = source_active.name for obj in context.selected_objects: if obj.type == 'ARMATURE': #Get the distance from the selected bones for bone_relative in obj.pose.bones: if not bone_relative.bone.select: continue if bone_relative == source_active: continue #Adding the offset from the armature transform both for the active and relative rig_offset = obj.matrix_world if source_active.id_data.type == 'ARMATURE': rig_offset = source_active.id_data.matrix_world.inverted() @ rig_offset matrix_dist = source_matrix.inverted() @ rig_offset @ bone_relative.matrix #store each bone matrix distance in a dictionary if obj.name in objs_matrix_dist: objs_matrix_dist[obj.name].update({bone_relative.name : matrix_dist}) else: objs_matrix_dist.update({obj.name : {bone_relative.name : matrix_dist}}) else: if obj == source_active: continue matrix_dist = source_matrix.inverted() @ obj.matrix_world objs_matrix_dist.update({obj.name : matrix_dist}) return {'FINISHED'} def paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens): #if autokey is turned on then add a keyframe if (scene.tool_settings.use_keyframe_insert_auto or scene.animtoolbox.range_type != 'CURRENT') and 'target' in locals(): if frame not in inbetweens: add_keyframe(target) #store the inbetween values elif len(frame_range) > 1: fcu_inbetweens = get_fcu_inbetweens(target, frame, fcu_inbetweens) def reverse_bone_constraints(context, bone, matrix_copied): matrix_copied = reverse_constraint_offset(bone.matrix, matrix_copied) matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied) matrix = bone.id_data.matrix_world.inverted() @ matrix_copied return matrix def reverse_constraint_offset(item_matrix, matrix_copied): '''adding the offset of all the bones that have constraints that are not child of''' con_offset = item_matrix.inverted() @ matrix_copied matrix_copied = item_matrix @ con_offset @ con_offset return matrix_copied def reverse_childof_constraint(source, matrix_source, constrained = set()): obj = source.id_data if not len(source.constraints): return matrix_source #@ obj.matrix_world.inverted() offsets = [] offsets_lerp = [] offset_inv = Matrix.Identity(4) offset_inv_lerp = Matrix.Identity(4) #If the source is a bone then get the Armature matrix to add to the calculation if type(source) == bpy.types.PoseBone: obj_offset = obj.matrix_world else: obj_offset = Matrix.Identity(4) #iterate and store all the inverted offsets of all the child constraints for con in source.constraints: if con.mute or not con.influence: continue if hasattr(con, 'target') and con.target is None: continue if con.type != 'CHILD_OF': constrained.add(source) continue if con.subtarget == '': parent_matrix = obj_offset.inverted() @ con.target.matrix_world #remove obj.matrix_world when connected to an object offset = parent_matrix @ con.inverse_matrix - Matrix.Identity(4) #offset for the scale with influence already included offset_lerp = Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence) else: parent_matrix = con.target.pose.bones[con.subtarget].matrix if con.target != obj: parent_matrix = obj_offset.inverted() @ con.target.matrix_world @ parent_matrix #Include armature object matrix offset = parent_matrix @ con.inverse_matrix - Matrix.Identity(4) offset_lerp = Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence) #Adding the influence to the offset offset = Matrix.Identity(4) + con.influence * offset offset_inv = offset_inv @ offset.inverted() offset_inv_lerp = offset_inv_lerp @ offset_lerp.inverted() offsets.append(offset) if not offsets: return matrix_source #final Matrix values matrix_basis = offset_inv @ matrix_source matrix_lerp = offset_inv_lerp @ matrix_source loc, rot, scale = matrix_basis.decompose() loc_lerp, rot_lerp, scale_lerp = matrix_lerp.decompose() matrix_basis = Matrix.LocRotScale(loc, rot_lerp, scale_lerp) return matrix_basis def reorder_bones_matrices(bones_matrices, constrained): #Reordering the bones, so that we apply first the matrix offset to the constrained bones re_bones_matrices = {bone: matrix for bone, matrix in bones_matrices.items() if bone in constrained} re_bones_matrices.update({bone: matrix for bone, matrix in bones_matrices.items() if bone not in constrained}) return re_bones_matrices def paste_bone_matrix(bone, matrix_copied, constrained, bones = {}, x_filter = True): #running again separatly in case the bones are in a hierarchy and influencing each other # for bone, matrix_copied in bones_matrices.items(): # Determine whether to use bone.matrix or bone.matrix_world if hasattr(bone, 'matrix'): # Check if it's a bone matrix_attr = 'matrix' elif hasattr(bone, 'matrix_world'): # Check if it's an object matrix_attr = 'matrix_world' else: raise AttributeError("The provided 'bone' does not have a valid matrix attribute.") context = bpy.context if x_filter : matrix_copied = filter_matrix_properties(context, getattr(bone, matrix_attr), matrix_copied) # bone.matrix = bone.id_data.matrix_world.inverted() @ matrix_copied setattr(bone, matrix_attr, matrix_copied) # bone.id_data.matrix_world.inverted() @ children = set(bone.children_recursive).intersection(bones) if children or bone in constrained: # print(f'found children {[child.name for child in children]} in bone {bone.name}' ) context.view_layer.update() #Check if the bone has constrainsts on it that need extra iteration if bone not in constrained: return matrix_copied = reverse_bone_constraints(context, bone, matrix_copied) if x_filter : matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied) # bone.matrix = matrix_copied setattr(bone, matrix_attr, matrix_copied) #important to have the extra update context.view_layer.update() def paste_bones_matrices(bones_matrices, constrained, x_filter = True): #running again separatly in case the bones are in a hierarchy and influencing each other pasted_bones = set() for bone, matrix_copied in bones_matrices.items(): #Get the rest of the bones to check if they are children of the current bone bones = set(bones_matrices.keys()).difference(pasted_bones) paste_bone_matrix(bone, matrix_copied, constrained, bones, x_filter) pasted_bones.add(bone) class PasteRelativeMatrix(bpy.types.Operator): """paste the relative matrix of the selection""" bl_idname = "anim.paste_relative_matrix" bl_label = "Paste_Relative" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object is not None def execute(self, context): obj = context.object scene = context.scene frame_current = scene.frame_current global objs_matrix_dist, matrix_dist, source_active_name, source_rig_name if 'objs_matrix_dist' not in globals(): return {'CANCELLED'} source_rig = None source_bone = None source_obj = None bones_matrices = dict() constrained = set() #get the source matrix if 'source_rig_name' in globals(): if source_rig_name in bpy.data.objects: source_rig = bpy.data.objects[source_rig_name] source_obj = source_rig source_bone = source_rig.pose.bones[source_active_name] #Get the current matrix of the source bone matrix_source = source_bone.matrix elif 'source_active_name' in globals(): if source_active_name in bpy.data.objects: source_obj = bpy.data.objects[source_active_name] matrix_source = source_obj.matrix_world else: return {'CANCELLED'} if source_obj is None and source_rig is None: return {'CANCELLED'} for obj in context.selected_objects: #if the source object was in object mode during copy and now it's pose mode then quit frame_range, inbetweens = get_frame_range(context, obj) fcu_inbetweens = dict() for frame in sorted(frame_range+inbetweens): scene.frame_set(int(frame)) if obj.mode == 'POSE': for bone in context.selected_pose_bones: if bone.id_data != obj: continue #check that the selected bone is not the source bone if bone == source_bone: continue bone_matrix_dist = matrix_dist #get the matrix from the bone itself if bone.id_data.name in objs_matrix_dist: if bone.name in objs_matrix_dist[bone.id_data.name]: bone_matrix_dist = objs_matrix_dist[bone.id_data.name][bone.name] #Adding the offset from the armature transform both for the active and relative #If it's the same Armature it will cancel each other rig_offset = obj.matrix_world.inverted() if source_rig: rig_offset = rig_offset @ source_rig.matrix_world matrix_new = rig_offset @ matrix_source @ bone_matrix_dist #Store the matrices for each bone that will use it matrix_new = reverse_childof_constraint(bone, matrix_new, constrained) bones_matrices.update({bone : matrix_new}) #Reordering the bones, so that we apply first the matrix offset to the constrained bones bones_matrices = reorder_bones_matrices(bones_matrices, constrained) paste_bones_matrices(bones_matrices, constrained) for bone in bones_matrices.keys(): paste_keyframes_get_inbetweens(scene, bone, inbetweens, frame, frame_range, fcu_inbetweens) else: target = obj #check that the selected obj is not the source object if source_active_name in bpy.data.objects and 'source_rig_name' not in globals(): if target == bpy.data.objects[source_active_name]: continue if obj.name in objs_matrix_dist: obj_matrix_dist = objs_matrix_dist[obj.name] else: obj_matrix_dist = matrix_dist matrix_new = matrix_source @ obj_matrix_dist matrix_new = reverse_childof_constraint(target, matrix_new) if target not in constrained: matrix_new = filter_matrix_properties(context, target.matrix_world, matrix_new) target.matrix_world = matrix_new if target in constrained: context.view_layer.update() matrix_new = reverse_constraint_offset(target.matrix_world, matrix_new) target.matrix_world = filter_matrix_properties(context, target.matrix_world, matrix_new) paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens) if inbetweens and len(frame_range) > 1: add_interpolations(fcu_inbetweens.keys(), fcu_inbetweens, frames = frame_range[:-1]) #return to the original frame in case of using frame range if context.scene.frame_current != frame_current: scene.frame_current = frame_current #Turn off markers frame range if it' activated if context.scene.animtoolbox.bake_frame_range: context.scene.animtoolbox.bake_frame_range = False return {'FINISHED'} def copy_paste_world_update(self, context): if self.copy_paste_world: self.copy_paste_relative = not self.copy_paste_world def copy_paste_relative_update(self, context): if self.copy_paste_relative: self.copy_paste_world = not self.copy_paste_relative def sharekeys_add_missing_fcurves(obj, attr_index, fcurves): transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"] #creating a set of tuple pairs with the data path and indexes datapaths_arrays = {(fcu.data_path, fcu.array_index) for fcu in fcurves} for attr, index in attr_index: if not hasattr(obj, attr): continue if (attr, index) in datapaths_arrays: continue if attr not in transformations and bpy.context.scene.animtoolbox.filter_custom_props: continue #check filter attributes, and continue if it's filtered f_transform = 'filter_rotation' if 'rotation' in attr else 'filter_' + attr if hasattr(bpy.context.scene.animtoolbox, f_transform): f_attr = getattr(bpy.context.scene.animtoolbox, f_transform) if index < len(f_attr): if f_attr[index]: continue if type(obj) == bpy.types.PoseBone: #obj in this case is actually a bone group = obj.name path = obj.path_from_id() + '.' + attr if attr in transformations else obj.path_from_id() + attr else: group = 'Object Transforms' if attr in transformations else '' path = attr action = obj.id_data.animation_data.action #Get the container, either action of channelbag because of adding groups fcurves_container = get_fcurves_container(obj, action) if 'rotation' in attr: #converting the rotation depending on the rotation mode mode = 'euler' if len(obj.rotation_mode) == 3 else obj.rotation_mode.lower() path_index = path.find('rotation_') rot = 'rotation_' + mode path = path[:path_index] + rot if rot == 'rotation_euler' and index == 3: #skipping the last index for euler rotation continue elif rot != 'rotation_euler' and (path, 3) not in datapaths_arrays: #adding an extra curve for quaternion or axis_angle extra_fcu = fcurves_container.fcurves.new(data_path = path, index = 3, action_group = group) fcurves.append(extra_fcu) datapaths_arrays.add((extra_fcu.data_path, extra_fcu.array_index)) if (path, index) in datapaths_arrays: continue fcu = fcurves_container.fcurves.new(data_path = path, index = index) add_group_to_fcurve(obj.id_data, fcu, group) fcurves.append(fcu) datapaths_arrays.add((fcu.data_path, fcu.array_index)) return fcurves def share_keyframes(fcu, frames): for keyframe in fcu.keyframe_points: if bpy.context.scene.animtoolbox.filter_keyframes and not keyframe.select_control_point: continue if keyframe.co[0] not in frames.keys(): frames.update({keyframe.co[0] : (keyframe.interpolation, keyframe.handle_right_type, keyframe.handle_left_type)}) return frames def get_fcurves_frames(selection, fcurves, all_fcurves, frames, attr_index): #get all the paths from the bones bone_paths = {bone.path_from_id() for bone in selection if type(bone) == bpy.types.PoseBone} # found_paths = set() # transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"] for fcu in fcurves: # print('fcu ', fcu) if not bone_paths and not fcu.data_path.startswith('pose.bones'): #in case it's just an object or data all_fcurves.append(fcu) frames = share_keyframes(fcu, frames) #update the dictionary with all the datapath and indeces attr_index.add((fcu.data_path, fcu.array_index)) continue #check bones if not fcu.data_path.startswith('pose.bones'): continue if not '"]' in fcu.data_path: continue path = fcu.data_path.split('"]')[0] + '"]' if path not in bone_paths: continue #get the attribute from the data path attr = fcu.data_path.replace(path,'') if attr[0] == '.' : attr = attr[1:] attr_index.add((attr, fcu.array_index)) all_fcurves.append(fcu) frames = share_keyframes(fcu, frames) return frames, attr_index class ShareKeys(bpy.types.Operator): """Share keyframes between all the selected objects and bones""" bl_idname = "anim.share_keyframes" bl_label = "Share_Keyframes" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object is not None def add_slot(self, obj, action): '''can be replaced with add_slot_to_animdata(anim_data)''' slot = add_action_slot(obj, action) if hasattr(obj.animation_data, 'action_slot'): obj.animation_data.action_slot = slot def get_fcurves_set(self, obj_actions): fcurves = set() for obj, action in obj_actions.items(): fcurves = fcurves.union(set(get_fcurves_channelbag(obj, action))) return fcurves def add_get_action(self, obj): '''Adding action if it doesnt exist, can be used both for obj and obj.data''' if obj.animation_data is None: obj.animation_data_create() if obj.animation_data.action is None: action_data = bpy.data.actions.new(obj.data.name) obj.animation_data.action = action_data action = obj.animation_data.action self.add_slot(obj, action) else: action = obj.animation_data.action return action def execute(self, context): #Get the frames and fcurves all_fcurves =[] frames = dict() attr_index_bone = set() attr_index_obj = set() actions_data_types = set() selected_bones = context.selected_pose_bones if selected_bones: objs = {bone.id_data for bone in selected_bones} obj_actions = {obj : obj.animation_data.action for obj in objs if obj.animation_data is not None} #get the fcurves from the bones fcurves = self.get_fcurves_set(obj_actions) frames, attr_index_bone = get_fcurves_frames(selected_bones, fcurves, all_fcurves, frames, attr_index_bone) #Apply to objects that are not in pose mode or not armatures for obj in context.selected_objects: if obj.mode == 'POSE': continue #if there is no animation data or an action then create it action = self.add_get_action(obj) fcurves = get_fcurves_channelbag(obj, action) frames, attr_index_obj = get_fcurves_frames([obj], fcurves, all_fcurves, frames, attr_index_obj) if obj.data.animation_data is None: continue if not obj.data.animation_data.action: continue #add the type of object if the data is also animated actions_data_types.add(obj.type) fcurves = get_fcurves_channelbag(obj.data, obj.data.animation_data.action) frames, attr_index_obj = get_fcurves_frames([obj], fcurves, all_fcurves, frames, attr_index_obj) #Getting all the available transforms and array index attr_index_bone = sorted(attr_index_bone, key=lambda x: (x[0], x[1])) attr_index_obj = sorted(attr_index_obj, key=lambda x: (x[0], x[1])) ####add missing fcurves to objects#### obj_fcurves = dict() if selected_bones and attr_index_bone: for bone in selected_bones: action_bone = self.add_get_action(bone.id_data) bone_path = bone.path_from_id() bone_fcurves = [fcu for fcu in all_fcurves if fcu.id_data == action_bone and bone_path in fcu.data_path] obj_fcurves.update({bone: bone_fcurves}) obj_fcurves[bone] = sharekeys_add_missing_fcurves(bone, attr_index_bone, obj_fcurves[bone]) if attr_index_obj: for obj in context.selected_objects: action = obj.animation_data.action obj_fcurves.update({obj: [fcu for fcu in all_fcurves if fcu.id_data == action]}) obj_fcurves[obj] = sharekeys_add_missing_fcurves(obj, attr_index_obj, obj_fcurves[obj]) #add animation to data if exists in other same type of objects if obj.type in actions_data_types: action_data = self.add_get_action(obj.data) # print('udating objfcurves animation data') obj_fcurves.update({obj.data: [fcu for fcu in all_fcurves if fcu.id_data == action_data]}) obj_fcurves[obj.data] = sharekeys_add_missing_fcurves(obj.data, attr_index_obj, obj_fcurves[obj.data]) ####write all the keyframes#### for obj, fcurves in obj_fcurves.items(): for fcu in fcurves: if filter_properties(context.scene.animtoolbox, fcu): continue found_frames = [keyframe.co[0] for keyframe in fcu.keyframe_points] for frame, interpolation in frames.items(): if frame in found_frames: continue if len(fcu.keyframe_points): # value = fcu.evaluate(frame) keyframe = fcu.keyframe_points.insert(frame = frame, value = fcu.evaluate(frame)) else: #Get the value from the bone or object if type(obj) == bpy.types.PoseBone: #Removing the bones path from the data path attr = fcu.data_path.replace(obj.path_from_id(),'') if attr[0] == '.' : attr = attr[1:] value = getattr(obj, attr) else: value = getattr(obj, fcu.data_path) if not isinstance(value, (int, float)): value = value[fcu.array_index] keyframe = fcu.keyframe_points.insert(frame = frame, value = value) keyframe.interpolation = interpolation[0] fcu.update() redraw_areas({'DOPESHEET_EDITOR', 'GRAPH_EDITOR'}) return {'FINISHED'} def rot_mode_to_channel(to_rot_mode): '''convert rotation mode to fcurve path transform''' if len(to_rot_mode) == 3: to_rot_mode_fcu = 'rotation_euler' elif to_rot_mode == 'QUATERNION': to_rot_mode_fcu = 'rotation_quaternion' else: to_rot_mode_fcu = 'rotation_axis_angle' return to_rot_mode_fcu ################################################# # actual euler filter def euler_to_string(e): return "%.2f, %.2f, %.2f" % (r(e[0]), r(e[1]), r(e[2])) def degrees(a): return a / 360.0 * 2 * pi def d(a): return degrees(a) def r(a): return a / (2 * pi) * 360.0 def wrap_angle(a): return (a + pi) % (2 * pi) - pi def euler_distance(e1, e2): return abs(e1[0] - e2[0]) + abs(e1[1] - e2[1]) + abs(e1[2] - e2[2]) def euler_axis_index(axis): if axis == 'X': return 0 if axis == 'Y': return 1 if axis == 'Z': return 2 return None def flip_euler(euler, rotation_mode): ret = euler.copy() inner_axis = rotation_mode[0] outer_axis = rotation_mode[2] middle_axis = rotation_mode[1] ret[euler_axis_index(inner_axis)] += pi ret[euler_axis_index(outer_axis)] += pi ret[euler_axis_index(middle_axis)] *= -1 ret[euler_axis_index(middle_axis)] += pi return ret def naive_flip_diff(a1, a2): while abs(a1 - a2) > pi: if a1 < a2: a2 -= 2 * pi else: a2 += 2 * pi return a2 def euler_filter(kfs, rotation_mode): if len(kfs) <= 1: return kfs # prev = kfs[0]["rotation_euler"] # ret = [{"key": kfs[0]["key"], # "rotation_euler": prev.copy()}] prev = kfs[0] ret = [prev.copy()] for i in range(1, len(kfs)): e = kfs[i].copy() e[0] = naive_flip_diff(prev[0], e[0]) e[1] = naive_flip_diff(prev[1], e[1]) e[2] = naive_flip_diff(prev[2], e[2]) fe = flip_euler(e, rotation_mode) fe[0] = naive_flip_diff(prev[0], fe[0]) fe[1] = naive_flip_diff(prev[1], fe[1]) fe[2] = naive_flip_diff(prev[2], fe[2]) de = euler_distance(prev, e) dfe = euler_distance(prev, fe) # print("distance: %s, flipped distance: %s Euler %s frame %s" % (de, dfe, kfs[i], i)) if dfe < de: e = fe prev = e ret.append(e) # ret += [{"key": kfs[i]["key"], # "rotation_euler": e}] return ret ################################################# def add_inbetweens(smartframes): inbetweens = [] for i, frame in enumerate(smartframes[:-1]): # if (smartframes[i+1] - frame) <= 1: # continue if (smartframes[i+1] - frame) <= 1: continue inbetweens.append(round(frame + (smartframes[i+1] - frame)*1/3, 2)) inbetweens.append(round(frame + (smartframes[i+1] - frame)*2/3, 2)) inbetweens.sort() # all_frames = sorted(self.frames + self.inbetweens) return inbetweens def add_interpolations(fcurves, fcu_inbetweens, frames = None): inbetweens = sorted(set([frame for inbetweens in fcu_inbetweens.values() for frame in inbetweens.keys()])) #turn inbetween keyframes values to handles for fcu in fcurves: fcu.update() #the index for the inbetweens P1index = 0 P2index = 1 keys = fcu.keyframe_points for i, key in enumerate(keys[:-1]):# or inbetweens[P1index] is None: if frames: if round(key.co[0], 2) not in frames: continue if round(keys[i+1].co[0], 2) - round(keys[i].co[0], 2) <= 1: keys[i].handle_right_type = 'AUTO' keys[i+1].handle_left_type = 'AUTO' fcu.update() continue if key.interpolation != 'BEZIER': P1index += 2 P2index += 2 continue P0 = round(keys[i].co[1], 2) P3 = round(keys[i+1].co[1], 2) P1 = fcu_inbetweens[fcu][inbetweens[P1index]] P2 = fcu_inbetweens[fcu][inbetweens[P2index]] 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 keys[i].handle_right_type = 'FREE' keys[i].handle_left_type = 'FREE' keys[i+1].handle_right_type = 'FREE' keys[i+1].handle_left_type = 'FREE' keys[i].handle_right = [inbetweens[P1index], cp1] keys[i+1].handle_left = [inbetweens[P2index], cp2] #iterate through the inbetween smartkeys P1index += 2 P2index += 2 fcu.update() class FindRotationMode(bpy.types.Operator): """Recommend an euler rotation to avoid gimbal lock""" bl_idname = "anim.find_rotation_mode" bl_label = "Find an Euler Rotation Mode" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): selected_bones = context.selected_pose_bones if not selected_bones: return {'CANCELLED'} smartframes = set() rotation_modes = ('XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX') axis_index = {'X' : 0, 'Y' : 1, 'Z' : 2} self.bone_rot_modes = dict() for posebone in selected_bones: if not posebone.id_data.animation_data: continue if not posebone.id_data.animation_data.action: continue #get the transform from the original rot mode transform = rot_mode_to_channel(posebone.rotation_mode) # data_path = posebone.path_from_id() + '.' + transform keyframes = emp.get_bone_keyframes(posebone, transform) smartframes = sorted(set(map(lambda x: round(x, 2), keyframes[::2]))) #store all the euler rotations of all the different options mid_axis_values = dict() axis_eulers = dict() for to_rot_mode in rotation_modes: rot_mode = posebone.rotation_mode obj = posebone.id_data # fcurves = obj.animation_data.action.fcurves fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) new_rot_eulers = [] #get the values from the original rotation for frame in smartframes: new_rot = convert_rotation(posebone, fcurves, frame, to_rot_mode) new_rot_eulers.append(new_rot) #apply Euler filter # new_rot_eulers = euler_filter(new_rot_eulers, to_rot_mode) axis_eulers.update({to_rot_mode : new_rot_eulers}) #get a dictionary with only the mid axis values mix_axis_index = axis_index[to_rot_mode[1]] mid_axis_values.update({to_rot_mode : [abs(rot[mix_axis_index]) for rot in new_rot_eulers]}) # get the axis that has the least 90 degrees on the mid axis self.rotation_90_counts = dict() for rot_mode, values in mid_axis_values.items(): avg_diff = average_difference(values, 1.57) self.rotation_90_counts.update({rot_mode : avg_diff}) #sort the dictionary using the values from of the avg_diff self.rotation_90_counts = dict(sorted(self.rotation_90_counts.items(), key=lambda item: item[1], reverse=True)) self.bone_rot_modes.update({posebone.name : self.rotation_90_counts}) context.scene.animtoolbox.rotation_mode = list(self.rotation_90_counts.keys())[0] if not self.bone_rot_modes: return {'CANCELLED'} wm = context.window_manager return wm.invoke_props_dialog(self, width=300) #+ max([len(bone.name) for bone in selected_bones]) def draw(self, context): layout = self.layout row = layout.row() row.alignment = 'CENTER' row.label(text = 'Avoid gimbal locks!') row = layout.row() row.alignment = 'CENTER' row.label(text = 'Rotation modes listed from best to worst') for bonename, rot_modes in self.bone_rot_modes.items(): row = layout.split(factor = 0.4) # row.alignment = 'CENTER' row.label(text = 'bone' + bonename + ':', icon = 'BONE_DATA') # row = layout.row() # row.alignment = 'CENTER' rot_list = ''.join([rot + ', ' for rot in rot_modes]) row.label(text = rot_list) def execute(self, context): return {'CANCELLED'} def average_difference(lst, target): return np.mean(np.abs(np.array(lst) - target)) def convert_rotation(posebone, fcurves, frame, to_rot_mode): #get the transform from the original rot mode transform = rot_mode_to_channel(posebone.rotation_mode) data_path = posebone.path_from_id() + '.' + transform # to_rot_mode = context.scene.animtoolbox.rotation_mode to_rot_mode_fcu = rot_mode_to_channel(to_rot_mode) #define array length from_rot_range = 3 if len(posebone.rotation_mode) == 3 else 4 #Get the original rotation rot = [] for i in range(from_rot_range): #get the original fcurve fcu = fcurves.find(data_path, index = i) if fcu is None: continue rot.append(fcu.evaluate(frame)) if len(posebone.rotation_mode) == 3: org_rot = mathutils.Euler(rot, posebone.rotation_mode) else: org_rot = mathutils.Quaternion(rot) #get the new value as Euler or quaternions if transform == 'rotation_euler': if to_rot_mode_fcu != 'rotation_euler': new_rot = org_rot.to_quaternion() else: matrix = org_rot.to_matrix() new_rot = matrix.to_euler(to_rot_mode) elif transform == 'rotation_quaternion': if to_rot_mode_fcu == 'rotation_euler': new_rot = org_rot.to_euler(to_rot_mode) elif to_rot_mode_fcu == 'rotation_axis_angle': new_rot = org_rot.to_euler(to_rot_mode) elif transform == 'rotation_axis_angle': if to_rot_mode_fcu == 'rotation_euler': new_rot = org_rot.to_euler(to_rot_mode) elif to_rot_mode_fcu == 'rotation_quaternion': new_rot = org_rot.to_quaternion() return new_rot class ConvertRotationMode(bpy.types.Operator): """Convert the rotation keyframes to a new rotation mode using smartbake""" bl_idname = "anim.convert_rotation_mode" bl_label = "Convert Rotation Mode" bl_options = {'REGISTER', 'UNDO'} # @classmethod # def poll(cls, context): # return context.object.type == 'ARMATURE' def switch_rot_mode_keyframes(self, obj, posebone): # Switching any rotation mode keyframes to the new rotation mode value using to_rot_mode_index bone_path = posebone.path_from_id() + '.' if type(posebone) == bpy.types.PoseBone else '' fcurves = get_fcurves_channelbag(obj, obj.animation_data.action) fcu_rotation_mode = fcurves.find(data_path = bone_path + 'rotation_mode', index = 0) if fcu_rotation_mode: for keyframe in fcu_rotation_mode.keyframe_points: keyframe.co[1] = self.to_rot_mode_index def execute(self, context): scene = context.scene selected_bones = context.selected_pose_bones # smartframes = set() to_rot_mode = scene.animtoolbox.rotation_mode to_rot_mode_fcu = rot_mode_to_channel(to_rot_mode) #Getting the index of the rotation mode we want to convert to self.to_rot_mode_index = list(scene.animtoolbox.bl_rna.properties['rotation_mode'].enum_items.keys()).index(to_rot_mode) #get the keyframes from the bones for posebone in selected_bones: if to_rot_mode == posebone.rotation_mode: continue obj = posebone.id_data # fcurves = obj.animation_data.action.fcurves action = obj.animation_data.action fcurves = get_fcurves_channelbag(obj, action) fcu_inbetweens = dict() fcu_keyframes = dict() #get the transform from the original rot mode transform = rot_mode_to_channel(posebone.rotation_mode) # data_path = posebone.path_from_id() + '.' + transform keyframes = emp.get_bone_keyframes(posebone, transform) # rotation_mode_keyframes = emp.get_bone_keyframes(posebone, 'rotation_mode') self.switch_rot_mode_keyframes(obj, posebone) inbetweens = [] smartframes = sorted(set(map(lambda x: round(x, 2), keyframes[::2]))) #get all interpolations and handle types of the keyframes if len(smartframes) > 1: handle_types = emp.get_bone_keyframes(posebone, transform, property = 'interpolation') interpolations = handle_types[::3] handle_left_type = handle_types[1::3] handle_right_type = handle_types[2::3] inbetweens = add_inbetweens(smartframes) all_frames = sorted(smartframes + inbetweens) new_path = posebone.path_from_id() + '.' + to_rot_mode_fcu #define array length to_rot_range = 3 if to_rot_mode_fcu == 'rotation_euler' else 4 # from_rot_range = 3 if len(posebone.rotation_mode) == 3 else 4 #create or find he fcurves new_fcurves = [] group = None for i in range(to_rot_range): #find the target fcurve or create a new one fcu_new = fcurves.find(data_path = new_path, index = i) #start fresh if fcu_new is None: fcu_new = fcurves.new(data_path = new_path, index = i) if group is None: group = add_group_to_fcurve(obj, fcu_new, posebone.name) else: fcu_new.group = group new_fcurves.append(fcu_new) new_rot_eulers = [] #get all the rotations in a list for frame in all_frames: new_rot = convert_rotation(posebone, fcurves, frame, to_rot_mode) new_rot_eulers.append(new_rot) #apply Euler filter if to_rot_mode_fcu == 'rotation_euler': new_rot_eulers = euler_filter(new_rot_eulers, to_rot_mode) #get the values from the original rotation for frame, new_rot in zip(all_frames, new_rot_eulers): for i in range(to_rot_range): new_fcu = new_fcurves[i] if frame in smartframes: if new_fcu in fcu_keyframes: frame_new_rot = fcu_keyframes[new_fcu] frame_new_rot.update({frame : new_rot[i]}) fcu_keyframes[new_fcu].update(frame_new_rot) else: fcu_keyframes.update({new_fcu : {frame : new_rot[i]}}) # Storing the inbetween values in dictionaries elif frame in inbetweens: if new_fcu in fcu_inbetweens: frame_new_rot = fcu_inbetweens[new_fcu] frame_new_rot.update({frame : new_rot[i]}) fcu_inbetweens[new_fcu].update(frame_new_rot) else: fcu_inbetweens.update({new_fcu : {frame : new_rot[i]}}) #write the keyframes for i in range(to_rot_range): new_fcu = new_fcurves[i] for frame_index, frame in enumerate(smartframes): found_key = False #find the keyframe for keyframe in new_fcu.keyframe_points: if keyframe.co[0] == frame: found_key = True break #if not found add a new one if not found_key: new_fcu.keyframe_points.add(1) keyframe = new_fcu.keyframe_points[-1] keyframe.co = (frame, fcu_keyframes[new_fcu][frame]) if inbetweens: keyframe.interpolation = interpolations[frame_index] keyframe.handle_left_type = handle_left_type[frame_index] keyframe.handle_right_type = handle_right_type[frame_index] new_fcu.update() if inbetweens: add_interpolations(new_fcurves, fcu_inbetweens) posebone.rotation_mode = to_rot_mode return {'FINISHED'} def bake_range_type(self, context): #start bake range with 10 frames range around the current frame if self.range_type != "RANGE": return if self.bake_frame_end: return self.bake_frame_start = context.scene.frame_current - 5 self.bake_frame_end = context.scene.frame_current + 5 def bake_frame_end_limit(self, context): #property update - limit the frame start to be smaller then frame end if self.bake_frame_start > self.bake_frame_end: self.bake_frame_start = self.bake_frame_end def bake_frame_start_limit(self, context): #property update - limit the frame start to be smaller then frame end if self.bake_frame_start > self.bake_frame_end: self.bake_frame_end = self.bake_frame_start def filter_matrix_properties(context, original_matrix, new_matrix): '''Filter Matrix Values''' scene = context.scene atb = scene.animtoolbox if not any(list(atb.filter_location) + list(atb.filter_rotation)[1:] + list(atb.filter_scale)): return new_matrix org_location, org_rotation, org_scale = original_matrix.decompose() new_location, new_rotation, new_scale = new_matrix.decompose() org_rotation = original_matrix.to_euler() new_rotation = new_matrix.to_euler() for transform in ('location', 'rotation', 'scale'): filter_attr = getattr(atb, 'filter_' + transform) #in case of rotation use only xyz, instead of the W if transform == 'rotation': filter_attr = filter_attr[1:] for i in range(len(filter_attr)): if not filter_attr[i]: continue #Get the original value into the new variable locals()['new_' + transform][i] = locals()['org_' + transform][i] return Matrix.LocRotScale(new_location, new_rotation, new_scale) def filter_properties(context, fcu): 'Filter the W X Y Z attributes of the transform properties for fcurves' transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "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 if context.filter_custom_props else False transform = fcu.data_path.split('"].')[1] if '"].' in fcu.data_path else fcu.data_path index = fcu.array_index if 'rotation' in transform : transform = 'rotation' f_transform = 'filter_' + transform #in case of channels like bbone_scalein that are no included then return if not hasattr(context, f_transform): return True attr = getattr(context, f_transform) if 'rotation' in transform: #when baking to ctrls it is using only 3 arrays in the constraints if not fcu.data_path.endswith('rotation_euler') and len(attr) == 3: index -= 1 elif fcu.data_path.endswith('rotation_euler') and len(attr) == 4: index += 1 if index >= len(attr): return True return True if attr[index] else False def filter_draw_ui(self, attr, titel = True): layout = self.layout if titel: split = layout.split(factor = 0.41) split.label(text = 'Filter :') split.label(text = 'W X Y Z') box = layout.box() row = box.row() row.label(text = 'Location') row.prop(attr, 'filter_location', text = '') row = box.row() row.label(text = 'Rotation') row.prop(attr, 'filter_rotation', text = '') row = box.row() row.label(text = 'Scale') row.prop(attr, 'filter_scale', text = '') if hasattr(attr, 'filter_custom_props'): row = layout.row(align = True) row.alignment = 'CENTER' row.label(text = 'Custom Properties ') row.prop(attr, 'filter_custom_props', text = '') if hasattr(attr, 'filter_keyframes'): row = layout.row(align = True) row.alignment = 'CENTER' row.label(text = 'Selected Keyframes ') row.prop(attr, 'filter_keyframes', text = '') class FilterUI(bpy.types.Operator): """Filter Location Rotation and Scale Properties""" bl_idname = "fcurves.filter_ui" bl_label = "Filter W X Y Z" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): wm = context.window_manager return wm.invoke_popup(self, width = 150) def draw(self, context): filter_draw_ui(self, context.scene.animtoolbox) def execute(self, context): return {'CANCELLED'} def filter_name_update(self, context): '''The name displayed on the filter button''' scene = context.scene atb = scene.animtoolbox atb.filter_name = '' if not any(list(atb.filter_location) + list(atb.filter_rotation) + list(atb.filter_scale)): return #Write the name of the transforms and array that are being used array_dict = {0 : 'x', 1 : 'y', 2 : 'z'} array_rot_dict = {0 : 'w', 1 : 'x', 2 : 'y', 3 : 'z'} for transform in ('location', 'rotation', 'scale'): filter_attr = getattr(atb, 'filter_' + transform) if not any(list(filter_attr)): continue if atb.filter_name != '': atb.filter_name = atb.filter_name + ' / ' atb.filter_name += transform[0].upper() + ' (' array_count = 0 for i in range(len(filter_attr)): if not filter_attr[i]: continue if array_count: atb.filter_name += '.' if transform == 'rotation': atb.filter_name += array_rot_dict[i] else: atb.filter_name += array_dict[i] array_count +=1 # if i != len(filter_attr)-1: # atb.filter_name += '.' atb.filter_name += ')' #remove the last point #atb.filter_name = atb.filter_name[:-1] redraw_areas(['VIEW_3D']) def redraw_areas(areas): for area in bpy.context.window.screen.areas: if area.type in areas: area.tag_redraw() def draw_func(self, context): layout = self.layout # Append your label to the frame range template ID layout.operator("anim.markers_framerange", icon = 'MARKER', text ='', depress = context.scene.animtoolbox.marker_frame_range) def get_obj_slot(obj, action): '''Get the slot in the action that this object is using either it's object, or shapekeys''' if not hasattr(action, 'slots'): return None for slot in action.slots: if obj in slot.users(): return slot return None def get_all_fcurves(action): '''Get all the fcurves of an action''' if not hasattr(action, 'layers'): yield from action.fcurves for layer in action.layers: for strip in layer.strips: for channelbag in strip.channelbags: yield from channelbag.fcurves def get_fcurves_channelbag(obj, action: bpy.types.Action): if hasattr(action, 'layers'): slot = get_obj_slot(obj, action) if not slot: return action.fcurves channelbag = anim_utils.action_get_channelbag_for_slot(action, slot) if channelbag: return channelbag.fcurves return action.fcurves def add_channelbag(obj, action): if not hasattr(action, 'layers'): return slot = get_obj_slot(obj, action) if not len(action.layers): layer = action.layers.new(obj.name) else: layer = action.layers[0] if not len(layer.strips): strip = layer.strips.new() else: strip = layer.strips[0] if not len(strip.channelbags): channelbag = strip.channelbags.new(slot) else: channelbag = anim_utils.action_get_channelbag_for_slot(action, slot) return channelbag def get_fcurves_container(obj: bpy.types.Object, action: bpy.types.Action): '''Getting the container of the fcurves, either the action or channelbag Using this when adding a new group to the action''' if hasattr(action, 'layers'): slot = get_obj_slot(obj, action) if not slot: return action channelbag = anim_utils.action_get_channelbag_for_slot(action, slot) return channelbag else: return action def add_group_to_fcurve(obj, fcu, groupname): '''Add an fcurve group based on the fcurve container, either action or channelbag''' action = fcu.id_data #get the container which is either a channelbag or a group fcu_container = get_fcurves_container(obj, action) group = fcu_container.groups.get(groupname) if group is None: group = fcu_container.groups.new(groupname) fcu.group = group return group def add_slot_to_animdata(anim_data): '''assign a slot to the object of anim data''' if not hasattr(anim_data, 'action_slot'): return None slot = add_action_slot(anim_data.id_data, anim_data.action) anim_data.action_slot = slot return slot def add_action_slot(obj, action): '''Adding a new slot or finding available to an action, Relevant only for Blender 4.4 +''' if not action: return None if not hasattr(action, 'layers'): return None if action.slots: for slot in action.slots: if obj in slot.users(): return slot slot = action.slots.new(obj.id_type, obj.name) return slot def remove_empty_slots(action): '''removing empty slots without users, using when extracting from a layer''' if not action: return if not hasattr(action, 'layers'): return if not action.slots: return for slot in action.slots: if not len(slot.users()): action.slots.remove(slot) classes = (ApplyKeyframesOffset, SelectKeyframesOffset, CopyMatrix, PasteMatrix, CopyRelativeMatrix, PasteRelativeMatrix, FilterUI, ShareKeys, Markers_FrameRange, Markers_BakeRange, Markers_Retimer, BlendToMirroModal, InbetweenWorldMatrix, ConvertRotationMode, FindRotationMode, Keyframe_Offset, RelativeCursor) def register(): from bpy.utils import register_class for cls in classes: register_class(cls) # bpy.types.Scene.marker_frame_range = bpy.props.BoolProperty(name = "Marker Frame Range", description = "Flag when marker frame range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'}) bpy.types.DOPESHEET_HT_header.append(draw_func) def unregister(): from bpy.utils import unregister_class for cls in classes: unregister_class(cls) #del bpy.types.Scene.marker_frame_range bpy.types.DOPESHEET_HT_header.remove(draw_func)