From 8f1f1a68827abd029a647bda464d79949ff4b6b4 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Mon, 25 Apr 2022 18:12:24 +0200 Subject: [PATCH] improved cycle bake (still need cycle modifier) 1.4.0 - added: preview of end frame for the cycle bake - fixed: better method to detect key cycle range --- CHANGELOG.md | 5 +++ OP_animate_path.py | 9 +---- OP_expand_cycle_step.py | 89 ++++++++++------------------------------- __init__.py | 2 +- fn.py | 54 ++++++++++++++++++++++++- 5 files changed, 82 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35d42e2..59a4794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +1.4.0 + +- added: preview of end frame for the cycle bake +- fixed: better method to detect key cycle range + 1.3.4 - fixed: bake keys with better cycle compatibility diff --git a/OP_animate_path.py b/OP_animate_path.py index a2addc0..b3c563a 100644 --- a/OP_animate_path.py +++ b/OP_animate_path.py @@ -106,14 +106,7 @@ def find_best_foot(ob): return ('ERROR', f'No action active on {ob.name}') # use original action as ref - if '_baked' in act.name: - base_act_name = act.name.split('_baked')[0] - base_act = bpy.data.actions.get(base_act_name) - if base_act: - act = base_act - print(f'Using for action {base_act_name} as reference') - else: - print(f'No base action found (searching for {base_act_name})') + act = fn.get_origin_action(act) if 'foot' in b.name.lower(): # if best is selected diff --git a/OP_expand_cycle_step.py b/OP_expand_cycle_step.py index 592ff6d..d9fa7f4 100644 --- a/OP_expand_cycle_step.py +++ b/OP_expand_cycle_step.py @@ -10,7 +10,7 @@ from time import time def bake_cycle(on_selection=True, end=None): print(fn.helper()) end = end or bpy.context.scene.frame_end - end += 20 # add an hardcoded margin ! + print('end: ', end) debug = fn.get_addon_prefs().debug obj = bpy.context.object @@ -22,7 +22,7 @@ def bake_cycle(on_selection=True, end=None): if not act: return - if debug: print('action', act.name) + if debug: print('action:', act.name) # obj.animation_data.action = act @@ -30,10 +30,10 @@ def bake_cycle(on_selection=True, end=None): ct = 0 ct_no_cycle = 0 - ## TODO calculate offset only once to avoid errors ! - all_keys = [k.co.x for fc in act.fcurves if not '.offset' in fc.data_path for k in fc.keyframe_points] - first = int(min(all_keys)) - last = int(max(all_keys)) + # all_keys = [k.co.x for fc in act.fcurves if not '.offset' in fc.data_path for k in fc.keyframe_points] + all_keys = fn.get_x_pos_of_visible_keys(obj, act) + first = min(all_keys) # int(min(all_keys)) + last = max(all_keys) # int(max(all_keys)) offset = last - first for fcu in act.fcurves: @@ -133,9 +133,6 @@ def bake_cycle(on_selection=True, end=None): else: setattr(new, att, val) current_offset += offset - - # FIXME on last cycle - re-add last keyframe to "close" the cycle - ct += 1 @@ -146,12 +143,12 @@ def bake_cycle(on_selection=True, end=None): 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)') + return ('ERROR', 'No fcurve with anim cycle found (on baked action)') obj.animation_data.action = org_action return ('ERROR', 'No fcurve with anim cycle found (back to unexpanded)') if not ct: - return ('ERROR', 'No fcurve treated (! action duplicated to _expand !)') + return ('ERROR', 'No fcurve treated (! action duplicated to _baked !)') # cleaning update fn.update_action(act) @@ -214,62 +211,6 @@ def step_path(): 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') - - # 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 - 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)) - - 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' - - # 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 keys" @@ -286,6 +227,17 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator): def invoke(self,context,event): self.end_frame = context.scene.frame_end + act = fn.get_obj_action(context.object) + if not act: + self.report({'ERROR'}, 'No Animation set on active object') + return {"CANCELLED"} + + act = fn.get_origin_action(act) + # all_keys = [k.co.x for fc in act.fcurves if not '.offset' in fc.data_path for k in fc.keyframe_points] + all_keys = fn.get_x_pos_of_visible_keys(context.object, act) # no offset and only visible bone layers + self.first = min(all_keys) # int(min(all_keys)) + self.last = max(all_keys) # int(max(all_keys)) + self.offset = self.last - self.first # 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 @@ -294,6 +246,9 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator): layout.use_property_split = True layout.label(text='End of cycle duplication') layout.prop(self, 'end_frame', text='End Frame') + iteration = ((self.end_frame - self.first) // self.offset) + 1 + real_end_cycle = iteration * self.offset + self.first + layout.label(text=f'Cycle will stop at frame: {real_end_cycle}') def execute(self, context): diff --git a/__init__.py b/__init__.py index 17c9be7..45a55f4 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "Auto Walk", "description": "Develop a walk/run cycles along a curve and pin feets", "author": "Samuel Bernou", - "version": (1, 3, 4), + "version": (1, 4, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index f474b60..e5d0374 100644 --- a/fn.py +++ b/fn.py @@ -202,6 +202,17 @@ def set_baked_action(obj): obj.animation_data.action = new_act return new_act +def get_origin_action(act): + '''Return original action if found, else return same''' + if '_baked' in act.name: + base_act_name = act.name.split('_baked')[0] + base_act = bpy.data.actions.get(base_act_name) + if base_act: + act = base_act + print(f'Using for action {base_act_name} as reference') + else: + print(f'No base action found (searching for {base_act_name})') + return act def get_curve_length(ob): '''Get a curve object, return a float representing world space length''' @@ -640,6 +651,9 @@ def create_cycle_modifiers(ob=None): continue if not '"' in fc.data_path: continue + # skip offset + if fc.data_path.endswith('.offset') and 'constraint' in fc.data_path: + continue b_name = fc.data_path.split('"')[1] if b_name.lower().startswith(('mch', 'def', 'org')): continue @@ -668,4 +682,42 @@ def go_edit_mode(ob, context=None): bpy.ops.object.mode_set(mode='OBJECT', toggle=False) ob.select_set(True) context.view_layer.objects.active = ob - bpy.ops.object.mode_set(mode='EDIT', toggle=False) \ No newline at end of file + bpy.ops.object.mode_set(mode='EDIT', toggle=False) + + +def get_visible_bones(ob): + '''Get name of all editable bones (unhided *and* on a visible bone layer''' + # visible bone layer index + visible_layers_indexes = [i for i, l in enumerate(ob.data.layers) if l] + # check if layers overlaps + visible_bones = [b for b in ob.data.bones \ + if not b.hide \ + if any(i for i, l in enumerate(b.layers) if l and i in visible_layers_indexes)] + + return visible_bones + + +def get_x_pos_of_visible_keys(ob, act): + '''Get an object and associated action + return x.coordinate of all fcurves.keys of all visible bones + ''' + + ## just skip offset + # return [k.co.x for fc in act.fcurves if not '.offset' in fc.data_path for k in fc.keyframe_points] + + ## skip offset + fcu related to invisible bones + viz_bones = get_visible_bones(ob) + visible_bone_names = [b.name for b in viz_bones] + + keys = [] + for fc in act.fcurves: + if '.offset' in fc.data_path: + continue + if not '"' in fc.data_path: + continue + if not fc.data_path.split('"')[1] in visible_bone_names: + continue + keys += [k.co.x for k in fc.keyframe_points] + + return keys + \ No newline at end of file