no need for cycle and better snap curve

1.5.0

- changed: no need to have a cycle on fcurve to bake keys anymore
- changed: snap curve does not create a curve copy
- added: allow to directly snap selected curve (`ctrl + Click` to keep shrinkwarp modfifier, need to apply to affect object)
- fixed: error when going in curve edit from object mode
master
Pullusb 2022-04-27 14:54:05 +02:00
parent 578e0d7266
commit c98bb520b8
6 changed files with 158 additions and 85 deletions

View File

@ -1,5 +1,12 @@
# Changelog
1.5.0
- changed: no need to have a cycle on fcurve to bake keys anymore
- changed: snap curve does not create a curve copy
- added: allow to directly snap selected curve (`ctrl + Click` to keep shrinkwarp modfifier, need to apply to affect object)
- fixed: error when going in curve edit from object mode
1.4.4
- changed: default start to 100

View File

@ -36,22 +36,18 @@ def bake_cycle(on_selection=True, end=None):
last = max(all_keys) # int(max(all_keys))
offset = last - first
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']:
ct_no_cycle += 1
continue
if debug: print(fcu.data_path, 'has cycle')
for fcu in fn.get_only_pose_keyable_fcurves(obj, action=act):
#-# old -- filter only on fcurve that have a cycle modifier (maybe as an option)
# if not [m for m in fcu.modifiers if m.type == 'CYCLES']:
# ct_no_cycle += 1
# 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]
if debug: print(b_name, 'has cycle')
pb = obj.pose.bones.get(b_name)
if not pb:
print(f'{b_name} is invalid')

View File

@ -104,7 +104,7 @@ class AW_OT_remove_follow_path(bpy.types.Operator):
class AW_OT_snap_curve_to_ground(bpy.types.Operator):
bl_idname = "autowalk.snap_curve_to_ground"
bl_label = "Snap Curve"
bl_description = "snap curve to ground determine in field"
bl_description = "Snap curve to ground determine in field"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@ -119,6 +119,34 @@ class AW_OT_snap_curve_to_ground(bpy.types.Operator):
return {"CANCELLED"}
return {"FINISHED"}
class AW_OT_snap_selected_curve(bpy.types.Operator):
bl_idname = "autowalk.snap_selected_curve"
bl_label = "Snap Selected Curve"
bl_description = "Snap selected curve to ground\
\nCtrl + Click : Not apply Shrinkwarp modifier (Apply manually)"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'CURVE'
def invoke(self, context, event):
self.apply = not event.ctrl # don't apply if ctrl is pressd
return self.execute(context)
def execute(self, context):
ob = context.object
# bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
ground = fn.get_gnd()
if not ground:
self.report({'ERROR'}, 'Need to specify ground (in curve options) or name an object in scene "Ground"')
return {"CANCELLED"}
fn.shrinkwrap_on_object(ob, ground, apply=self.apply)
if self.apply:
self.report({'INFO'}, 'ShrinkWrap Modifier need to be applyed manually')
return {"FINISHED"}
class AW_OT_edit_curve(bpy.types.Operator):
bl_idname = "autowalk.edit_curve"
bl_label = "Edit Curve"
@ -130,6 +158,7 @@ class AW_OT_edit_curve(bpy.types.Operator):
return context.object and context.object.type == 'ARMATURE'
def execute(self, context):
ob = context.object
b = context.active_pose_bone
curve = None
@ -139,7 +168,7 @@ class AW_OT_edit_curve(bpy.types.Operator):
# get from 'root' bone
if not curve:
curve, _const = fn.get_follow_curve_from_armature(context.object)
curve, _const = fn.get_follow_curve_from_armature(ob)
if isinstance(curve, str):
self.report({curve}, _const)
if curve == 'ERROR':
@ -150,7 +179,9 @@ class AW_OT_edit_curve(bpy.types.Operator):
# curve context.mode -> EDIT_CURVE
# Deselect armature object
b.id_data.select_set(False)
# b.id_data.select_set(False)
ob.select_set(False)
return {"FINISHED"}
class AW_OT_go_to_object(bpy.types.Operator):
@ -235,6 +266,7 @@ AW_OT_create_curve_path,
AW_OT_create_follow_path,
AW_OT_remove_follow_path,
AW_OT_snap_curve_to_ground,
AW_OT_snap_selected_curve,
AW_OT_edit_curve,
AW_OT_go_to_object,
AW_OT_object_from_curve, # use set_choice_id is used to set an index in object_from_curve pop up menu

