519 lines
19 KiB
Python
519 lines
19 KiB
Python
import bpy
|
|
from . import fn
|
|
from mathutils import Vector, Euler
|
|
from mathutils.geometry import intersect_line_plane
|
|
|
|
## step 2 : Auto animate the path (with feet selection) and modal to adjust speed
|
|
|
|
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 has_extremes(b, act=None):
|
|
'''tell if a bone has extreme marked '''
|
|
if not act:
|
|
act = fn.get_obj_action(b.id_data)
|
|
if not act:
|
|
return False
|
|
|
|
for fcu in act.fcurves:
|
|
if not '"' in fcu.data_path:
|
|
continue
|
|
if fcu.data_path.split('"')[1] == b.name: # name of the bone
|
|
if [k for k in fcu.keyframe_points if k.type == 'EXTREME']:
|
|
return True
|
|
return False
|
|
|
|
def get_extreme_range(ob, act, b):
|
|
b_fcurves = [fcu for fcu in act.fcurves if '"' in fcu.data_path and fcu.data_path.split('"')[1] == b.bone.name]
|
|
# for f in b_fcurves:
|
|
# print('fc:', f.data_path, f.array_index)
|
|
curves = []
|
|
for fcu in b_fcurves:
|
|
start_frame = end_frame = None
|
|
e_ct = 0
|
|
encountered_marks = False # flag to stop after last extreme of each fcu
|
|
for k in fcu.keyframe_points:
|
|
# if k.select_control_point: # based on selection ?
|
|
if k.type == 'EXTREME':
|
|
encountered_marks = True
|
|
|
|
f = k.co.x
|
|
if start_frame is None:
|
|
start_frame = f
|
|
if start_frame > f:
|
|
start_frame = f
|
|
|
|
if end_frame is None:
|
|
end_frame = f
|
|
if end_frame < f:
|
|
end_frame = f
|
|
|
|
e_ct += 1
|
|
continue
|
|
|
|
# it's a normal keyframe
|
|
if encountered_marks:
|
|
if e_ct == 1:
|
|
# reset (continue scan to next frame range)
|
|
e_ct = 0
|
|
start_frame = end_frame = None
|
|
encountered_marks = False
|
|
|
|
else:
|
|
## means back to other frame type after passed sufficient key
|
|
## (stop for this fcu)
|
|
break
|
|
|
|
print(fcu.data_path, fcu.array_index, start_frame, end_frame)
|
|
if start_frame is None or end_frame is None:
|
|
continue
|
|
if end_frame - start_frame == 0: # same key
|
|
continue
|
|
|
|
print('ok')
|
|
curves.append(end_frame - start_frame)
|
|
|
|
if not curves:
|
|
return
|
|
print('curves: ', curves)
|
|
curves.sort()
|
|
return curves[-1]
|
|
|
|
def find_best_foot(ob):
|
|
'''Get an armature object and do some wizardry to return best match for foot bone'''
|
|
|
|
b = bpy.context.active_pose_bone
|
|
|
|
act = fn.get_obj_action(ob)
|
|
if not act:
|
|
return ('ERROR', f'No action active on {ob.name}')
|
|
|
|
# use original action as ref
|
|
act = fn.get_origin_action(act)
|
|
|
|
if 'foot' in b.name.lower():
|
|
# if best is selected
|
|
return b
|
|
|
|
## auto detect reference foot
|
|
ref = 'foot.R'
|
|
target_bones = [pb for pb in ob.pose.bones if not b.name.lower().startswith(('mch', 'org', 'def'))\
|
|
and has_extremes(pb, act=act)]
|
|
|
|
if not target_bones:
|
|
return ('ERROR', f'No keys in action "{act.name}" are marked as Extreme (red keys)')
|
|
|
|
target_bones.sort(key=lambda x: fn.fuzzy_match_ratio(ref, x.name))
|
|
# print('target_bones: ', [b.name for b in target_bones]) #Dbg
|
|
|
|
# analyse best bone for contact length
|
|
b = target_bones[0]
|
|
|
|
print(f'auto-detected best bone as {b.name}')
|
|
|
|
# determine contact chain length
|
|
|
|
flip_name = fn.get_flipped_name(b.name)
|
|
flipped = next((b for b in target_bones if b.name == flip_name), None)
|
|
|
|
if not flipped:
|
|
print(f'No flipped name found (using "{flip_name}")')
|
|
return b
|
|
|
|
flipped_contact_range = get_extreme_range(ob, act, flipped)
|
|
print('flipped_contact_range: ', flipped_contact_range)
|
|
|
|
bone_contact_range = get_extreme_range(ob, act, b)
|
|
print('bone_contact_range: ', bone_contact_range)
|
|
|
|
if bone_contact_range:
|
|
if flipped_contact_range and (bone_contact_range < flipped_contact_range):
|
|
return flipped
|
|
else:
|
|
return b
|
|
elif flipped_contact_range:
|
|
return flipped
|
|
|
|
return ('ERROR', f'No Extreme (red keys) on bone "{b.name}" for action "{act.name}"')
|
|
|
|
|
|
def anim_path_from_translate():
|
|
'''Calculate step size from selected foot and forward axis and animate curve'''
|
|
print(fn.helper())
|
|
|
|
ob = bpy.context.object
|
|
|
|
debug = fn.get_addon_prefs().debug
|
|
settings = bpy.context.scene.anim_cycle_settings
|
|
axis = settings.forward_axis
|
|
|
|
curve = None
|
|
if settings.path_to_follow:
|
|
curve = settings.path_to_follow
|
|
|
|
if curve and not bpy.context.scene.objects.get(curve.name):
|
|
return 'ERROR', f'Curve {curve.name} is not in scene'
|
|
|
|
# if curve is not defined try to track it from constraints on armature
|
|
if not curve:
|
|
curve, _const = fn.get_follow_curve_from_armature(ob)
|
|
if isinstance(curve, str):
|
|
return curve, _const
|
|
|
|
act = fn.get_obj_action(ob)
|
|
if not act:
|
|
return ('ERROR', f'No action active on {ob.name}')
|
|
|
|
base_act = None
|
|
# use original action as ref
|
|
if '_baked' in act.name:
|
|
base_act_name = act.name.split('_baked')[0]
|
|
base_act = bpy.data.actions.get(base_act_name)
|
|
if base_act:
|
|
act = base_act
|
|
print(f'Using for action {base_act_name} as reference')
|
|
else:
|
|
print(f'No base action found (searching for {base_act_name})')
|
|
|
|
# b = bpy.context.active_pose_bone
|
|
b = find_best_foot(ob)
|
|
if isinstance(b, tuple):
|
|
return b # return error and message
|
|
print('best: ', b.name)
|
|
|
|
# if not 'foot' in b.bone.name:
|
|
# return ('ERROR', 'No "foot" in active bone name\n-> Select foot that has the most reliable contact')
|
|
|
|
## calculate offset from bones by evaluating distance at extremes
|
|
# fcurve parsing:
|
|
# name : fcu.data_path.split('"')[1] (bone_name)
|
|
# properties: fcu.data_path.split('.')[-1] ('location', rotation_euler)
|
|
# axis : fcu.array_index (to get axis letter : {0:'X', 1:'Y', 2:'Z'}[fcu.array_index])
|
|
|
|
## get only fcurves relative to selected bone
|
|
b_fcurves = [fcu for fcu in act.fcurves if '"' in fcu.data_path and fcu.data_path.split('"')[1] == b.bone.name]
|
|
print('b_fcurves: ', len(b_fcurves))
|
|
|
|
# find best fcurve
|
|
|
|
for fcu in b_fcurves:
|
|
start_frame = end_frame = None
|
|
# skip problematic keys
|
|
if not len(fcu.keyframe_points):
|
|
if debug: print(fcu.data_path, fcu.array_index, '>> no keys !')
|
|
continue
|
|
|
|
if all(k.type == 'EXTREME' for k in fcu.keyframe_points):
|
|
# True if all are extreme or no keyframe in fcu
|
|
if debug: print(fcu.data_path, fcu.array_index, '>> all keys are marked as extremes !')
|
|
continue
|
|
|
|
encountered_marks = False # flag to stop after last extreme of each fcu
|
|
e_ct = 0
|
|
for k in fcu.keyframe_points:
|
|
if k.type == 'EXTREME':
|
|
encountered_marks = True
|
|
|
|
f = k.co.x
|
|
if start_frame is None:
|
|
start_frame = f
|
|
if start_frame > f:
|
|
start_frame = f
|
|
|
|
if end_frame is None:
|
|
end_frame = f
|
|
if end_frame < f:
|
|
end_frame = f
|
|
|
|
e_ct += 1
|
|
continue
|
|
|
|
# this is a normal keyframe
|
|
if encountered_marks:
|
|
if e_ct == 1:
|
|
# mean only one extreme has been scanned
|
|
# reset and continue de keys scan
|
|
e_ct = 0
|
|
start_frame = end_frame = None
|
|
encountered_marks = False
|
|
else:
|
|
## means back to other frame type after passed breakdown we stop
|
|
## (for this fcu)
|
|
break
|
|
|
|
if start_frame is None or end_frame is None:
|
|
continue
|
|
if start_frame == end_frame:
|
|
continue
|
|
|
|
# we have a range and were probably happy with this one.
|
|
break
|
|
|
|
if start_frame is None or end_frame is None:
|
|
return ('ERROR', f'No / All or not enough keyframe marked Extreme {ob.name} > {b.name}')
|
|
if start_frame == end_frame:
|
|
return ('ERROR', f'Only one key detected as extreme (at frame {start_frame}) !\nNeed at least two chained marked keys')
|
|
|
|
print(f'Offset from key range. start: {start_frame} - end: {end_frame}')
|
|
move_frame = end_frame - start_frame
|
|
|
|
## Find move_val from diff position at start and end frame wihtin character forward axis
|
|
|
|
## FIXME: problem when cycle axis is not Forward compare to character
|
|
## apply rotations in real world ? quat_diff = b.matrix_basis.to_quaternion().rotation_difference(b.matrix.to_quaternion())
|
|
|
|
start_transform = get_bone_transform_at_frame(b, act, start_frame)
|
|
start_mat = fn.compose_matrix(start_transform['location'], start_transform['rotation_euler'], start_transform['scale'])
|
|
|
|
end_transform = get_bone_transform_at_frame(b, act, end_frame)
|
|
end_mat = fn.compose_matrix(end_transform['location'], end_transform['rotation_euler'], end_transform['scale'])
|
|
|
|
|
|
## Determine direction vector of the charater (root)
|
|
root = ob.pose.bones.get(fn.get_root_name())
|
|
if not root:
|
|
print('No root found')
|
|
return {"CANCELLED"}
|
|
|
|
root_axis_vec = fn.get_direction_vector_from_enum(axis) # world space
|
|
root_axis_vec = root.bone.matrix_local @ root_axis_vec # aligned with object
|
|
# bpy.context.scene.cursor.location = root_axis_vec # Dbg root direction
|
|
|
|
## Get difference between start_loc and ends loc on forward axis
|
|
start_loc = (b.bone.matrix_local @ start_mat).to_translation()
|
|
end_loc = (b.bone.matrix_local @ end_mat).to_translation()
|
|
# bpy.context.scene.cursor.location = start_loc # Dbg foot start position
|
|
|
|
if debug:
|
|
print('root vec : ', root_axis_vec)
|
|
print('start loc: ', start_loc)
|
|
print('end loc: ', end_loc)
|
|
|
|
## get distance on forward axis
|
|
move_val = (intersect_line_plane(start_loc, start_loc + root_axis_vec, end_loc, root_axis_vec) - start_loc).length
|
|
print('Detected move value: ', move_val)
|
|
|
|
length = fn.get_curve_length(curve)
|
|
|
|
## steps: number of repetitions (how many times the move distance fit in the lenght of the curve)
|
|
steps = length / move_val
|
|
|
|
## frame_duration: number of frame for the move multiplied by the step
|
|
frame_duration = int(steps * move_frame)
|
|
|
|
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)
|
|
|
|
anim_frame = settings.start_frame
|
|
# curve.data.path_duration = frame_duration # set only 100
|
|
curve.data.path_duration = 100
|
|
|
|
const.offset = 0
|
|
const.keyframe_insert('offset', frame=anim_frame) # , options={'INSERTKEY_AVAILABLE'}
|
|
|
|
## negative (offset time rewinding so character move forward)
|
|
|
|
# const.offset = -frame_duration # Valid when duration is set same as curve's path_duration
|
|
# const.offset = -(((move_frame * 100) / frame_duration) * steps) # works and slightly more precise but super convoluted
|
|
const.offset = -100
|
|
const.keyframe_insert('offset', frame=anim_frame + frame_duration)
|
|
|
|
## All to linear (will be set to CONSTANT at the moment of sampling)
|
|
fcu = ob.animation_data.action.fcurves.find(offset_data_path)
|
|
if fcu:
|
|
for k in fcu.keyframe_points:
|
|
k.interpolation = 'LINEAR'
|
|
|
|
# Set extrapolation to linear to avoid a stop # TODO: maybe expose choice ?
|
|
fcu.extrapolation = 'LINEAR' # default is 'CONSTANT'
|
|
# defaut to linear (avoid mothion blur problem if start is set exactly on scene.frame_start)
|
|
|
|
## set all to constant
|
|
# for k in t_fcu.keyframe_points:
|
|
# k.interpolation = 'CONSTANT'
|
|
|
|
# speed indicator if animator wants to adapt the cycle
|
|
## Show a marker to determine cycle range if needed to reach a
|
|
|
|
''' # Does not work yet
|
|
if settings.end_frame > settings.start_frame:
|
|
all_keys_list = [k.co.x for fc in act.fcurves if 'foot' in fc.data_path for k in fc.keyframe_points]
|
|
base_start = int(min(all_keys_list))
|
|
base_end = int(max(all_keys_list))
|
|
base_range = base_end - base_start
|
|
|
|
wanted_duration = settings.end_frame - settings.start_frame # defined in "autowalk > motion" interface
|
|
range_needed_to_go_full_curve = base_range * (wanted_duration / frame_duration)
|
|
|
|
# create marker
|
|
mark_name = 'range to fill path'
|
|
speed_mark = bpy.context.scene.timeline_markers.get(mark_name)
|
|
if not speed_mark:
|
|
speed_mark = bpy.context.scene.timeline_markers.new(mark_name)
|
|
speed_mark.frame = int(base_start + range_needed_to_go_full_curve)
|
|
'''
|
|
|
|
|
|
if debug: print('end of set_follow_path_anim')
|
|
|
|
class AW_OT_animate_path(bpy.types.Operator):
|
|
bl_idname = "autowalk.animate_path"
|
|
bl_label = "Animate Path"
|
|
bl_description = "Use most representative 'in contact' feet of the cycle\
|
|
\nSelect foot bone to use as reference\
|
|
\nSelected bone should have two keyframe marked as type extreme (red):\
|
|
\nSelect keyframe and use R > Extreme)"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object and context.object.type == 'ARMATURE'
|
|
|
|
# def invoke(self, context, event):
|
|
# self.shift = event.shift
|
|
# return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
# TODO clear previous animation (keys) if there is any
|
|
if context.mode == 'OBJECT':
|
|
# Go in pose mode
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
|
|
err = anim_path_from_translate()
|
|
if err:
|
|
self.report({err[0]}, err[1])
|
|
if err[0] == 'ERROR':
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
|
|
class AW_OT_adjust_animation_length(bpy.types.Operator):
|
|
bl_idname = "autowalk.adjust_animation_length"
|
|
bl_label = "Adjust Anim speed"
|
|
bl_description = "Adjust speed\nOnce pressed, move up/down to move animation path last key value"
|
|
bl_options = {"REGISTER"} # , "UNDO"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
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()
|
|
|
|
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"}
|
|
|
|
# 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 {ob.name} data')
|
|
return {"CANCELLED"}
|
|
|
|
# if '_baked' in self.act.name:
|
|
# self.report({'WARNING'}, f'Action is expanded')
|
|
|
|
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'
|
|
|
|
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'{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
|
|
self.init_my = event.mouse_y
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
def modal(self, context, event):
|
|
# added reduction factor
|
|
self.val = self.init_ky + ((event.mouse_y - self.init_my) * 0.1)
|
|
offset = self.val - self.init_ky
|
|
display_text = f"Path animation end key value {self.val:.3f}, offset {offset:.3f}"
|
|
context.area.header_text_set(display_text)
|
|
self.k.co.y = self.val
|
|
if event.type == 'LEFTMOUSE' and event.value == "PRESS":
|
|
context.area.header_text_set(None)
|
|
self.execute(context)
|
|
return {"FINISHED"}
|
|
|
|
if event.type in ('RIGHTMOUSE', 'ESC') and event.value == "PRESS":
|
|
self.k.co.y = self.init_ky
|
|
context.area.header_text_set(None)
|
|
return {"CANCELLED"}
|
|
if event.type in ('MIDDLEMOUSE', 'SPACE'): # Mmaybe not mid mouse ?
|
|
return {'PASS_THROUGH'}
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def execute(self, context):
|
|
self.k.co.y = self.val
|
|
return {"FINISHED"}
|
|
|
|
# def draw(self, context):
|
|
# layout = self.layout
|
|
# layout.prop(self, "val")
|
|
|
|
classes=(
|
|
AW_OT_animate_path,
|
|
AW_OT_adjust_animation_length,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls) |