pinning feet in reverse

0.5.0

- pin feet working
master
Pullusb 2022-04-08 19:35:20 +02:00
parent b05b0e2b6c
commit 29623d4c3b
6 changed files with 246 additions and 108 deletions

View File

@ -63,10 +63,10 @@ def anim_path_from_translate():
# use original action as ref
if '_expanded' in act.name:
base_act_name = act.name.split('_expanded')[0]
base_act = bpy.data.action.get(base_act_name)
base_act = bpy.data.actions.get(base_act_name)
if base_act:
act = base_act
print(f'Using for {base_act_name} as reference')
print(f'Using for action {base_act_name} as reference')
else:
print(f'No base action found (searching for {base_act_name})')
@ -157,26 +157,39 @@ def anim_path_from_translate():
frame_duration = int(steps * move_frame)
## Clear 'eval_time' keyframe before creating new ones # curve.data.animation_data_clear() # too much.. delete only eval_time
if curve.data.animation_data and curve.data.animation_data.action:
for fcu in curve.data.animation_data.action.fcurves:
if fcu.data_path == 'eval_time':
curve.data.animation_data.action.fcurves.remove(fcu)
break
const = root.constraints.get("Follow Path")
if not const:
return 'ERROR', f'No "Follow Path" constraint on bone "{root.name}"'
offset_data_path = f'pose.bones["{root.name}"].constraints["{const.name}"].offset'
fcu = ob.animation_data.action.fcurves.find(offset_data_path)
if fcu:
ob.animation_data.action.fcurves.remove(fcu)
## add eval time animation on curve
anim_frame = settings.start_frame
curve.data.path_duration = frame_duration
curve.data.eval_time = 0
curve.data.keyframe_insert('eval_time', frame=anim_frame) # , options={'INSERTKEY_AVAILABLE'}
# curve.data.eval_time = 0
# curve.data.keyframe_insert('eval_time', frame=anim_frame) # , options={'INSERTKEY_AVAILABLE'}
const.offset = 0
const.keyframe_insert('offset', frame=anim_frame)
curve.data.eval_time = frame_duration
curve.data.keyframe_insert('eval_time', frame=anim_frame + frame_duration)
# curve.data.eval_time = frame_duration
# curve.data.keyframe_insert('eval_time', frame=anim_frame + frame_duration)
const.offset = -frame_duration # negative (offset time rewinding so character move forward)
const.keyframe_insert('offset', frame=anim_frame + frame_duration)
## all to linear (will be set to CONSTANT at the moment of sampling)
for fcu in curve.data.animation_data.action.fcurves:
if fcu.data_path == 'eval_time':
for k in fcu.keyframe_points:
k.interpolation = 'LINEAR'
fcu = ob.animation_data.action.fcurves.find(offset_data_path)
if fcu:
for k in fcu.keyframe_points:
k.interpolation = 'LINEAR'
## set all to constant
# for k in t_fcu.keyframe_points:
@ -219,44 +232,53 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type in ('ARMATURE', 'CURVE')
return context.object and context.object.type == 'ARMATURE' # in ('ARMATURE', 'CURVE')
val : bpy.props.FloatProperty(name='End key value')
def invoke(self, context, event):
# check animation data of curve
# self.pref = fn.get_addon_prefs()
curve = bpy.context.scene.anim_cycle_settings.path_to_follow
if not curve:
if context.object.type != 'ARMATURE':
self.report({'ERROR'}, 'no curve targeted in "Path" field')
return {"CANCELLED"}
curve, _const = fn.get_follow_curve_from_armature(context.object)
if isinstance(curve, str):
self.report({curve}, _const)
return {"CANCELLED"}
ob = context.object
root_name = fn.get_root_name()
root = ob.pose.bones.get(root_name)
if not root:
self.report({'ERROR'}, f'no bone {root_name} found in {ob.name}')
return {"CANCELLED"}
self.act = fn.get_obj_action(curve.data)
# TODO replace fcurve getter by fn.get_offset_fcu(root)
# self.fcu = fn.get_offset_fcu(root)
# if isinstance(self.fcu, str):
# self.report({'ERROR'}, self.fcu)
# return {"CANCELLED"}
self.act = fn.get_obj_action(ob)
if not self.act:
self.report({'ERROR'}, f'No action on {curve.name} data')
self.report({'ERROR'}, f'No action on {ob.name} data')
return {"CANCELLED"}
# if '_expanded' in self.act.name:
# self.report({'WARNING'}, f'Action is expanded')
self.fcu = None
for fcu in self.act.fcurves:
if fcu.data_path == 'eval_time':
self.fcu = fcu
break
const = root.constraints.get("Follow Path")
if not const:
self.report({'ERROR'}, f'No "Follow Path" constraint on bone "{root.name}"')
return {"CANCELLED"}
offset_data_path = f'pose.bones["{root.name}"].constraints["{const.name}"].offset'
if not self.fcu or not len(self.fcu.keyframe_points):
self.report({'ERROR'}, f'No eval_time animated on {curve.name} data action (or no keys)')
self.fcu = self.act.fcurves.find(offset_data_path)
if not self.fcu:
self.report({'ERROR'}, f'No fcurve at datapath: {offset_data_path}')
return {"CANCELLED"}
if not len(self.fcu.keyframe_points):
self.report({'ERROR'}, f'No keys on fcurve {self.fcu.data_path}')
return {"CANCELLED"}
if len(self.fcu.keyframe_points) > 2:
self.report({'WARNING'}, f'{curve.name} eval_time has {len(self.fcu.keyframe_points)} keyframe (should just have 2 to redefine speed)')
self.report({'WARNING'}, f'{ob.name} offset anim has {len(self.fcu.keyframe_points)} keyframe (should just have 2 to redefine speed)')
self.k = self.fcu.keyframe_points[-1]
self.val = self.init_ky = self.k.co.y

