auto_walk/OP_animate_path.py

308 lines
11 KiB
Python
Raw Normal View History

2021-04-05 01:35:12 +02:00
import bpy
from . import fn
2022-03-29 18:46:33 +02:00
from mathutils import Vector, Euler
2022-03-30 17:40:52 +02:00
from mathutils.geometry import intersect_line_plane
2021-04-06 18:30:25 +02:00
## step 2 : Auto animate the path (with feet selection) and modal to adjust speed
2022-03-29 18:46:33 +02:00
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:
2022-03-30 17:40:52 +02:00
# print(frame, channel, 'not animated ! using current value') # Dbg
2022-03-29 18:46:33 +02:00
chan_list.append(getattr(b, channel)) # get current value since not animated
continue
chan_list.append(f.evaluate(frame))
2022-03-30 17:40:52 +02:00
# print(frame, b.name, channel, chan_list) # Dbg
2022-03-29 18:46:33 +02:00
if channel == 'rotation_euler':
transform[channel] = Euler(chan_list)
else:
transform[channel] = Vector(chan_list)
return transform # loc, rot, scale
def anim_path_from_translate():
2022-03-30 17:40:52 +02:00
'''Calculate step size from selected foot and forward axis and animate curve'''
2021-04-06 18:30:25 +02:00
print(fn.helper())
2022-03-30 17:40:52 +02:00
ob = bpy.context.object
if ob.type != 'ARMATURE':
2021-04-06 18:30:25 +02:00
return ('ERROR', 'active is not an armature type')
# found curve through constraint
b = bpy.context.active_pose_bone
2022-03-29 18:46:33 +02:00
# if not 'foot' in b.bone.name:
# return ('ERROR', 'No "foot" in active bone name\n-> Select foot that has the most reliable contact')
2022-03-30 17:40:52 +02:00
settings = bpy.context.scene.anim_cycle_settings
axis = settings.forward_axis
2021-04-06 18:30:25 +02:00
curve = None
2022-03-30 17:40:52 +02:00
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'
2021-04-06 18:30:25 +02:00
# if curve is not defined try to track it from constraints on armature
if not curve:
2022-03-30 17:40:52 +02:00
curve, _const = fn.get_follow_curve_from_armature(ob)
2021-04-06 18:30:25 +02:00
if isinstance(curve, str):
return curve, _const
2022-03-30 17:40:52 +02:00
act = fn.get_obj_action(ob)
2021-04-06 18:30:25 +02:00
if not act:
2022-03-30 17:40:52 +02:00
return ('ERROR', f'No action active on {ob.name}')
2021-04-06 18:30:25 +02:00
# 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)
if base_act:
act = base_act
print(f'Using for {base_act_name} as reference')
else:
print(f'No base action found (searching for {base_act_name})')
# CHANGE - retiré le int de la frame
# keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points]
2022-04-07 14:46:00 +02:00
## calculate offset from bones by evaluating distance at extremes
2022-03-29 18:46:33 +02:00
2022-03-31 17:07:04 +02:00
# fcurve parsing:
2022-03-29 18:46:33 +02:00
# 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 fcu.data_path.split('"')[1] == b.bone.name]
start_frame = end_frame = None
for fcu in b_fcurves:
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
else:
if encountered_marks:
## 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:
2022-03-30 17:40:52 +02:00
return ('ERROR', f'No (or not enough) keyframe marked Extreme {ob.name} > {b.name}')
2022-03-29 18:46:33 +02:00
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
2021-04-06 18:30:25 +02:00
2022-03-29 18:46:33 +02:00
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'])
2022-03-30 17:40:52 +02:00
## Determine direction vector of the charater (root)
2022-03-31 17:07:04 +02:00
root = ob.pose.bones.get(fn.get_root_name())
2022-03-30 17:40:52 +02:00
if not root:
print('No root found')
return {"CANCELLED"}
2022-03-31 17:07:04 +02:00
root_axis_vec = fn.get_direction_vector_from_enum(axis) # world space
2022-03-30 17:40:52 +02:00
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
print('root vec : ', root_axis_vec)
print('start loc: ', start_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('move_val: ', move_val)
length = fn.get_curve_length(curve)
steps = length / move_val
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
## 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)
## 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'
## set all to constant
# for k in t_fcu.keyframe_points:
# k.interpolation = 'CONSTANT'
print('end of set_follow_path_anim')
2021-04-05 01:39:27 +02:00
class UAC_OT_animate_path(bpy.types.Operator):
2021-04-05 01:35:12 +02:00
bl_idname = "anim.animate_path"
bl_label = "Animate Path"
2022-03-29 18:46:33 +02:00
bl_description = "Select the most representative 'in contact' feet of the cycle\
\nSelected bone should have two keyframe marked as type extreme (red):\
\nA key for foot first ground contact and other key for foot last contact frame\
\nSelect keyframe and use R > Extreme)"
2021-04-05 01:35:12 +02:00
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
2022-03-29 18:46:33 +02:00
err = anim_path_from_translate()
2021-04-05 01:35:12 +02:00
if err:
self.report({err[0]}, err[1])
if err[0] == 'ERROR':
return {"CANCELLED"}
return {"FINISHED"}
2021-04-05 01:39:27 +02:00
class UAC_OT_adjust_animation_length(bpy.types.Operator):
2021-04-05 01:35:12 +02:00
bl_idname = "anim.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 in ('ARMATURE', 'CURVE')
2021-04-06 18:30:25 +02:00
val : bpy.props.FloatProperty(name='End key value')
2021-04-05 01:35:12 +02:00
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"}
2021-04-06 18:30:25 +02:00
curve, _const = fn.get_follow_curve_from_armature(context.object)
2021-04-05 01:35:12 +02:00
if isinstance(curve, str):
self.report({curve}, _const)
return {"CANCELLED"}
self.act = fn.get_obj_action(curve.data)
if not self.act:
self.report({'ERROR'}, f'No action on {curve.name} data')
return {"CANCELLED"}
2021-04-06 18:30:25 +02:00
# if '_expanded' in self.act.name:
# self.report({'WARNING'}, f'Action is expanded')
2021-04-05 01:35:12 +02:00
self.fcu = None
for fcu in self.act.fcurves:
if fcu.data_path == 'eval_time':
self.fcu = fcu
break
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)')
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.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"}
2021-04-06 18:30:25 +02:00
if event.type in ('MIDDLEMOUSE', 'SPACE'): # Mmaybe not mid mouse ?
return {'PASS_THROUGH'}
2021-04-05 01:35:12 +02:00
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=(
2021-04-05 01:39:27 +02:00
UAC_OT_animate_path,
UAC_OT_adjust_animation_length,
2021-04-05 01:35:12 +02:00
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)