better curve creation
parent
10e563c476
commit
8f280d37e0
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -39,6 +39,10 @@ Sidebar > Anim > unfold anim cycle
|
|||
|
||||
## Changelog:
|
||||
|
||||
0.4.0
|
||||
|
||||
- better curve creation
|
||||
|
||||
0.3.5
|
||||
|
||||
- partly working
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
106
fn.py
106
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
|
24
panels.py
24
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')
|
||||
|
|
|
@ -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', ''),
|
||||
|
|
Loading…
Reference in New Issue