diff --git a/OP_animate_path.py b/OP_animate_path.py index c6c8db6..fd0f065 100644 --- a/OP_animate_path.py +++ b/OP_animate_path.py @@ -1,6 +1,7 @@ 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): @@ -11,12 +12,12 @@ def get_bone_transform_at_frame(b, act, frame): 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') + # 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) + # print(frame, b.name, channel, chan_list) # Dbg if channel == 'rotation_euler': transform[channel] = Euler(chan_list) else: @@ -26,10 +27,11 @@ def get_bone_transform_at_frame(b, act, frame): def anim_path_from_translate(): + '''Calculate step size from selected foot and forward axis and animate curve''' print(fn.helper()) - obj = bpy.context.object - if obj.type != 'ARMATURE': + ob = bpy.context.object + if ob.type != 'ARMATURE': return ('ERROR', 'active is not an armature type') # found curve through constraint @@ -37,20 +39,24 @@ def anim_path_from_translate(): # if not 'foot' in b.bone.name: # return ('ERROR', 'No "foot" in active bone name\n-> Select foot that has the most reliable contact') - + settings = bpy.context.scene.anim_cycle_settings + axis = settings.forward_axis curve = None - if bpy.context.scene.anim_cycle_settings.path_to_follow: - curve = bpy.context.scene.anim_cycle_settings.path_to_follow + 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(obj) + curve, _const = fn.get_follow_curve_from_armature(ob) if isinstance(curve, str): return curve, _const - act = fn.get_obj_action(obj) + act = fn.get_obj_action(ob) if not act: - return ('ERROR', f'No action active on {obj.name}') + return ('ERROR', f'No action active on {ob.name}') # use original action as ref if '_expanded' in act.name: @@ -104,32 +110,91 @@ def anim_path_from_translate(): break if start_frame is None or end_frame is None: - return ('ERROR', f'No (or not enough) keyframe marked Extreme {obj.name} > {b.name}') + return ('ERROR', f'No (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 - 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']) - # bpy.context.scene.cursor.location = (b.bone.matrix_local @ start_mat).to_translation() # obj.matrix_world @ - bpy.context.scene.cursor.location = (b.bone.matrix_local @ end_mat).to_translation() # obj.matrix_world @ - # bpy.context.scene.cursor.location = (b.matrix_basis.inverted() @ start_mat).to_translation() # obj.matrix_world @ - # bpy.context.scene.cursor.location = start_transform['location'] # obj.matrix_world @ - return # FIXME - ### old method using directly one axis - """ + ## Determine direction vector of the charater (root) + orient_vectors = { + 'FORWARD_X' : Vector((1,0,0)), + 'FORWARD_Y' : Vector((0,1,0)), + 'FORWARD_Z' : Vector((0,0,1)), + 'TRACK_NEGATIVE_X' : Vector((-1,0,0)), + 'TRACK_NEGATIVE_Y' : Vector((0,-1,0)), + 'TRACK_NEGATIVE_Z' : Vector((0,0,-1)) + } + + ## TODO root need to be user defined + root = ob.pose.bones.get('world') + if not root: + print('No root found') + return {"CANCELLED"} + + root_axis_vec = orient_vectors[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 + + 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') + + + """ ### old method using directly one axis axis = {'X':0, 'Y':1, 'Z':2} ## calculate offset from bones loc_fcu = None @@ -175,44 +240,7 @@ def anim_path_from_translate(): # Y positive value (forward) -> move_val = abs(start_val - end_val) """ - - - print('move_val: ', move_val) - - length = fn.get_curve_length(curve) - steps = length / move_val - - frame_duration = steps * move_frame - - - ### Clear 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 - - anim_frame = bpy.context.scene.anim_cycle_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') class UAC_OT_animate_path(bpy.types.Operator): bl_idname = "anim.animate_path" diff --git a/OP_setup.py b/OP_setup.py index 01cd1d7..a3b9701 100644 --- a/OP_setup.py +++ b/OP_setup.py @@ -27,7 +27,7 @@ class UAC_OT_autoset_axis(bpy.types.Operator): def execute(self, context): ob = context.object - ## root need to be user defined to assume corner cases + ## TODO root need to be user defined to assume corner cases root = ob.pose.bones.get('world') if not root: print('No root found') diff --git a/README.md b/README.md index 84f5737..5c9c875 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ Sidebar > Anim > unfold anim cycle ## Changelog: +0.3.5 + +- partly working + 0.3.3 - total wip diff --git a/__init__.py b/__init__.py index 30fe315..e1d2bc5 100644 --- a/__init__.py +++ b/__init__.py @@ -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, 3, 3), + "version": (0, 3, 5), "blender": (3, 0, 0), "location": "View3D", "warning": "WIP", diff --git a/properties.py b/properties.py index 62297fb..60b9e3b 100644 --- a/properties.py +++ b/properties.py @@ -17,11 +17,12 @@ class UAC_PG_settings(bpy.types.PropertyGroup) : expand_on_selected_bones : bpy.props.BoolProperty( name="On selected", description="Expand on selected bones", - default=True, options={'HIDDEN'}) + default=False, options={'HIDDEN'}) linear : bpy.props.BoolProperty( - name="Linear", description="keep the animation path linear (Else step the path usings cycle keys)", - default=False, options={'HIDDEN'}) + name="Linear", description="keep the animation path linear\ + \nElse step the path usings follow path cycle keys)", + default=True, options={'HIDDEN'}) start_frame : bpy.props.IntProperty( name="Start Frame", description="Starting frame for animation path",