diff --git a/OP_animate_path.py b/OP_animate_path.py index eb9dfa2..cd00f4d 100644 --- a/OP_animate_path.py +++ b/OP_animate_path.py @@ -1,7 +1,127 @@ import bpy from . import fn -from . import animate_path +## step 2 : Auto animate the path (with feet selection) and modal to adjust speed + +def anim_path_from_y_translate(): + print(fn.helper()) + + obj = bpy.context.object + if obj.type != 'ARMATURE': + return ('ERROR', 'active is not an armature type') + + # found curve through constraint + b = bpy.context.active_pose_bone + + if not 'shoe' in b.bone.name: + return ('ERROR', 'No "shoe" in active bone name\n-> Select foot that has the most reliable contact') + + curve = None + if bpy.context.scene.anim_cycle_settings.path_to_follow: + curve = bpy.context.scene.anim_cycle_settings.path_to_follow + + # 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) + if isinstance(curve, str): + return curve, _const + + act = fn.get_obj_action(obj) + if not act: + return ('ERROR', f'No action active on {obj.name}') + + # 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] + + + + ## calculate offset from bones + locy_fcu = None + for fcu in act.fcurves: + if fcu.data_path.split('"')[1] != b.bone.name: + continue + if fcu.data_path.split('.')[-1] == 'location' and fcu.array_index == 1: + locy_fcu = fcu + + if not locy_fcu: + return ('ERROR', 'Current bone, location.y animation not found') + + start = None + end = None + + ## based on extreme + for k in locy_fcu.keyframe_points: + # if k.select_control_point: # based on selection + if k.type == 'EXTREME': # using extreme keys. + if not start: + start = k + end = k + else: + if start is not None: + ## means back to other frame type after passed breaskdown we stop + break + + if not start: + return ('ERROR', f"No extreme marked frame was found on bone {b.bone.name}.{['x','y','z'][locy_fcu.array_index]}") + if start == end: + return ('ERROR', 'Seems like only one key was marked as extreme ! Need at least two chained') + + start_frame = start.co.x + start_val = start.co.y + + end_frame = end.co.x + end_val = end.co.y + + move_frame = end_frame - start_frame + + # 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" @@ -19,7 +139,7 @@ class UAC_OT_animate_path(bpy.types.Operator): def execute(self, context): # TODO clear previous animation (keys) if there is any - err = animate_path.anim_path_from_y_translate() + err = anim_path_from_y_translate() if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': @@ -37,7 +157,7 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator): def poll(cls, context): return context.object and context.object.type in ('ARMATURE', 'CURVE') - val = bpy.props.FloatProperty(name='End key value') + val : bpy.props.FloatProperty(name='End key value') def invoke(self, context, event): # check animation data of curve @@ -48,7 +168,7 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator): self.report({'ERROR'}, 'no curve targeted in "Path" field') return {"CANCELLED"} - curve, _const = fn.get_follow_curve_from_armature(obj) + curve, _const = fn.get_follow_curve_from_armature(context.object) if isinstance(curve, str): self.report({curve}, _const) return {"CANCELLED"} @@ -58,6 +178,9 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator): self.report({'ERROR'}, f'No action on {curve.name} data') return {"CANCELLED"} + # if '_expanded' in self.act.name: + # self.report({'WARNING'}, f'Action is expanded') + self.fcu = None for fcu in self.act.fcurves: if fcu.data_path == 'eval_time': @@ -95,8 +218,8 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator): self.k.co.y = self.init_ky context.area.header_text_set(None) return {"CANCELLED"} - - # return {'PASS_THROUGH'} + if event.type in ('MIDDLEMOUSE', 'SPACE'): # Mmaybe not mid mouse ? + return {'PASS_THROUGH'} return {"RUNNING_MODAL"} def execute(self, context): @@ -110,7 +233,6 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator): classes=( UAC_OT_animate_path, UAC_OT_adjust_animation_length, - ) def register(): diff --git a/OP_expand_cycle_step.py b/OP_expand_cycle_step.py new file mode 100644 index 0000000..2fdcd78 --- /dev/null +++ b/OP_expand_cycle_step.py @@ -0,0 +1,312 @@ +import bpy +from . import fn + +## step 3 +# - Bake cycle modifier keys -chained with- step the animation path +# - Pin the feet (separated ops) + +def bake_cycle(on_selection=True): + print(fn.helper()) + obj = bpy.context.object + if obj.type != 'ARMATURE': + print('ERROR', 'active is not an armature type') + return + + act = fn.set_expanded_action(obj) + if not act: + return + print('action', act.name) + + act = obj.animation_data.action + + ct = 0 + for fcu in act.fcurves: + + ## if a curve is not cycled don't touch + if not [m for m in fcu.modifiers if m.type == 'CYCLES']: + continue + + #-# only on location : + # if not fcu.data_path.endswith('.location'): + # continue + + # prop = fcu.data_path.split('.')[-1] + + b_name = fcu.data_path.split('"')[1] + pb = obj.pose.bones.get(b_name) + if not pb: + print(f'{b_name} is invalid') + continue + + #-# limit on selection if passed + if on_selection and not pb.bone.select: + continue + + #-# only on selected and visible curve + # if not fcu.select or fcu.hide: + # continue + + fcu_kfs = [] + for k in fcu.keyframe_points: + k_dic = {} + # k_dic['k'] = k + k_dic['co'] = k.co + k_dic['interpolation'] = k.interpolation + k_dic['type'] = k.type + fcu_kfs.append(k_dic) + + first = fcu_kfs[0]['co'][0] + # second = fcu_kfs[1]['co'][0] + # before_last= fcu_kfs[-2]['co'][0] + last = fcu_kfs[-1]['co'][0] + # first_offset = second - first + + current_offset = offset = last - first + print('offset: ', offset) + + ## expand to end frame + + end = bpy.context.scene.frame_end # maybe add possibility define target manually + iterations = ((end - last) // offset) + 1 + print('iterations: ', iterations) + for _i in range(int(iterations)): + for kf in fcu_kfs: + # create a new key, adding offset to keys + fcu.keyframe_points.add(1) + new = fcu.keyframe_points[-1] + for att, val in kf.items(): + if att == 'co': + new.co = (val[0] + current_offset, val[1]) + else: + setattr(new, att, val) + current_offset += offset + + ct += 1 + + if not ct: + return ('ERROR', 'No fcurve treated (! action duplicated to _expand !)') + + print('end of anim cycle keys baking') + # C.scene.frame_current = org_frame + # detect last key in contact + + +def step_path(): + print(fn.helper()) + + obj = bpy.context.object + if obj.type != 'ARMATURE': + return ('ERROR', 'active is not an armature type') + + # found curve through constraint + curve, const = fn.get_follow_curve_from_armature(obj) + if not const: + return ('ERROR', 'No constraints found') + + act = fn.get_obj_action(obj) + if not act: return + + # CHANGE - retiré le int de la frame + # keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points] + keyframes = [k.co[0] for fcu in act.fcurves for k in fcu.keyframe_points] + keyframes = list(set(keyframes)) + + curve = const.target + if not curve: + return ('ERROR', f'no target set for {curve.name}') + + # get a new generated action for the curve + # Follow path animation is on the DATA of the fcurve + fact = fn.set_generated_action(curve.data) + if not fact: + return + + t_fcu = False + for fcu in fact.fcurves: + ## fcu data_path is just a string + if fcu.data_path == 'eval_time': + t_fcu = fcu + + if not t_fcu: + return ('ERROR', f'no eval_time animation in {curve.name}') + + timevalues = [t_fcu.evaluate(kf) for kf in keyframes] + for kf, value in zip(keyframes, timevalues): + ## or use t_fcu.keyframe_points.add(len(kf)) + curve.data.eval_time = value + curve.data.keyframe_insert('eval_time', frame=kf, options={'INSERTKEY_AVAILABLE'}) + # ``INSERTKEY_NEEDED````INSERTKEY_AVAILABLE`` (only available channels) + + ## set all to constant + for k in t_fcu.keyframe_points: + k.interpolation = 'CONSTANT' + + print('end of step_anim') + + +class UAC_OT_bake_cycle_and_step(bpy.types.Operator): + bl_idname = "anim.bake_cycle_and_step" + bl_label = "Bake key and step path " + bl_description = "Bake the key and step the animation path according to those key" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + + err = bake_cycle(context.scene.anim_cycle_settings.expand_on_selected_bones) + if err: + self.report({err[0]}, err[1]) + if err[0] == 'ERROR': + return {"CANCELLED"} + + # CHAINED ACTION : step the path of the curve path + err = step_path() + if err: + self.report({err[0]}, err[1]) + if err[0] == 'ERROR': + return {"CANCELLED"} + + # CHAINED ACTION pin feet ?? : Step the path of the curve path + + return {"FINISHED"} + + +def pin_down_feets(): + print(fn.helper()) + obj = bpy.context.object + if obj.type != 'ARMATURE': + print('ERROR', 'active is not an armature type') + return + + # Delete current action if its not the main one + # create a new '_autogen' one + act = fn.set_generated_action(obj) + if not act: + return ('ERROR', f'No action on {obj.name}') + print('action', act.name) + + # detect contact key + # [ 'array_index', 'auto_smoothing', 'bl_rna', 'color', 'color_mode', 'convert_to_keyframes', 'convert_to_samples', 'data_path', + # 'driver', 'evaluate', 'extrapolation', 'group', 'hide', 'is_empty', 'is_valid', 'keyframe_points', 'lock', 'modifiers', 'mute', + # 'range', 'rna_type', 'sampled_points', 'select', 'update', 'update_autoflags'] + + act = obj.animation_data.action + org_frame = bpy.context.scene.frame_current + + # TODO autodetect contact frame ? + + done = {} + for fcu in act.fcurves: + + # check only location + if not fcu.data_path.endswith('.location'): + continue + + # prop = fcu.data_path.split('.')[-1] + + b_name = fcu.data_path.split('"')[1] + # print('b_name: ', b_name, fcu.is_valid) + + pb = obj.pose.bones.get(b_name) + if not pb: + print(f'{b_name} is invalid') + continue + + start_contact = None + for k in fcu.keyframe_points: + + if k.type == 'EXTREME': + bpy.context.scene.frame_set(k.co[0]) + if start_contact is None: + start_contact=k.co[0] + # record coordinate relative to referent object (or world coord) + bone_mat = pb.matrix.copy() + # bone_mat = obj.matrix_world @ pb.matrix.copy() + continue + + if b_name in done.keys(): + if k.co[0] in done[b_name]: + continue + else: + # mark as treated (all curve of this bone at this time) + done[b_name] = [k.co[0]] + + #-# Insert keyframe to match Hold position + # print(f'Apply on {b_name} at {k.co[0]}') + + #-# assign previous matrix + pbl = pb.location.copy() + # l, _r, _s = bone_mat.decompose() + + pb.matrix = bone_mat # Exact same position + # pb.location.x = pbl.x # dont touch x either + pb.location.z = pbl.z + # pb.location.y = l.y (weirdly not working) + + # bpy.context.view_layer.update() + + #-# moyenne des 2 ? + # pb.location, pb.rotation_euler, pb.scale = average_two_matrix(pb.matrix, bone_mat) ## marche pas du tout ! + + ## insert keyframe + pb.keyframe_insert('location') + # only touched Y location + # pb.keyframe_insert('rotation_euler') + + k.type = 'JITTER' # 'BREAKDOWN' 'MOVING_HOLD' 'JITTER' + + else: + if start_contact is not None: + # print('fcu.data_path: ', fcu.data_path, fcu.array_index) + # print(f'{b_name} contact range {start_contact} - {k.co[0]}') + start_contact = None + + # print(i, fcu.data_path, fcu.array_index) + # print('time', k.co[0], '- value', k.co[1]) + + #k.handle_left + #k.handle_right + ##change handler type ([‘FREE’, ‘VECTOR’, ‘ALIGNED’, ‘AUTO’, ‘AUTO_CLAMPED’], default ‘FREE’) + #k.handle_left_type = 'AUTO_CLAMPED' + #k.handle_right_type = 'AUTO_CLAMPED' + + bpy.context.scene.frame_current = org_frame + # detect last key in contact + + + +class UAC_OT_pin_feets(bpy.types.Operator): + bl_idname = "anim.pin_feets" + bl_label = "Pin Feets" + bl_description = "Pin feets on keys marked as extreme" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + # context.scene.anim_cycle_settings.expand_on_selected_bones + err = pin_down_feets() + if err: + self.report({err[0]}, err[1]) + if err[0] == 'ERROR': + return {"CANCELLED"} + + return {"FINISHED"} + +classes=( +UAC_OT_bake_cycle_and_step, +UAC_OT_pin_feets, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/OP_setup_curve_path.py b/OP_setup_curve_path.py index fb29269..a150808 100644 --- a/OP_setup_curve_path.py +++ b/OP_setup_curve_path.py @@ -1,20 +1,55 @@ import bpy, re - from . import fn +## step 1 : Create the curve and/or follow path constraint, snap to ground + + +def create_follow_path_constraint(ob, curve): + pref = fn.get_addon_prefs() + bone_name = pref.tgt_bone + root = ob.pose.bones.get(bone_name) + if not root: + return ('ERROR', f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name') + + # Clear bone follow path constraint + exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH'] + for c in exiting_fp_constraints: + root.constraints.remove(c) + + # loc = ob.matrix_world @ root.matrix.to_translation() + if root.name == 'root' and root.location != (0,0,0): + old_loc = root.location + root.location = (0,0,0) + print(f'root moved from {old_loc} to (0,0,0) to counter curve offset') + + const = root.constraints.new('FOLLOW_PATH') + const.target = curve + # axis only in this case, should be in addon to prefs + const.forward_axis = 'FORWARD_X' + const.use_curve_follow = True + return curve, const def snap_curve(): obj = bpy.context.object + + curve = const = None if obj.type == 'ARMATURE': curve, const = fn.get_follow_curve_from_armature(obj) + + to_follow = bpy.context.scene.anim_cycle_settings.path_to_follow + if not curve and not to_follow: + return ('ERROR', f'No curve pointed by "Path" filed') - elif obj.type == 'CURVE': - print('ERROR', f'Select the armature related to curve {obj.name}') - return - ## need to find object using follow curve to find constraint... dirty not EZ - else: - print('ERROR', 'Not an armature object') - return + # get curve from field + if not curve: + curve, const = create_follow_path_constraint(obj, to_follow) + if not curve: + return (curve, const) # those are error message + + # if obj.type == 'CURVE': + # return ('ERROR', f'Select the armature related to curve {obj.name}') + # else: + # return ('ERROR', 'Not an armature object') gnd = fn.get_gnd() if not gnd: @@ -56,10 +91,8 @@ def snap_curve(): # Apply and decimate bpy.ops.object.modifier_apply({'object': nc}, modifier="Shrinkwrap", report=False) - - -# TODO Create the follow path modifier automatically - + bpy.context.scene.anim_cycle_settings.path_to_follow = nc + # return 0, nc class UAC_OT_create_curve_path(bpy.types.Operator): bl_idname = "anim.create_curve_path" @@ -105,6 +138,8 @@ class UAC_OT_create_curve_path(bpy.types.Operator): context.scene.anim_cycle_settings.path_to_follow = curve ## back to objct mode ? # bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + + ## TODO replace this part with >> create_follow_path_constraint(ob, curve) if root.name == 'root' and root.location != (0,0,0): old_loc = root.location @@ -120,6 +155,24 @@ class UAC_OT_create_curve_path(bpy.types.Operator): return {"FINISHED"} + +class UAC_OT_create_follow_path(bpy.types.Operator): + bl_idname = "anim.create_follow_path" + bl_label = "Create follow path constraint" + bl_description = "Create follow path targeting curve in field" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + ob = context.object + curve = context.scene.anim_cycle_settings.path_to_follow + create_follow_path_constraint(ob, curve) + + return {"FINISHED"} + class UAC_OT_snap_curve_to_ground(bpy.types.Operator): bl_idname = "anim.snap_curve_to_ground" bl_label = "snap_curve_to_ground" @@ -131,11 +184,16 @@ class UAC_OT_snap_curve_to_ground(bpy.types.Operator): return context.object and context.object.type == 'ARMATURE' def execute(self, context): - snap_curve() + err = snap_curve() + if err: + self.report({err[0]}, err[1]) + if err[0] == 'ERROR': + return {"CANCELLED"} return {"FINISHED"} classes=( UAC_OT_create_curve_path, +UAC_OT_create_follow_path, UAC_OT_snap_curve_to_ground, ) diff --git a/OP_snap_contact.py b/OP_snap_contact.py new file mode 100644 index 0000000..9922f2c --- /dev/null +++ b/OP_snap_contact.py @@ -0,0 +1,269 @@ +import bpy +import bmesh +from mathutils import Vector + + +def raycast_from_loc_to_obj(src, tgt, direction=None, dg=None): + ''' Raycast from a world space source on a target ovject using a direction Vector + + :origin: a world coordinate location as source of the ray + :tgt: an object onto project the ray + :direction: a direction vector. If not passed, point bottom : Vector((0,0,-1)) + return world coordiante of the hit if any + ''' + + if direction is None: + direction = Vector((0,0,-1)) + + mw = tgt.matrix_world + origin = mw.inverted() @ src + + hit, loc, _norm, _face = tgt.ray_cast(origin, direction, depsgraph=dg) + + if hit: + # print("Hit at ", loc, " (local)") + world_loc = mw @ loc + # bpy.ops.object.empty_add(location = world_loc) # test + return world_loc + + return False + +def worldspace_move_posebone(b, vec, is_target=False): + ''' Move or snap a posebone using a vector in worldspace + :b: posebone + :vec: Vector to move posebone worldspace (if not attached to parent) + :is_target: if True the posebone snap to the vector, else the vector is added + ''' + + a = b.id_data + + if is_target: + target_vec = vec + else: + target_vec = (a.matrix_world @ b.matrix).translation + vec + + mw = a.convert_space(pose_bone=b, + matrix=b.matrix, + from_space='POSE', + to_space='WORLD') + + mw.translation = target_vec + + b.matrix = a.convert_space(pose_bone=b, + matrix=mw, + from_space='WORLD', + to_space='POSE') + return target_vec + +def snap_foot(pb, gnd): + '''Get posebone and ground to touch''' + + # arm = bpy.context.object# bpy.context.scene.objects.get('Armature') + arm = pb.id_data + print('arm: ', arm) + + # find tip bone : + tip = [p for p in pb.children_recursive if p.name.startswith('DEF')][-1] + print('tip: ', tip) + # get deformed object VG (find skinned mesh) + + ob = None + + for o in arm.proxy_collection.instance_collection.all_objects: + if o.type != 'MESH': + continue + for m in o.modifiers: + if m.type == 'ARMATURE': + # print(o.name, m.object) + if m.object == arm.proxy: # if point to orignal rig + ## here we want body, not body_deform + if not 'body' in o.name: + continue + if '_deform' in o.name: + continue + ob = o + break + if not ob: + print('ERROR', 'no skinned mesh found') + return + + print('check skinning of', ob.name) + ### MESH baking + #-# Get Vertices position for a specific vertex group if over weight limit + + # me0 = simple_to_mesh(ob) # if no need to apply modifier just make ob.data.copy() + # # generate new + # bm = + # bm.from_mesh(me0) + # bm.verts.ensure_lookup_table() + # bm.edges.ensure_lookup_table() + # bm.faces.ensure_lookup_table() + + # # store weight values + # weight = [] + # ob_tmp = bpy.data.objects.new("temp", me0) + # for g in ob.vertex_groups: + # ob_tmp.vertex_groups.new(name=g.name) + # for v in me0.vertices: + # try: + # weight.append(ob_tmp.vertex_groups[tip.name].weight(v.index)) + # except: + # weight.append(0) + + # verts = [vert for vid, vert in enumerate(bake_mesh.vertices) \ + # if ob_tmp.vertex_groups[tip.name].index in [i.group for i in vert.groups] \ + # and weight[vid] > 0.5] + + #-# / + + #-# Get Vertices position for a specific vertex group if over weight limit + #-# (Does not work if a subdivision modifier is on) + + for m in ob.modifiers: + # if m.type in ('SUBSURF', 'TRIANGULATE'): + if m.type == 'SUBSURF': + m.show_viewport = False + + dg = bpy.context.evaluated_depsgraph_get() + obeval = ob.evaluated_get(dg).copy() + print('object: ', ob.name) + + ## bpy.context.object.proxy_collection.instance_collection.all_objects['body_deform'] + ## bpy.context.object.proxy_collection.instance_collection.all_objects['body'] + + ## Hide modifier + for m in obeval.modifiers: + if m.type == 'SUBSURF': + m.show_viewport = False # m.levels = 0 + + bake_mesh = obeval.to_mesh(preserve_all_data_layers=True, depsgraph=dg) + + ct = 0 + vg = obeval.vertex_groups[tip.name] + world_co = [] + for idx, vert in enumerate(bake_mesh.vertices): + grp_indexes = [i.group for i in vert.groups] + if vg.index in grp_indexes and vg.weight(idx) > 0.5: + ct +=1 + world_co.append(ob.matrix_world @ vert.co) + + if not ct: + print('ERROR', 'No vertices found') + return + + #-# # list comprehension + # verts = [vert for vid, vert in enumerate(bake_mesh.vertices) \ + # if obeval.vertex_groups[tip.name].index in [i.group for i in vert.groups] \ + # and obeval.vertex_groups[tip.name].weight(vid) > 0.5] + #world_co = [ob.matrix_world @ v.co for v in verts] + #-# / + + + print(len(world_co), 'vertices') + + # sort by height + world_co.sort(key=lambda x: x[2]) + + ### Raycast and find lowest distance + up_check = True + updists = [] + dists = [] + + for co in world_co: # [:6] (no neede to get all) + contact = raycast_from_loc_to_obj(co, gnd, Vector((0,0,-1)), dg=dg) + if contact: + dists.append((co - contact).length) + + if not contact and up_check: + contact = raycast_from_loc_to_obj(co, gnd, Vector((0,0,1)), dg=dg) + if contact: + updists.append((co - contact).length) + + if not contact: + continue + # empty_at(contact, size=0.2) + + if not dists and not updists: + print('ERROR', 'raycast could not found contact') + return + + # move bones by the minimal amount. + if updists: + move = max(updists) + vec = Vector((0,0, move)) + worldspace_move_posebone(pb, vec) + print('INFO', f'move up by {move}') + + else: + move = min(dists) + vec = Vector((0,0, -move)) + worldspace_move_posebone(pb, vec) + print('INFO', f'move down by {move}') + + + ## restore + + for m in ob.modifiers: + if m.type == 'SUBSURF': + # if m.type in ('SUBSURF', 'TRIANGULATE'): + m.show_viewport = True + # obeval.to_mesh_clear() + +def snap_feet(): + + ## add undo push if launched from shelf (TODO need test !!!) + # bpy.ops.ed.undo_push(message='Snap to ground') + + # if bpy.context.object.type != 'ARMATURE': + # print('ERROR', 'Selection is not an armature') + # return + + if bpy.context.mode != 'POSE': + print('ERROR', 'Not in pose mode') + return + + + gnd = bpy.context.scene.anim_cycle_settings.gnd + print('ground: ', gnd.name) + if not gnd: + return ('ERROR', 'Need to point ground object in "ground" field') + # Snap all selected feets, posebone to ground + + for pb in bpy.context.selected_pose_bones: + ## find the foot bones. + if '_shoe' in pb.name: + # get pb lowest surface deformed point + snap_foot(pb, gnd) + +class UAC_OT_contact_to_ground(bpy.types.Operator): + bl_idname = "anim.contact_to_ground" + bl_label = "Ground Feet" + bl_description = "Ground selected feets" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + # context.scene.anim_cycle_settings.expand_on_selected_bones + err = snap_feet() + if err: + self.report({err[0]}, err[1]) + if err[0] == 'ERROR': + return {"CANCELLED"} + + return {"FINISHED"} + + +classes=( +UAC_OT_contact_to_ground, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/__init__.py b/__init__.py index e8bfea7..9cee7fa 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "Unfold Anim Cycle", "description": "Anim utility to develop walk/run cycles along a curve", "author": "Samuel Bernou", - "version": (0, 1, 1), + "version": (0, 2, 0), "blender": (2, 92, 0), "location": "View3D", "warning": "WIP", @@ -15,6 +15,8 @@ import bpy from . import OP_setup_curve_path from . import OP_animate_path +from . import OP_expand_cycle_step +from . import OP_snap_contact from . import panels class UAC_PGT_settings(bpy.types.PropertyGroup) : @@ -32,12 +34,14 @@ class UAC_PGT_settings(bpy.types.PropertyGroup) : # gnd : bpy.props.StringProperty( # name="Ground object", description="Choose the ground object to use") - expand_on_selected_boned : bpy.props.BoolProperty( + expand_on_selected_bones : bpy.props.BoolProperty( name="On selected", description="Expand on selected bones", default=True, options={'HIDDEN'}) - # IntProperty : bpy.props.IntProperty( - # name="int prop", description="", default=25, min=1, max=2**31-1, soft_min=1, soft_max=2**31-1, step=1, options={'HIDDEN'})#, subtype='PIXEL' + start_frame : bpy.props.IntProperty( + name="Start Frame", description="Starting frame for animation path", + default=100, + min=0, max=2**31-1, soft_min=0, soft_max=2**31-1, step=1, options={'HIDDEN'})#, subtype='PIXEL' class UAC_addon_prefs(bpy.types.AddonPreferences): @@ -57,55 +61,16 @@ class UAC_addon_prefs(bpy.types.AddonPreferences): def draw(self, context): layout = self.layout - ## some 2.80 UI options # layout.use_property_split = True # flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) # layout = flow.column() layout.label(text='Create') - layout.prop(self, "tgt_bone") - # display the bool prop layout.prop(self, "debug") - # draw something only if a prop evaluate True - # if self.super_special_option: - # layout.label(text="/!\ Carefull, the super special option is especially powerfull") - # layout.label(text=" and with great power... well you know !") - - - -''' -addon_keymaps = [] -def register_keymaps(): - addon = bpy.context.window_manager.keyconfigs.addon - km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D") - # km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW') - - # kmi = km.keymap_items.new( - # name="name", - # idname="anim.snap_curve_to_ground", - # type="F", - # value="PRESS", - # shift=True, - # ctrl=True, - # alt = False, - # oskey=False - # ) - - kmi = km.keymap_items.new('anim.snap_curve_to_ground', type='F5', value='PRESS') - - - addon_keymaps.append((km, kmi)) - -def unregister_keymaps(): - for km, kmi in addon_keymaps: - km.keymap_items.remove(kmi) - addon_keymaps.clear() -''' - ### --- REGISTER --- classes=( @@ -118,6 +83,8 @@ def register(): bpy.utils.register_class(cls) OP_setup_curve_path.register() OP_animate_path.register() + OP_expand_cycle_step.register() + OP_snap_contact.register() panels.register() # if not bpy.app.background: @@ -128,6 +95,8 @@ def unregister(): # if not bpy.app.background: #unregister_keymaps() panels.unregister() + OP_snap_contact.unregister() + OP_expand_cycle_step.unregister() OP_animate_path.unregister() OP_setup_curve_path.unregister() diff --git a/animate_path.py b/animate_path.py deleted file mode 100644 index f31bd96..0000000 --- a/animate_path.py +++ /dev/null @@ -1,113 +0,0 @@ -import bpy -from . import fn - -def anim_path_from_y_translate(): - print(fn.helper()) - - obj = bpy.context.object - if obj.type != 'ARMATURE': - return ('ERROR', 'active is not an armature type') - - # found curve through constraint - b = bpy.context.active_pose_bone - - if not 'shoe' in b.bone.name: - return ('ERROR', 'no "shoe" in active bone name') - - curve = None - if bpy.context.scene.anim_cycle_settings.path_to_follow: - curve = bpy.context.scene.anim_cycle_settings.path_to_follow - - # 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) - if isinstance(curve, str): - return curve, _const - - act = fn.get_obj_action(obj) - if not act: - return ('ERROR', f'No action active on {obj.name}') - - # CHANGE - retiré le int de la frame - # keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points] - - - - ## calculate offset from bones - locy_fcu = None - for fcu in act.fcurves: - if fcu.data_path.split('"')[1] != b.bone.name: - continue - if fcu.data_path.split('.')[-1] == 'location' and fcu.array_index == 1: - locy_fcu = fcu - - if not locy_fcu: - return ('ERROR', 'Current bone, location.y animation not found') - - start = None - end = None - - ## based on extreme - for k in locy_fcu.keyframe_points: - # if k.select_control_point: # based on selection - if k.type == 'EXTREME': # using extreme keys. - if not start: - start = k - end = k - else: - if start is not None: - ## means back to other frame type after passed breaskdown we stop - break - - if not start: - return ('ERROR', f"No extreme marked frame was found on bone {b.bone.name}.{['x','y','z'][locy_fcu.array_index]}") - if start == end: - return ('ERROR', 'Seems like only one key was marked as extreme ! Need at least two chained') - - start_frame = start.co.x - start_val = start.co.y - - end_frame = end.co.x - end_val = end.co.y - - move_frame = end_frame - start_frame - - # 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 - - # TODO check if need to start at 100 or at current frame... - anim_frame = 100 # C.scene.frame_current - 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') \ No newline at end of file diff --git a/fn.py b/fn.py index b611db0..bec12e3 100644 --- a/fn.py +++ b/fn.py @@ -41,7 +41,8 @@ def get_follow_curve_from_armature(arm): name = pref.tgt_bone parents = [] - root = b.id_data.pose.bones.get(name) + # root = b.id_data.pose.bones.get(name) + root = arm.pose.bones.get(name) for c in root.constraints: if c.type == 'FOLLOW_PATH': const = c @@ -67,6 +68,8 @@ def get_follow_curve_from_armature(arm): return curve, const +# --- ACTIONS + def get_obj_action(obj): print(helper()) act = obj.animation_data @@ -111,6 +114,36 @@ def set_generated_action(obj): return new_act +def set_expanded_action(obj): + '''Backup object action and return a new action + associated with the object + ''' + print(helper()) + + rexpand = re.compile(r'_expanded\.?\d{0,3}$') + act = obj.animation_data.action + + if rexpand.search(act.name): + # is an autogenerated one + org_action_name = rexpand.sub('', act.name) + org_action = bpy.data.actions.get(org_action_name) + if not org_action: + print('ERROR', f'{org_action_name} not found') + return + + obj.animation_data.action = org_action + bpy.data.actions.remove(act) + + act = org_action + + # backup action before doing anything crazy + act.use_fake_user = True + new_act = act.copy() + new_act.name = act.name + '_expanded' + obj.animation_data.action = new_act + return new_act + + def get_curve_length(ob): dg = bpy.context.evaluated_depsgraph_get() obeval = ob.evaluated_get(dg)#.copy() diff --git a/panels.py b/panels.py index 8b268c2..f541d7d 100644 --- a/panels.py +++ b/panels.py @@ -1,5 +1,5 @@ import bpy - +from . import fn class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): bl_space_type = "VIEW_3D" @@ -9,31 +9,65 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): def draw(self, context): layout = self.layout - layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') + if not context.scene.anim_cycle_settings.path_to_follow: + layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') #-# path and ground objects layout.prop_search(context.scene.anim_cycle_settings, "path_to_follow", context.scene, "objects") layout.prop_search(context.scene.anim_cycle_settings, "gnd", context.scene, "objects") - layout.operator('anim.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON') - layout.operator('anim.animate_path', text='Animate Path (select foot)', icon='ANIM') - layout.operator('anim.adjust_animation_length', icon='MOD_TIME') - # layout.label(text='Loop cycle') - ## Créer automatiquement le follow path et l'anim de base + + prefs = fn.get_addon_prefs() + ob = context.object + + # Determine if already has a constraint (a bit too much condition in a panel...) + constrained = False + if ob and ob.type == 'ARMATURE': + pb = ob.pose.bones.get(prefs.tgt_bone) + if pb: + follow = pb.constraints.get('Follow Path') + if follow and follow.target: + layout.label(text=f'{pb.name} -> {follow.target.name}', icon='CON_FOLLOWPATH') + constrained = True + + if not constrained: + ## Créer automatiquement le follow path TODO et l'anim de base + layout.operator('anim.create_follow_path', text='Add follow path constraint', icon='CON_FOLLOWPATH') + + + layout.operator('anim.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON') + + + row=layout.row() + row.operator('anim.animate_path', text='Animate Path (select foot)', icon='ANIM') + row.prop(context.scene.anim_cycle_settings, "start_frame", text='start') + + row=layout.row() + row.operator('anim.adjust_animation_length', icon='MOD_TIME') ## Bake cycle (on selected) - - ## Expand Cycle - - - # expand cycle - # layout.prop(context.scene.anim_cycle_settings, "expand_on_selected_boned") + row=layout.row() + row.operator('anim.bake_cycle_and_step', text='Bake key and step path', icon='SHAPEKEY_DATA') + row.prop(context.scene.anim_cycle_settings, "expand_on_selected_bones") # Pin feet + layout.operator('anim.pin_feets', text='Pin feets', icon='PINNED') + + +class UAC_PT_anim_tools_panel(bpy.types.Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Anim" + bl_label = "Unicorn Tools" + + def draw(self, context): + layout = self.layout + layout.operator('anim.contact_to_ground', text='Ground selected feet', icon='SNAP_OFF') classes=( UAC_PT_walk_cycle_anim_panel, +UAC_PT_anim_tools_panel, ) def register():