From d4ba199b63c062cf459ca251919498c96b9e5529 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Thu, 21 Apr 2022 17:44:35 +0200 Subject: [PATCH] add anim strip key resync fix baking and step mode 1.3.3 - changed: `Set Time Keys` in NLA do not remove keys if exists but offset to match start of strip (if has moved) to resync - added: button to remove animated Strip time (delete keys but not fcurve) - fixed: step mode for baking --- CHANGELOG.md | 6 +++ OP_expand_cycle_step.py | 95 ++++++++++++++++++++++++++++++++++++----- OP_nla_tweak.py | 81 ++++++++++++++++++++++++++++++++++- fn.py | 40 +++++++++++++---- panels.py | 4 +- 5 files changed, 205 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2210bcd..44c2c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +1.3.3 + +- changed: `Set Time Keys` in NLA do not remove keys if exists but offset to match start of strip (if has moved) to resync +- added: button to remove animated Strip time (delete keys but not fcurve) +- fixed: step mode for baking + 1.3.2 - removed: ground feet (added initial support for override compatibility wip) diff --git a/OP_expand_cycle_step.py b/OP_expand_cycle_step.py index ca765d0..b126e2b 100644 --- a/OP_expand_cycle_step.py +++ b/OP_expand_cycle_step.py @@ -1,3 +1,4 @@ +from doctest import SKIP import bpy, re from . import fn from time import time @@ -6,8 +7,9 @@ from time import time # - Bake cycle modifier keys -chained with- step the animation path # - Pin the feet (separated ops) -def bake_cycle(on_selection=True): +def bake_cycle(on_selection=True, end=None): print(fn.helper()) + end = end or bpy.context.scene.frame_end debug = fn.get_addon_prefs().debug obj = bpy.context.object if obj.type != 'ARMATURE': @@ -90,7 +92,8 @@ def bake_cycle(on_selection=True): ## expand to end frame # maybe add possibility define target manually ? - end = bpy.context.scene.frame_end + 10 # add a margin + + end += 20 # add an hardcoded margin ! iterations = ((end - last) // offset) + 1 if debug >= 2: print('iterations: ', iterations) for _i in range(int(iterations)): @@ -108,10 +111,10 @@ def bake_cycle(on_selection=True): ct += 1 if ct_fcu == ct_no_cycle: # skipped because no cycle exists - rexpand = re.compile(r'_baked\.?\d{0,3}$') - if rexpand.search(act.name): + re_baked = re.compile(r'_baked\.?\d{0,3}$') + if re_baked.search(act.name): # is an autogenerated one - org_action_name = rexpand.sub('', act.name) + org_action_name = re_baked.sub('', act.name) org_action = bpy.data.actions.get(org_action_name) if not org_action: return ('ERROR', 'No fcurve with anim cycle found (on expanded action)') @@ -125,12 +128,67 @@ def bake_cycle(on_selection=True): fn.update_action(act) print('end of anim cycle keys baking') # C.scene.frame_current = org_frame - # detect last key in contact + # detect last key in contact def step_path(): '''Step the path anim of the curve to constant''' print(fn.helper()) + ob = bpy.context.object + if ob.type != 'ARMATURE': + return ('ERROR', 'active is not an armature type') + + ## found curve through constraint + # curve, const = fn.get_follow_curve_from_armature(ob) + # if not const: + # return ('ERROR', 'No constraints found') + + act = fn.get_obj_action(ob) + if not act: + return + + # CHANGE - removed int from 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)) + + ## get constraint + # curve = const.target + # if not curve: + # return ('ERROR', f'no target set for {curve.name}') + + offset_fc = None + for fc in act.fcurves: + if all(x in fc.data_path for x in ('pose.bones', 'constraints', 'offset')): + offset_fc = fc + + if not offset_fc: + return ('ERROR', f'no offset animation in action {act.name}') + + data_path = offset_fc.data_path + const = ob.path_resolve(data_path.rsplit('.', 1)[0]) + + timevalues = [offset_fc.evaluate(kf) for kf in keyframes] + for kf, value in zip(keyframes, timevalues): + ## or use t_fcu.keyframe_points.add(len(kf)) + + + const.offset = value + const.keyframe_insert('offset', frame=kf, options={'INSERTKEY_AVAILABLE'}) + # ``INSERTKEY_NEEDED````INSERTKEY_AVAILABLE`` (only available channels) + + ## set all to constant + for k in offset_fc.keyframe_points: + k.interpolation = 'CONSTANT' + + # cleaning update (might not be needed here) + fn.update_action(act) + print('end of step_anim') + +''' +def step_path(): + print(fn.helper()) + obj = bpy.context.object if obj.type != 'ARMATURE': return ('ERROR', 'active is not an armature type') @@ -141,7 +199,8 @@ def step_path(): return ('ERROR', 'No constraints found') act = fn.get_obj_action(obj) - if not act: return + if not act: + return # CHANGE - removed int from frame # keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points] @@ -181,11 +240,12 @@ def step_path(): # cleaning update (might not be needed here) fn.update_action(act) print('end of step_anim') - +''' class AW_OT_bake_cycle_and_step(bpy.types.Operator): bl_idname = "autowalk.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_label = "Bake keys" + bl_description = "Bake the keys to a new baked animation\ + \nStep path animation according to those key (if not in Linear)\ \n(duplicate to a new 'baked' action)" bl_options = {"REGISTER", "UNDO"} @@ -193,9 +253,22 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator): def poll(cls, context): return context.object and context.object.type == 'ARMATURE' + end_frame : bpy.props.IntProperty(name='End Frame',options={'SKIP_SAVE'}) + + def invoke(self,context,event): + self.end_frame = context.scene.frame_end + # return self.execute(context) # uncomment only this to skip pop-up and keep scene.end + return context.window_manager.invoke_props_dialog(self) # width=400 + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.label(text='End of cycle duplication') + layout.prop(self, 'end_frame', text='End Frame') + def execute(self, context): - err = bake_cycle(context.scene.anim_cycle_settings.expand_on_selected_bones) + err = bake_cycle(context.scene.anim_cycle_settings.expand_on_selected_bones, end=self.endframe) if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': diff --git a/OP_nla_tweak.py b/OP_nla_tweak.py index ecaa501..eb93351 100644 --- a/OP_nla_tweak.py +++ b/OP_nla_tweak.py @@ -33,7 +33,83 @@ class AW_OT_nla_key_speed(bpy.types.Operator): return context.object and context.object.type == 'ARMATURE' def execute(self, context): - nla_strip = get_active_nla_strip() + nla_strip = fn.get_nla_strip(context.object) # get_active_nla_strip() + if not nla_strip: + self.report({'ERROR'}, 'no active NLA strip') + return {"CANCELLED"} + + fcu = nla_strip.fcurves.find('strip_time') + # if fcu: + # for k in reversed(fcu.keyframe_points): + # fcu.keyframe_points.remove(k) + + nla_strip.use_animated_time = True + if not fcu or len(fcu.keyframe_points) == 0: + # create if not exists + nla_strip.strip_time = nla_strip.action_frame_start + nla_strip.keyframe_insert('strip_time', frame=nla_strip.frame_start) + + nla_strip.strip_time = nla_strip.action_frame_end + nla_strip.keyframe_insert('strip_time', frame=nla_strip.frame_end) + + fcu = nla_strip.fcurves.find('strip_time') + + # Go linear + for k in fcu.keyframe_points: + k.interpolation = 'LINEAR' + + return {"FINISHED"} + + ## if already exists : match offset (usefull when moving a strip) + first = min([k.co.x for k in fcu.keyframe_points]) + + offset = nla_strip.frame_start - first + + for k in fcu.keyframe_points: + k.co.x += offset + + return {"FINISHED"} + +class AW_OT_nla_remove_key_speed(bpy.types.Operator): + bl_idname = "autowalk.nla_remove_key_speed" + bl_label = "NLA Remove Key Speed" + bl_description = "Remove strip time animation on active nla strip" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + nla_strip = fn.get_nla_strip(context.object) # get_active_nla_strip() + if not nla_strip: + self.report({'ERROR'}, 'no active NLA strip') + return {"CANCELLED"} + + fcu = nla_strip.fcurves.find('strip_time') + if fcu: + for k in reversed(fcu.keyframe_points): + fcu.keyframe_points.remove(k) + nla_strip.use_animated_time = False + else: + self.report({'ERROR'}, f'No strip time animation on active strip {nla_strip.name}') + return {"CANCELLED"} + + return {"FINISHED"} + +''' +class AW_OT_nla_reset_key_speed(bpy.types.Operator): + bl_idname = "autowalk.nla_reset_key_speed" + bl_label = "NLA Reset Key Speed" + bl_description = "Activate animate strip time and Keyframe linear for first and last animation frame" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + nla_strip = fn.get_nla_strip(context.object) # get_active_nla_strip() if not nla_strip: self.report({'ERROR'}, 'no active NLA strip') return {"CANCELLED"} @@ -60,9 +136,12 @@ class AW_OT_nla_key_speed(bpy.types.Operator): k.interpolation = 'LINEAR' return {"FINISHED"} +''' classes=( AW_OT_nla_key_speed, +AW_OT_nla_remove_key_speed, +# AW_OT_nla_reset_key_speed, ) def register(): diff --git a/fn.py b/fn.py index a9c11b0..f474b60 100644 --- a/fn.py +++ b/fn.py @@ -105,17 +105,41 @@ def get_follow_curve_from_armature(arm): # --- ACTIONS -def get_obj_action(obj): +def get_nla_strip(ob): + # get all strips in all tracks (can only be one active) + strips = [s for t in ob.animation_data.nla_tracks for s in t.strips] + if len(strips) == 1: + return strips[0].action + + if len(strips) > 1: + # return active strip + for s in strips: + if s.active: + return s.action + +def get_obj_action(ob): print(helper()) - act = obj.animation_data + act = ob.animation_data if not act: - print('ERROR', f'no animation data on {obj.name}') + print('ERROR', f'no animation data on {ob.name}') return act = act.action if not act: - print('ERROR', f'no action on {obj.name}') + # check NLA + strip = get_nla_strip(ob) + + if strip: + return strip + + # there are multiple strips but no active + if len([s for t in ob.animation_data.nla_tracks for s in t.strips]): + print('ERROR', f'no active strip on NLA for {ob.name}') + return + + print('ERROR', f'no action on {ob.name}') return + return act def set_generated_action(obj): @@ -155,12 +179,12 @@ def set_baked_action(obj): ''' print(helper()) - rexpand = re.compile(r'_baked\.?\d{0,3}$') + re_baked = re.compile(r'_baked\.?\d{0,3}$') act = obj.animation_data.action - if rexpand.search(act.name): + if re_baked.search(act.name): # is an autogenerated one - org_action_name = rexpand.sub('', act.name) + org_action_name = re_baked.sub('', act.name) org_action = bpy.data.actions.get(org_action_name) if not org_action: print('ERROR', f'{org_action_name} not found') @@ -439,7 +463,7 @@ def update_action(act): # update fcurves for fcu in act.fcurves: fcu.update() - + # redraw graph area for window in bpy.context.window_manager.windows: screen = window.screen diff --git a/panels.py b/panels.py index 7df6546..a1bd5bf 100644 --- a/panels.py +++ b/panels.py @@ -166,7 +166,9 @@ class AW_PT_nla_tools_panel(bpy.types.Panel): def draw(self, context): layout = self.layout # layout.label(text='Retime Tools') - layout.operator('autowalk.nla_key_speed', text='Set Time Keys', icon='TIME') + row = layout.row(align=True) + row.operator('autowalk.nla_key_speed', text='Set/Update Time Keys', icon='TIME') + row.operator('autowalk.nla_remove_key_speed', text='', icon='X')