this is a tool i wrote for Blender 2.79 to help with creating actions and action constraints. there may be bugs so please modify/use at your own risk. to use the addon each of the files need to be saved in a folder and put in Blender add-on paths. Happy Sketching!
__init__.py creating.py editing.py utils.py ui.py
which can be saved in folder example (naActionTools)
#__init__.py
import bpy
import imp
bl_info = {
"name": "actions/action constraint tool",
"author": "Nathaniel Anozie",
"version": (0, 1),
"blender": (2, 79, 0),
"location": "3D View",
"description": "actions/action constraint tool",
"category": "Object",
}
from . import ui
imp.reload(ui)
from . import creating
imp.reload(creating)
from . import utils
imp.reload(utils)
def register():
ui.register()
def unregister():
ui.unregister()
if __name__ == "__main__":
register()
#creating.py
import bpy
import logging
import re
import math
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console
from . import utils
class PoseMirror(object):
"""responsible for mirroring actions and action constraints
"""
"""
import imp
testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/addJointBlendshape.py") #change to path to python file
#print(testTool.PoseMirror("Armature")._getSourceActions())
testTool.PoseMirror("Armature").mirrorActions()
testTool.PoseMirror("Armature").mirrorActionConstraints()
"""
def __init__(self, armature):
"""
@param armature str name for armature object
"""
self._armature = armature
def mirrorActionConstraints(self, bones=[]): #
"""mirror all action constraints from left side to right - assumes all destination bones/actions exist
@param bones (list of str) optional bone names to mirror action constraints for. if not specified looks at all left side bones
"""
armature = self._armature
#find all action constraints on a bone that ends in .L - for simplicity require action constraint to end in .L
allSourceBones = []
if bones:
allSourceBones = [bone for bone in bones if bone.endswith('.L')]
else:
allSourceBones = [b.name for b in bpy.data.objects[armature].pose.bones if b.name.endswith('.L')]
#allSourceBones = [b.name for b in bpy.data.objects[armature].pose.bones if b.name.endswith('.L')]
if not allSourceBones:
logger.info("doing nothing - requires a source bone ending in .L to exist")
return
#for each action constraint find the .L bone and .R bone
for boneSource in allSourceBones:
#if bone doesnt have a destination skip it
boneDestination = boneSource.replace(".L",".R")
if not boneDestination in bpy.data.objects[armature].pose.bones:
continue
#if source bone doesnt have action constraint skip it
sourceBoneActionConstraints = [cnt.name for cnt in bpy.data.objects[armature].pose.bones[boneSource].constraints if cnt.type == "ACTION"]
if not sourceBoneActionConstraints:
continue
###
#if destination bone has any action constraints remove them
#so we dont double up constraints
destinationBoneExistingConstraints = [cnt.name for cnt in bpy.data.objects[armature].pose.bones[boneDestination].constraints if cnt.type == "ACTION"]
for cntName in destinationBoneExistingConstraints:
cntObj = bpy.data.objects[armature].pose.bones[boneDestination].constraints[cntName]
bpy.data.objects[armature].pose.bones[boneDestination].constraints.remove(cntObj)
###
for sourceActionConstraint in sourceBoneActionConstraints:
logger.debug("sourceActionConstraint:"+sourceActionConstraint)
#make the same constraint on destination bone
#copy constraint data from source to destination
cntObjSource = bpy.data.objects[armature].pose.bones[boneSource].constraints[sourceActionConstraint]
#make the destination constraint name - might want to enforce source constraint ends in proper source suffix
cntDestinationName = ""
cntDestinationName = cntObjSource.name.replace(".L", ".R")
"""
if cntObjSource.name.endswith(".L"):
cntDestinationName = cntObjSource.name.replace(".L", ".R")
else:
cntDestinationName = cntObjSource.name+".R"
"""
#if destination action constraint already exists skip it
"""
allActionConstraints = self._getAllActionConstraints()
if cntDestinationName in allActionConstraints:
logger.debug("skipping: {} because it already exists".format(cntDestinationName))
continue
"""
#skip if cant find destination action
sourceActionName = cntObjSource.action.name
if sourceActionName.endswith(".L"):
destinationAction = sourceActionName.replace(".L", ".R")
elif sourceActionName.endswith(".C"):
destinationAction = sourceActionName
#else:
# destinationAction = sourceActionName+"_mir"
if not destinationAction in bpy.data.actions:
logger.warning("could not find destination action {action} for source bone {bone} skipping. supports destination suffix .R or _mir".format(action=destinationAction, bone=boneSource))
continue
cntObjDestination = bpy.data.objects[armature].pose.bones[boneDestination].constraints.new("ACTION")
cntObjDestination.name = cntDestinationName
cntObjDestination.target = cntObjSource.target
cntObjDestination.subtarget = cntObjSource.subtarget.replace(".L",".R")#todo check it exists
cntObjDestination.target_space = cntObjSource.target_space
srcTransformChannel = cntObjSource.transform_channel
cntObjDestination.transform_channel=srcTransformChannel
cntObjDestination.min = cntObjSource.min
srcMax = cntObjSource.max
if "LOCATION_X" in srcTransformChannel or "ROTATION_Z" in srcTransformChannel:
srcMax = -1*srcMax #to support mirroring of action constraint
cntObjDestination.max = srcMax
cntObjDestination.frame_start = cntObjSource.frame_start
cntObjDestination.frame_end = cntObjSource.frame_end
#logger.debug("destination action {}".format(destinationAction))
cntObjDestination.action = bpy.data.actions[destinationAction]
utils.turnOffActions(armature)
def mirrorActions(self, actionNames=[], replace=False):
"""mirror given source action names - if none provide it mirrors all source side actions
@param actionNames (list of str) - optional list of action names ex: ["testAction.L"]
@param replace (bool) - whether to replace the action on opposite side if it exists - defaults to False
"""
actionsSource = []
if actionNames:
actionsSource = actionNames
else:
#tries to mirror all source actions
actionsSource = self._getSourceActions() or [] #should be str names for action
for action in actionsSource:
if self._isMirrorActionExists(action):
if not replace:
#skip mirroring actions that already have a mirror
continue
else:
#overrite mirror action by first deleting it
destinationActionName = self._getMirrorAction(action)
if destinationActionName in bpy.data.actions:
bpy.data.actions.remove(bpy.data.actions[destinationActionName])
self.mirrorSingleAction(action)
utils.turnOffActions(self._armature)
def _getMirrorAction(self, actionName):
"""return str for mirrored action name if it exists - None otherwise
"""
if actionName.endswith(".L"):
destinationActionName = actionName.replace(".L",".R")
else:
destinationActionName = actionName+"_mir"
if destinationActionName in bpy.data.actions:
return destinationActionName
return None
def _isMirrorActionExists(self, actionName):
"""return true if mirror of action already exists
"""
if self._getMirrorAction(actionName):
return True
return False
def mirrorSingleAction(self, sourceAction):
"""mirrors action for all bones in action - going from source left to destination right
@param sourceAction str name for source action to mirror
"""
armature = self._armature
if not sourceAction in bpy.data.actions:
raise RuntimeError("requires source action to exist in blend file")
sourceData = bpy.data.actions[sourceAction]
if sourceAction.endswith(".L"):
destinationActionName = sourceAction.replace(".L",".R") #todo: find better name - like using .R suffix
else:
destinationActionName = sourceAction+"_mir"
obj = bpy.data.objects[armature]
#create a new action on object. for now ignoring additional way
#to find action name to prevent trailing digits on action name.
obj.animation_data_create() #so we have .action attribute
mirAction = bpy.data.actions.new(name=destinationActionName)
obj.animation_data.action = mirAction
#might have .R side bones animated when starting mirror so skip these fcurves
sourceBoneFcurves = [fcrv for fcrv in sourceData.fcurves if ".R" not in fcrv.data_path] #going from source left to destination right
#create fcurves on action
for fc in sourceBoneFcurves:
logging.debug("in:{}".format(fc.data_path))
logging.debug("in:{}".format(fc.array_index))
#create new fcurve
mirrorDataPath = fc.data_path.replace(".L",".R")
mirrorArrayIndex = fc.array_index
logging.debug("mirrorDataPath: "+mirrorDataPath)
logging.debug("mirrorArrayIndex: "+str(fc.array_index))
fc_scene = bpy.data.actions[mirAction.name].fcurves.new(mirrorDataPath, fc.array_index)
logging.debug("out:{}".format(fc_scene.data_path))
logging.debug("out:{}".format(fc_scene.array_index))
#add keyframe points for fcurve
for pt in fc.keyframe_points:
ptco = list(pt.co)
logging.debug(ptco)
##flip rotate y and z. quaternion y and z. flip translate x
#todo: see if the tangent handles need to be flipped as well
shouldFlip = False
if ("location" in mirrorDataPath and mirrorArrayIndex == 0) \
or ("rotation_euler" in mirrorDataPath and mirrorArrayIndex in [1,2]) \
or ("rotation_quaternion" in mirrorDataPath and mirrorArrayIndex in [2,3]):
shouldFlip = True
if shouldFlip:
ptco[1] = -1*ptco[1]
##
kp = fc_scene.keyframe_points.insert(ptco[0], ptco[1])
#need to edit tangents here
kp.handle_left = pt.handle_left
kp.handle_right = pt.handle_right
kp.handle_left_type = pt.handle_left_type
kp.handle_right_type = pt.handle_right_type
kp.interpolation = pt.interpolation
#handle addiditional fcurve attributes
fc_scene.extrapolation = fc.extrapolation
fc_scene.color_mode = fc.color_mode
#go to current frame to make action creation stick
curFrame = bpy.context.scene.frame_current
bpy.context.scene.frame_set(curFrame)
def _getSourceActions(self):
"""get all actions that use a source side bone
"""
armature = self._armature
result = []
for act in bpy.data.actions:
for fcrv in act.fcurves:
if ".L" in fcrv.data_path:
result.append(act.name)
#dont need to check any more fcurves for this action
break
return result
def _getAllActionConstraints(self):
"""get all action constraints
"""
armature = self._armature
result = []
for bone in bpy.data.objects[armature].pose.bones:
for cnt in bone.constraints:
if cnt.type == "ACTION":
result.append(cnt.name)
return result
#todo: have it support pose where theres no animation
#todo: support if driver bone deleted in edit mode - i think its animation is still left over
class AnimationDriver(object):
"""a class for driving a pose in an animation by a driver bone
assumes driver bone is animated at identical frames to pose and has a naming suffix see kDriverBoneSuffix
when done animator should be able to pose left side lip corner. hooks up lip corner up/dn/in/out to shapes
for lip corner want up/dn/in/out shapes created from a single animation
"""
"""
import imp
testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/addJointBlendshape.py") #change to path to python file
testTool.AnimationDriver("Armature", "Bone.001_macroAnim","ArmatureAction").doIt()
#mp = testTool.AnimationDriver("Armature", "lipCorner_anim.L","LipCornerAction")
#mp.doIt()
"""
kDriverBoneSuffix = "macroAnim"
def __init__(self, armature, driverBone, actionName):
"""
@param armature str data object name of armature
@param driverBone str name for driver bone of macro pose to be animated. it should have keyframes for its driving range at frames identical to pose
(for simplicity assume a *_macroAnim.L or *_macroAnim.R suffix for driver bones)
@param actionName str name for action that has both pose and driver bone at different values for the different poses
"""
self._armature = armature
#assert driverBone has proper naming
if self.kDriverBoneSuffix not in driverBone:
raise RuntimeError("requires driver bone name to have {} in its name".format(self.kDriverBoneSuffix))
if driverBone not in bpy.data.objects[armature].pose.bones:
raise RuntimeError("cannot find driver bone - please check its naming")
self._driverBone = driverBone
self._actionName = actionName
def doIt(self):
"""
"""
#turn on animation for action
self._setAnimation(on=True)
#get fcurve for driver bone
#loop keyframe points of that fcurve
#save frame of pose (assumes driver and pose have identical frames)
#get axis and on point from driver bone (save driver value example axis 'z' value 3)
driverInfo = self.getAllOnPointInfoForDriver() or []
for dinfo in driverInfo:
#for a single channel could have multiple frames
#logger.debug(dinfo)
for frameInfo in dinfo:
self._setAnimation(on=True)
logger.debug(frameInfo)
frame = frameInfo.get("frame")
onPoint = frameInfo.get("onPoint")
channel = frameInfo.get("other")[0]
axis = ["x","y","z"][frameInfo.get("other")[1]]
logger.debug("frame:{frame} onPoint:{onPoint} channel:{channel} axis:{axis}".format(frame=frame, onPoint=onPoint, channel=channel, axis=axis))
#ex: DEBUG:tool:frame:44.0 onPoint:-1.032 channel:location axis:z
#go to frame
self._goToFrame(frame)
#turn off animation for action keeping pose
self._setAnimation(on=False)
#get list of all driver bones (for simplicity assume a certain suffix for driver bones)
#zero out all driver bone ts/rs/sc - this is so it doesnt get driven by the pose we will create - since its in same armature
#i think above will exclude driver bone(s) so it doesnt get driven
self._setDriverBonesToDefault()
#createAnimatablePose with the info (needs a channel parameter to differentiate translates from rotates/scale)
#todo: support rotation/scale drivers
#todo: dont loose initial driver animation
createAnimatablePose(self._armature, self._driverBone, self._actionName, onPoint=(onPoint, axis), offPoint=(0, axis), channel=channel )
#ex: createAnimatablePose(armature, driverBone, "lipUp_Action.L", onPoint=(2,'z'), offPoint=(0,'z') )
#cleanup
self._goToFrame(0)
self._setAnimation(on=False)
"""
#step through each frame of animationAction - turnoff animation keeping pose - create the animatable pose
self.setAnimation(on=True) #turn on animation for animationAction - shouldnt need to do first time arround
self.goToFrame(frame=fr)
self.setAnimation(on=False)
createAnimatablePose(armature, driverBone, "lipUp_Action.L", onPoint=(2,'z'), offPoint=(0,'z')) #armature, driver bone, name for action, when blendshape on using driver bone
"""
def _setAnimation(self, on=True):
"""set animation on or off - by turning off have a static posed armature on the frame it was turned off
@param on bool whether to turn on current action or temporarily turn it off - doesnt delete the animation
"""
if on:
bpy.data.objects[self._armature].animation_data.action = bpy.data.actions[self._actionName]
else:
bpy.data.objects[self._armature].animation_data.action = None
def _getFcurvesForDriver(self):
"""get all fcurves for driver bone
a few fcurves for a single bone
suppose bone ty fcurve is (0,0) (10,2) (20,0) (30,-2)
and bone tz fcurve is (0,0) (10,0) (20,5) (30,0)
"""
allFcurves = bpy.data.objects[self._armature].animation_data.action.fcurves
return [ fcrv for fcrv in allFcurves if self._driverBone == re.search( r'.*\["(.*)"\].*', fcrv.data_path ).group(1) ] #ex data path: 'pose.bones["Bone"].location'
def getAllOnPointInfoForDriver(self):
"""get the info needed from the driver that can be used for driving poses
a few fcurves for a single bone
suppose bone ty fcurve is (0,0) (10,2) (20,0) (30,-2)
and bone tz fcurve is (0,0) (10,0) (20,5) (30,0)
get a list like:
[{"frame":10,"axis":'y',"onPoint":2,"channel":"t"},{"frame":30,"axis":'y',"onPoint":-2,"channel":"t"}]
for each fcurve - need to include channel either t, r, s (only supporting euler rotation for drivers)
@param fcurves list of fcurve objects for driver bone
ex item: >>> type(bpy.data.objects['Armature'].animation_data.action.fcurves[1])
start at first fcurve
ty fcurve
start at first keyframe point of fcurve - any thing non default/ non zero? no
got to next keyframe point of fcurve - any thing non zero? yes - so save the frame 10 and the value 2 - save this dict {"frame":10,"axis":'y',"onPoint":2} to list
go to next keyframe point - any thin non default? no
go to next keyframe point - ... save {"frame":30,"axis":'y',"onPoint":-2}
no more keyframe points so stop
"""
result = []
fcurves = self._getFcurvesForDriver()
#assert fcurves
for fcrv in fcurves:
fcrvInfo = []
logger.debug(fcrv.data_path)
channel = fcrv.data_path.split('.')[-1] #ex: "location"
for kpoint in fcrv.keyframe_points:
#logger.debug("channel:{}".format(channel))
#skip default keyframe points
if (kpoint.co[1] == 0 and channel != "scale") or (kpoint.co[1] == 1 and channel == "scale"):
continue
onValue = kpoint.co[1]
#if rotation convert radians to degrees
if channel == "rotation_euler":
onValue = onValue*(180/math.pi)
driver_info = dict(frame=kpoint.co[0], onPoint= round(onValue,3), other=(channel, fcrv.array_index) ) #other ex: ("location",1) for ty
fcrvInfo.append(driver_info)
if fcrvInfo:
result.append(fcrvInfo)
logger.debug(result)
#ex:
"""
[[{'other': ('location', 1), 'onPoint': 1.044, 'frame': 14.0}, {'other': ('location', 1), 'onPoint': -1.239, 'frame': 26.0}]]
"""
return result
def _getAllDriverBones(self):
"""get list of all driver bones in armature (for simplicity assumes a certain suffix for driver bones)
"""
return [bone.name for bone in bpy.data.objects[self._armature].pose.bones if self.kDriverBoneSuffix in bone.name ]
def _setDriverBonesToDefault(self):
"""zero out all driver bone ts/rs/sc - this is so it doesnt get driven by the pose we will create - since its in same armature
"""
allDriverBones = self._getAllDriverBones() or []
for dbone in allDriverBones:
logger.debug(dbone)
logger.info("setting driver bone {} to default".format(dbone))
bpy.data.objects[self._armature].pose.bones[dbone].location=(0.0,0.0,0.0)
bpy.data.objects[self._armature].pose.bones[dbone].rotation_euler=(0.0,0.0,0.0)
bpy.data.objects[self._armature].pose.bones[dbone].scale=(1.0,1.0,1.0)
def _goToFrame(self, frame=0):
"""go to given frame number
@param frame float frame to go to
"""
bpy.context.scene.frame_set(frame)
class PoseCreate(object):
"""class that does meat for creating a pose based on actions.
its recommended to not use this directly but instead use a PoseCreateApplication
"""
def __init__(self, action,
armature,
animbone,
onFrame=10,
offFrame=0,
onPoint=(10,'z'),
offPoint=(0,'z'),
channel="location"
):
"""
@param action: (str) name of action
@param armature: (str) name of armature its object name
@param animbone: (str) name of bone to be animated to turn on pose
@param onFrame: (double) frame that joint blendshape is on in action (default 10)
@param offFrame: (double) frame that joint blendshape is off in action (default 0)
@param onPoint: tuple (double,str) ex: (10,z) it says when bone is at 10 in given channel in z axis blendshape should be on.
for rotation it requires degrees input
@param offPoint: tuple (double,str) ex: (0,z) it says when bone is at 0 in z axis blendshape should be off
@param channel: (str) channel ex: location or rotation_euler
"""
#essential variables
self._action = action
self._armature = armature
self._animbone = animbone
self._actionConstraintDict = {} #keys bone affected - value is action constraint name - it will be created
#optional variables
self.__onFrame = onFrame
self.__offFrame = offFrame
self.__onPoint = onPoint
self.__offPoint = offPoint
self.__channel = channel
def doIt(self):
#assign action to affected bones
self._assignActionToAffectedBones()
#set proper action frame inputs
status = self._setActionInputs()
utils.turnOffActions(self._armature) #example go to first frame and turn off actions.
return True
def _assignActionToAffectedBones(self):
"""
create an action constraint for each affected bone
"""
affectedbones = self._getActionBones() or []
logger.info("affectedbones:")
logger.info(affectedbones)
armature = self._armature
action = self._action
#create action constraint for each bone
for bone in affectedbones:
act = bpy.data.objects[armature].pose.bones[bone].constraints.new('ACTION')
act.name = action+'_'+'cnt'
self._actionConstraintDict[bone] = act.name
logger.info("creating action constraint on bone {0} constraint name {1}".format(bone, act.name))
def _setActionInputs(self):
"""sets the inputs (action,frame data) to all created action constraints
"""
actionConstraintDict = self._actionConstraintDict
armature = self._armature
action = self._action
animbone = self._animbone
onPoint = self.__onPoint
offPoint = self.__offPoint
onFrame = self.__onFrame
offFrame = self.__offFrame
animaxis = onPoint[1].upper() #ex: Z
channelType = "LOCATION"
if self.__channel == "rotation_euler":
channelType = "ROTATION"
elif self.__channel == "scale":
channelType = "SCALE"
#no support for quaternion - i dont think its available for action constraints
if not self._actionConstraintDict:
logger.info("no pose found. doing nothing")
return False
assert armature
assert action
for bone,cnt in actionConstraintDict.items():
logger.info("cnt: {0} bone: {1}".format(cnt,bone))
cntObj = bpy.data.objects[armature].pose.bones[bone].constraints[cnt]
cntObj.target = bpy.data.objects[armature]
cntObj.subtarget = animbone #bone animator animates to achieve pose
cntObj.target_space = 'LOCAL'
cntObj.transform_channel = channelType+"_"+animaxis #'LOCATION_'+animaxis
cntObj.min = offPoint[0] #for animator
cntObj.max = onPoint[0]
cntObj.frame_start = offFrame #for action
cntObj.frame_end = onFrame
logger.info("action name")
logger.info(action)
cntObj.action = bpy.data.actions[action]
return True
def _getActionBones(self):
#get bones involved in action
#@return (list of str) of bone names
result = []
action = self._action
fcurves = bpy.data.actions[action].fcurves
for fcurve in fcurves:
#find the bone name from data path that looks like: 'pose.bones["lip_up_tail.L"].location'
result.append( re.search( r'.*\["(.*)"\].*', fcurve.data_path ).group(1) )
return list(set(result))
"""
def _getActionBones(self):
#get bones involved in action
#@return (list of str) of bone names
result = []
action = self._action
result = [actgrp.name for actgrp in bpy.data.actions[action].groups]
return result
"""
#action creation class
class ActionCreator(object):
"""for creating an action out of a static pose.
todo: be able to create actions out of animated poses - one action for each keyframe
todo: able to not have to keyframe all location channels but just ones changed in pose
"""
kDefaultTranslates = [0.0,0.0,0.0]
kDefaultEuler = [0.0,0.0,0.0]
kDefaultQuats = [1.0,0.0,0.0,0.0]
def __init__(self, name, armature='', isolateBones=[]):
"""
@param name (str) name to use for action
@param armature (str) data object name for armature
@param isolateBones (list of str) optional list of bone names to only consider - if not specified it looks for posed bones in entire armature
"""
assert armature
self.armature = armature
self.name = name
self._action = None
self._bonesToDataPathDict = {} #bones used in action and what attribute trs,rots,or scale should be keyframed
#ex: {'Bone': {'location':(0,2,1),'rotation':(90,0,0)}, 'Bone.001': {'location':(10,0,0)} }
self._isolateBones = isolateBones
def doIt(self):
#figure out bones to use in action
self._getBonesForAction()
#print(self._bonesToDataPathDict)
#creates action/fcurves/ and keys the bones used in action in a progression from default to pose
self._setKeyframes()
#to register the action
bpy.ops.object.mode_set(mode="OBJECT") #todo: make cleaner
return self._action
def _getBonesForAction(self):
"""figure out bones to use in action (also include data path)
(todo: support modes not starting in pose)
"""
self._bones = []
armatureObj = bpy.data.objects[self.armature]
#go through all bones of armature and save any bone whose trs,rots,scales are different from default
#also save the transform for the pose. (todo: support poses that are animated - assumes pose is static)
for bone in armatureObj.pose.bones:
#if isolating bones skip any bones not in isolate list
isoBones = self._isolateBones or []
if isoBones and (bone.name not in isoBones):
continue
#if bone translate not equal default
if not (tuple(bone.location) == tuple(self.kDefaultTranslates)):
self._bonesToDataPathDict.setdefault(bone.name,{}).setdefault('location',[]).extend( list(bone.location) )
#ex: {'Bone.001': {'location': [0.0, 0.76, 0.0]}, 'Bone': {'location': [0.0, 0.38, 0.0]}}
if bone.rotation_mode == 'QUATERNION':
if not (tuple(bone.rotation_quaternion) == tuple(self.kDefaultQuats)):
self._bonesToDataPathDict.setdefault(bone.name,{}).setdefault('rotation_quaternion',[]).extend( list(bone.rotation_quaternion) )
else:
if not (tuple(bone.rotation_euler) == tuple(self.kDefaultEuler)):
self._bonesToDataPathDict.setdefault(bone.name,{}).setdefault('rotation_euler',[]).extend(list(bone.rotation_euler))
def _setKeyframes(self):
"""key the bones used in action in a progression from default to pose
"""
boneDict = self._bonesToDataPathDict #ex: {'Bone': ['location'], 'Bone.001': ['location']}
#print("boneDict:")
#print(boneDict)
startFrame = 0
endFrame = 10 #todo: support different frame ranges
armatureObj = bpy.data.objects[self.armature]
actionNamePrefix = self.name
armatureObj.animation_data_clear() #todo: see if this is needed
#create a new action on object. for now ignoring additional way
#to find action name to prevent trailing digits on action name.
armatureObj.animation_data_create() #so we have .action attribute
self._action = bpy.data.actions.new(name=actionNamePrefix) #name= actionNamePrefix+'Action'
armatureObj.animation_data.action = self._action
#make fcurves and add keyframes
for bone, attrDict in boneDict.items():
for attr, transform in attrDict.items():
print("bone:{0} attr:{1}".format(bone,attr))
#create fcurve for all location channels
print("creating fcurve")
fcurveArrayIndexRange = [j for j in range(0,len(transform))] #todo: make more specific - so not using all axis
for i in fcurveArrayIndexRange:
fcrv = armatureObj.animation_data.action.fcurves.new('pose.bones["{bone}"].{attr}'.format(bone=bone, attr=attr),i)
print("creating keyframes for action")
#handle first frame
valueFirst = 0 #the default is at 0 for loc scal roteuler
if attr == "rotation_quaternion" and i == 0:
valueFirst = 1 #quaternion default for first index should be 1
valueLast = transform[i]
fcrv.keyframe_points.insert(startFrame,valueFirst) #frame, value
#handle last frame
fcrv.keyframe_points.insert(endFrame,valueLast)
#editing.py
import re
import logging
import bpy
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
#public method to add to addon
def completeMissingConstraintsBySourceBone(armatureName='', sourceBoneName='', actionName=''):
"""
Args:
sourceBoneName (str): name of source bone that has constraints we want to apply to other bones
"""
#if was going to use selection
#armatureName = bpy.context.object.name
#actionName = bpy.data.objects[armatureName].animation_data.action.name
bonesMissingActionConstraints = getBonesInActionMissingSourceConstraint(armatureName=armatureName, actionName=actionName, sourceBoneName=sourceBoneName)
if not bonesMissingActionConstraints:
return False
#copy source constraints to bones missing action constraints
cc = ConstraintCopier(armatureName=armatureName, constraintType="ACTION")
cc.copyConstraint(sourceBoneName=sourceBoneName, destinationBoneNames=bonesMissingActionConstraints)
return True
#could move these to utils
def getBonesInAction(actionName=''):
"""get list of all bone names with at least one animation curve in given action.
#ex:
print(getBonesInAction('testSimple.C'))
Returns:
List(str): for names of bones with at least one animation curve in given action
"""
if not actionName in bpy.data.actions:
logger.warning("could not find action. cannot find bones in action")
return []
dataPaths = [fcurve.data_path for fcurve in bpy.data.actions[actionName].fcurves]
bonesInAction = [re.match('.*\["(.*)"\].*', dataPath).groups()[0] for dataPath in dataPaths] #'pose.bones["Bone"].location'
result = list(set(bonesInAction))
return result
def getBonesInActionMissingSourceConstraint(armatureName='', actionName='', sourceBoneName=''):
"""get all bones in action missing a constraint that source bone has
#ex:
#print(getBonesInActionMissingSourceConstraint(armatureName='Armature', actionName='testSimple.C', sourceBoneName='Bone.002'))
Args:
armatureName (str): armature data object name
actionName (str): action name
sourceBoneName (str): source bone name
Returns:
List(str): for names of bones in action that are missing at least one source bone constraint
"""
result = []
bonesInAction = getBonesInAction(actionName=actionName) or []
if not bonesInAction:
return []
sourceBone = bpy.data.objects[armatureName].pose.bones[sourceBoneName]
sourceConstraintNames = [cnt.name for cnt in sourceBone.constraints]
for bone in bonesInAction:
#if bone missing constraint save it
boneObj = bpy.data.objects[armatureName].pose.bones[bone]
bonesConstraints = [cnt.name for cnt in boneObj.constraints]
if set(sourceConstraintNames) != set(bonesConstraints):
result.append(bone)
result = list(set(result)) #remove duplicates
return result
#end move to utils
def activateAction(armatureName='', actionName=''):
"""activate given action
"""
if (not armatureName) or (not actionName):
logger.warning("couldnt find action or armature. skipping activating action.")
return False
#go to first frame in case an action was already on
bpy.context.scene.frame_set(0)
#clear current pose before turning on action
bpy.ops.pose.select_all(action='DESELECT')
bpy.ops.pose.select_all(True)
bpy.ops.pose.transforms_clear()
bpy.data.objects[armatureName].animation_data.action = bpy.data.actions[actionName]
bpy.ops.pose.select_all(action='DESELECT')
return True
class ActionFinder(object):
"""an api to help find action constraints or actions.
it requires some inputs to be set first.
example usage:
import sys
sys.path.append('/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/naActionTools')
import editing
actFinder = editing.ActionFinder()
actFinder.setArmature("Armature")
actFinder.setAnimBone("browAllAnim.L") #animator control
actFinder.setTransformChannel("LOCATION_Y") #channel and axis control movement turns on pose
actFinder.setIsPositive(True) #set whether moving control in positive direction turns on pose
actFinder.doIt() #needs to be called before querying action constraint
print("action constraint found:")
print(actFinder.getActionConstraint())
print("action found:")
print(actFinder.getAction())
"""
def __init__(self):
self._action = '' #this is the action name that can be isolated
self._actionConstraint = '' #this is action constraint name computed from inputs
#for computing variables
self._armature = ''
self._animBone = ''
self._transformChannel = '' #ex: 'LOCATION_Y' ex: 'ROTATION_Z'
self._isPositive = True
def doIt(self):
#figure out action constraint to find
self._computeActionConstraintForAnimBone()
#figure out action to find
self._computeAction()
def setArmature(self, value):
self._armature = value
def setAnimBone(self, value):
self._animBone = value
def setTransformChannel(self, value):
self._transformChannel = value
def setIsPositive(self, value):
self._isPositive = value
def getAction(self):
return self._action
def getActionConstraint(self):
return self._actionConstraint
def _computeActionConstraintForAnimBone(self):
#loop all action constraints
#find ones with driver bone and axis and direction
#return first action constraint found with match
armature = self._armature
animBone = self._animBone
transformChannel = self._transformChannel
isPositive = self._isPositive
#assert inputs
if (not armature) or (not animBone) or (not transformChannel):
logger.warning("missing inputs. skipping computing of action constraint")
return
#find action constraint from pose bones
for bone in bpy.data.objects[armature].pose.bones:
for constraint in bone.constraints:
if constraint.type != 'ACTION':
continue
if constraint.subtarget != animBone:
continue
if constraint.transform_channel != transformChannel:
continue
#check direction
isConstraintPositive = True if constraint.max >= 0 else False
if isPositive != isConstraintPositive:
continue
#we found our constraint
self._actionConstraint = constraint.name
#we dont need to check any others
return
def _computeAction(self):
"""use found action constraint and finds its action name
"""
armature = self._armature
for bone in bpy.data.objects[armature].pose.bones:
for constraint in bone.constraints:
if constraint.name == self._actionConstraint:
self._action = constraint.action.name
#we dont need to check any other actions
return
class ConstraintCopier(object):
"""responsible for copying specified constraint type from source bone to destination bone
#example usage
cc = ConstraintCopier(armatureName="Armature", constraintType="ACTION")
cc.copyConstraint(sourceBoneName="boneA", destinationBoneNames=["bone.001"])
"""
def __init__(self, armatureName='', constraintType='ACTION'):
"""
Args:
armatureName (str): armature data object name
constraintType (str): constraint type name ex: 'ACTION'
"""
self._armatureName = armatureName
self._constraintType = constraintType
#public api
def setConstraintType(self, constraintType):
if not constraintType:
logger.warning("not valid constraint type")
return
self._constraintType = constraintType
def setArmature(self, armatureName):
if not armatureName in bpy.data.objects:
logger.warning("not valid armature data object name")
return
self._armatureName = armatureName
def copyConstraint(self, sourceBoneName='', destinationBoneNames=[]):
"""copy constraints from a source bone to destination bone
Args:
sourceBoneName (str): source bone name
destinationBoneName (List[str]): list of destination bone names
"""
armatureName = self._armatureName
constraintType = self._constraintType
for dBone in destinationBoneNames:
if dBone not in bpy.data.objects[armatureName].pose.bones:
logger.warning("could not find destination bone {}".format(dBone))
return
if not armatureName in bpy.data.objects or not sourceBoneName in bpy.data.objects[armatureName].pose.bones:
logger.warning("could not find inputs. exiting copying constraints")
return
sourceBone = bpy.data.objects[armatureName].pose.bones[sourceBoneName]
srcActionConstraints = [cnt for cnt in sourceBone.constraints if cnt.type == constraintType]
if not srcActionConstraints:
logger.warning("could not find any constraints to copy on source bone {}. doing nothing".format(sourceBoneName))
return
#loop through all destination bones copying source bone constraint to destination bone
for destinationBoneName in destinationBoneNames:
destinationBone = bpy.data.objects[armatureName].pose.bones[destinationBoneName]
for cnt in srcActionConstraints:
#skip creating constraint if constraint already exists on destination bone
if cnt.name in [destCnt.name for destCnt in destinationBone.constraints]:
new_cnt = destinationBone.constraints[cnt.name]
else:
new_cnt = destinationBone.constraints.new(cnt.type)
#destination constraint name same as source
#copy attributes of constraint
for prop in dir(cnt):
#dont copy target
if prop == "target":
setattr(new_cnt, prop, bpy.data.objects[armatureName])
continue
try:
setattr(new_cnt, prop, getattr(cnt, prop))
except:
pass
class ActionBoneSelector(object):
"""has api to be able to make bone selections related to bones in action
"""
def __init__(self):
self._obj = bpy.context.active_object
self._action = self._obj.animation_data.action
def selectLeftSideBones(self):
"""select all left side bones in action
"""
bonesInActionLeftSide = self.getLeftSideBones() or []
if not bonesInActionLeftSide:
return False
#select them
self._selectBones(bonesInActionLeftSide)
return True
def selectRightSideBones(self):
"""select all right side bones for all left side bones in action
"""
bonesInActionLeftSide = self.getLeftSideBones() or []
if not bonesInActionLeftSide:
return False
#get all right side bones
bonesRightSide = [bone.replace('.L', '.R') for bone in bonesInActionLeftSide]
#print('right side', bonesRightSide)
#select them
self._selectBones(bonesRightSide)
return True
def getLeftSideBones(self):
"""
Returns:
(list[str]): names of all bones on left side of armature
"""
obj = self._obj
action = self._action
dataPaths = [fcurve.data_path for fcurve in action.fcurves]
#print(dataPaths)
bonesInAction = [re.match('.*\["(.*)"\].*', dataPath).groups()[0] for dataPath in dataPaths] #'pose.bones["Bone"].location'
bonesInAction = list(set(bonesInAction)) #remove duplicates
bonesInActionLeftSide = [b for b in bonesInAction if b.endswith('.L')]
return bonesInActionLeftSide
def _selectBones(self, bones=[]):
if not bones:
return
obj = self._obj
#deselect all pose bones
bpy.ops.pose.select_all(action='DESELECT')
for b in bones:
obj.pose.bones[b].bone.select = True
########
class MirrorPose(object):
"""has tools for mirroring a single pose of armature. not no keyframes are set
"""
def __init__(self):
pass
def doIt(self):
"""primary method to be called by client tools
"""
#select left side bones of action first.
boneSelector = ActionBoneSelector()
boneSelector.selectLeftSideBones()
self.mirrorPoseOfSelected()
def mirrorPoseOfSelected(self):
"""mirror pose for selected bones. ex select .L side bones first.
"""
#todo: do nothing if no bones selected
bpy.ops.pose.copy()
bpy.ops.pose.paste(flipped=True)
"""
import bpy
import imp
cntTool = imp.load_source("tool", "/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/naActionTools/editing.py")
cc = cntTool.ConstraintCopier(armatureName="Armature", constraintType="ACTION")
cc.copyConstraint(sourceBoneName="Bone.002", destinationBoneNames=["Bone"])
"""
#inspired by
#blender dot stack exchange dot com ‚How to copy constraints from one bone to another post
#utils.py
import bpy
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console
def getTransformChannelData(armature, bone):
"""
example
getTransformChannelData("Armature", "lipCornerAnim.L") #returns ("LOCATION_Y", "True")
"""
attrsData = [("location.x", "LOCATION_X"),
("location.y", "LOCATION_Y"),
("location.z", "LOCATION_Z"),
("rotation_euler.x", "ROTATION_X"), #not supporting quaterion
("rotation_euler.y", "ROTATION_Y"),
("rotation_euler.z", "ROTATION_Z")] #todo add scale support.
poseBone = bpy.data.objects[armature].pose.bones[bone] #no error checking
for attrDat in attrsData:
attr, transformChannel = attrDat
val = eval("poseBone.{0}".format(attr))
direction = "True" if val >= 0 else "False"
#if its not default return it
if (val > 0.0) or (val < 0.0):
return (transformChannel, direction)
return None
def turnOffActions(armature):
"""go to 0 frame and turn off active action.
"""
bpy.context.scene.frame_set(0)
#unlink action. make action not active, without deleting it.
bpy.data.objects[armature].animation_data.action = None
#ui.py
import bpy
from bpy.props import(
StringProperty,
PointerProperty,
EnumProperty
)
from bpy.types import(
Operator,
Panel,
PropertyGroup
)
from . import creating
from . import editing
from . import utils
import importlib
importlib.reload(creating)
importlib.reload(editing)
importlib.reload(utils)
class ActionPanel(Panel):
bl_label = "Action Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
def draw(self, context):
layout = self.layout
layout.label(text="Action Creating/Editing Tool")
layout.label(text="-"*80)
layout.label(text="Creating Section:")
layout.label(text="first pose static mesh to posed shape with no keyframes")
#add text fields
layout.prop(context.scene.action_prop, "actionName", text="actionName")
layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
layout.prop(context.scene.action_prop, "animBone", text="animBone")
layout.prop(context.scene.action_prop, "onPointValue", text="onPointValue")
layout.prop(context.scene.action_prop, "onPointAxis", text="onPointAxis")
layout.prop(context.scene.action_prop, "onFrame", text="onFrame")
#button
layout.operator("obj.do_actioncreate") #uses bl_idname
layout.label(text="-"*80)
layout.label(text="Mirroring Section:")
layout.prop(context.scene.action_prop, "actionName", text="actionName")
layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
layout.operator("obj.do_actionmirror")
layout.label(text="-"*80)
layout.label(text="Editing Section:")
layout.operator("obj.do_editingloadselected")
layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
layout.prop(context.scene.action_prop, "animBone", text="animBone")
layout.prop(context.scene.action_prop, "transformChannel", text="transformChannel")
layout.prop(context.scene.action_prop, "isPositive", text="isPositive")
layout.operator("obj.do_editselectaction")
layout.label(text="Adding Missing Constraints:")
layout.prop(context.scene.action_prop, "actionName", text="actionName")
layout.prop(context.scene.action_prop, "armatureName", text="armatureName")
layout.prop(context.scene.action_prop, "sourceBoneNameForConstraints", text="sourceBone")
layout.operator("obj.do_editcompleteconstraint")
layout.label(text="Tools for editing pose of action:")
layout.operator("obj.do_editmirrorpose")
layout.operator("obj.do_editselectrightsidebones")
class ActionProperties(PropertyGroup):
actionName = StringProperty(
name="actionName",
description="action name ex: lipUUUAction.C"
)
armatureName = StringProperty(
name="armatureName",
description="armature name"
)
animBone = StringProperty(
name="animBone",
description="animator control bone used to turn on pose ex: lipUUUAnim.C"
)
onPointValue = StringProperty(
name="onPointValue",
default="1",
description="what value in channel box of animator control turns on pose ex: 1"
)
onPointAxis = StringProperty(
name="onPointAxis",
default="y",
description="axis for animator control to turn on pose ex: y"
)
onFrame = StringProperty(
name="onFrame",
default="10",
description="what frame in the action has the mesh posed. ex: 10"
)
#editing
transformChannel = EnumProperty(
name="transformChannel",
description="transform channel for animator control driving action",
items=[ ("LOCATION_X", "LOCATION_X", ""),
("LOCATION_Y", "LOCATION_Y", ""),
("LOCATION_Z", "LOCATION_Z", ""),
("ROTATION_X", "ROTATION_X", ""),
("ROTATION_Y", "ROTATION_Y", ""),
("ROTATION_Z", "ROTATION_Z", "")
],
default="LOCATION_Y"
)
isPositive = EnumProperty(
name="isPositive",
description="is direction positive to drive action",
items=[ ("True", "True", ""),
("False", "False", "")
],
default="True"
)
sourceBoneNameForConstraints = StringProperty(
name="sourceBone",
description="source bone name that has constraints we wish to apply to other bones ex: jaw.C"
)
class ActionCreateOperator(Operator):
"""create action. first pose static mesh to posed shape with no keyframes, fill in values then click create
"""
bl_idname = "obj.do_actioncreate"
bl_label = "Action Create"
bl_options = {"REGISTER"}
def execute(self, context):
self.report({'INFO'}, "Creating Action")
actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
armatureName = context.scene.action_prop.armatureName #"Armature"
onPointValue = int(context.scene.action_prop.onPointValue) #1
onPointAxis = context.scene.action_prop.onPointAxis #'y'
onPoint = (onPointValue, onPointAxis) #(1,'y') #animator control movement for pose
animBone = context.scene.action_prop.animBone
onFrame = int(context.scene.action_prop.onFrame)
ac = creating.ActionCreator(actionName, armature=armatureName)
ac.doIt()
pc = creating.PoseCreate(actionName,
armatureName,
animbone=animBone,#"lipUUUAnim.C",
onFrame=onFrame,
offFrame=0,
onPoint=onPoint,
offPoint=(0,onPointAxis))
pc.doIt()
return {'FINISHED'}
class ActionMirrorOperator(Operator):
"""mirror given action and action constraints
"""
bl_idname = "obj.do_actionmirror"
bl_label = "Action Mirror"
bl_options = {"REGISTER"}
def execute(self, context):
self.report({'INFO'}, "Mirroring Action")
actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
armatureName = context.scene.action_prop.armatureName #"Armature"
creating.PoseMirror(armatureName).mirrorActions(actionNames=[actionName], replace=True)
creating.PoseMirror(armatureName).mirrorActionConstraints()
return {'FINISHED'}
class ActionEditingSelectOperator(Operator):
"""select a computed action for quicker editing of existing actions.
"""
bl_idname = "obj.do_editselectaction"
bl_label = "Load Action"
bl_options = {"REGISTER"}
def execute(self, context):
self.report({'INFO'}, "Editing Action")
actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
armatureName = context.scene.action_prop.armatureName #"Armature"
animBone = context.scene.action_prop.animBone #"browAllAnim.L"
transformChannel = context.scene.action_prop.transformChannel #"LOCATION_Y"
isPositive = True if context.scene.action_prop.isPositive == "True" else False #True
actFinder = editing.ActionFinder()
actFinder.setArmature(armatureName)
actFinder.setAnimBone(animBone) #animator control
actFinder.setTransformChannel(transformChannel) #channel and axis control movement turns on pose
actFinder.setIsPositive(isPositive) #set whether moving control in positive direction turns on pose
actFinder.doIt() #needs to be called before querying action constraint
#print("action constraint found:")
#print(actFinder.getActionConstraint())
#print("action found:")
#print(actFinder.getAction())
actionName = actFinder.getAction()
if not actionName:
self.report({'INFO'}, "Couldnt find action for given inputs. doing nothing")
else:
#turn on computed action
editing.activateAction(armatureName=armatureName, actionName=actionName)
return {'FINISHED'}
class ActionEditingLoadSelectedOperator(Operator):
"""load selected bone. first pose bone to get direction.
"""
bl_idname = "obj.do_editingloadselected"
bl_label = "Load Selected Bone"
def execute(self, context):
self.report({'INFO'}, "Load Selected Bone")
if not context.selected_pose_bones:
self.report({'INFO'}, "requires a single selected pose bone. doing nothing")
return {'FINISHED'}
#compute values to use from selection
armatureName = context.selected_objects[0].name
animBone = context.selected_pose_bones[0].name
transformData = utils.getTransformChannelData(armatureName, animBone)
if not transformData:
self.report({'INFO'}, "requires selected bone to be nudged in a single axis to determine transform channel and direction. doing nothing")
return {'FINISHED'}
transformChannel = ""
isPositive = "True"
if transformData:
transformChannel, isPositive = transformData
context.scene.action_prop.armatureName = armatureName #'TestArmature'
context.scene.action_prop.animBone = animBone#'TestAnimBone'
context.scene.action_prop.transformChannel = transformChannel #"LOCATION_Z"
context.scene.action_prop.isPositive = isPositive #"True"
return {'FINISHED'}
class ActionEditingCompleteConstraintOperator(Operator):
"""add action constraints to all bones in action missing them. using a source bone
"""
bl_idname = "obj.do_editcompleteconstraint"
bl_label = "Add Action Constraints to Missing Bones"
bl_options = {"REGISTER"}
def execute(self, context):
self.report({'INFO'}, "Editing Action")
actionName = context.scene.action_prop.actionName #"lipUUUAction.C"
armatureName = context.scene.action_prop.armatureName #"Armature"
sourceBone = context.scene.action_prop.sourceBoneNameForConstraints #"browAllAnim.L"
editing.completeMissingConstraintsBySourceBone(armatureName=armatureName, sourceBoneName=sourceBone, actionName=actionName)
return {'FINISHED'}
class ActionEditingMirrorPoseOperator(Operator):
"""mirror pose
"""
bl_idname = "obj.do_editmirrorpose"
bl_label = "Mirror pose for current action"
bl_options = {"REGISTER"}
def execute(self, context):
self.report({'INFO'}, "Mirroring single pose")
mirPose = editing.MirrorPose()
mirPose.doIt()
return {'FINISHED'}
class ActionEditingSelectRightSideBonesOperator(Operator):
"""select right side bones
"""
bl_idname = "obj.do_editselectrightsidebones"
bl_label = "Select all right side bones of current action"
bl_options = {"REGISTER"}
def execute(self, context):
self.report({'INFO'}, "Selecting right side bones")
boneSelector = editing.ActionBoneSelector()
boneSelector.selectRightSideBones()
return {'FINISHED'}
def register():
bpy.utils.register_class(ActionCreateOperator)
bpy.utils.register_class(ActionMirrorOperator)
bpy.utils.register_class(ActionEditingSelectOperator)
bpy.utils.register_class(ActionEditingLoadSelectedOperator)
bpy.utils.register_class(ActionEditingCompleteConstraintOperator)
bpy.utils.register_class(ActionEditingMirrorPoseOperator)
bpy.utils.register_class(ActionEditingSelectRightSideBonesOperator)
bpy.utils.register_class(ActionProperties)
bpy.utils.register_class(ActionPanel)
#name property that holds text fields
bpy.types.Scene.action_prop = PointerProperty(type=ActionProperties)
def unregister():
bpy.utils.unregister_class(ActionCreateOperator)
bpy.utils.unregister_class(ActionMirrorOperator)
bpy.utils.unregister_class(ActionEditingSelectOperator)
bpy.utils.unregister_class(ActionEditingLoadSelectedOperator)
bpy.utils.unregister_class(ActionEditingCompleteConstraintOperator)
bpy.utils.unregister_class(ActionEditingMirrorPoseOperator)
bpy.utils.unregister_class(ActionEditingSelectRightSideBonesOperator)
bpy.utils.unregister_class(ActionProperties)
bpy.utils.unregister_class(ActionPanel)
del bpy.types.Scene.action_prop
