fix anim path and bake - add custom axis pinning

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
master
Pullusb 2022-10-15 19:13:50 +02:00
parent b698f003d9
commit a1d7aff92c
7 changed files with 121 additions and 18 deletions

View File

@ -1,5 +1,11 @@
# Changelog # 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 1.6.1
- changed: `on select` options using `pin feet` target selected bones instead (wuthout the option only target bones with `foot` in name) - changed: `on select` options using `pin feet` target selected bones instead (wuthout the option only target bones with `foot` in name)

View File

@ -13,11 +13,12 @@ def get_bone_transform_at_frame(b, act, frame):
for i in range(3): for i in range(3):
f = act.fcurves.find(f'pose.bones["{b.name}"].{channel}', index=i) f = act.fcurves.find(f'pose.bones["{b.name}"].{channel}', index=i)
if not f: if not f:
# print(frame, channel, 'not animated ! using current value') # Dbg print(frame, channel, 'Not animated ! Using current value') # Dbg
chan_list.append(getattr(b, channel)) # get current value since not animated chan_list.append(getattr(b, channel)[i]) # get current value since not animated
continue continue
chan_list.append(f.evaluate(frame)) chan_list.append(f.evaluate(frame))
# print('chan_list: ', chan_list)
# print(frame, b.name, channel, chan_list) # Dbg # print(frame, b.name, channel, chan_list) # Dbg
if channel == 'rotation_euler': if channel == 'rotation_euler':
transform[channel] = Euler(chan_list) transform[channel] = Euler(chan_list)

View File

@ -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)') return ('ERROR', 'No fcurve with cyclic modifier found (used to determine what to bake)')
if not ct: if not ct:
return ('ERROR', 'No fcurve treated (! action duplicated to _baked !)') return ('ERROR', 'No fcurve affected (! action duplicated to _baked !)')
# cleaning update # cleaning update
fn.update_action(act) fn.update_action(act)
@ -203,6 +203,7 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator):
if not act: if not act:
self.report({'ERROR'}, 'No Animation set on active object') self.report({'ERROR'}, 'No Animation set on active object')
return {"CANCELLED"} return {"CANCELLED"}
self.starting_action = act
act = fn.get_origin_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] # 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: if err:
self.report({err[0]}, err[1]) self.report({err[0]}, err[1])
if err[0] == 'ERROR': 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"} return {"CANCELLED"}
## all followup is not needed when animating on one ## all followup is not needed when animating on one
## step or smooth path animation
if not context.scene.anim_cycle_settings.linear: if not context.scene.anim_cycle_settings.linear:
# CHAINED ACTION : step the path of the curve path # CHAINED ACTION : step the path of the curve path
err = step_path() err = step_path()
if err: if err:
self.report({err[0]}, err[1]) self.report({err[0]}, err[1])
if err[0] == 'ERROR': if err[0] == 'ERROR':
# context.object.animation_data.action = self.starting_action # restore act
return {"CANCELLED"} return {"CANCELLED"}
else: else:
# Delete points in curve action between first and last and go LINEAR # 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: for k in timef.keyframe_points:
k.interpolation = 'LINEAR' k.interpolation = 'LINEAR'
print(f'Anim path to linear : Deleted all keys ({keys_ct - 2}) on anim path except first and last') 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"} return {"FINISHED"}
@ -277,7 +289,8 @@ def pin_down_feets():
debug = fn.get_addon_prefs().debug debug = fn.get_addon_prefs().debug
scn = bpy.context.scene 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 # Delete current action if its not the main one
# create a new '_pinned' one # create a new '_pinned' one
act = fn.set_generated_action(obj) act = fn.set_generated_action(obj)
@ -387,7 +400,7 @@ def pin_down_feets():
# iterate in reverse ranges (not really necessary) # iterate in reverse ranges (not really necessary)
for r in reversed(contact_ranges): for r in reversed(contact_ranges):
print(f'range: {r}') if debug >= 1: print(f'range: {r}')
first = True 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)[::-1]: # start from the end of the range
# for i in range(r[0], r[1]+1): # 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() # bone_mat = obj.matrix_world @ pb.matrix.copy()
first = False first = False
continue continue
# TODO: don't insert non-needed keyframe in step mode ?
# print(f'Apply on {b_name} at {i}') # print(f'Apply on {b_name} at {i}')
#-# assign previous matrix #-# assign previous matrix
# pbl = pb.location.copy() # pbl = pb.location.copy()
if not settings.custom_pin:
pb.matrix = bone_mat # Exact same position 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 ## maybe align on a specific axis
# pb.location.x = pbl.x # dont touch x either # pb.location.x = pbl.x # dont touch x either
@ -421,12 +458,8 @@ def pin_down_feets():
## insert keyframe ## insert keyframe
pb.keyframe_insert('location') pb.keyframe_insert('location')
# only touched Y location if not settings.custom_pin or any((settings.pin_rot_x, settings.pin_rot_y, settings.pin_rot_z)):
pb.keyframe_insert('rotation_euler') pb.keyframe_insert('rotation_euler')
# if i == r[1]+1: # (last key) in normal
# if i == r[0]: # (last key) in reverse
# continue
# k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER' # k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER'
ct += 1 ct += 1

View File

@ -4,7 +4,7 @@ bl_info = {
"name": "Auto Walk", "name": "Auto Walk",
"description": "Develop a walk/run cycles along a curve and pin feets", "description": "Develop a walk/run cycles along a curve and pin feets",
"author": "Samuel Bernou", "author": "Samuel Bernou",
"version": (1, 6, 1), "version": (1, 7, 0),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "View3D", "location": "View3D",
"warning": "", "warning": "",

31
fn.py
View File

@ -773,4 +773,33 @@ def get_x_pos_of_visible_keys(ob, act):
keys += [k.co.x for k in fc.keyframe_points] keys += [k.co.x for k in fc.keyframe_points]
return keys return keys
# 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')

View File

@ -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' txt = 'Bake keys' if settings.linear else 'Bake keys and step path'
col.operator('autowalk.bake_cycle_and_step', text=txt, icon='SHAPEKEY_DATA') 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 # Pin feet
col.operator('autowalk.pin_feets', text='Pin feets', icon='PINNED') col.operator('autowalk.pin_feets', text='Pin feets', icon='PINNED')

View File

@ -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 not needed (not always aligned with character direction)
foot_axis : EnumProperty( foot_axis : EnumProperty(