parent
b05b0e2b6c
commit
29623d4c3b
|
@ -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,24 +157,37 @@ 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 = frame_duration
|
||||
curve.data.keyframe_insert('eval_time', frame=anim_frame + frame_duration)
|
||||
# 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)
|
||||
|
||||
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':
|
||||
fcu = ob.animation_data.action.fcurves.find(offset_data_path)
|
||||
if fcu:
|
||||
for k in fcu.keyframe_points:
|
||||
k.interpolation = 'LINEAR'
|
||||
|
||||
|
@ -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')
|
||||
|
||||
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"}
|
||||
|
||||
curve, _const = fn.get_follow_curve_from_armature(context.object)
|
||||
if isinstance(curve, str):
|
||||
self.report({curve}, _const)
|
||||
return {"CANCELLED"}
|
||||
# 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(curve.data)
|
||||
|
||||
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
|
||||
|
|
|
@ -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}')
|
||||
|
||||
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]
|
||||
|
||||
b_name = fcu.data_path.split('"')[1]
|
||||
# print('b_name: ', b_name, fcu.is_valid)
|
||||
# skip same data path with different array index to avoid multiple iteration
|
||||
if fcu.data_path in viewed_data_paths:
|
||||
continue
|
||||
|
||||
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')
|
||||
|
||||
k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER'
|
||||
# only touched Y location
|
||||
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'
|
||||
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)
|
||||
|
|
39
README.md
39
README.md
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
89
fn.py
89
fn.py
|
@ -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,22 +312,91 @@ 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
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue