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 animatedmaster
parent
b698f003d9
commit
a1d7aff92c
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
31
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]
|
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')
|
||||||
|
|
||||||
|
|
15
panels.py
15
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'
|
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')
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue