first completed working version

master
Pullusb 2021-04-06 18:30:25 +02:00
parent bead484bb7
commit 477646129b
8 changed files with 874 additions and 190 deletions

View File

@ -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():

312
OP_expand_cycle_step.py Normal file
View File

@ -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)

View File

@ -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,
)

269
OP_snap_contact.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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')

35
fn.py
View File

@ -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()

View File

@ -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():