fix errors with translation calc

0.9.0

- fix problem with translation calculation when all keys are marked
- add button to create cycle un tested
- added addon pref button
master
Pullusb 2022-04-13 18:38:03 +02:00
parent b983183504
commit 7031e60376
11 changed files with 289 additions and 58 deletions

View File

@ -1,5 +1,11 @@
# Changelog
0.9.0
- fix problem with translation calculation when all keys are marked
- add button to create cycle un tested
- added addon pref button
0.8.0
- Easy jump to previous action
@ -9,6 +15,7 @@
0.7.1
- customizable panel category name
- changed tab category name to `Walk`
- Change generated action name:
- `expanded` -> `baked`
- `autogen` -> `pinned`

View File

@ -139,7 +139,7 @@ def anim_path_from_translate():
ob = bpy.context.object
debug = fn.get_addon_prefs().debug
settings = bpy.context.scene.anim_cycle_settings
axis = settings.forward_axis
@ -187,10 +187,20 @@ def anim_path_from_translate():
## get only fcurves relative to selected bone
b_fcurves = [fcu for fcu in act.fcurves if fcu.data_path.split('"')[1] == b.bone.name]
print('b_fcurves: ', len(b_fcurves))
start_frame = end_frame = None
for fcu in b_fcurves:
# skip problematic keys
if not len(fcu.keyframe_points):
if debug: print(fcu.data_path, fcu.array_index, '>> no keys !')
continue
if all(k.type == 'EXTREME' for k in fcu.keyframe_points):
# True if all are extreme or no keyframe in fcu
if debug: print(fcu.data_path, fcu.array_index, '>> all keys are marked as extremes !')
continue
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 ?
@ -215,7 +225,7 @@ def anim_path_from_translate():
break
if start_frame is None or end_frame is None:
return ('ERROR', f'No (or not enough) keyframe marked Extreme {ob.name} > {b.name}')
return ('ERROR', f'No / All / not enough keyframe marked Extreme {ob.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')
@ -224,6 +234,9 @@ def anim_path_from_translate():
## Find move_val from diff position at start and end frame wihtin character forward axis
## FIXME: problem when cycle axis is not Forward compare to character
## apply rotations in real world ? quat_diff = b.matrix_basis.to_quaternion().rotation_difference(b.matrix.to_quaternion())
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'])
@ -246,12 +259,14 @@ def anim_path_from_translate():
end_loc = (b.bone.matrix_local @ end_mat).to_translation()
# bpy.context.scene.cursor.location = start_loc # Dbg foot start position
print('root vec : ', root_axis_vec)
print('start loc: ', start_loc)
if debug:
print('root vec : ', root_axis_vec)
print('start loc: ', start_loc)
print('end loc: ', end_loc)
## get distance on forward axis
move_val = (intersect_line_plane(start_loc, start_loc + root_axis_vec, end_loc, root_axis_vec) - start_loc).length
print('move_val: ', move_val)
print('Detected move value: ', move_val)
length = fn.get_curve_length(curve)
@ -297,7 +312,7 @@ def anim_path_from_translate():
# for k in t_fcu.keyframe_points:
# k.interpolation = 'CONSTANT'
print('end of set_follow_path_anim')
if debug: print('end of set_follow_path_anim')
class UAC_OT_animate_path(bpy.types.Operator):
bl_idname = "anim.animate_path"
@ -318,7 +333,10 @@ class UAC_OT_animate_path(bpy.types.Operator):
def execute(self, context):
# TODO clear previous animation (keys) if there is any
if context.mode == 'OBJECT':
# Go in pose mode
bpy.ops.object.mode_set(mode='POSE')
err = anim_path_from_translate()
if err:
self.report({err[0]}, err[1])

View File