View File

@ -76,22 +76,22 @@ def bake_cycle(on_selection=True):
current_offset = offset = last - first
keys_num = len(fcu_kfs)
if debug: print(keys_num)
if debug >= 2: print(keys_num)
if keys_num <= 1:
if debug: print(b_name, f'{keys_num} key')
if debug >= 2: print(b_name, f'{keys_num} key')
continue
## ! important: delete last after computing offset IF cycle have first frame repeatead as last !
fcu_kfs.pop()
# print('offset: ', offset)
if debug: print('keys', len(fcu_kfs))
if debug >= 2: print('keys', len(fcu_kfs))
## expand to end frame
end = bpy.context.scene.frame_end # maybe add possibility define target manually
iterations = ((end - last) // offset) + 1
if debug: print('iterations: ', iterations)
if debug >= 2: print('iterations: ', iterations)
for _i in range(int(iterations)):
for kf in fcu_kfs:
# create a new key, adding offset to keys
@ -240,18 +240,21 @@ def pin_down_feets():
if obj.type != 'ARMATURE':
print('ERROR', 'active is not an armature type')
return
debug = fn.get_addon_prefs().debug
scn = bpy.context.scene
# Delete current action if its not the main one
# create a new '_autogen' one
act = fn.set_generated_action(obj)
if not act:
return ('ERROR', f'No action on {obj.name}')
print('action', act.name)
if debug:
print('action', act.name)
act = obj.animation_data.action
to_change_list = [
(bpy.context.scene, 'frame_current', '_undefined'), # use '_undefined' when no value to assign for now
(bpy.context.scene, 'frame_current'),
(bpy.context.scene.render, 'use_simplify', True),
(bpy.context.scene.render, 'simplify_subdivision', 0),
]
@ -277,51 +280,81 @@ def pin_down_feets():
t0 = time()
ct = 0
done = {}
viewed_data_paths = []
for fcu in act.fcurves:
# check only location
if not fcu.data_path.endswith('.location'):
continue
# prop = fcu.data_path.split('.')[-1]
# skip same data path with different array index to avoid multiple iteration
if fcu.data_path in viewed_data_paths:
continue
b_name = fcu.data_path.split('"')[1]
# print('b_name: ', b_name, fcu.is_valid)
viewed_data_paths.append(fcu.data_path)
prop = fcu.data_path.split('.')[-1]
b_name = fcu.data_path.split('"')[1] # print('b_name: ', b_name, fcu.is_valid)
## TEST : only foot bones
if not 'foot' in b_name:
continue
pb = obj.pose.bones.get(b_name)
if not pb:
print(f'{b_name} is invalid')
continue
contact_ranges = []
start_contact = None
prev = None
for k in fcu.keyframe_points:
if k.type == 'EXTREME':
bpy.context.scene.frame_set(int(k.co[0]))
prev = k
if start_contact is None:
start_contact=k.co[0]
start_contact = int(k.co.x)
continue
else:
if start_contact is not None:
# print(f'contact range {start_contact} - {k.co.x:.0f}')
if prev:
contact_ranges.append((start_contact, int(prev.co.x)))
start_contact = None
prev = None
if not contact_ranges:
if debug >= 2: print(f'SKIP (no extreme keys): {b_name} > {prop}')
continue
if debug: print(f'fcurve: {b_name} > {prop}')
for r in reversed(contact_ranges): # iterate in reverse ranges (not really necessary)
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):
bpy.context.scene.frame_set(i)
if first:
# record coordinate relative to referent object (or world coord)
bone_mat = pb.matrix.copy()
# bone_mat = obj.matrix_world @ pb.matrix.copy()
first = False
continue
if b_name in done.keys():
if k.co[0] in done[b_name]:
continue
else:
# mark as treated (all curve of this bone at this time)
done[b_name] = [k.co[0]]
#-# Insert keyframe to match Hold position
# print(f'Apply on {b_name} at {k.co[0]}')
# print(f'Apply on {b_name} at {i}')
#-# assign previous matrix
pbl = pb.location.copy()
# l, _r, _s = bone_mat.decompose()
# pbl = pb.location.copy()
pb.matrix = bone_mat # Exact same position
## maybe align on a specific axis
# pb.location.x = pbl.x # dont touch x either
pb.location.z = pbl.z
#pb.location.z = pbl.z # Z is not necessarily up in local axis, need to check first
# pb.location.y = l.y (weirdly not working)
# bpy.context.view_layer.update()
@ -331,33 +364,23 @@ def pin_down_feets():
## insert keyframe
pb.keyframe_insert('location')
# only touched Y location
# pb.keyframe_insert('rotation_euler')
pb.keyframe_insert('rotation_euler')
k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER'
# 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'
ct += 1
else:
if start_contact is not None:
# print('fcu.data_path: ', fcu.data_path, fcu.array_index)
# print(f'{b_name} contact range {start_contact} - {k.co[0]}')
start_contact = None
# print(i, fcu.data_path, fcu.array_index)
# print('time', k.co[0], '- value', k.co[1])
#k.handle_left
#k.handle_right
##change handler type ([FREE, VECTOR, ALIGNED, AUTO, AUTO_CLAMPED], default FREE)
#k.handle_left_type = 'AUTO_CLAMPED'
#k.handle_right_type = 'AUTO_CLAMPED'
print(f'--\n{ct} keys changed in {time()-t0:.2f}s\n--') # fcurves treated in
print(f'--\n{ct} keys changed/added in {time()-t0:.2f}s\n--') # fcurves treated in
## RESTORE
# without >> 433 keys changed in 29.15s
# with all collection excluded >> 433 keys changed in 25.00s
# with simplify >> 9.57s
# with simplify set to 0 >> 9.57s
tmp_col.objects.unlink(obj)
bpy.data.collections.remove(tmp_col)

