auto_walk/OP_wrap_anim.py

398 lines
13 KiB
Python

## Wrap an expanded animation to an on place loop
## Define a bone that will be offseted to stay fix above root
import bpy
import math
from mathutils import Vector, Matrix, Euler
def get_bone_transform_at_frame(b, act, frame):
'''Find every loc, rot, scale values at given frame'''
transform = {}
for channel in ('location', 'rotation_euler', 'scale'):
chan_list = []
for i in range(3):
f = act.fcurves.find(f'pose.bones["{b.name}"].{channel}', index=i)
if not f:
# print(frame, channel, 'not animated ! using current value') # Dbg
chan_list.append(getattr(b, channel)) # get current value since not animated
continue
chan_list.append(f.evaluate(frame))
# print(frame, b.name, channel, chan_list) # Dbg
if channel == 'rotation_euler':
transform[channel] = Euler(chan_list)
else:
transform[channel] = Vector(chan_list)
return transform # loc, rot, scale
def dopesheet_summary(obj_list=None):
if obj_list is None:
obj_list = bpy.context.selected_objects
elif isinstance(obj_list, bpy.types.Object):
obj_list = [obj_list]
start = bpy.context.scene.frame_start
end = bpy.context.scene.frame_end
frames = []
for obj in obj_list:
for fcurve in obj.animation_data.action.fcurves:
for keyframe_point in fcurve.keyframe_points:
x, y = keyframe_point.co
if x >= start and x <= end and x not in frames:
frames.append(x)
## for returning an int (import math)
#frames.append((math.ceil(x)))
return sorted(frames)
def pose_bone_frame_summary(bone_list=None, filter_channel=None) -> list[int]:
'''Return frame numbers where bone(s) have keys
:bone_list: a single pose bone or a list of pose bone, if not porvided, context.selected_pose_bone
:filter_channel: str or tuple of str to filter channel (ex: 'location')
'''
if bone_list is None:
bone_list = bpy.context.selected_pose_bone
elif isinstance(bone_list, bpy.types.PoseBone):
bone_list = [bone_list]
start, end = bpy.context.scene.frame_start, bpy.context.scene.frame_end
frames = []
for bone in bone_list:
for fcurve in bone.id_data.animation_data.action.fcurves:
if not fcurve.data_path.startswith(f'pose.bones["{bone.name}"]'):
continue
if filter_channel:
if not fcurve.data_path.endswith(filter_channel):
continue
# print('fcurve.data_path: ', fcurve.data_path)
for keyframe_point in fcurve.keyframe_points:
x, _y = keyframe_point.co
if x >= start and x <= end and x not in frames:
frames.append(x)
## for returning an int (import math)
#frames.append((math.ceil(x)))
return sorted(frames)
def get_all_keyframe(use_only = True):
sum = set()
for action in D.actions:
if use_only and action.use_fake_user and action.users == 1:
#avoid saved (fake user) but unused actions
pass
elif use_only and action.users == 0:
#avoid 0 user actions
pass
else:
for fcurve in action.fcurves:
for key in fcurve.keyframe_points:
sum.add(key.co[0])
return sum
def get_keyframes(obj_list):
keyframes = []
for obj in obj_list:
anim = obj.animation_data
if anim is not None and anim.action is not None:
for fcu in anim.action.fcurves:
for keyframe in fcu.keyframe_points:
x, y = keyframe.co
if x not in keyframes:
keyframes.append((math.ceil(x)))
return keyframes
def scale_matrix_from_vector(scale):
'''recreate a neutral mat scale'''
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
matscale = matscale_x @ matscale_y @ matscale_z
return matscale
def has_key_at_frame(item, act=None, frame=None, channel='location', verbose=False):
'''Return True if pose bone has a key at passed frame'''
if frame is None:
frame = bpy.context.scene.frame_current
if isinstance(item, bpy.types.Object):
## Object
if act is None:
act = item.animation_data.action
data_path = channel
else:
## Consider it's a Pose bone
if act is None:
act = item.id_data.animation_data.action
data_path = f'pose.bones["{item.name}"].{channel}'
for i in range(0,3):
f = act.fcurves.find(data_path, index=i)
if not f:
if verbose:
print(f'{item.name} has not {data_path}')
continue
if f.is_empty:
if verbose:
print(f'fcurve has not keyframes: {f.data_path} {i}')
continue
if not f.is_valid:
if verbose:
print(f'fcurve is invalid {f.data_path} {i}')
continue
## ? int frame ?
if next((k for k in f.keyframe_points if k.co.x == frame), None) is not None:
if verbose:
print(f'{item.name} {channel} is keyframed')
return True
return False
"""
def has_channel_key_at_frame(pb, act, frame, channel='location'):
'''Return Rrue if pose bone has a key at passed frame'''
for i in range(0,3):
f = act.fcurves.find(f'pose.bones["{pb.name}"].{channel}', index=i)
if not f:
continue
if f.is_empty:
print(f'fcurve has not keyframes: {f.data_path} {i}')
continue
if not f.is_valid:
print(f'fcurve is invalid {f.data_path} {i}')
continue
## ? int frame ?
if next((k for k in f.keyframe_points if k.co.x == frame), None) is not None:
return True
return False
"""
def assign_world_location_to_pose_bone(pb, newloc):
# mat = scn.cursor.matrix
# get world space matrix
mat = pb.id_data.matrix_world @ pb.matrix
loc, rot, _scale = mat.decompose()
# compose
# loc_mat = Matrix.Translation(loc)
loc_mat = Matrix.Translation(newloc)
rot_mat = rot.to_matrix().to_4x4()
scale_mat = scale_matrix_from_vector(pb.scale)
new_mat = loc_mat @ rot_mat @ scale_mat
# assign
pb.matrix = pb.id_data.matrix_world.inverted() @ new_mat
def sort_bones_by_hierarchy_depth(bones):
hierarchy_depth = {}
def get_depth(bone):
if bone.parent is None:
return 0
if bone.name in hierarchy_depth:
return hierarchy_depth[bone.name]
else:
depth = get_depth(bone.parent) + 1
hierarchy_depth[bone.name] = depth
return depth
for bone in bones:
get_depth(bone)
# print('hierarchy_depth: ', hierarchy_depth) # Dbg
# Ex: hierarchy_depth -> {'world': 1, 'walk': 2, 'scale-all': 3, 'rotate-hip': 4, 'spine01': 5, 'spine02':6, 'foot.R': 1, 'foot.L': 1}
sorted_bones = sorted(bones, key=lambda b: hierarchy_depth[b.name])
return sorted_bones
def wrap_animation(ref, root=None, verbose=False):
scn = bpy.context.scene
org_frame = scn.frame_current
ob = bpy.context.object
ac = ob.animation_data.action
## Auto set root with classic names
if not root:
root = ob.pose.bones.get('root')
if not root:
root = ob.pose.bones.get('world')
# if not root:
# root = ob.pose.bones.get('walk')
target_bones = bpy.context.selected_pose_bones
## During the loop, move upper hiercharchy bones first
## (Not 100% sure that's needed, as it's usually not targetting dependent bones)
target_bones = sort_bones_by_hierarchy_depth(target_bones)
for i in range(len(target_bones))[::-1]:
## Remove the root:
if target_bones[i] == root:
target_bones.pop(i)
## Always put the ref bone at the end:
if target_bones[i] == ref:
target_bones.append(target_bones.pop(i))
print('Targeted bones', [b.name for b in target_bones]) #Dbg
'''
### Iterate in whole summary:
### -> Frame -> Bones
frames = dopesheet_summary(ob)
## remove duplicates
if verbose:
print('-Keyframes-')
print(frames)
### Note: /!\ Still some weird error, work when select and place bones one by one !
### Not optimized but need to iterate in bone first then frame for each...
for f in reversed(frames):
frame = int(f)
if not verbose:
print(f'{frame:04d}', end='\r')
scn.frame_set(frame)
## calc distance
ref_loc = (ref.id_data.matrix_world @ ref.matrix).translation.copy()
ref_loc = Vector((ref_loc.x, ref_loc.y, 0)) # Remove Z
## On a moving root, recalculate root_loc on each iteration
root_loc = (root.id_data.matrix_world @ root.matrix).translation.copy()
root_loc = Vector((root_loc.x, root_loc.y, 0)) # Remove Z
# get reset_vector
reset_vec = root_loc - ref_loc
for pb in sorted_selected_pose_bones:
# if not has_channel_key_at_frame(pb, ac, frame, channel='location'):
if not has_key_at_frame(pb, ac, frame, channel='location', verbose=verbose):
if verbose:
print(f'{frame:03d}: {pb.name} no location keys')
continue
#### assign the matrix from world
## bone world position
bone_world_loc = (pb.id_data.matrix_world @ pb.matrix).translation.copy()
new_pos = bone_world_loc + reset_vec
# scn.cursor.location = new_pos #Dbg
assign_world_location_to_pose_bone(pb, new_pos)
## create the keyframe
pb.keyframe_insert('location', frame=frame)
'''
### Iterate in individual bone summary:
### -> bone -> frames
for pb in target_bones:
# Calculate single bone frame summary
frames = pose_bone_frame_summary(pb, filter_channel='location')
print(f'{pb.name} : {len(frames)} location frames')
for f in reversed(frames):
frame = int(f)
if not verbose:
print(f'{pb.name} : frame {frame:04d}', end='\r')
scn.frame_set(frame)
## calc distance
ref_loc = (ref.id_data.matrix_world @ ref.matrix).translation.copy()
ref_loc = Vector((ref_loc.x, ref_loc.y, 0)) # Remove Z
## On a moving root, recalculate root_loc on each iteration
root_loc = (root.id_data.matrix_world @ root.matrix).translation.copy()
root_loc = Vector((root_loc.x, root_loc.y, 0)) # Remove Z
# get reset_vector
reset_vec = root_loc - ref_loc
if not has_key_at_frame(pb, ac, frame, channel='location', verbose=verbose):
if verbose:
print(f'{frame:03d}: {pb.name} no location keys')
continue
#### assign the matrix from world
## bone world position
bone_world_loc = (pb.id_data.matrix_world @ pb.matrix).translation.copy()
new_pos = bone_world_loc + reset_vec
assign_world_location_to_pose_bone(pb, new_pos)
## create the keyframe
pb.keyframe_insert('location', frame=frame)
if not verbose:
print()
scn.frame_current = org_frame
class AW_OT_wrap_animation(bpy.types.Operator):
bl_idname = "autowalk.wrap_animation"
bl_label = "Wrap Animation"
bl_description = "Wrap the current animation on selected bones.\
\nApply Reference -> root offset to selected bones on each frames"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'ARMATURE'
def execute(self, context):
ob = context.object
settings = context.scene.anim_cycle_settings
if not settings.wrap_ref_bone or not settings.wrap_root_bone:
self.report({'ERROR'}, 'Need to specify both reference and root bone fields')
return {"CANCELLED"}
if not ob.animation_data or not ob.animation_data.action:
self.report({'ERROR'}, f'No animation data on {ob.name}')
return {"CANCELLED"}
ref = settings.wrap_ref_bone
root = settings.wrap_root_bone
## as strings
ref = ob.pose.bones.get(ref)
root = ob.pose.bones.get(root)
if not ref:
self.report({'ERROR'}, f'Reference bone not found in object: {context.object.name}')
return {"CANCELLED"}
if not root:
self.report({'ERROR'}, f'Root bone not found in object: {context.object.name}')
return {"CANCELLED"}
wrap_animation(ref, root)
return {"FINISHED"}
classes=(
AW_OT_wrap_animation,
)
def register():
# bpy.types.Object.pose_bone = bpy.props.PointerProperty(type=bpy.types.PoseBone)
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# del bpy.types.Object.pose_bone