398 lines
13 KiB
Python
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 |