View File

@ -22,23 +22,44 @@ sequencial set of tools:
- Expand anim cycle and step curve animation
- Pin feet on ground (use contact keys marked as 'EXTREME' for each feets)
# TODO :
- create nurb path instead of curve
- align curve to root ?
- Smoothing keys after last freezed to avoid too much gap "pose click".
### Where ?
Sidebar > Anim > unfold anim cycle
### TODO
- pin feets:
- iterate in reverse in keys when pinning so last foot position is correct
- create intermediate keys (at each frame when necessary) to prevent lateral sliding on curved path
(maybe expose as an option... not needed if path is straight for example... or auto detect if path is full straight at the moment of pin ops)
- Expose methods to go back in action history
*things to consider*:
- Expose foot ?
- Store path animation on a separate action (but that mean NLA hadto be used every time)
Bonus:
- Use position A-B method to generate curve (with retimed animation to prioritize speed over fidelity)
- auto-determine foot bone to use for distance reference
- create nurb path instead of curve
- Smoothing keys after last freezed to avoid too much gap "pose click".
<!--
### DONE
- align curve to root
- Put curve forward motion on bones modifier's offset value as negative time offset (instead of using curve )
-->
---
## Changelog:
0.5.0
- pin feet working
0.4.2
- context manager for `expand cycle step` store / restore

View File

