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
master
Pullusb 2022-04-21 17:44:35 +02:00
parent 072a483188
commit d4ba199b63
5 changed files with 205 additions and 21 deletions

View File

@ -1,5 +1,11 @@
# Changelog # 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 1.3.2
- removed: ground feet (added initial support for override compatibility wip) - removed: ground feet (added initial support for override compatibility wip)

View File

@ -1,3 +1,4 @@
from doctest import SKIP
import bpy, re import bpy, re
from . import fn from . import fn
from time import time from time import time
@ -6,8 +7,9 @@ from time import time
# - Bake cycle modifier keys -chained with- step the animation path # - Bake cycle modifier keys -chained with- step the animation path
# - Pin the feet (separated ops) # - Pin the feet (separated ops)
def bake_cycle(on_selection=True): def bake_cycle(on_selection=True, end=None):
print(fn.helper()) print(fn.helper())
end = end or bpy.context.scene.frame_end
debug = fn.get_addon_prefs().debug debug = fn.get_addon_prefs().debug
obj = bpy.context.object obj = bpy.context.object
if obj.type != 'ARMATURE': if obj.type != 'ARMATURE':
@ -90,7 +92,8 @@ def bake_cycle(on_selection=True):
## expand to end frame ## expand to end frame
# maybe add possibility define target manually ? # 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 iterations = ((end - last) // offset) + 1
if debug >= 2: print('iterations: ', iterations) if debug >= 2: print('iterations: ', iterations)
for _i in range(int(iterations)): for _i in range(int(iterations)):
@ -108,10 +111,10 @@ def bake_cycle(on_selection=True):
ct += 1 ct += 1
if ct_fcu == ct_no_cycle: # skipped because no cycle exists if ct_fcu == ct_no_cycle: # skipped because no cycle exists
rexpand = re.compile(r'_baked\.?\d{0,3}$') re_baked = re.compile(r'_baked\.?\d{0,3}$')
if rexpand.search(act.name): if re_baked.search(act.name):
# is an autogenerated one # 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) org_action = bpy.data.actions.get(org_action_name)
if not org_action: if not org_action:
return ('ERROR', 'No fcurve with anim cycle found (on expanded 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) fn.update_action(act)
print('end of anim cycle keys baking') print('end of anim cycle keys baking')
# C.scene.frame_current = org_frame # C.scene.frame_current = org_frame
# detect last key in contact # detect last key in contact
def step_path(): def step_path():
'''Step the path anim of the curve to constant''' '''Step the path anim of the curve to constant'''
print(fn.helper()) 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 obj = bpy.context.object
if obj.type != 'ARMATURE': if obj.type != 'ARMATURE':
return ('ERROR', 'active is not an armature type') return ('ERROR', 'active is not an armature type')
@ -141,7 +199,8 @@ def step_path():
return ('ERROR', 'No constraints found') return ('ERROR', 'No constraints found')
act = fn.get_obj_action(obj) act = fn.get_obj_action(obj)
if not act: return if not act:
return
# CHANGE - removed int from frame # CHANGE - removed int from frame
# keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points] # 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) # cleaning update (might not be needed here)
fn.update_action(act) fn.update_action(act)
print('end of step_anim') print('end of step_anim')
'''
class AW_OT_bake_cycle_and_step(bpy.types.Operator): class AW_OT_bake_cycle_and_step(bpy.types.Operator):
bl_idname = "autowalk.bake_cycle_and_step" bl_idname = "autowalk.bake_cycle_and_step"
bl_label = "Bake key and step path" bl_label = "Bake keys"
bl_description = "Bake the key and step the animation path according to those key\ 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)" \n(duplicate to a new 'baked' action)"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@ -193,9 +253,22 @@ class AW_OT_bake_cycle_and_step(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'ARMATURE' 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): 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: if err:
self.report({err[0]}, err[1]) self.report({err[0]}, err[1])
if err[0] == 'ERROR': if err[0] == 'ERROR':

View File

@ -33,7 +33,83 @@ class AW_OT_nla_key_speed(bpy.types.Operator):
return context.object and context.object.type == 'ARMATURE' return context.object and context.object.type == 'ARMATURE'
def execute(self, context): 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: if not nla_strip:
self.report({'ERROR'}, 'no active NLA strip') self.report({'ERROR'}, 'no active NLA strip')
return {"CANCELLED"} return {"CANCELLED"}
@ -60,9 +136,12 @@ class AW_OT_nla_key_speed(bpy.types.Operator):
k.interpolation = 'LINEAR' k.interpolation = 'LINEAR'
return {"FINISHED"} return {"FINISHED"}
'''
classes=( classes=(
AW_OT_nla_key_speed, AW_OT_nla_key_speed,
AW_OT_nla_remove_key_speed,
# AW_OT_nla_reset_key_speed,
) )
def register(): def register():

40
fn.py
View File

@ -105,17 +105,41 @@ def get_follow_curve_from_armature(arm):
# --- ACTIONS # --- 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()) print(helper())
act = obj.animation_data act = ob.animation_data
if not act: if not act:
print('ERROR', f'no animation data on {obj.name}') print('ERROR', f'no animation data on {ob.name}')
return return
act = act.action act = act.action
if not act: 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
return act return act
def set_generated_action(obj): def set_generated_action(obj):
@ -155,12 +179,12 @@ def set_baked_action(obj):
''' '''
print(helper()) print(helper())
rexpand = re.compile(r'_baked\.?\d{0,3}$') re_baked = re.compile(r'_baked\.?\d{0,3}$')
act = obj.animation_data.action act = obj.animation_data.action
if rexpand.search(act.name): if re_baked.search(act.name):
# is an autogenerated one # 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) org_action = bpy.data.actions.get(org_action_name)
if not org_action: if not org_action:
print('ERROR', f'{org_action_name} not found') print('ERROR', f'{org_action_name} not found')
@ -439,7 +463,7 @@ def update_action(act):
# update fcurves # update fcurves
for fcu in act.fcurves: for fcu in act.fcurves:
fcu.update() fcu.update()
# redraw graph area # redraw graph area
for window in bpy.context.window_manager.windows: for window in bpy.context.window_manager.windows:
screen = window.screen screen = window.screen

View File

@ -166,7 +166,9 @@ class AW_PT_nla_tools_panel(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
# layout.label(text='Retime Tools') # 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')