this is a collection of some personal work time series prediction methods in c++
https://github.com/nathanielanozie/naTimeSeries
Thanks for looking
this is a collection of some personal work time series prediction methods in c++
https://github.com/nathanielanozie/naTimeSeries
Thanks for looking
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
i'm still very new to Unreal Engine's control rig but thought this doodle may be helpful for learning. there may be better ways to implement.
here is an example of driving 4 blendshapes (the blendshapes were created in Blender and exported to fbx so they could be imported into Unreal Engine):
here is a picture of the control rig graph for a simpler example driving 2 blendshapes. similar logic can be used by copying and pasting these nodes and changing up the blendshape names and driving transformation channel to get it to work for 4 blendshapes.
here is a picture of the control rig graph that has the various nodes used to drive 2 blendshapes.
basically starting left to right:
on far left is the animator control that will be used to drive the blendshapes.
next it uses a 'Greater' node to decide if the controls transform z channel is positive or negative. here the z axis of the control will be used to drive 2 blendshapes. if the z translate axis is positive it uses its value after clamping it to a number between 0 and 1 to drive the first blendshape which is set in the 'set curve value' node. if the control's z is negative it first multiplies its value by -1 to get a positive number then it also eventually clamps its value and sends it to the second blendshape 'set curve value' node. (some of the float multiplies was to tweak the sensitivity of how much the animator control moves is enough to turn on blendshapes).
Hope this is helpful.
Happy Sketching!
also i highly recommend checking out JC's YouTube channel for learning about control rig.
inspired by,
JC's '3D Education with JC' YouTube channel's 'Build a control rig from scratch' video