@ -2,7 +2,7 @@ bl_info = {
"name": "Unfold Anim Cycle",
"description": "Anim tools to develop walk/run cycles along a curve",
"author": "Samuel Bernou",
"version": (0, 4, 3),
"version": (0, 5, 0),
"blender": (3, 0, 0),
"location": "View3D",
"warning": "WIP",

93
fn.py
View File

@ -1,4 +1,5 @@
import bpy
import bpy_types
import re
import numpy as np
from mathutils import Matrix, Vector, Color
@ -55,8 +56,7 @@ def get_gnd():
def get_follow_curve_from_armature(arm):
"""Return curve and constraint or a tuple of string ('error', 'message')
"""
pref = get_addon_prefs()
name = pref.tgt_bone
name = get_root_name()
parents = []
# root = b.id_data.pose.bones.get(name)
@ -312,26 +312,95 @@ def update_action(act):
if area.type == 'GRAPH_EDITOR':
area.tag_redraw()
def get_offset_fcu(act=None):
'''Get an action, object, pose_bone or a bone constraint (if nothing is passed use active object)
return offset fcurve or a string describing error
'''
ob = None
bone_name = None
const_name = None
if act is None:
if not bpy.context.object:
return 'No active object'
act = bpy.context.object
if isinstance(act, bpy.types.FollowPathConstraint):
ob = act.id_data
const = act
bones = [b for b in ob.pose.bones for c in b.constraints if b.constraints if c == act]
if not bones:
return f'no bone found with constraint {ob.name}'
bone = bones[0]
bone_name = bone.name
const_name = const.name
if isinstance(act, bpy_types.PoseBone):
bone = act
bone_name = act.name
ob = act = act.id_data # fall_back to armature object
if not const_name:
consts = [c for c in bone.constraints if isinstance(c, bpy.types.FollowPathConstraint)]
if not consts:
return f'no follow path constraint on bone {bone_name}'
const_name = consts[0].name
if isinstance(act, bpy.types.Object):
ob = act
if not ob.animation_data:
return f'{ob.name} has no animation_data'
act = ob.animation_data
if isinstance(act, bpy.types.AnimData):
ob = act.id_data
if not act.action:
return f'{ob.name} has animation_data but no action'
act = act.action
if bone_name and const_name:
offset_data_path = f'pose.bones["{bone_name}"].constraints["{const_name}"].offset'
fcu = act.fcurves.find(offset_data_path)
if not fcu:
return f'No fcurve found with data_path {offset_data_path}'
return fcu
# bone_name = get_root_name()
# find from determined action
fcus = [fcu for fcu in act.fcurves if all(x in fcu.data_path for x in ('pose.bones', 'constraints', 'offset'))]
if not fcus:
return f'no offset fcurves found for: {act.name}'
if len(fcus) > 1:
print(f'/!\ multiple fcurves seem to have a follow path constraint')
return fcus[0]
### --- context manager - store / restore
class attr_set():
'''Receive a list of tuple [(data_path:python_obj, "attribute":str, "wanted value":str)]
before with statement : Store existing values, assign wanted value
after with statement: Restore values to their old values
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
entering with-statement : Store existing values, assign wanted value (if any)
exiting with-statement: Restore values to their old values
'''
def __init__(self, attrib_list):
self.store = []
for prop, attr, new_val in attrib_list:
# item = (prop, attr, [new_val])
for item in attrib_list:
prop, attr = item[:2]
self.store.append( (prop, attr, getattr(prop, attr)) )
if new_val == '_undefined': # None -> what if we want to apply None state ?
continue
setattr(prop, attr, new_val)
if len(item) >= 3:
setattr(prop, attr, item[2])
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
for prop, attr, old_val in self.store:
setattr(prop, attr, old_val)

View File

@ -6,10 +6,13 @@ class UAC_addon_prefs(bpy.types.AddonPreferences):
bl_idname = __name__.split('.')[0] # or with: os.path.splitext(__name__)[0]
# some_bool_prop to display in the addon pref
debug : bpy.props.BoolProperty(
debug : bpy.props.IntProperty(
name='Debug',
description="Enable Debug prints",
default=False)
description="Enable Debug prints\n\
0 = no prints\n\
1 = basic\n\
2 = full prints",
default=1)
tgt_bone : bpy.props.StringProperty(
name="Constrained Pose bone name", default='world',