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 bakingmaster
parent
072a483188
commit
d4ba199b63
|
@ -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)
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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
40
fn.py
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue