first completed working version
parent
bead484bb7
commit
477646129b
|
@ -1,7 +1,127 @@
|
||||||
import bpy
|
import bpy
|
||||||
from . import fn
|
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):
|
class UAC_OT_animate_path(bpy.types.Operator):
|
||||||
bl_idname = "anim.animate_path"
|
bl_idname = "anim.animate_path"
|
||||||
|
@ -19,7 +139,7 @@ class UAC_OT_animate_path(bpy.types.Operator):
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# TODO clear previous animation (keys) if there is any
|
# 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:
|
if err:
|
||||||
self.report({err[0]}, err[1])
|
self.report({err[0]}, err[1])
|
||||||
if err[0] == 'ERROR':
|
if err[0] == 'ERROR':
|
||||||
|
@ -37,7 +157,7 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator):
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return context.object and context.object.type in ('ARMATURE', 'CURVE')
|
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):
|
def invoke(self, context, event):
|
||||||
# check animation data of curve
|
# 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')
|
self.report({'ERROR'}, 'no curve targeted in "Path" field')
|
||||||
return {"CANCELLED"}
|
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):
|
if isinstance(curve, str):
|
||||||
self.report({curve}, _const)
|
self.report({curve}, _const)
|
||||||
return {"CANCELLED"}
|
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')
|
self.report({'ERROR'}, f'No action on {curve.name} data')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# if '_expanded' in self.act.name:
|
||||||
|
# self.report({'WARNING'}, f'Action is expanded')
|
||||||
|
|
||||||
self.fcu = None
|
self.fcu = None
|
||||||
for fcu in self.act.fcurves:
|
for fcu in self.act.fcurves:
|
||||||
if fcu.data_path == 'eval_time':
|
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
|
self.k.co.y = self.init_ky
|
||||||
context.area.header_text_set(None)
|
context.area.header_text_set(None)
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
if event.type in ('MIDDLEMOUSE', 'SPACE'): # Mmaybe not mid mouse ?
|
||||||
# return {'PASS_THROUGH'}
|
return {'PASS_THROUGH'}
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
@ -110,7 +233,6 @@ class UAC_OT_adjust_animation_length(bpy.types.Operator):
|
||||||
classes=(
|
classes=(
|
||||||
UAC_OT_animate_path,
|
UAC_OT_animate_path,
|
||||||
UAC_OT_adjust_animation_length,
|
UAC_OT_adjust_animation_length,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
|
|
@ -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)
|
|
@ -1,20 +1,55 @@
|
||||||
import bpy, re
|
import bpy, re
|
||||||
|
|
||||||
from . import fn
|
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():
|
def snap_curve():
|
||||||
obj = bpy.context.object
|
obj = bpy.context.object
|
||||||
|
|
||||||
|
curve = const = None
|
||||||
if obj.type == 'ARMATURE':
|
if obj.type == 'ARMATURE':
|
||||||
curve, const = fn.get_follow_curve_from_armature(obj)
|
curve, const = fn.get_follow_curve_from_armature(obj)
|
||||||
|
|
||||||
elif obj.type == 'CURVE':
|
to_follow = bpy.context.scene.anim_cycle_settings.path_to_follow
|
||||||
print('ERROR', f'Select the armature related to curve {obj.name}')
|
if not curve and not to_follow:
|
||||||
return
|
return ('ERROR', f'No curve pointed by "Path" filed')
|
||||||
## need to find object using follow curve to find constraint... dirty not EZ
|
|
||||||
else:
|
# get curve from field
|
||||||
print('ERROR', 'Not an armature object')
|
if not curve:
|
||||||
return
|
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()
|
gnd = fn.get_gnd()
|
||||||
if not gnd:
|
if not gnd:
|
||||||
|
@ -56,10 +91,8 @@ def snap_curve():
|
||||||
|
|
||||||
# Apply and decimate
|
# Apply and decimate
|
||||||
bpy.ops.object.modifier_apply({'object': nc}, modifier="Shrinkwrap", report=False)
|
bpy.ops.object.modifier_apply({'object': nc}, modifier="Shrinkwrap", report=False)
|
||||||
|
bpy.context.scene.anim_cycle_settings.path_to_follow = nc
|
||||||
|
# return 0, nc
|
||||||
# TODO Create the follow path modifier automatically
|
|
||||||
|
|
||||||
|
|
||||||
class UAC_OT_create_curve_path(bpy.types.Operator):
|
class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
bl_idname = "anim.create_curve_path"
|
bl_idname = "anim.create_curve_path"
|
||||||
|
@ -106,6 +139,8 @@ class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
## back to objct mode ?
|
## back to objct mode ?
|
||||||
# bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
# 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):
|
if root.name == 'root' and root.location != (0,0,0):
|
||||||
old_loc = root.location
|
old_loc = root.location
|
||||||
root.location = (0,0,0)
|
root.location = (0,0,0)
|
||||||
|
@ -120,6 +155,24 @@ class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
|
|
||||||
return {"FINISHED"}
|
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):
|
class UAC_OT_snap_curve_to_ground(bpy.types.Operator):
|
||||||
bl_idname = "anim.snap_curve_to_ground"
|
bl_idname = "anim.snap_curve_to_ground"
|
||||||
bl_label = "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'
|
return context.object and context.object.type == 'ARMATURE'
|
||||||
|
|
||||||
def execute(self, context):
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
classes=(
|
classes=(
|
||||||
UAC_OT_create_curve_path,
|
UAC_OT_create_curve_path,
|
||||||
|
UAC_OT_create_follow_path,
|
||||||
UAC_OT_snap_curve_to_ground,
|
UAC_OT_snap_curve_to_ground,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
55
__init__.py
55
__init__.py
|
@ -2,7 +2,7 @@ bl_info = {
|
||||||
"name": "Unfold Anim Cycle",
|
"name": "Unfold Anim Cycle",
|
||||||
"description": "Anim utility to develop walk/run cycles along a curve",
|
"description": "Anim utility to develop walk/run cycles along a curve",
|
||||||
"author": "Samuel Bernou",
|
"author": "Samuel Bernou",
|
||||||
"version": (0, 1, 1),
|
"version": (0, 2, 0),
|
||||||
"blender": (2, 92, 0),
|
"blender": (2, 92, 0),
|
||||||
"location": "View3D",
|
"location": "View3D",
|
||||||
"warning": "WIP",
|
"warning": "WIP",
|
||||||
|
@ -15,6 +15,8 @@ import bpy
|
||||||
|
|
||||||
from . import OP_setup_curve_path
|
from . import OP_setup_curve_path
|
||||||
from . import OP_animate_path
|
from . import OP_animate_path
|
||||||
|
from . import OP_expand_cycle_step
|
||||||
|
from . import OP_snap_contact
|
||||||
from . import panels
|
from . import panels
|
||||||
|
|
||||||
class UAC_PGT_settings(bpy.types.PropertyGroup) :
|
class UAC_PGT_settings(bpy.types.PropertyGroup) :
|
||||||
|
@ -32,12 +34,14 @@ class UAC_PGT_settings(bpy.types.PropertyGroup) :
|
||||||
# gnd : bpy.props.StringProperty(
|
# gnd : bpy.props.StringProperty(
|
||||||
# name="Ground object", description="Choose the ground object to use")
|
# 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",
|
name="On selected", description="Expand on selected bones",
|
||||||
default=True, options={'HIDDEN'})
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
# IntProperty : bpy.props.IntProperty(
|
start_frame : 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'
|
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):
|
class UAC_addon_prefs(bpy.types.AddonPreferences):
|
||||||
|
@ -57,55 +61,16 @@ class UAC_addon_prefs(bpy.types.AddonPreferences):
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
## some 2.80 UI options
|
## some 2.80 UI options
|
||||||
# layout.use_property_split = True
|
# layout.use_property_split = True
|
||||||
# flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False)
|
# flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False)
|
||||||
# layout = flow.column()
|
# layout = flow.column()
|
||||||
|
|
||||||
layout.label(text='Create')
|
layout.label(text='Create')
|
||||||
|
|
||||||
layout.prop(self, "tgt_bone")
|
layout.prop(self, "tgt_bone")
|
||||||
|
|
||||||
# display the bool prop
|
|
||||||
layout.prop(self, "debug")
|
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 ---
|
### --- REGISTER ---
|
||||||
|
|
||||||
classes=(
|
classes=(
|
||||||
|
@ -118,6 +83,8 @@ def register():
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
OP_setup_curve_path.register()
|
OP_setup_curve_path.register()
|
||||||
OP_animate_path.register()
|
OP_animate_path.register()
|
||||||
|
OP_expand_cycle_step.register()
|
||||||
|
OP_snap_contact.register()
|
||||||
panels.register()
|
panels.register()
|
||||||
|
|
||||||
# if not bpy.app.background:
|
# if not bpy.app.background:
|
||||||
|
@ -128,6 +95,8 @@ def unregister():
|
||||||
# if not bpy.app.background:
|
# if not bpy.app.background:
|
||||||
#unregister_keymaps()
|
#unregister_keymaps()
|
||||||
panels.unregister()
|
panels.unregister()
|
||||||
|
OP_snap_contact.unregister()
|
||||||
|
OP_expand_cycle_step.unregister()
|
||||||
OP_animate_path.unregister()
|
OP_animate_path.unregister()
|
||||||
OP_setup_curve_path.unregister()
|
OP_setup_curve_path.unregister()
|
||||||
|
|
||||||
|
|
113
animate_path.py
113
animate_path.py
|
@ -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
35
fn.py
|
@ -41,7 +41,8 @@ def get_follow_curve_from_armature(arm):
|
||||||
name = pref.tgt_bone
|
name = pref.tgt_bone
|
||||||
|
|
||||||
parents = []
|
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:
|
for c in root.constraints:
|
||||||
if c.type == 'FOLLOW_PATH':
|
if c.type == 'FOLLOW_PATH':
|
||||||
const = c
|
const = c
|
||||||
|
@ -67,6 +68,8 @@ def get_follow_curve_from_armature(arm):
|
||||||
|
|
||||||
return curve, const
|
return curve, const
|
||||||
|
|
||||||
|
# --- ACTIONS
|
||||||
|
|
||||||
def get_obj_action(obj):
|
def get_obj_action(obj):
|
||||||
print(helper())
|
print(helper())
|
||||||
act = obj.animation_data
|
act = obj.animation_data
|
||||||
|
@ -111,6 +114,36 @@ def set_generated_action(obj):
|
||||||
return new_act
|
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):
|
def get_curve_length(ob):
|
||||||
dg = bpy.context.evaluated_depsgraph_get()
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
obeval = ob.evaluated_get(dg)#.copy()
|
obeval = ob.evaluated_get(dg)#.copy()
|
||||||
|
|
60
panels.py
60
panels.py
|
@ -1,5 +1,5 @@
|
||||||
import bpy
|
import bpy
|
||||||
|
from . import fn
|
||||||
|
|
||||||
class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
|
class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
|
||||||
bl_space_type = "VIEW_3D"
|
bl_space_type = "VIEW_3D"
|
||||||
|
@ -9,31 +9,65 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
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
|
#-# 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, "path_to_follow", context.scene, "objects")
|
||||||
layout.prop_search(context.scene.anim_cycle_settings, "gnd", 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)
|
## Bake cycle (on selected)
|
||||||
|
row=layout.row()
|
||||||
## Expand Cycle
|
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")
|
||||||
|
|
||||||
# expand cycle
|
|
||||||
# layout.prop(context.scene.anim_cycle_settings, "expand_on_selected_boned")
|
|
||||||
|
|
||||||
# Pin feet
|
# 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=(
|
classes=(
|
||||||
UAC_PT_walk_cycle_anim_panel,
|
UAC_PT_walk_cycle_anim_panel,
|
||||||
|
UAC_PT_anim_tools_panel,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
|
Loading…
Reference in New Issue