import bpy from . import fn ## step 3 # - Bake cycle modifier keys -chained with- step the animation path # - Pin the feet (separated ops) def bake_cycle(on_selection=True): print(fn.helper()) obj = bpy.context.object if obj.type != 'ARMATURE': print('ERROR', 'active is not an armature type') return act = fn.set_expanded_action(obj) if not act: return print('action', act.name) act = obj.animation_data.action ct = 0 for fcu in act.fcurves: ## if a curve is not cycled don't touch if not [m for m in fcu.modifiers if m.type == 'CYCLES']: continue #-# only on location : # if not fcu.data_path.endswith('.location'): # continue # prop = fcu.data_path.split('.')[-1] b_name = fcu.data_path.split('"')[1] pb = obj.pose.bones.get(b_name) if not pb: print(f'{b_name} is invalid') continue #-# limit on selection if passed if on_selection and not pb.bone.select: continue #-# only on selected and visible curve # if not fcu.select or fcu.hide: # continue fcu_kfs = [] for k in fcu.keyframe_points: k_dic = {} # k_dic['k'] = k k_dic['co'] = k.co k_dic['interpolation'] = k.interpolation k_dic['type'] = k.type fcu_kfs.append(k_dic) first = fcu_kfs[0]['co'][0] # second = fcu_kfs[1]['co'][0] # before_last= fcu_kfs[-2]['co'][0] last = fcu_kfs[-1]['co'][0] # first_offset = second - first current_offset = offset = last - first print('offset: ', offset) ## expand to end frame end = bpy.context.scene.frame_end # maybe add possibility define target manually iterations = ((end - last) // offset) + 1 print('iterations: ', iterations) for _i in range(int(iterations)): for kf in fcu_kfs: # create a new key, adding offset to keys fcu.keyframe_points.add(1) new = fcu.keyframe_points[-1] for att, val in kf.items(): if att == 'co': new.co = (val[0] + current_offset, val[1]) else: setattr(new, att, val) current_offset += offset ct += 1 if not ct: return ('ERROR', 'No fcurve treated (! action duplicated to _expand !)') print('end of anim cycle keys baking') # C.scene.frame_current = org_frame # detect last key in contact def step_path(): print(fn.helper()) obj = bpy.context.object if obj.type != 'ARMATURE': return ('ERROR', 'active is not an armature type') # found curve through constraint curve, const = fn.get_follow_curve_from_armature(obj) if not const: return ('ERROR', 'No constraints found') act = fn.get_obj_action(obj) if not act: return # CHANGE - retiré le int de la frame # keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points] keyframes = [k.co[0] for fcu in act.fcurves for k in fcu.keyframe_points] keyframes = list(set(keyframes)) curve = const.target if not curve: return ('ERROR', f'no target set for {curve.name}') # get a new generated action for the curve # Follow path animation is on the DATA of the fcurve fact = fn.set_generated_action(curve.data) if not fact: return t_fcu = False for fcu in fact.fcurves: ## fcu data_path is just a string if fcu.data_path == 'eval_time': t_fcu = fcu if not t_fcu: return ('ERROR', f'no eval_time animation in {curve.name}') timevalues = [t_fcu.evaluate(kf) for kf in keyframes] for kf, value in zip(keyframes, timevalues): ## or use t_fcu.keyframe_points.add(len(kf)) curve.data.eval_time = value curve.data.keyframe_insert('eval_time', frame=kf, options={'INSERTKEY_AVAILABLE'}) # ``INSERTKEY_NEEDED````INSERTKEY_AVAILABLE`` (only available channels) ## set all to constant for k in t_fcu.keyframe_points: k.interpolation = 'CONSTANT' print('end of step_anim') class UAC_OT_bake_cycle_and_step(bpy.types.Operator): bl_idname = "anim.bake_cycle_and_step" bl_label = "Bake key and step path " bl_description = "Bake the key and step the animation path according to those key" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): err = bake_cycle(context.scene.anim_cycle_settings.expand_on_selected_bones) if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': return {"CANCELLED"} # CHAINED ACTION : step the path of the curve path err = step_path() if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': return {"CANCELLED"} # CHAINED ACTION pin feet ?? : Step the path of the curve path return {"FINISHED"} def pin_down_feets(): print(fn.helper()) obj = bpy.context.object if obj.type != 'ARMATURE': print('ERROR', 'active is not an armature type') return # Delete current action if its not the main one # create a new '_autogen' one act = fn.set_generated_action(obj) if not act: return ('ERROR', f'No action on {obj.name}') print('action', act.name) # detect contact key # [ 'array_index', 'auto_smoothing', 'bl_rna', 'color', 'color_mode', 'convert_to_keyframes', 'convert_to_samples', 'data_path', # 'driver', 'evaluate', 'extrapolation', 'group', 'hide', 'is_empty', 'is_valid', 'keyframe_points', 'lock', 'modifiers', 'mute', # 'range', 'rna_type', 'sampled_points', 'select', 'update', 'update_autoflags'] act = obj.animation_data.action org_frame = bpy.context.scene.frame_current # TODO autodetect contact frame ? done = {} for fcu in act.fcurves: # check only location if not fcu.data_path.endswith('.location'): continue # prop = fcu.data_path.split('.')[-1] b_name = fcu.data_path.split('"')[1] # print('b_name: ', b_name, fcu.is_valid) pb = obj.pose.bones.get(b_name) if not pb: print(f'{b_name} is invalid') continue start_contact = None for k in fcu.keyframe_points: if k.type == 'EXTREME': bpy.context.scene.frame_set(k.co[0]) if start_contact is None: start_contact=k.co[0] # record coordinate relative to referent object (or world coord) bone_mat = pb.matrix.copy() # bone_mat = obj.matrix_world @ pb.matrix.copy() continue if b_name in done.keys(): if k.co[0] in done[b_name]: continue else: # mark as treated (all curve of this bone at this time) done[b_name] = [k.co[0]] #-# Insert keyframe to match Hold position # print(f'Apply on {b_name} at {k.co[0]}') #-# assign previous matrix pbl = pb.location.copy() # l, _r, _s = bone_mat.decompose() pb.matrix = bone_mat # Exact same position # pb.location.x = pbl.x # dont touch x either pb.location.z = pbl.z # pb.location.y = l.y (weirdly not working) # bpy.context.view_layer.update() #-# moyenne des 2 ? # pb.location, pb.rotation_euler, pb.scale = average_two_matrix(pb.matrix, bone_mat) ## marche pas du tout ! ## insert keyframe pb.keyframe_insert('location') # only touched Y location # pb.keyframe_insert('rotation_euler') k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER' else: if start_contact is not None: # print('fcu.data_path: ', fcu.data_path, fcu.array_index) # print(f'{b_name} contact range {start_contact} - {k.co[0]}') start_contact = None # print(i, fcu.data_path, fcu.array_index) # print('time', k.co[0], '- value', k.co[1]) #k.handle_left #k.handle_right ##change handler type ([‘FREE’, ‘VECTOR’, ‘ALIGNED’, ‘AUTO’, ‘AUTO_CLAMPED’], default ‘FREE’) #k.handle_left_type = 'AUTO_CLAMPED' #k.handle_right_type = 'AUTO_CLAMPED' bpy.context.scene.frame_current = org_frame # detect last key in contact class UAC_OT_pin_feets(bpy.types.Operator): bl_idname = "anim.pin_feets" bl_label = "Pin Feets" bl_description = "Pin feets on keys marked as extreme" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type == 'ARMATURE' def execute(self, context): # context.scene.anim_cycle_settings.expand_on_selected_bones err = pin_down_feets() if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': return {"CANCELLED"} return {"FINISHED"} classes=( UAC_OT_bake_cycle_and_step, UAC_OT_pin_feets, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)