diff --git a/OP_animate_path.py b/OP_animate_path.py index fd0f065..d115743 100644 --- a/OP_animate_path.py +++ b/OP_animate_path.py @@ -74,7 +74,7 @@ def anim_path_from_translate(): ## calculate offset from bones by evaluating distance at extreme distance - # fcurve : + # 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]) @@ -127,22 +127,12 @@ def anim_path_from_translate(): ## 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') + root = ob.pose.bones.get(fn.get_root_name()) if not root: print('No root found') return {"CANCELLED"} - root_axis_vec = orient_vectors[axis] # world space + 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 diff --git a/OP_setup_curve_path.py b/OP_setup_curve_path.py index 4148e52..2fe211c 100644 --- a/OP_setup_curve_path.py +++ b/OP_setup_curve_path.py @@ -3,13 +3,13 @@ from . import fn ## step 1 : Create the curve and/or follow path constraint, snap to ground - -def create_follow_path_constraint(ob, curve): +def create_follow_path_constraint(ob, curve, follow_curve=True): prefs = fn.get_addon_prefs() - bone_name = prefs.tgt_bone - root = ob.pose.bones.get(bone_name) + root_name = prefs.tgt_bone + root = ob.pose.bones.get(root_name) + if not root: - return ('ERROR', f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name') + 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'] @@ -17,15 +17,19 @@ def create_follow_path_constraint(ob, curve): root.constraints.remove(c) # loc = ob.matrix_world @ root.matrix.to_translation() - if root.name == 'root' and root.location != (0,0,0): + 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 curve offset') + 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 - const.forward_axis = 'FORWARD_X' + + ## 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 @@ -43,7 +47,7 @@ def snap_curve(): # get curve from field if not curve: curve, const = create_follow_path_constraint(obj, to_follow) - if not curve: + if isinstance(curve, str): return (curve, const) # those are error message # if obj.type == 'CURVE': @@ -125,55 +129,30 @@ class UAC_OT_create_curve_path(bpy.types.Operator): # use root (or other specified bone) to find where to put the curve prefs = fn.get_addon_prefs() ob = context.object + settings = context.scene.anim_cycle_settings bpy.ops.object.mode_set(mode='OBJECT', toggle=False) - bone_name = prefs.tgt_bone - root = ob.pose.bones.get(bone_name) + + root_name = fn.get_root_name(context=context) + root = ob.pose.bones.get(root_name) if not root: - self.report({'ERROR'}, f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name') + self.report({'ERROR'}, f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name') return {"CANCELLED"} ## create curve at bone position loc = ob.matrix_world @ root.matrix.to_translation() - bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=True, align='WORLD', location=loc, scale=(1, 1, 1)) + root_axis_vec = fn.get_direction_vector_from_enum(settings.forward_axis) - # TODO propose nurbs instead of curve - # TODO mode elegantly create the curve using data... - - # fast straighten - print('context.object: ', context.object.name) - curve = context.object - curve.name = 'curve_path' - curve.show_in_front = True - bpy.ops.curve.handle_type_set(type='VECTOR') - bpy.ops.curve.handle_type_set(type='ALIGNED') - - # offset to have start aligned - bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL', - orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL', - constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False) - - context.space_data.overlay.show_curve_normals = True - context.space_data.overlay.normals_length = 0.2 + # get real world direction of the root + world_forward = (root.matrix @ root_axis_vec) - root.matrix.to_translation() - 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) - - ## reset location to avoid double transform offset from root position - if root.name.lower() in ('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 curve offset') - - # make follow path constraint - const = root.constraints.new('FOLLOW_PATH') - const.target = curve - # axis only in this case, should be in addon to prefs - const.forward_axis = prefs.default_forward_axis # 'FORWARD_X', 'TRACK_NEGATIVE_Y' - const.use_curve_follow = True + curve = fn.generate_curve(location=loc, direction=world_forward.normalized() * 10, name='curve_path', context=context) + settings.path_to_follow = curve + + 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"} @@ -190,8 +169,10 @@ 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 - create_follow_path_constraint(ob, curve) - + err, const = create_follow_path_constraint(ob, curve) + if isinstance(err, str): + self.report({'ERROR'}, err) + return {"CANCELLED"} return {"FINISHED"} class UAC_OT_snap_curve_to_ground(bpy.types.Operator): diff --git a/README.md b/README.md index 5c9c875..8e01ce9 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ Sidebar > Anim > unfold anim cycle ## Changelog: +0.4.0 + +- better curve creation + 0.3.5 - partly working diff --git a/__init__.py b/__init__.py index e1d2bc5..045fb70 100644 --- a/__init__.py +++ b/__init__.py @@ -2,12 +2,12 @@ bl_info = { "name": "Unfold Anim Cycle", "description": "Anim tools to develop walk/run cycles along a curve", "author": "Samuel Bernou", - "version": (0, 3, 5), + "version": (0, 4, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "WIP", "doc_url": "https://gitlab.com/autour-de-minuit/blender/unfold_anim_cycle", - "category": "Object" } + "category": "Object"} # from . import other_file diff --git a/fn.py b/fn.py index ef7009f..5ef78c2 100644 --- a/fn.py +++ b/fn.py @@ -191,3 +191,109 @@ def compose_matrix(loc, rot, scale): rot_mat = rot.to_matrix().to_4x4() scale_mat = scale_matrix_from_vector(scale) return loc_mat @ rot_mat @ scale_mat + +def get_direction_vector_from_enum(string) -> Vector: + 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)) + } + return orient_vectors[string] + +def orentation_track_from_vector(input_vector) -> str: + '''return closest world track orientation name from passed vector direction''' + 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)) + } + + orient = None + min_angle = 10000 + for track, v in orient_vectors.items(): + angle = input_vector.angle(v) + if angle < min_angle: + min_angle = angle + orient = track + + return orient + +def get_root_name(context=None): + '''return name of rig root name''' + + ## auto-detect ? + ## TODO root need to be user defined (or at least quickly adjustable) + ## need to expose a scene prop from anim_cycle_settings + + if context is None: + context = bpy.context + # settings = context.scene.anim_cycle_settings + ## maybe use a field on interface + prefs = get_addon_prefs() + return prefs.tgt_bone + +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 + + ## using ops (dirty) + # bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1)) + # curve = context.object + # curve.name = 'curve_path' + # # fast straighten + # bpy.ops.curve.handle_type_set(type='VECTOR') + # bpy.ops.curve.handle_type_set(type='ALIGNED') + # bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL', + # orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL', + # constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False) + + ## using data + curve_data = bpy.data.curves.new(name, 'CURVE') # ('CURVE', 'SURFACE', 'FONT') + curve_data.dimensions = '3D' + curve_data.use_path = True + + curve = bpy.data.objects.new(name, curve_data) + spl = curve_data.splines.new('BEZIER') # ('POLY', 'BEZIER', 'NURBS') + spl.bezier_points.add(1) # one point already exists + for i in range(2): + spl.bezier_points[i].handle_left_type = 'VECTOR' # ('FREE', 'VECTOR', 'ALIGNED', 'AUTO') + spl.bezier_points[i].handle_right_type = 'VECTOR' + spl.bezier_points[1].co = direction + + # Back to aligned mode + for i in range(2): + spl.bezier_points[i].handle_right_type = spl.bezier_points[i].handle_left_type = 'ALIGNED' + + # Select second point + spl.bezier_points[1].select_control_point = True + spl.bezier_points[1].select_left_handle = True + spl.bezier_points[1].select_right_handle = True + + # link + context.scene.collection.objects.link(curve) + + + # curve object settings + curve.location = location + curve.show_in_front = True + + # enter edit + if enter_edit and context.mode == 'OBJECT': + curve.select_set(True) + context.view_layer.objects.active = curve + bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE + + ## set viewport overlay visibility for better view + if context.space_data.type == 'VIEW_3D': + context.space_data.overlay.show_curve_normals = True + context.space_data.overlay.normals_length = 0.2 + + return curve \ No newline at end of file diff --git a/panels.py b/panels.py index e2a6091..180d879 100644 --- a/panels.py +++ b/panels.py @@ -10,18 +10,23 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): def draw(self, context): layout = self.layout settings = context.scene.anim_cycle_settings - if not settings.path_to_follow: - layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') - # need to know root orientation forward) ## know direction to evaluate feet moves ## Define Constraint axis (depend on root orientation) layout.prop(settings, "forward_axis") layout.operator("uac.autoset_axis", text='Auto-Set Axis') + + box = layout.box() + if not settings.path_to_follow: + box.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') + #-# path and ground objects - layout.prop_search(settings, "path_to_follow", context.scene, "objects") - layout.prop_search(settings, "gnd", context.scene, "objects") + box.prop_search(settings, "path_to_follow", context.scene, "objects") + box.prop_search(settings, "gnd", context.scene, "objects") + row = box.row() + row.operator('anim.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON') + row.active = bool(settings.gnd) prefs = fn.get_addon_prefs() ob = context.object @@ -33,16 +38,15 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): 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') + box.label(text=f'{pb.name} -> {follow.target.name}', icon='CON_FOLLOWPATH') constrained = True ## Put this in a setting popup or submenu 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') + box.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') col=layout.column() col.prop(settings, "start_frame", text='Start') @@ -57,7 +61,9 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): row=layout.row() row.prop(settings, "linear", text='Linear') row.prop(settings, "expand_on_selected_bones") - layout.operator('anim.bake_cycle_and_step', text='Bake key and step path', icon='SHAPEKEY_DATA') + + txt = 'Bake keys' if settings.linear else 'Bake keys and step path' + layout.operator('anim.bake_cycle_and_step', text=txt, icon='SHAPEKEY_DATA') # Pin feet layout.operator('anim.pin_feets', text='Pin feets', icon='PINNED') diff --git a/properties.py b/properties.py index 60b9e3b..bde76fe 100644 --- a/properties.py +++ b/properties.py @@ -32,7 +32,7 @@ class UAC_PG_settings(bpy.types.PropertyGroup) : forward_axis : bpy.props.EnumProperty( name='Forward Axis', default='TRACK_NEGATIVE_Y', # Modifier default is FORWARD_X - description='Axis of the root bone that point forward', + description='Local axis of the "root" bone that point forward', items=( ('FORWARD_X', 'X', ''), ('FORWARD_Y', 'Y', ''),