@ -89,7 +89,8 @@ def bake_cycle(on_selection=True):
if debug >= 2: print('keys', len(fcu_kfs))
## expand to end frame
end = bpy.context.scene.frame_end # maybe add possibility define target manually
# maybe add possibility define target manually ?
end = bpy.context.scene.frame_end + 10 # add a margin
iterations = ((end - last) // offset) + 1
if debug >= 2: print('iterations: ', iterations)
for _i in range(int(iterations)):
@ -185,7 +186,8 @@ def step_path():
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_description = "Bake the key and step the animation path according to those key\
\n(duplicate to a new 'baked' action)"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@ -258,6 +260,8 @@ def pin_down_feets():
to_change_list = [
(bpy.context.scene, 'frame_current'),
(bpy.context.scene, 'frame_start', bpy.context.scene.frame_start-100),
(bpy.context.scene, 'frame_end', bpy.context.scene.frame_end+100),
(bpy.context.scene.render, 'use_simplify', True),
(bpy.context.scene.render, 'simplify_subdivision', 0),
]
@ -333,7 +337,8 @@ def pin_down_feets():
if debug: print(f'fcurve: {b_name} > {prop}')
for r in reversed(contact_ranges): # iterate in reverse ranges (not really necessary)
# iterate in reverse ranges (not really necessary)
for r in reversed(contact_ranges):
print(f'range: {r}')
first = True
for i in range(r[0], r[1]+1)[::-1]: # start from the end of the range
@ -392,7 +397,7 @@ def pin_down_feets():
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_description = "Pin feets on keys marked as extreme\n(duplicate to a new 'pinned' action)"
bl_options = {"REGISTER", "UNDO"}
@classmethod
@ -410,6 +415,8 @@ class UAC_OT_pin_feets(bpy.types.Operator):
return {"FINISHED"}
# --- Quick action management
class UAC_OT_set_action(bpy.types.Operator):
bl_idname = "uac.set_action"
bl_label = "Set action by name"
@ -430,7 +437,7 @@ class UAC_OT_set_action(bpy.types.Operator):
class UAC_OT_step_back_actions(bpy.types.Operator):
bl_idname = "uac.step_back_actions"
bl_label = "Actions Step Back"
bl_description = "Step back to a previous action when 'baked' or 'pinned' action are not ok"
bl_description = "Step back to a previous action if 'baked' or 'pinned' action are not ok"
bl_options = {"REGISTER", "INTERNAL", "UNDO"}
@classmethod
@ -438,6 +445,10 @@ class UAC_OT_step_back_actions(bpy.types.Operator):
return context.object and context.object.type == 'ARMATURE'
def invoke(self, context, event):
if context.object.animation_data.use_tweak_mode:
self.report({'ERROR'}, f'Cannot access animation in NLA')
return {"CANCELLED"}
act = context.object.animation_data.action
base_name = act.name.replace('_baked', '').replace('_pinned', '')
base_name = re.sub(r'\.\d{3}', '', base_name) # remove duplicate to search everything that has the same base

74
OP_nla_tweak.py Normal file
View File

@ -0,0 +1,74 @@
import bpy
from . import fn
def get_active_nla_strip(all_nla=False):
'''Return object active strip
:all_nla: return first active strip found on all objects > NLA tracks
'''
if all_nla:
objs = [o for o in bpy.data.objects if ob.animation_data]
else:
if not bpy.context.object:
return
objs = [bpy.context.object]
for ob in objs:
if not (anim := ob.animation_data):
continue
for nla in anim.nla_tracks:
for strip in nla.strips:
if strip.active:
print(f'{strip.name} on Track {nla.name}')
return strip
class UAC_OT_nla_key_speed(bpy.types.Operator):
bl_idname = "anim.nla_key_speed"
bl_label = "NLA Key Speed"
bl_description = "Activate animate strip time and Keyframe linear for first and last animation frame"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'ARMATURE'
def execute(self, context):
nla_strip = get_active_nla_strip()
if not nla_strip:
self.report({'ERROR'}, 'no active NLA strip')
return {"CANCELLED"}
# Clear if exists (or just move first and last point ?)
fcu = nla_strip.fcurves.find('strip_time')
if fcu:
for k in reversed(fcu.keyframe_points):
fcu.keyframe_points.remove(k)
nla_strip.use_animated_time = True
nla_strip.strip_time = nla_strip.action_frame_start
nla_strip.keyframe_insert('strip_time', frame=nla_strip.frame_start)
nla_strip.strip_time = nla_strip.action_frame_end
nla_strip.keyframe_insert('strip_time', frame=nla_strip.frame_end)
fcu = nla_strip.fcurves.find('strip_time')
if not fcu:
return {"CANCELLED"}
# Go linear
for k in fcu.keyframe_points:
k.interpolation = 'LINEAR'
return {"FINISHED"}
classes=(
UAC_OT_nla_key_speed,
)
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,6 +2,7 @@ import bpy
from mathutils import Vector, Quaternion
from math import sin, cos, radians
import numpy as np
from . import fn
## all action needed to setup the walk
@ -73,10 +74,31 @@ class UAC_OT_autoset_axis(bpy.types.Operator):
context.scene.anim_cycle_settings.forward_axis = best_axis
return {"FINISHED"}
class UAC_OT_create_cycles_modifiers(bpy.types.Operator):
bl_idname = "uac.create_cycles_modifiers"
bl_label = "Add Cycles Modifiers"
bl_description = "Add cycles modifier on all bones not starting with [mch, org, def]\
\nand that are non-deforming"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'ARMATURE'
def execute(self, context):
fn.create_cycle_modifiers(context.object)
return {"FINISHED"}
classes=(
UAC_OT_autoset_axis,
UAC_OT_create_cycles_modifiers,
)
def register():
bpy.utils.register_class(UAC_OT_autoset_axis)
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
bpy.utils.unregister_class(UAC_OT_autoset_axis)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -178,8 +178,8 @@ class UAC_OT_create_follow_path(bpy.types.Operator):
class UAC_OT_snap_curve_to_ground(bpy.types.Operator):
bl_idname = "anim.snap_curve_to_ground"
bl_label = "snap_curve_to_ground"
bl_description = "Snap Curve"
bl_label = "Snap Curve"
bl_description = "snap curve to ground determine in field"
bl_options = {"REGISTER", "UNDO"}
@classmethod

View File

@ -4,7 +4,7 @@ bl_info = {
"name": "Unfold Anim Cycle",
"description": "Anim tools to develop walk/run cycles along a curve",
"author": "Samuel Bernou",
"version": (0, 8, 0),
"version": (0, 9, 0),
"blender": (3, 0, 0),
"location": "View3D",
"warning": "WIP",
@ -21,6 +21,7 @@ if 'bpy' in locals():
imp.reload(OP_expand_cycle_step)
imp.reload(OP_snap_contact)
imp.reload(OP_world_copy_paste)
imp.reload(OP_nla_tweak)
imp.reload(panels)
else:
from . import properties
@ -31,6 +32,7 @@ else:
from . import OP_expand_cycle_step
from . import OP_snap_contact
from . import OP_world_copy_paste
from . import OP_nla_tweak
from . import panels
import bpy
@ -45,22 +47,40 @@ mods = (
OP_expand_cycle_step,
OP_snap_contact,
OP_world_copy_paste,
OP_nla_tweak,
panels,
)
from bpy.app.handlers import persistent
## Not
# @persistent
# def set_target_bone(scene):
# # prefill constrained bone field
# settings = bpy.context.scene.anim_cycle_settings
# if not settings.tgt_bone:
# settings.tgt_bone = fn.get_addon_prefs().tgt_bone
def register():
if bpy.app.background:
return
for module in mods:
module.register()
panels.update_panel(fn.get_addon_prefs(), bpy.context)
prefs = fn.get_addon_prefs()
panels.update_panel(prefs, bpy.context)
# bpy.app.handlers.load_post.append(set_target_bone)
def unregister():
if bpy.app.background:
return
# bpy.app.handlers.load_post.remove(set_target_bone)
for module in reversed(mods):
module.unregister()

64
fn.py
View File

@ -15,6 +15,18 @@ def get_addon_prefs():
addon_prefs = preferences.addons[addon_name].preferences
return (addon_prefs)
def open_addon_prefs():
'''Open addon prefs windows with focus on current addon'''
from .__init__ import bl_info
wm = bpy.context.window_manager
wm.addon_filter = 'All'
if not 'COMMUNITY' in wm.addon_support: # reactivate community
wm.addon_support = set([i for i in wm.addon_support] + ['COMMUNITY'])
wm.addon_search = bl_info['name']
bpy.context.preferences.active_section = 'ADDONS'
bpy.ops.preferences.addon_expand(module=__package__)
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
def helper(name: str = '') -> str:
'''Return name and arguments from calling obj as str
:name: - replace definition name by your own str
@ -232,16 +244,19 @@ def orentation_track_from_vector(input_vector) -> str:
def get_root_name(context=None):
'''return name of rig root name'''
## auto-detect ?
## TODO root need to be user defined (or at least quickly adjustable)
## need to expose a scene prop from anim_cycle_settings
if context is None:
context = bpy.context
# settings = context.scene.anim_cycle_settings
## maybe use a field on interface
prefs = get_addon_prefs()
return prefs.tgt_bone
# Auto set bone with pref default value if nothing entered
settings = context.scene.anim_cycle_settings
if not settings.tgt_bone:
settings.tgt_bone = prefs.tgt_bone
## auto-detect mode ?
return settings.tgt_bone
def generate_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None):
'''Create curve at provided location and direction vector'''
@ -458,3 +473,38 @@ class attr_set():
for prop, attr, old_val in self.store:
setattr(prop, attr, old_val)
# fcurve modifiers
def remove_all_cycles_modifier():
for fc in bpy.context.object.animation_data.action.fcurves:
# if fc.data_path.split('"')[1] in selected_names:
for m in reversed(fc.modifiers):
if m.type == 'CYCLES':
fc.modifiers.remove(m)
def create_cycle_modifiers(ob=None):
if not ob:
ob = bpy.context.object
# skip bones that are on protected layers ?
# protected = [i for i, l in enumerate(ob.data.layers_protected) if l]
# for b in ob.data.bones:
# if b.use_deform: # don't affect deform bones
# continue
## b_layers = [i for i, l in enumerate(b.layers) if l]
name_list = [b.name for b in ob.data.bones] # if not b.use_deform (too limiting)
for fc in ob.animation_data.action.fcurves:
if [m for m in fc.modifiers if m.type == 'CYCLES']:
# skip already existing modifier
continue
if not '"' in fc.data_path:
continue
b_name = fc.data_path.split('"')[1]
if b_name.lower().startswith(('mch', 'def', 'org')):
continue
if b_name not in name_list:
continue
print(f'Adding cycle modifier {fc.data_path}')
_m = fc.modifiers.new(type='CYCLES')

View File

@ -19,10 +19,13 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
## Define Constraint axis (depend on root orientation)
# layout.prop(settings, "forward_axis") # plain prop
row = layout.row()
col = layout.column()
row = col.row()
row.label(text='Forward Axis')
row.prop(settings, "forward_axis", text='')
layout.operator("uac.autoset_axis", text='Auto-Set Axis')
row.operator('uac.open_addon_prefs', icon='PREFERENCES', text='')
col.operator("uac.autoset_axis", text='Auto-Set Axis') # maybe check for fcruve cycle at the end of autoset axis ? (like a check)
col.operator("uac.create_cycles_modifiers", text='Add Cycles Modifiers', icon='GRAPH')
pb = None
constrained = False
@ -47,8 +50,10 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
box = layout.box()
expand_icon = 'TRIA_DOWN' if tweak else 'TRIA_RIGHT'
box.prop(settings, 'tweak', text='Curve Options', icon=expand_icon)
if tweak:
#-# path and ground objects
box.prop(settings, "tgt_bone", text='Bone')
box.prop_search(settings, "path_to_follow", context.scene, "objects")
box.prop_search(settings, "gnd", context.scene, "objects")
@ -86,7 +91,8 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
## Bake cycle (on selected)
box = layout.box()
col=box.column()
col.label(text='Actions:')
col.label(text='Action:')
row=col.row()
row.prop(settings, "linear", text='Linear')
row.prop(settings, "expand_on_selected_bones")
@ -99,12 +105,12 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel):
## show a dropdown allowing to go back to unpinned, unbaked version of the animation
if ob and ob.type == 'ARMATURE':
if ob.animation_data and ob.animation_data.action:
if 'baked' in ob.animation_data.action.name or 'pinned' in ob.animation_data.action.name:
anim = ob.animation_data
if anim and anim.action and not anim.use_tweak_mode:
# skipped if in NLA tweak mode because anim.is_property_readonly('action') = True
if 'baked' in anim.action.name or 'pinned' in anim.action.name:
col=box.column()
col.operator('uac.step_back_actions', text='Use Previous Actions', icon= 'ACTION')
class UAC_PT_anim_tools_panel(bpy.types.Panel):
bl_space_type = "VIEW_3D"
@ -137,20 +143,38 @@ class UAC_PT_anim_tools_panel(bpy.types.Panel):
row.operator('anim.world_space_paste_next_frame', text='', icon='FRAME_NEXT').prev = False
row.scale_x = 2
class UAC_PT_nla_tools_panel(bpy.types.Panel):
bl_space_type = "NLA_EDITOR"
bl_region_type = "UI"
bl_category = "Strip"
bl_label = "Walk Tools"
def draw(self, context):
layout = self.layout
# layout.label(text='Retime Tools')
layout.operator('anim.nla_key_speed', text='Set Time Keys', icon='TIME')
classes=(
UAC_PT_walk_cycle_anim_panel,
UAC_PT_anim_tools_panel,
UAC_PT_nla_tools_panel,
)
classes_override_category =(
UAC_PT_walk_cycle_anim_panel,
UAC_PT_anim_tools_panel,
)
## Addons Preferences Update Panel
def update_panel(self, context):
for cls in classes:
for cls in classes_override_category: # classes
try:
bpy.utils.unregister_class(cls)
except:
pass
cls.bl_category = self.category#fn.get_addon_prefs().category
cls.bl_category = self.category # fn.get_addon_prefs().category
bpy.utils.register_class(cls)
def register():

View File

@ -1,6 +1,15 @@
import bpy
from .panels import update_panel
from . import fn
class UAC_OT_open_addon_prefs(bpy.types.Operator):
bl_idname = "uac.open_addon_prefs"
bl_label = "Open Addon Prefs"
bl_description = "Open user preferences window in addon tab and prefill the search with addon name"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
fn.open_addon_prefs()
return {'FINISHED'}
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)
@ -15,16 +24,18 @@ class UAC_addon_prefs(bpy.types.AddonPreferences):
# some_bool_prop to display in the addon pref
debug : bpy.props.IntProperty(
name='Debug',
default=0,
description="Enable Debug prints\n\
0 = no prints\n\
1 = basic\n\
2 = full prints",
default=0)
)
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
@ -54,8 +65,15 @@ class UAC_addon_prefs(bpy.types.AddonPreferences):
layout.prop(self, "debug")
classes = (
UAC_addon_prefs,
UAC_OT_open_addon_prefs,
)
def register():
bpy.utils.register_class(UAC_addon_prefs)
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
bpy.utils.unregister_class(UAC_addon_prefs)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -14,6 +14,10 @@ class UAC_PG_settings(bpy.types.PropertyGroup) :
name="Tweak", description="Show Tweaking options",
default=False, options={'HIDDEN'})
tgt_bone : bpy.props.StringProperty(
name="Bone to constrain", default='',
description="Name of the bone to constrain with follow path (usually the root bone)")
path_to_follow : bpy.props.PointerProperty(type=bpy.types.Object,
name="Path", description="Curve object used")
@ -48,7 +52,6 @@ class UAC_PG_settings(bpy.types.PropertyGroup) :
),
)
"""
## foot axis not needed (not always aligned with character direction)
foot_axis : EnumProperty(
@ -63,22 +66,6 @@ class UAC_PG_settings(bpy.types.PropertyGroup) :
# ('-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)