diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3a461..b01e44d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +1.7.0 + +- added: `Custom Pinning` Possibility to pin selectively lobc/rot x/y/z components +- fixed: bug on baking, some keys can be duplicated on same frame after (auto-clean) +- fixed: bug on animate path, when a channel on reference bone is not animated + 1.6.1 - changed: `on select` options using `pin feet` target selected bones instead (wuthout the option only target bones with `foot` in name) diff --git a/OP_animate_path.py b/OP_animate_path.py index 8647ff2..251a103 100644 --- a/OP_animate_path.py +++ b/OP_animate_path.py @@ -13,11 +13,12 @@ def get_bone_transform_at_frame(b, act, frame): for i in range(3): f = act.fcurves.find(f'pose.bones["{b.name}"].{channel}', index=i) if not f: - # print(frame, channel, 'not animated ! using current value') # Dbg - chan_list.append(getattr(b, channel)) # get current value since not animated + print(frame, channel, 'Not animated ! Using current value') # Dbg + chan_list.append(getattr(b, channel)[i]) # get current value since not animated continue chan_list.append(f.evaluate(frame)) + # print('chan_list: ', chan_list) # print(frame, b.name, channel, chan_list) # Dbg if channel == 'rotation_euler': transform[channel] = Euler(chan_list) diff --git a/OP_expand_cycle_step.py b/OP_expand_cycle_step.py index a2eac6a..10c147b 100644 --- a/OP_expand_cycle_step.py +++ b/OP_expand_cycle_step.py @@ -130,7 +130,7 @@ def bake_cycle(on_selection=True, end=None): return ('ERROR', 'No fcurve with cyclic modifier found (used to determine what to bake)') if not ct: - return ('ERROR', 'No fcurve treated (! action duplicated to _baked !)') + return ('ERROR', 'No fcurve affected (! action duplicated to _baked !)') # cleaning update fn.update_action(act) @@ -203,6 +203,7 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator): if not act: self.report({'ERROR'}, 'No Animation set on active object') return {"CANCELLED"} + self.starting_action = act 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] @@ -228,16 +229,28 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator): if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': + # context.object.animation_data.action = self.starting_action # restore act + return {"CANCELLED"} + + ## Clean overlap + act = fn.get_obj_action(context.object) + print('Action name:', act.name) + clean_error = fn.clean_fcurve(act) + if clean_error: + self.report({clean_error[0]}, clean_error[1]) + if clean_error[0] == 'ERROR': + # context.object.animation_data.action = self.starting_action # restore act return {"CANCELLED"} ## all followup is not needed when animating on one - + ## step or smooth path animation if not context.scene.anim_cycle_settings.linear: # CHAINED ACTION : step the path of the curve path err = step_path() if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': + # context.object.animation_data.action = self.starting_action # restore act return {"CANCELLED"} else: # Delete points in curve action between first and last and go LINEAR @@ -255,9 +268,8 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator): for k in timef.keyframe_points: k.interpolation = 'LINEAR' print(f'Anim path to linear : Deleted all keys ({keys_ct - 2}) on anim path except first and last') - - # CHAINED ACTION pin feet ?? : Step the path of the curve path + ## CHAINED ACTION pin feet ?? : Step the path of the curve path return {"FINISHED"} @@ -277,7 +289,8 @@ def pin_down_feets(): debug = fn.get_addon_prefs().debug scn = bpy.context.scene - on_selected = scn.anim_cycle_settings.expand_on_selected_bones + settings = scn.anim_cycle_settings + on_selected = settings.expand_on_selected_bones # Delete current action if its not the main one # create a new '_pinned' one act = fn.set_generated_action(obj) @@ -387,7 +400,7 @@ def pin_down_feets(): # iterate in reverse ranges (not really necessary) for r in reversed(contact_ranges): - print(f'range: {r}') + if debug >= 1: print(f'range: {r}') first = True for i in range(r[0], r[1]+1)[::-1]: # start from the end of the range # for i in range(r[0], r[1]+1): @@ -398,13 +411,37 @@ def pin_down_feets(): # bone_mat = obj.matrix_world @ pb.matrix.copy() first = False continue + + # TODO: don't insert non-needed keyframe in step mode ? # print(f'Apply on {b_name} at {i}') #-# assign previous matrix # pbl = pb.location.copy() - - pb.matrix = bone_mat # Exact same position + if not settings.custom_pin: + pb.matrix = bone_mat # Exact same position + else: + pbl = pb.location.copy() + pbr = pb.rotation_euler.copy() + + pb.matrix = bone_mat # Exact same position + + # Selectively restore initial bone transform + # per channel according to filters + if not settings.pin_loc_x: + setattr(pb.location, 'x', getattr(pbl, 'x')) + if not settings.pin_loc_y: + setattr(pb.location, 'y', getattr(pbl, 'y')) + if not settings.pin_loc_z: + setattr(pb.location, 'z', getattr(pbl, 'z')) + + if not settings.pin_rot_x: + setattr(pb.rotation_euler, 'x', getattr(pbr, 'x')) + if not settings.pin_rot_y: + setattr(pb.rotation_euler, 'y', getattr(pbr, 'y')) + if not settings.pin_rot_z: + setattr(pb.rotation_euler, 'z', getattr(pbr, 'z')) + ## maybe align on a specific axis # pb.location.x = pbl.x # dont touch x either @@ -421,12 +458,8 @@ def pin_down_feets(): ## insert keyframe pb.keyframe_insert('location') - # only touched Y location - pb.keyframe_insert('rotation_euler') - - # if i == r[1]+1: # (last key) in normal - # if i == r[0]: # (last key) in reverse - # continue + if not settings.custom_pin or any((settings.pin_rot_x, settings.pin_rot_y, settings.pin_rot_z)): + pb.keyframe_insert('rotation_euler') # k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER' ct += 1 diff --git a/__init__.py b/__init__.py index f3bed84..a3e2cca 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, 6, 1), + "version": (1, 7, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index ca56f02..bdf4be3 100644 --- a/fn.py +++ b/fn.py @@ -773,4 +773,33 @@ def get_x_pos_of_visible_keys(ob, act): keys += [k.co.x for k in fc.keyframe_points] return keys - \ No newline at end of file + +# def clear_duplicated_keys_in_fcurves() +def clean_fcurve(action=None): + '''clear duplicated keys at same frame in fcurves''' + + cleaned = 0 + problems = 0 + if action is None: + bpy.context.object.animation_data.action + for fcu in action.fcurves: + prev = None + for k in reversed(fcu.keyframe_points): + if not prev: + prev = k + continue + if prev.co.x == k.co.x: + if prev.co.y == k.co.y: + print(f'autoclean: 2 idential keys at {k.co.x} ', fcu.data_path, fcu.array_index) + fcu.keyframe_points.remove(prev) + cleaned += 1 + else: + print(f'/!\ 2 keys with different value at {k.co.x} ! : ', fcu.data_path, fcu.array_index) + problems += 1 + prev = k + + if problems: + return ('ERROR', f'{problems} keys are overlapping (see console)') + if cleaned: + return ('WARNING', f'{cleaned} keys autocleaned') + diff --git a/panels.py b/panels.py index 64c3a6f..7f776b3 100644 --- a/panels.py +++ b/panels.py @@ -117,6 +117,21 @@ class AW_PT_walk_cycle_anim_panel(bpy.types.Panel): txt = 'Bake keys' if settings.linear else 'Bake keys and step path' col.operator('autowalk.bake_cycle_and_step', text=txt, icon='SHAPEKEY_DATA') + # Custom Axis Pinning + subox = col.box() + subox.prop(settings, "custom_pin", text='Custom Pinning') + if settings.custom_pin: + row=subox.row(align=True) + row.label(text='Location') + row.prop(settings, 'pin_loc_x', text='X', toggle=True) + row.prop(settings, 'pin_loc_y', text='Y', toggle=True) + row.prop(settings, 'pin_loc_z', text='Z', toggle=True) + row=subox.row(align=True) + row.label(text='Rotation') + row.prop(settings, 'pin_rot_x', text='X', toggle=True) + row.prop(settings, 'pin_rot_y', text='Y', toggle=True) + row.prop(settings, 'pin_rot_z', text='Z', toggle=True) + # Pin feet col.operator('autowalk.pin_feets', text='Pin feets', icon='PINNED') diff --git a/properties.py b/properties.py index e43ae9d..91f32aa 100644 --- a/properties.py +++ b/properties.py @@ -57,6 +57,25 @@ class AW_PG_settings(bpy.types.PropertyGroup) : ), ) + custom_pin : bpy.props.BoolProperty( + name="Custom Pinning", description="Pin only specific axis\ + \nElse pin all location and rotation", + default=False, options={'HIDDEN'}) + + pin_loc_x : bpy.props.BoolProperty( + name="Pin Loc X", description="Pin bones location X", default=True, options={'HIDDEN'}) + pin_loc_y : bpy.props.BoolProperty( + name="Pin Loc Y", description="Pin bones location Y", default=True, options={'HIDDEN'}) + pin_loc_z : bpy.props.BoolProperty( + name="Pin Loc Z", description="Pin bones location Z", default=True, options={'HIDDEN'}) + + pin_rot_x : bpy.props.BoolProperty( + name="Pin Rot X", description="Pin bones rotation X", default=True, options={'HIDDEN'}) + pin_rot_y : bpy.props.BoolProperty( + name="Pin Rot Y", description="Pin bones rotation Y", default=True, options={'HIDDEN'}) + pin_rot_z : bpy.props.BoolProperty( + name="Pin Rot Z", description="Pin bones rotation Z", default=True, options={'HIDDEN'}) + """ ## foot axis not needed (not always aligned with character direction) foot_axis : EnumProperty(