View File

@ -4,7 +4,7 @@ bl_info = {
"name": "Auto Walk",
"description": "Develop a walk/run cycles along a curve and pin feets",
"author": "Samuel Bernou",
"version": (1, 4, 4),
"version": (1, 5, 0),
"blender": (3, 0, 0),
"location": "View3D",
"warning": "",

170
fn.py
View File

@ -389,66 +389,98 @@ def create_follow_path_constraint(ob, curve, follow_curve=True):
const.use_curve_follow = True
return curve, const
def snap_curve():
def shrinkwrap_on_object(source, target, apply=True):
# shrinkwrap or cast on ground
mod = source.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 = target
if apply:
# Apply and decimate
switch = False
if bpy.context.mode == 'EDIT_CURVE':
switch = True
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.modifier_apply({'object': source}, modifier="Shrinkwrap", report=False)
if switch:
bpy.ops.object.mode_set(mode='EDIT')
def snap_curve(create_copy=False):
obj = bpy.context.object
gnd = get_gnd()
if not gnd:
return
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
if create_copy:
# get curve from field
if not curve and not to_follow:
return ('ERROR', f'No curve pointed by "Path" filed')
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')
# 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
# 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)
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
if org_curve:
const.target = org_curve
curve.users_collection[0].objects.link(nc)
# 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)
else:
if not curve:
return ('ERROR', 'Path not found')
nc = curve
## If object mode is Curve subdivide it (TODO if nurbs needs conversion)
#-# subdivide the curve (if curve is not nurbs)
@ -458,18 +490,7 @@ def snap_curve():
# 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)
shrinkwrap_on_object(nc, gnd)
bpy.context.scene.anim_cycle_settings.path_to_follow = nc
# return 0, nc
@ -646,10 +667,11 @@ def remove_all_cycles_modifier(ob=None):
print(f'Remove cyclic modifiers on {ct} fcurve(s)')
return ct
def create_cycle_modifiers(ob=None):
ob = ob or bpy.context.object
def get_only_pose_keyable_fcurves(ob, action=None):
'''Can action providing another action (must be for the same object)'''
# skip bones that are on protected layers ?
act = action or ob.animation_data.action
## skip bones that are on protected layers ?
# protected = [i for i, l in enumerate(ob.data.layers_protected) if l]
# for b in ob.data.bones:
# if b.use_deform: # don't affect deform bones
@ -657,24 +679,34 @@ def create_cycle_modifiers(ob=None):
## b_layers = [i for i, l in enumerate(b.layers) if l]
name_list = [b.name for b in ob.data.bones] # if not b.use_deform (too limiting)
fcus = []
re_prefix = re.compile(r'^(mch|def|org|vis|fld|ctp)[\._-]', flags=re.I)
ct = 0
for fc in ob.animation_data.action.fcurves:
if [m for m in fc.modifiers if m.type == 'CYCLES']:
# skip already existing modifier
continue
if not '"' in fc.data_path:
continue
for fc in act.fcurves:
# skip offset
if fc.data_path.endswith('.offset') and 'constraint' in fc.data_path:
continue
# skip fcus that are not bones
if not '"' in fc.data_path:
continue
b_name = fc.data_path.split('"')[1]
if b_name not in name_list:
continue
if re_prefix.match(b_name):
continue
fcus.append(fc)
return fcus
if b_name not in name_list:
def create_cycle_modifiers(ob=None):
ob = ob or bpy.context.object
ct = 0
keyable_fcurves = get_only_pose_keyable_fcurves(ob)
for fc in keyable_fcurves:
if [m for m in fc.modifiers if m.type == 'CYCLES']:
# skip if already existing modifier
continue
# print(f'Adding cycle modifier {fc.data_path}')
_m = fc.modifiers.new(type='CYCLES')

View File

@ -66,7 +66,13 @@ class AW_PT_walk_cycle_anim_panel(bpy.types.Panel):
box.prop_search(settings, "gnd", context.scene, "objects")
row = box.row()
row.operator('autowalk.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON')
if ob and ob.type == 'ARMATURE':
row.operator('autowalk.snap_curve_to_ground', text='Snap Curve To Ground', icon='SNAP_ON')
elif ob and ob.type == 'CURVE':
row.operator('autowalk.snap_selected_curve', text='Snap Selected Curve To Ground', icon='SNAP_ON')
else:
row.label(text='Select curve or armature to snap', icon='INFO')
row.active = bool(settings.gnd)