From f07d395289160c27b0bd8125d1ce0d1133730a0e Mon Sep 17 00:00:00 2001 From: Pullusb Date: Tue, 19 Apr 2022 15:33:06 +0200 Subject: [PATCH] Straight curve creation method 1.1.0 - added: another method to add curve placing character in two position --- CHANGELOG.md | 4 ++ OP_setup_curve_a_to_b.py | 111 +++++++++++++++++++++++++++++ OP_setup_curve_path.py | 126 ++------------------------------- __init__.py | 5 +- fn.py | 146 +++++++++++++++++++++++++++++++++++++-- panels.py | 8 +++ 6 files changed, 274 insertions(+), 126 deletions(-) create mode 100644 OP_setup_curve_a_to_b.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 84935d2..b3ad2c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +1.1.0 + +- added: another method to add curve placing character in two position + 1.0.0 - fix: broken algo for extreme keys range detection diff --git a/OP_setup_curve_a_to_b.py b/OP_setup_curve_a_to_b.py new file mode 100644 index 0000000..ccc0c8c --- /dev/null +++ b/OP_setup_curve_a_to_b.py @@ -0,0 +1,111 @@ +import bpy, re +from . import fn + +## step 1 : Create the curve + +# need to determine a specific acceleration envelope factor to go to end of curve +# (set on curves) + +def remove_spacetime_keys(context=None): + if not context: + context = bpy.context + # remove all spacetime markers and walk collection + col_name = 'walk_markers' + walk_col = context.scene.collection.children.get(col_name) + if walk_col: + for o in reversed(walk_col.objects): + if o.name.startswith('spacetime_marker_'): + bpy.data.objects.remove(o) + bpy.data.collections.remove(walk_col) + +class UAC_OT_create_a_b_step(bpy.types.Operator): + bl_idname = "anim.create_a_b_step" + bl_label = "Create Two Points Curve" + bl_description = "Create a straight curve between two defined position and time" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + # first time ops is launched just set an empty (with market time) + # second time (if a mark empty exists determine a AB path to set curve) + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def create_marker(self, context): + m_name = f'spacetime_marker_{context.scene.frame_current}' + mark = bpy.data.objects.new(m_name, None) + mark.location = self.position + mark.empty_display_size = 2 + self.walk_col.objects.link(mark) + return mark + + def execute(self, context): + ob = context.object + prefs = fn.get_addon_prefs() + root_name = prefs.tgt_bone + root = ob.pose.bones.get(root_name) + self.position = (ob.matrix_world @ root.matrix).to_translation() + + col_name = 'walk_markers' + self.walk_col = fn.get_col(col_name, create=True) # link in scene collection by default + + markers = [o for o in self.walk_col.objects if o.type == 'EMPTY' and o.name.startswith('spacetime_marker_')] + if not markers: + self.create_marker(context) + return {"FINISHED"} + + # Create the second marker and append to marker list + markers.append(self.create_marker(context)) + markers.sort(key=lambda x: x.name) # sort by time (frame written in name) + + a = markers[0].location + b = markers[1].location - a # remove a postion to get position in curve object space + + # Set the curve and constraint + curve = fn.generate_curve(location=a, direction=b, name='curve_path', context=context) + settings = context.scene.anim_cycle_settings + settings.path_to_follow = curve + fn.create_follow_path_constraint(ob, curve) + + # refresh evaluation so constraint shows up correctly + bpy.context.scene.frame_set(bpy.context.scene.frame_current) + + # set offset animation ??? (but movement not in sync until action is retimed in NLA) + + # remove all markers or keep for later reuse ? + remove_spacetime_keys(context=None) + # if speed calculation is done later need to know start and end frame... + return {"FINISHED"} + +class UAC_OT_remove_a_b_step(bpy.types.Operator): + bl_idname = "anim.remove_a_b_step" + bl_label = "Remove First Position" + bl_description = "remove first point defining step" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + # Define what is first and last according to time + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def execute(self, context): + remove_spacetime_keys(context=None) + return {"FINISHED"} + + +classes=( +UAC_OT_create_a_b_step, +UAC_OT_remove_a_b_step, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + +# if __name__ == "__main__": +# register() \ No newline at end of file diff --git a/OP_setup_curve_path.py b/OP_setup_curve_path.py index 30bb213..5c91b74 100644 --- a/OP_setup_curve_path.py +++ b/OP_setup_curve_path.py @@ -3,118 +3,6 @@ from . import fn ## step 1 : Create the curve and/or follow path constraint, snap to ground -def create_follow_path_constraint(ob, curve, follow_curve=True): - prefs = fn.get_addon_prefs() - root_name = prefs.tgt_bone - root = ob.pose.bones.get(root_name) - - if not root: - return ('ERROR', f'posebone {root_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 == ('world', '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 follow curve offset') - - const = root.constraints.new('FOLLOW_PATH') - const.target = curve - # axis only in this case, should be in addon to prefs - - ## determine which axis to use... maybe found orientation in world space from matrix_basis ? - root_world_base_direction = root.bone.matrix_local @ fn.get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis) - const.forward_axis = fn.orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X' - print('const.forward_axis: ', const.forward_axis) - 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') - - # get curve from field - if not curve: - curve, const = create_follow_path_constraint(obj, to_follow) - if isinstance(curve, str): - 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: - return - - curve_act = None - anim_frame = None - # if it's on a snap curve, fetch original - if '_snap' in curve.name: - org_name = re.sub(r'_snap\.?\d{0,3}$', '', curve.name) - org_curve = bpy.context.scene.objects.get(org_name) - if org_curve: - const.target = org_curve - - # keep action - if curve.data.animation_data and curve.data.animation_data.action: - curve_act = curve.data.animation_data.action - - anim_frame = curve.data.path_duration - # delete old snap - bpy.data.objects.remove(curve) - # assign old curve as main one - curve = org_curve - - nc = curve.copy() - name = re.sub(r'\.\d{3}$', '', curve.name) + '_snap' - const.target = nc - nc.name = name - nc.data = curve.data.copy() - nc.data.name = name + '_data' - if curve_act: - nc.data.animation_data_create() - nc.data.animation_data.action = curve_act - if anim_frame: - nc.data.path_duration = anim_frame - - curve.users_collection[0].objects.link(nc) - - ## If object mode is Curve subdivide it (TODO if nurbs needs conversion) - #-# subdivide the curve (if curve is not nurbs) - - # bpy.ops.object.mode_set(mode='EDIT') - # bpy.ops.curve.select_all(action='SELECT') - # bpy.ops.curve.subdivide(number_cuts=4) - # bpy.ops.object.mode_set(mode='OBJECT') - - # shrinkwrap or cast on ground - mod = nc.modifiers.new('Shrinkwrap', 'SHRINKWRAP') - # mod.wrap_method = 'TARGET_PROJECT' - mod.wrap_method = 'PROJECT' - mod.wrap_mode = 'ON_SURFACE' - mod.use_project_z = True - mod.use_negative_direction = True - mod.use_positive_direction = True - mod.target = gnd - - # Apply and decimate - bpy.ops.object.modifier_apply({'object': nc}, modifier="Shrinkwrap", report=False) - 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" bl_label = "Create Curve" @@ -149,13 +37,12 @@ class UAC_OT_create_curve_path(bpy.types.Operator): settings.path_to_follow = curve - create_follow_path_constraint(ob, curve) + fn.create_follow_path_constraint(ob, curve) # refresh evaluation so constraint shows up correctly bpy.context.scene.frame_set(bpy.context.scene.frame_current) return {"FINISHED"} - class UAC_OT_create_follow_path(bpy.types.Operator): bl_idname = "anim.create_follow_path" bl_label = "Create Follow Path Constraint" @@ -170,7 +57,7 @@ class UAC_OT_create_follow_path(bpy.types.Operator): def execute(self, context): ob = context.object curve = context.scene.anim_cycle_settings.path_to_follow - err, const = create_follow_path_constraint(ob, curve) + err, const = fn.create_follow_path_constraint(ob, curve) if isinstance(err, str): self.report({'ERROR'}, err) return {"CANCELLED"} @@ -187,7 +74,7 @@ class UAC_OT_snap_curve_to_ground(bpy.types.Operator): return context.object and context.object.type == 'ARMATURE' def execute(self, context): - err = snap_curve() + err = fn.snap_curve() if err: self.report({err[0]}, err[1]) if err[0] == 'ERROR': @@ -221,11 +108,10 @@ class UAC_OT_edit_curve(bpy.types.Operator): return {"CANCELLED"} # set mode to object set curve as active and go Edit - bpy.ops.object.mode_set(mode='OBJECT', toggle=False) - curve.select_set(True) - context.view_layer.objects.active = curve - bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE + fn.go_edit_mode(curve) + # curve context.mode -> EDIT_CURVE + # Deselect armature object b.id_data.select_set(False) return {"FINISHED"} diff --git a/__init__.py b/__init__.py index bb44d1b..026f1d7 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "Unfold Anim Cycle", "description": "Anim tools to develop walk/run cycles along a curve", "author": "Samuel Bernou", - "version": (1, 0, 0), + "version": (1, 1, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "", @@ -17,6 +17,7 @@ if 'bpy' in locals(): imp.reload(preferences) imp.reload(OP_setup) imp.reload(OP_setup_curve_path) + imp.reload(OP_setup_curve_a_to_b) imp.reload(OP_animate_path) imp.reload(OP_expand_cycle_step) imp.reload(OP_snap_contact) @@ -28,6 +29,7 @@ else: from . import preferences from . import OP_setup from . import OP_setup_curve_path + from . import OP_setup_curve_a_to_b from . import OP_animate_path from . import OP_expand_cycle_step from . import OP_snap_contact @@ -43,6 +45,7 @@ mods = ( preferences, OP_setup, OP_setup_curve_path, + OP_setup_curve_a_to_b, OP_animate_path, OP_expand_cycle_step, OP_snap_contact, diff --git a/fn.py b/fn.py index ea90b58..a9c11b0 100644 --- a/fn.py +++ b/fn.py @@ -244,8 +244,7 @@ def orentation_track_from_vector(input_vector) -> str: def get_root_name(context=None): '''return name of rig root name''' - if context is None: - context = bpy.context + context = context or bpy.context prefs = get_addon_prefs() @@ -258,11 +257,12 @@ def get_root_name(context=None): return settings.tgt_bone +## --- curve funcs + def generate_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None): '''Create curve at provided location and direction vector''' - if context is None: - context = bpy.context + context = context or bpy.context ## using ops (dirty) # bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1)) @@ -318,6 +318,121 @@ def generate_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter return curve + +def create_follow_path_constraint(ob, curve, follow_curve=True): + prefs = get_addon_prefs() + root_name = prefs.tgt_bone + root = ob.pose.bones.get(root_name) + + if not root: + return ('ERROR', f'posebone {root_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 == ('world', '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 follow curve offset') + + const = root.constraints.new('FOLLOW_PATH') + const.target = curve + # axis only in this case, should be in addon to prefs + + ## determine which axis to use... maybe found orientation in world space from matrix_basis ? + root_world_base_direction = root.bone.matrix_local @ get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis) + const.forward_axis = orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X' + print('const.forward_axis: ', const.forward_axis) + const.use_curve_follow = True + return curve, const + +def snap_curve(): + obj = bpy.context.object + + curve = const = None + if obj.type == 'ARMATURE': + curve, const = 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') + + # get curve from field + if not curve: + curve, const = create_follow_path_constraint(obj, to_follow) + if isinstance(curve, str): + 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 = get_gnd() + if not gnd: + return + + curve_act = None + anim_frame = None + # if it's on a snap curve, fetch original + if '_snap' in curve.name: + org_name = re.sub(r'_snap\.?\d{0,3}$', '', curve.name) + org_curve = bpy.context.scene.objects.get(org_name) + if org_curve: + const.target = org_curve + + # keep action + if curve.data.animation_data and curve.data.animation_data.action: + curve_act = curve.data.animation_data.action + + anim_frame = curve.data.path_duration + # delete old snap + bpy.data.objects.remove(curve) + # assign old curve as main one + curve = org_curve + + nc = curve.copy() + name = re.sub(r'\.\d{3}$', '', curve.name) + '_snap' + const.target = nc + nc.name = name + nc.data = curve.data.copy() + nc.data.name = name + '_data' + if curve_act: + nc.data.animation_data_create() + nc.data.animation_data.action = curve_act + if anim_frame: + nc.data.path_duration = anim_frame + + curve.users_collection[0].objects.link(nc) + + ## If object mode is Curve subdivide it (TODO if nurbs needs conversion) + #-# subdivide the curve (if curve is not nurbs) + + # bpy.ops.object.mode_set(mode='EDIT') + # bpy.ops.curve.select_all(action='SELECT') + # bpy.ops.curve.subdivide(number_cuts=4) + # bpy.ops.object.mode_set(mode='OBJECT') + + # shrinkwrap or cast on ground + mod = nc.modifiers.new('Shrinkwrap', 'SHRINKWRAP') + # mod.wrap_method = 'TARGET_PROJECT' + mod.wrap_method = 'PROJECT' + mod.wrap_mode = 'ON_SURFACE' + mod.use_project_z = True + mod.use_negative_direction = True + mod.use_positive_direction = True + mod.target = gnd + + # Apply and decimate + bpy.ops.object.modifier_apply({'object': nc}, modifier="Shrinkwrap", report=False) + bpy.context.scene.anim_cycle_settings.path_to_follow = nc + # return 0, nc + +## --- action funcs + def update_action(act): '''update fcurves (often broken after generation through API)''' @@ -508,4 +623,25 @@ def create_cycle_modifiers(ob=None): continue # print(f'Adding cycle modifier {fc.data_path}') _m = fc.modifiers.new(type='CYCLES') - fc.update() \ No newline at end of file + fc.update() + +## Get collection, create if necessary +def get_col(name, parent=None, create=True): + parent = parent or bpy.context.scene.collection + col = bpy.data.collections.get(name) + if not col and create: + col = bpy.data.collections.new(name) + + if col not in parent.children[:]: + parent.children.link(col) + + return col + +def go_edit_mode(ob, context=None): + '''set mode to object, set passed obhject as active and go Edit''' + + context = context or bpy.context + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + ob.select_set(True) + context.view_layer.objects.active = ob + bpy.ops.object.mode_set(mode='EDIT', toggle=False) \ No newline at end of file diff --git a/panels.py b/panels.py index d77352b..1ddd5de 100644 --- a/panels.py +++ b/panels.py @@ -38,6 +38,14 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): if not settings.path_to_follow and not constrained: layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') + if (w_co := context.scene.collection.children.get('walk_markers')) and w_co.objects: + row=layout.row(align=True) + row.label(text=f"1st Point Frame {w_co.objects[0].name.split('_')[-1]}") + row.operator('anim.remove_a_b_step', text='', icon='X') # Remove + layout.operator('anim.create_a_b_step', text='Set 2nd Point', icon='CURVE_PATH') + else: + layout.operator('anim.create_a_b_step', text='Set 1st Point', icon='CURVE_PATH') + else: layout.operator('uac.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE') # FORCE_CURVE