wip working on path animate
parent
62aeddfd49
commit
5a221aa0fe
|
@ -1,9 +1,31 @@
|
||||||
import bpy
|
import bpy
|
||||||
from . import fn
|
from . import fn
|
||||||
|
from mathutils import Vector, Euler
|
||||||
## step 2 : Auto animate the path (with feet selection) and modal to adjust speed
|
## step 2 : Auto animate the path (with feet selection) and modal to adjust speed
|
||||||
|
|
||||||
def anim_path_from_y_translate():
|
def get_bone_transform_at_frame(b, act, frame):
|
||||||
|
'''Find every loc, rot, scale values at given frame'''
|
||||||
|
transform = {}
|
||||||
|
for channel in ('location', 'rotation_euler', 'scale'):
|
||||||
|
chan_list = []
|
||||||
|
for i in range(3):
|
||||||
|
f = act.fcurves.find(f'pose.bones["{b.name}"].{channel}', index=i)
|
||||||
|
if not f:
|
||||||
|
print(frame, channel, 'not animated ! using current value')
|
||||||
|
chan_list.append(getattr(b, channel)) # get current value since not animated
|
||||||
|
continue
|
||||||
|
chan_list.append(f.evaluate(frame))
|
||||||
|
|
||||||
|
print(frame, b.name, channel, chan_list)
|
||||||
|
if channel == 'rotation_euler':
|
||||||
|
transform[channel] = Euler(chan_list)
|
||||||
|
else:
|
||||||
|
transform[channel] = Vector(chan_list)
|
||||||
|
|
||||||
|
return transform # loc, rot, scale
|
||||||
|
|
||||||
|
|
||||||
|
def anim_path_from_translate():
|
||||||
print(fn.helper())
|
print(fn.helper())
|
||||||
|
|
||||||
obj = bpy.context.object
|
obj = bpy.context.object
|
||||||
|
@ -13,8 +35,8 @@ def anim_path_from_y_translate():
|
||||||
# found curve through constraint
|
# found curve through constraint
|
||||||
b = bpy.context.active_pose_bone
|
b = bpy.context.active_pose_bone
|
||||||
|
|
||||||
if not 'shoe' in b.bone.name:
|
# if not 'foot' in b.bone.name:
|
||||||
return ('ERROR', 'No "shoe" in active bone name\n-> Select foot that has the most reliable contact')
|
# return ('ERROR', 'No "foot" in active bone name\n-> Select foot that has the most reliable contact')
|
||||||
|
|
||||||
curve = None
|
curve = None
|
||||||
if bpy.context.scene.anim_cycle_settings.path_to_follow:
|
if bpy.context.scene.anim_cycle_settings.path_to_follow:
|
||||||
|
@ -43,24 +65,87 @@ def anim_path_from_y_translate():
|
||||||
# CHANGE - retiré le int de la frame
|
# CHANGE - retiré le int de la frame
|
||||||
# keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points]
|
# keyframes = [int(k.co[0]) for fcu in act.fcurves for k in fcu.keyframe_points]
|
||||||
|
|
||||||
|
## calculate offset from bones by evaluating distance at extreme distance
|
||||||
|
|
||||||
|
|
||||||
|
# fcurve :
|
||||||
|
# name : fcu.data_path.split('"')[1] (bone_name)
|
||||||
|
# properties: fcu.data_path.split('.')[-1] ('location', rotation_euler)
|
||||||
|
# axis : fcu.array_index (to get axis letter : {0:'X', 1:'Y', 2:'Z'}[fcu.array_index])
|
||||||
|
|
||||||
|
## get only fcurves relative to selected bone
|
||||||
|
b_fcurves = [fcu for fcu in act.fcurves if fcu.data_path.split('"')[1] == b.bone.name]
|
||||||
|
|
||||||
|
|
||||||
|
start_frame = end_frame = None
|
||||||
|
|
||||||
|
for fcu in b_fcurves:
|
||||||
|
encountered_marks = False # flag to stop after last extreme of each fcu
|
||||||
|
for k in fcu.keyframe_points:
|
||||||
|
# if k.select_control_point: # based on selection ?
|
||||||
|
if k.type == 'EXTREME':
|
||||||
|
encountered_marks = True
|
||||||
|
|
||||||
|
f = k.co.x
|
||||||
|
if start_frame is None:
|
||||||
|
start_frame = f
|
||||||
|
if start_frame > f:
|
||||||
|
start_frame = f
|
||||||
|
|
||||||
|
if end_frame is None:
|
||||||
|
end_frame = f
|
||||||
|
if end_frame < f:
|
||||||
|
end_frame = f
|
||||||
|
|
||||||
|
else:
|
||||||
|
if encountered_marks:
|
||||||
|
## means back to other frame type after passed breakdown we stop
|
||||||
|
## (for this fcu)
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_frame is None or end_frame is None:
|
||||||
|
return ('ERROR', f'No (or not enough) keyframe marked Extreme {obj.name} > {b.name}')
|
||||||
|
if start_frame == end_frame:
|
||||||
|
return ('ERROR', f'Only one key detected as extreme (at frame {start_frame}) !\nNeed at least two chained marked keys')
|
||||||
|
|
||||||
|
print(f'Offset from key range. start: {start_frame} - end: {end_frame}')
|
||||||
|
move_frame = end_frame - start_frame
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Find move_val from diff position at start and end frame wihtin character forward axis
|
||||||
|
|
||||||
|
|
||||||
|
start_transform = get_bone_transform_at_frame(b, act, start_frame)
|
||||||
|
start_mat = fn.compose_matrix(start_transform['location'], start_transform['rotation_euler'], start_transform['scale'])
|
||||||
|
|
||||||
|
end_transform = get_bone_transform_at_frame(b, act, end_frame)
|
||||||
|
end_mat = fn.compose_matrix(end_transform['location'], end_transform['rotation_euler'], end_transform['scale'])
|
||||||
|
|
||||||
|
# bpy.context.scene.cursor.location = (b.bone.matrix_local @ start_mat).to_translation() # obj.matrix_world @
|
||||||
|
bpy.context.scene.cursor.location = (b.bone.matrix_local @ end_mat).to_translation() # obj.matrix_world @
|
||||||
|
# bpy.context.scene.cursor.location = (b.matrix_basis.inverted() @ start_mat).to_translation() # obj.matrix_world @
|
||||||
|
# bpy.context.scene.cursor.location = start_transform['location'] # obj.matrix_world @
|
||||||
|
return # FIXME
|
||||||
|
|
||||||
|
### old method using directly one axis
|
||||||
|
"""
|
||||||
|
axis = {'X':0, 'Y':1, 'Z':2}
|
||||||
## calculate offset from bones
|
## calculate offset from bones
|
||||||
locy_fcu = None
|
loc_fcu = None
|
||||||
for fcu in act.fcurves:
|
for fcu in act.fcurves:
|
||||||
if fcu.data_path.split('"')[1] != b.bone.name:
|
if fcu.data_path.split('"')[1] != b.bone.name:
|
||||||
continue
|
continue
|
||||||
if fcu.data_path.split('.')[-1] == 'location' and fcu.array_index == 1:
|
if fcu.data_path.split('.')[-1] == 'location' and fcu.array_index == axis['Y']:
|
||||||
locy_fcu = fcu
|
loc_fcu = fcu
|
||||||
|
if not loc_fcu:
|
||||||
if not locy_fcu:
|
|
||||||
return ('ERROR', 'Current bone, location.y animation not found')
|
return ('ERROR', 'Current bone, location.y animation not found')
|
||||||
|
|
||||||
start = None
|
start = None
|
||||||
end = None
|
end = None
|
||||||
|
|
||||||
## based on extreme
|
## based on extreme
|
||||||
for k in locy_fcu.keyframe_points:
|
for k in loc_fcu.keyframe_points:
|
||||||
# if k.select_control_point: # based on selection
|
# if k.select_control_point: # based on selection
|
||||||
if k.type == 'EXTREME': # using extreme keys.
|
if k.type == 'EXTREME': # using extreme keys.
|
||||||
if start is None:
|
if start is None:
|
||||||
|
@ -75,7 +160,7 @@ def anim_path_from_y_translate():
|
||||||
print(f'Offset from key range. start: {start.co.x} - end: {end.co.x}')
|
print(f'Offset from key range. start: {start.co.x} - end: {end.co.x}')
|
||||||
|
|
||||||
if not start:
|
if not start:
|
||||||
return ('ERROR', f"No extreme marked frame was found on bone {b.bone.name}.{['x','y','z'][locy_fcu.array_index]}")
|
return ('ERROR', f"No extreme marked frame was found on bone {b.bone.name}.{['x','y','z'][loc_fcu.array_index]}")
|
||||||
if start == end:
|
if start == end:
|
||||||
return ('ERROR', f'Only one key detected as extreme (at frame {start.co.x}) !\nNeed at least two chained marked keys')
|
return ('ERROR', f'Only one key detected as extreme (at frame {start.co.x}) !\nNeed at least two chained marked keys')
|
||||||
|
|
||||||
|
@ -89,6 +174,9 @@ def anim_path_from_y_translate():
|
||||||
|
|
||||||
# Y positive value (forward) ->
|
# Y positive value (forward) ->
|
||||||
move_val = abs(start_val - end_val)
|
move_val = abs(start_val - end_val)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
print('move_val: ', move_val)
|
print('move_val: ', move_val)
|
||||||
|
|
||||||
length = fn.get_curve_length(curve)
|
length = fn.get_curve_length(curve)
|
||||||
|
@ -123,13 +211,16 @@ def anim_path_from_y_translate():
|
||||||
## set all to constant
|
## set all to constant
|
||||||
# for k in t_fcu.keyframe_points:
|
# for k in t_fcu.keyframe_points:
|
||||||
# k.interpolation = 'CONSTANT'
|
# k.interpolation = 'CONSTANT'
|
||||||
|
|
||||||
print('end of set_follow_path_anim')
|
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"
|
||||||
bl_label = "Animate Path"
|
bl_label = "Animate Path"
|
||||||
bl_description = "Select the most representative 'in contact' feet of the cycle"
|
bl_description = "Select the most representative 'in contact' feet of the cycle\
|
||||||
|
\nSelected bone should have two keyframe marked as type extreme (red):\
|
||||||
|
\nA key for foot first ground contact and other key for foot last contact frame\
|
||||||
|
\nSelect keyframe and use R > Extreme)"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -142,7 +233,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 = anim_path_from_y_translate()
|
err = anim_path_from_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':
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector, Quaternion
|
||||||
|
from math import sin, cos, radians
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
## all action needed to setup the walk
|
||||||
|
|
||||||
|
## Need to know what is the forward axis
|
||||||
|
|
||||||
|
|
||||||
|
## https://www.meccanismocomplesso.org/en/3d-rotations-and-euler-angles-in-python/
|
||||||
|
|
||||||
|
class UAC_OT_autoset_axis(bpy.types.Operator):
|
||||||
|
bl_idname = "uac.autoset_axis"
|
||||||
|
bl_label = "Auto Set Axis"
|
||||||
|
bl_description = "Define forward axis from armature"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'ARMATURE'
|
||||||
|
|
||||||
|
# def invoke(self, context, event):
|
||||||
|
# self.shift = event.shift
|
||||||
|
# return self.execute(context)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
ob = context.object
|
||||||
|
|
||||||
|
## root need to be user defined to assume corner cases
|
||||||
|
root = ob.pose.bones.get('world')
|
||||||
|
if not root:
|
||||||
|
print('No root found')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
root_pos = ob.matrix_world @ root.matrix.to_translation()
|
||||||
|
|
||||||
|
up = Vector((0,0,1))
|
||||||
|
|
||||||
|
# gather all .R suffixed bones
|
||||||
|
|
||||||
|
all_right_pos = [ob.matrix_world @ b.matrix.to_translation() for b in ob.pose.bones if b.bone.name.lower().endswith('.r')]
|
||||||
|
|
||||||
|
if not all_right_pos:
|
||||||
|
print('No .r suffix found, need to adjust manually')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# get median position
|
||||||
|
median_r_pos = (sum(all_right_pos, Vector()) / len(all_right_pos)) - root_pos
|
||||||
|
|
||||||
|
median_r_vec = Vector((median_r_pos[0], median_r_pos[1], 0))
|
||||||
|
median_r_vec.normalize()
|
||||||
|
|
||||||
|
# Determine forward direction by rotating 90 degrees CCW on up axis
|
||||||
|
guessed_forward = Quaternion(Vector((0,0,1)), radians(90)) @ median_r_vec
|
||||||
|
|
||||||
|
## TODO determine what axis of root align bests with guessed_forward (minimum 3D angle)
|
||||||
|
|
||||||
|
axises = {'FORWARD_Y':(0,1,0), 'TRACK_NEGATIVE_Y':(0,-1,0), 'FORWARD_Z':(0,0,1), 'TRACK_NEGATIVE_Z':(0,0,-1)} # 'FORWARD_X':(1,0,0), 'TRACK_NEGATIVE_X':(-1,0,0)
|
||||||
|
ref_angle = 10
|
||||||
|
best_axis = False
|
||||||
|
for axis, vec in axises.items(): # (1,0,0), (-1,0,0)
|
||||||
|
angle = guessed_forward.angle(ob.matrix_world @ root.matrix @ Vector(vec))
|
||||||
|
print(axis, 'angle: ', angle)
|
||||||
|
if angle < ref_angle:
|
||||||
|
ref_angle = angle
|
||||||
|
best_axis = axis
|
||||||
|
|
||||||
|
if not best_axis:
|
||||||
|
print('No axis found')
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
context.scene.anim_cycle_settings.forward_axis = best_axis
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(UAC_OT_autoset_axis)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(UAC_OT_autoset_axis)
|
||||||
|
|
|
@ -5,8 +5,8 @@ from . import fn
|
||||||
|
|
||||||
|
|
||||||
def create_follow_path_constraint(ob, curve):
|
def create_follow_path_constraint(ob, curve):
|
||||||
pref = fn.get_addon_prefs()
|
prefs = fn.get_addon_prefs()
|
||||||
bone_name = pref.tgt_bone
|
bone_name = prefs.tgt_bone
|
||||||
root = ob.pose.bones.get(bone_name)
|
root = ob.pose.bones.get(bone_name)
|
||||||
if not root:
|
if not root:
|
||||||
return ('ERROR', f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name')
|
return ('ERROR', f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name')
|
||||||
|
@ -123,20 +123,22 @@ class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
# use root (or other specified bone) to find where to put the curve
|
# use root (or other specified bone) to find where to put the curve
|
||||||
pref = fn.get_addon_prefs()
|
prefs = fn.get_addon_prefs()
|
||||||
ob = context.object
|
ob = context.object
|
||||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||||
bone_name = pref.tgt_bone
|
bone_name = prefs.tgt_bone
|
||||||
root = ob.pose.bones.get(bone_name)
|
root = ob.pose.bones.get(bone_name)
|
||||||
if not root:
|
if not root:
|
||||||
self.report({'ERROR'}, f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name')
|
self.report({'ERROR'}, f'posebone {bone_name} not found in armature {ob.name} check addon preferences to change name')
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
## create curve at bone position
|
||||||
loc = ob.matrix_world @ root.matrix.to_translation()
|
loc = ob.matrix_world @ root.matrix.to_translation()
|
||||||
|
bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=True, align='WORLD', location=loc, scale=(1, 1, 1))
|
||||||
|
|
||||||
# TODO propose nurbs instead of curve
|
# TODO propose nurbs instead of curve
|
||||||
# TODO mode elegantly create the curve using data...
|
# TODO mode elegantly create the curve using data...
|
||||||
bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=True, align='WORLD', location=loc, scale=(1, 1, 1))
|
|
||||||
# fast straighten
|
# fast straighten
|
||||||
print('context.object: ', context.object.name)
|
print('context.object: ', context.object.name)
|
||||||
curve = context.object
|
curve = context.object
|
||||||
|
@ -144,7 +146,8 @@ class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
curve.show_in_front = True
|
curve.show_in_front = True
|
||||||
bpy.ops.curve.handle_type_set(type='VECTOR')
|
bpy.ops.curve.handle_type_set(type='VECTOR')
|
||||||
bpy.ops.curve.handle_type_set(type='ALIGNED')
|
bpy.ops.curve.handle_type_set(type='ALIGNED')
|
||||||
# offset to have start
|
|
||||||
|
# offset to have start aligned
|
||||||
bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL',
|
bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL',
|
||||||
orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL',
|
orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL',
|
||||||
constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False)
|
constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False)
|
||||||
|
@ -157,8 +160,9 @@ class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
# 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)
|
## TODO replace this part with >> create_follow_path_constraint(ob, curve)
|
||||||
|
|
||||||
if root.name == 'root' and root.location != (0,0,0):
|
## reset location to avoid double transform offset from root position
|
||||||
|
if root.name.lower() in ('world', 'root') and root.location != (0,0,0):
|
||||||
old_loc = root.location
|
old_loc = root.location
|
||||||
root.location = (0,0,0)
|
root.location = (0,0,0)
|
||||||
print(f'root moved from {old_loc} to (0,0,0) to counter curve offset')
|
print(f'root moved from {old_loc} to (0,0,0) to counter curve offset')
|
||||||
|
@ -167,7 +171,7 @@ class UAC_OT_create_curve_path(bpy.types.Operator):
|
||||||
const = root.constraints.new('FOLLOW_PATH')
|
const = root.constraints.new('FOLLOW_PATH')
|
||||||
const.target = curve
|
const.target = curve
|
||||||
# axis only in this case, should be in addon to prefs
|
# axis only in this case, should be in addon to prefs
|
||||||
const.forward_axis = 'FORWARD_X' # 'TRACK_NEGATIVE_Y'
|
const.forward_axis = prefs.default_forward_axis # 'FORWARD_X', 'TRACK_NEGATIVE_Y'
|
||||||
const.use_curve_follow = True
|
const.use_curve_follow = True
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
|
@ -39,6 +39,10 @@ Sidebar > Anim > unfold anim cycle
|
||||||
|
|
||||||
## Changelog:
|
## Changelog:
|
||||||
|
|
||||||
|
0.3.3
|
||||||
|
|
||||||
|
- total wip
|
||||||
|
|
||||||
0.3.0
|
0.3.0
|
||||||
|
|
||||||
- Rework interface
|
- Rework interface
|
||||||
|
|
107
__init__.py
107
__init__.py
|
@ -1,9 +1,9 @@
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Unfold Anim Cycle",
|
"name": "Unfold Anim Cycle",
|
||||||
"description": "Anim utility to develop walk/run cycles along a curve",
|
"description": "Anim tools to develop walk/run cycles along a curve",
|
||||||
"author": "Samuel Bernou",
|
"author": "Samuel Bernou",
|
||||||
"version": (0, 3, 2),
|
"version": (0, 3, 3),
|
||||||
"blender": (2, 92, 0),
|
"blender": (3, 0, 0),
|
||||||
"location": "View3D",
|
"location": "View3D",
|
||||||
"warning": "WIP",
|
"warning": "WIP",
|
||||||
"doc_url": "https://gitlab.com/autour-de-minuit/blender/unfold_anim_cycle",
|
"doc_url": "https://gitlab.com/autour-de-minuit/blender/unfold_anim_cycle",
|
||||||
|
@ -13,6 +13,9 @@ bl_info = {
|
||||||
|
|
||||||
if 'bpy' in locals():
|
if 'bpy' in locals():
|
||||||
import importlib as imp
|
import importlib as imp
|
||||||
|
imp.reload(properties)
|
||||||
|
imp.reload(preferences)
|
||||||
|
imp.reload(OP_setup)
|
||||||
imp.reload(OP_setup_curve_path)
|
imp.reload(OP_setup_curve_path)
|
||||||
imp.reload(OP_animate_path)
|
imp.reload(OP_animate_path)
|
||||||
imp.reload(OP_expand_cycle_step)
|
imp.reload(OP_expand_cycle_step)
|
||||||
|
@ -20,6 +23,9 @@ if 'bpy' in locals():
|
||||||
imp.reload(OP_world_copy_paste)
|
imp.reload(OP_world_copy_paste)
|
||||||
imp.reload(panels)
|
imp.reload(panels)
|
||||||
else:
|
else:
|
||||||
|
from . import properties
|
||||||
|
from . import preferences
|
||||||
|
from . import OP_setup
|
||||||
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_expand_cycle_step
|
||||||
|
@ -28,89 +34,30 @@ else:
|
||||||
from . import panels
|
from . import panels
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
class UAC_PGT_settings(bpy.types.PropertyGroup) :
|
|
||||||
## HIDDEN to hide the animatable dot thing
|
|
||||||
path_to_follow : bpy.props.PointerProperty(type=bpy.types.Object,
|
|
||||||
name="Path", description="Curve object used")
|
|
||||||
|
|
||||||
gnd : bpy.props.PointerProperty(type=bpy.types.Object,
|
|
||||||
name="Ground", description="Choose the ground object to use")
|
|
||||||
|
|
||||||
expand_on_selected_bones : bpy.props.BoolProperty(
|
|
||||||
name="On selected", description="Expand on selected bones",
|
|
||||||
default=True, options={'HIDDEN'})
|
|
||||||
|
|
||||||
linear : bpy.props.BoolProperty(
|
|
||||||
name="Linear", description="keep the animation path linear (Else step the path usings cycle keys)",
|
|
||||||
default=False, options={'HIDDEN'})
|
|
||||||
|
|
||||||
start_frame : bpy.props.IntProperty(
|
mods = (
|
||||||
name="Start Frame", description="Starting frame for animation path",
|
properties,
|
||||||
default=100,
|
preferences,
|
||||||
min=0, max=2**31-1, soft_min=0, soft_max=2**31-1, step=1, options={'HIDDEN'})#, subtype='PIXEL'
|
OP_setup,
|
||||||
|
OP_setup_curve_path,
|
||||||
|
OP_animate_path,
|
||||||
class UAC_addon_prefs(bpy.types.AddonPreferences):
|
OP_expand_cycle_step,
|
||||||
## can be just __name__ if prefs are in the __init__ mainfile
|
OP_snap_contact,
|
||||||
# Else need the splitext '__name__ = addonname.subfile' (or use a static name)
|
OP_world_copy_paste,
|
||||||
bl_idname = __name__.split('.')[0] # or with: os.path.splitext(__name__)[0]
|
panels,
|
||||||
|
|
||||||
# some_bool_prop to display in the addon pref
|
|
||||||
debug : bpy.props.BoolProperty(
|
|
||||||
name='Debug',
|
|
||||||
description="Enable Debug prints",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
tgt_bone : bpy.props.StringProperty(
|
|
||||||
name="Constrained Pose bone name", default='root',
|
|
||||||
description="name of the bone that suppose to hold the constraint")
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
layout.prop(self, "debug")
|
|
||||||
|
|
||||||
### --- REGISTER ---
|
|
||||||
|
|
||||||
classes=(
|
|
||||||
UAC_PGT_settings,
|
|
||||||
UAC_addon_prefs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
if bpy.app.background:
|
||||||
bpy.utils.register_class(cls)
|
return
|
||||||
OP_setup_curve_path.register()
|
for module in mods:
|
||||||
OP_animate_path.register()
|
module.register()
|
||||||
OP_expand_cycle_step.register()
|
|
||||||
OP_snap_contact.register()
|
|
||||||
OP_world_copy_paste.register()
|
|
||||||
panels.register()
|
|
||||||
|
|
||||||
# if not bpy.app.background:
|
|
||||||
#register_keymaps()
|
|
||||||
bpy.types.Scene.anim_cycle_settings = bpy.props.PointerProperty(type = UAC_PGT_settings)
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
# if not bpy.app.background:
|
if bpy.app.background:
|
||||||
#unregister_keymaps()
|
return
|
||||||
panels.unregister()
|
for module in reversed(mods):
|
||||||
OP_world_copy_paste.unregister()
|
module.unregister()
|
||||||
OP_snap_contact.unregister()
|
|
||||||
OP_expand_cycle_step.unregister()
|
|
||||||
OP_animate_path.unregister()
|
|
||||||
OP_setup_curve_path.unregister()
|
|
||||||
|
|
||||||
for cls in reversed(classes):
|
|
||||||
bpy.utils.unregister_class(cls)
|
|
||||||
del bpy.types.Scene.anim_cycle_settings
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
register()
|
register()
|
||||||
|
|
23
fn.py
23
fn.py
|
@ -1,6 +1,7 @@
|
||||||
import bpy
|
import bpy
|
||||||
import re
|
import re
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from mathutils import Matrix, Vector, Color
|
||||||
|
|
||||||
def get_addon_prefs():
|
def get_addon_prefs():
|
||||||
'''
|
'''
|
||||||
|
@ -28,7 +29,7 @@ def helper(name: str = '') -> str:
|
||||||
|
|
||||||
def convertAttr(Attr):
|
def convertAttr(Attr):
|
||||||
'''Convert given value to a Json serializable format'''
|
'''Convert given value to a Json serializable format'''
|
||||||
if type(Attr) in [type(mathutils.Vector()),type(mathutils.Color())]:
|
if type(Attr) in [type(Vector()),type(Color())]:
|
||||||
if len(Attr) == 3:
|
if len(Attr) == 3:
|
||||||
return([Attr[0],Attr[1],Attr[2]])
|
return([Attr[0],Attr[1],Attr[2]])
|
||||||
elif len(Attr) == 2:
|
elif len(Attr) == 2:
|
||||||
|
@ -37,7 +38,7 @@ def convertAttr(Attr):
|
||||||
return([Attr[0]])
|
return([Attr[0]])
|
||||||
elif len(Attr) == 4:
|
elif len(Attr) == 4:
|
||||||
return([Attr[0],Attr[1],Attr[2],Attr[3]])
|
return([Attr[0],Attr[1],Attr[2],Attr[3]])
|
||||||
elif type(Attr) == type(mathutils.Matrix()):
|
elif type(Attr) == type(Matrix()):
|
||||||
return (np.matrix(Attr).tolist())
|
return (np.matrix(Attr).tolist())
|
||||||
else:
|
else:
|
||||||
return(Attr)
|
return(Attr)
|
||||||
|
@ -162,6 +163,7 @@ def set_expanded_action(obj):
|
||||||
|
|
||||||
|
|
||||||
def get_curve_length(ob):
|
def get_curve_length(ob):
|
||||||
|
'''Get a curve object, return a float representing world space length'''
|
||||||
dg = bpy.context.evaluated_depsgraph_get()
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
obeval = ob.evaluated_get(dg)#.copy()
|
obeval = ob.evaluated_get(dg)#.copy()
|
||||||
baked_me = obeval.to_mesh(preserve_all_data_layers=False, depsgraph=dg)
|
baked_me = obeval.to_mesh(preserve_all_data_layers=False, depsgraph=dg)
|
||||||
|
@ -173,4 +175,19 @@ def get_curve_length(ob):
|
||||||
total_length += ((mat @ baked_me.vertices[i-1].co) - (mat @ baked_me.vertices[i].co)).length
|
total_length += ((mat @ baked_me.vertices[i-1].co) - (mat @ baked_me.vertices[i].co)).length
|
||||||
|
|
||||||
print("total_length", total_length)#Dbg
|
print("total_length", total_length)#Dbg
|
||||||
return total_length
|
return total_length
|
||||||
|
|
||||||
|
|
||||||
|
def scale_matrix_from_vector(scale):
|
||||||
|
# recreate a neutral mat scale
|
||||||
|
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
||||||
|
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
||||||
|
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
||||||
|
matscale = matscale_x @ matscale_y @ matscale_z
|
||||||
|
return matscale
|
||||||
|
|
||||||
|
def compose_matrix(loc, rot, scale):
|
||||||
|
loc_mat = Matrix.Translation(loc)
|
||||||
|
rot_mat = rot.to_matrix().to_4x4()
|
||||||
|
scale_mat = scale_matrix_from_vector(scale)
|
||||||
|
return loc_mat @ rot_mat @ scale_mat
|
||||||
|
|
29
panels.py
29
panels.py
|
@ -5,15 +5,22 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
|
||||||
bl_space_type = "VIEW_3D"
|
bl_space_type = "VIEW_3D"
|
||||||
bl_region_type = "UI"
|
bl_region_type = "UI"
|
||||||
bl_category = "Anim"
|
bl_category = "Anim"
|
||||||
bl_label = "Walk Cycle anim"
|
bl_label = "Walk Cycle Anim"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
if not context.scene.anim_cycle_settings.path_to_follow:
|
settings = context.scene.anim_cycle_settings
|
||||||
|
if not settings.path_to_follow:
|
||||||
layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE')
|
layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE')
|
||||||
|
|
||||||
|
# need to know root orientation forward)
|
||||||
|
## know direction to evaluate feet moves
|
||||||
|
## Define Constraint axis (depend on root orientation)
|
||||||
|
layout.prop(settings, "forward_axis")
|
||||||
|
layout.operator("uac.autoset_axis", text='Auto-Set Axis')
|
||||||
#-# 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(settings, "path_to_follow", context.scene, "objects")
|
||||||
layout.prop_search(context.scene.anim_cycle_settings, "gnd", context.scene, "objects")
|
layout.prop_search(settings, "gnd", context.scene, "objects")
|
||||||
|
|
||||||
|
|
||||||
prefs = fn.get_addon_prefs()
|
prefs = fn.get_addon_prefs()
|
||||||
|
@ -28,6 +35,7 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
|
||||||
if follow and follow.target:
|
if follow and follow.target:
|
||||||
layout.label(text=f'{pb.name} -> {follow.target.name}', icon='CON_FOLLOWPATH')
|
layout.label(text=f'{pb.name} -> {follow.target.name}', icon='CON_FOLLOWPATH')
|
||||||
constrained = True
|
constrained = True
|
||||||
|
## Put this in a setting popup or submenu
|
||||||
|
|
||||||
if not constrained:
|
if not constrained:
|
||||||
## Créer automatiquement le follow path TODO et l'anim de base
|
## Créer automatiquement le follow path TODO et l'anim de base
|
||||||
|
@ -36,18 +44,19 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
|
||||||
|
|
||||||
layout.operator('anim.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON')
|
layout.operator('anim.snap_curve_to_ground', text='Snap curve to ground', icon='SNAP_ON')
|
||||||
|
|
||||||
|
col=layout.column()
|
||||||
# row=layout.row()
|
col.prop(settings, "start_frame", text='Start')
|
||||||
layout.prop(context.scene.anim_cycle_settings, "start_frame", text='Start')
|
# col.prop(settings, "foot_axis", text='Foot Axis')
|
||||||
layout.operator('anim.animate_path', text='Animate Path (select foot)', icon='ANIM')
|
col.operator('anim.animate_path', text='Animate Path (select foot)', icon='ANIM')
|
||||||
|
|
||||||
|
|
||||||
row=layout.row()
|
row=layout.row()
|
||||||
row.operator('anim.adjust_animation_length', icon='MOD_TIME')
|
row.operator('anim.adjust_animation_length', icon='MOD_TIME')
|
||||||
|
|
||||||
## Bake cycle (on selected)
|
## Bake cycle (on selected)
|
||||||
row=layout.row()
|
row=layout.row()
|
||||||
row.prop(context.scene.anim_cycle_settings, "linear", text='Linear')
|
row.prop(settings, "linear", text='Linear')
|
||||||
row.prop(context.scene.anim_cycle_settings, "expand_on_selected_bones")
|
row.prop(settings, "expand_on_selected_bones")
|
||||||
layout.operator('anim.bake_cycle_and_step', text='Bake key and step path', icon='SHAPEKEY_DATA')
|
layout.operator('anim.bake_cycle_and_step', text='Bake key and step path', icon='SHAPEKEY_DATA')
|
||||||
|
|
||||||
# Pin feet
|
# Pin feet
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
class UAC_addon_prefs(bpy.types.AddonPreferences):
|
||||||
|
## can be just __name__ if prefs are in the __init__ mainfile
|
||||||
|
# Else need the splitext '__name__ = addonname.subfile' (or use a static name)
|
||||||
|
bl_idname = __name__.split('.')[0] # or with: os.path.splitext(__name__)[0]
|
||||||
|
|
||||||
|
# some_bool_prop to display in the addon pref
|
||||||
|
debug : bpy.props.BoolProperty(
|
||||||
|
name='Debug',
|
||||||
|
description="Enable Debug prints",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
tgt_bone : bpy.props.StringProperty(
|
||||||
|
name="Constrained Pose bone name", default='world',
|
||||||
|
description="name of the bone that suppose to hold the constraint")
|
||||||
|
|
||||||
|
""" default_forward_axis : bpy.props.EnumProperty(
|
||||||
|
name='Forward Axis',
|
||||||
|
default='TRACK_NEGATIVE_Y', # Modifier default is FORWARD_X
|
||||||
|
description='Axis that points forward along the path',
|
||||||
|
items=(
|
||||||
|
('FORWARD_X', 'X', ''),
|
||||||
|
('FORWARD_Y', 'Y', ''),
|
||||||
|
('FORWARD_Z', 'Z', ''),
|
||||||
|
('TRACK_NEGATIVE_X', '-X', ''),
|
||||||
|
('TRACK_NEGATIVE_Y', '-Y', ''),
|
||||||
|
('TRACK_NEGATIVE_Z', '-Z', ''),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text='Curve creation parameters:')
|
||||||
|
box.prop(self, "tgt_bone")
|
||||||
|
# box.prop(self, "default_forward_axis", text='Default Forward Axis')
|
||||||
|
|
||||||
|
layout.prop(self, "debug")
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(UAC_addon_prefs)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(UAC_addon_prefs)
|
|
@ -0,0 +1,83 @@
|
||||||
|
import bpy
|
||||||
|
from bpy.props import (
|
||||||
|
IntProperty,
|
||||||
|
BoolProperty,
|
||||||
|
StringProperty,
|
||||||
|
FloatProperty,
|
||||||
|
EnumProperty,
|
||||||
|
)
|
||||||
|
|
||||||
|
class UAC_PG_settings(bpy.types.PropertyGroup) :
|
||||||
|
## HIDDEN to hide the animatable dot thing
|
||||||
|
path_to_follow : bpy.props.PointerProperty(type=bpy.types.Object,
|
||||||
|
name="Path", description="Curve object used")
|
||||||
|
|
||||||
|
gnd : bpy.props.PointerProperty(type=bpy.types.Object,
|
||||||
|
name="Ground", description="Choose the ground object to use")
|
||||||
|
|
||||||
|
expand_on_selected_bones : bpy.props.BoolProperty(
|
||||||
|
name="On selected", description="Expand on selected bones",
|
||||||
|
default=True, options={'HIDDEN'})
|
||||||
|
|
||||||
|
linear : bpy.props.BoolProperty(
|
||||||
|
name="Linear", description="keep the animation path linear (Else step the path usings cycle keys)",
|
||||||
|
default=False, options={'HIDDEN'})
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
forward_axis : bpy.props.EnumProperty(
|
||||||
|
name='Forward Axis',
|
||||||
|
default='TRACK_NEGATIVE_Y', # Modifier default is FORWARD_X
|
||||||
|
description='Axis of the root bone that point forward',
|
||||||
|
items=(
|
||||||
|
('FORWARD_X', 'X', ''),
|
||||||
|
('FORWARD_Y', 'Y', ''),
|
||||||
|
('FORWARD_Z', 'Z', ''),
|
||||||
|
('TRACK_NEGATIVE_X', '-X', ''),
|
||||||
|
('TRACK_NEGATIVE_Y', '-Y', ''),
|
||||||
|
('TRACK_NEGATIVE_Z', '-Z', ''),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
## foot axis not needed (not always aligned with character direction)
|
||||||
|
foot_axis : EnumProperty(
|
||||||
|
name="Foot Axis", description="Foot forward axis",
|
||||||
|
default='Y', options={'HIDDEN', 'SKIP_SAVE'},
|
||||||
|
items=(
|
||||||
|
('X', 'X', '', 0),
|
||||||
|
('Y', 'Y', '', '', 1),
|
||||||
|
('Z', 'Z', '', '', 2),
|
||||||
|
# ('-X', '-X', '', 3),
|
||||||
|
# ('-Y', '-Y', '', '', 4),
|
||||||
|
# ('-Z', '-Z', '', '', 5),
|
||||||
|
))
|
||||||
|
"""
|
||||||
|
# someBool : BoolProperty(
|
||||||
|
# name="", description="",
|
||||||
|
# default=True,
|
||||||
|
# options={'HIDDEN'})
|
||||||
|
|
||||||
|
# keyframe_type : EnumProperty(
|
||||||
|
# name="Keyframe Filter", description="Only jump to defined keyframe type",
|
||||||
|
# default='ALL', options={'HIDDEN', 'SKIP_SAVE'},
|
||||||
|
# items=(
|
||||||
|
# ('ALL', 'All', '', 0), # 'KEYFRAME'
|
||||||
|
# ('KEYFRAME', 'Keyframe', '', 'KEYTYPE_KEYFRAME_VEC', 1),
|
||||||
|
# ('BREAKDOWN', 'Breakdown', '', 'KEYTYPE_BREAKDOWN_VEC', 2),
|
||||||
|
# ('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 3),
|
||||||
|
# ('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 4),
|
||||||
|
# ('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 5),
|
||||||
|
# ))
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(UAC_PG_settings)
|
||||||
|
bpy.types.Scene.anim_cycle_settings = bpy.props.PointerProperty(type = UAC_PG_settings)
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(UAC_PG_settings)
|
||||||
|
del bpy.types.Scene.anim_cycle_settings
|
Loading…
Reference in New Issue