Skip to content

Instantly share code, notes, and snippets.

@nathanielanozie
Created June 15, 2024 19:30
Show Gist options
  • Save nathanielanozie/31faba611bfc405798a8a0bb582bf588 to your computer and use it in GitHub Desktop.
Save nathanielanozie/31faba611bfc405798a8a0bb582bf588 to your computer and use it in GitHub Desktop.
bendy bone animator control class in blender 2.79
#has some tools for adding animator controls to bendy bones
#
#tested in blender 2.79
#please modify/use at your own risk
import bpy
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in console
class BendyBoneAnimatorController(object):
"""add animator posability to a single bendy bone segment. currently supports adding two animator controls per bendy bone
example usage:
#bb = BendyBoneAnimatorController("Bone","Armature") #name of bendybone, name of armature
#bb.doIt()
#here specifying the names used for the animator controllers (they need to be unique in the armature)
bb = BendyBoneAnimatorController("Bone","Armature", headControllerName="arm_bottom_anim", tailControllerName="arm_top_anim") #name of bendybone, name of armature
bb.doIt()
"""
def __init__(self, bbone, armatureObjName, boneSize=0.3, headControllerName='', tailControllerName='', verbose=False):
"""
@param bbone (str) name for bendy bone we want to add animator posability
@param armatureObjName (str) name of armature object to add bones to
@param boneSize (float) scale for animator control bones smaller number makes it appear smaller
@param headControllerName (str) optional name for head animator control. if none provided it computes it
@param tailControllerName (str) optional name for tail animator control. if none provided it computes it
@param verbose (bool) whether to show debug prints
"""
self.armatureObjName = armatureObjName
self.boneSize = boneSize
self._verbose = verbose
#vaildate possible controller names
allArmatureBones = self._getArmatureBones() or []
if headControllerName and headControllerName in allArmatureBones:
raise RuntimeError("please specify a unique head controller name")
if tailControllerName and tailControllerName in allArmatureBones:
raise RuntimeError("please specify a unique tail controller name")
#
#need to determine these from input bendy bone
###
uppos = _getStartBonePositions(bbone,armatureObjName)
lowpos = _getEndBonePositions(bbone,armatureObjName)
if self._verbose:
logger.debug("uppos:")
logger.debug(uppos)
logger.debug("lowpos:")
logger.debug(lowpos)
upHeadPos=uppos[0] #bbone tail
upTailPos=uppos[1]
lowHeadPos=lowpos[0]
lowTailPos=lowpos[1] #bbone head
roll = _getRoll(bbone,armatureObjName)
###
self.upHeadPos = tuple(upHeadPos) #i think there was a bug using Vector
self.upTailPos = tuple(upTailPos)
self.lowHeadPos = tuple(lowHeadPos)
self.lowTailPos = tuple(lowTailPos)
self.roll = roll
#self.segments = segments
rigPrefix = self._getRigPrefix(bbone)
side = self._getRigSide(bbone)
self.headBoneName = rigPrefix+'_'+'head'+'.'+side if not headControllerName else headControllerName
self.tailBoneName = rigPrefix+'_'+'tail'+'.'+side if not tailControllerName else tailControllerName
self.midBoneName = bbone
def _getRigPrefix(self, bone):
"""
@return (str)
"""
result = ''
if '.' in bone:
result = bone.split('.')[0]
else:
result = "test"+bone
return result
def _getRigSide(self, bone):
"""
@return (str)
"""
if '.' in bone:
result = bone.split('.')[-1] #the last item
else:
result = "testSide"
return result
def _getArmatureBones(self):
"""get all armature bones names
@return (list of str)
"""
return [b.name for b in bpy.data.objects[self.armatureObjName].data.bones]
def doIt(self):
self.createBones()
self.scaleControlBones()
self.addConstraint()
self.addAnimatorControls()
def createBones(self):
"""create up and low. the middle parented to up the low not parented to anything
"""
#start with nothing selected
#bpy.ops.object.select_all(action='DESELECT')
arm_obj = bpy.data.objects[self.armatureObjName]
bpy.context.scene.objects.active = arm_obj
#make sure armature in bendy bone view mode
arm_obj.data.draw_type = 'BBONE'
bpy.ops.object.mode_set(mode='OBJECT') #in case started in edit mode and want to update changes
bpy.ops.object.mode_set(mode='EDIT')
#make head bone
headBone = arm_obj.data.edit_bones.new(self.headBoneName)
headBone.head = self.upHeadPos
headBone.tail = self.upTailPos
headBone.use_deform = False
#make tail bone
tailBone = arm_obj.data.edit_bones.new(self.tailBoneName)
tailBone.head = self.lowHeadPos
tailBone.tail = self.lowTailPos
tailBone.use_deform = False
#testing - parent head/tail bones to the parent of midbone
midboneParent = arm_obj.data.edit_bones[self.midBoneName].parent
if midboneParent:
if self._verbose:
logger.debug("midboneParent:{}".format(midboneParent.name))
arm_obj.data.edit_bones[self.headBoneName].parent = midboneParent
arm_obj.data.edit_bones[self.tailBoneName].parent = midboneParent
#
#parent bbone to headBone
arm_obj.data.edit_bones[self.midBoneName].parent = arm_obj.data.edit_bones[self.headBoneName]
bpy.context.scene.objects.active = arm_obj
bpy.ops.object.mode_set(mode='OBJECT')
#set the handles for bendy bone
arm_obj.pose.bones[self.midBoneName].use_bbone_custom_handles = True
arm_obj.pose.bones[self.midBoneName].bbone_custom_handle_start = arm_obj.pose.bones[self.headBoneName]
arm_obj.pose.bones[self.midBoneName].bbone_custom_handle_end = arm_obj.pose.bones[self.tailBoneName]
def addConstraint(self):
"""add stretch-to constraint on middle bone aiming at end handle tail bone
"""
arm_obj = bpy.data.objects[self.armatureObjName]
bpy.context.scene.objects.active = arm_obj
bpy.ops.object.mode_set(mode='OBJECT')
constraint = arm_obj.pose.bones[self.midBoneName].constraints.new('STRETCH_TO')
constraint.target = arm_obj
constraint.subtarget = self.tailBoneName
def addAnimatorControls(self):
"""todo
"""
#draw animator controls
#add control curves to appropriate bones
def scaleControlBones(self):
"""
"""
arm_obj = bpy.data.objects[self.armatureObjName]
bpy.context.scene.objects.active = arm_obj
bpy.ops.object.mode_set(mode='OBJECT')
#select the two end bones
_deselectAllBones(self.armatureObjName)
arm_obj.data.bones[self.headBoneName].select = True
arm_obj.data.bones[self.tailBoneName].select = True
#scale them
bpy.ops.object.mode_set(mode='POSE')
bpy.ops.transform.transform(mode='BONE_SIZE', value=(self.boneSize, self.boneSize, self.boneSize,0))
def _getEndBonePositions(self):
"""simple method that uses the direction of bbone to determine positions for a posible end control bone
@return list of 2 tuples [startWorldPosition,endWorldPosition]
"""
bbone = self.midBoneName
armature = self.armatureObjName
obj = bpy.data.objects[armature]
bone = obj.data.bones[bbone]
pctbbone_length = 0.25 #1/4 might want to try bigger for a longer control bone
return [ tuple(bone.tail_local), tuple( bone.tail_local + (bone.tail_local-bone.head_local)*(0.25) ) ]
def _getStartBonePositions(self):
"""simple method that uses the direction of bbone to determine positions for a posible start control bone
@return list of 2 tuples [startWorldPosition,endWorldPosition]
"""
bbone = self.midBoneName
armature = self.armatureObjName
obj = bpy.data.objects[armature]
bone = obj.data.bones[bbone]
pctbbone_length = 0.25 #1/4 might want to try bigger for a longer control bone
return [ tuple(bone.head_local - (bone.tail_local-bone.head_local)*(0.25)), tuple(bone.head_local) ]
def _getRoll(self):
"""
@return double - roll of bone
"""
bbone = self.midBoneName
armature = self.armatureObjName
obj = bpy.data.objects[armature]
bpy.context.scene.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
roll = obj.data.edit_bones[bone].roll
#todo restore mode
return roll
def _deselectAllBones(self):
"""
"""
armature = self.armatureObjName
assert armature
obj = bpy.data.objects[armature]
bpy.context.scene.objects.active = obj
bpy.ops.object.mode_set(mode='OBJECT')
for bone in obj.data.bones:
bone.select=False
def getAllDeformBones(armature):
"""return all deform bones of armature
"""
result = []
obj = bpy.data.objects[armature]
bpy.context.scene.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
result = [bone.name for bone in obj.data.edit_bones if bone.use_deform]
bpy.ops.object.mode_set(mode='OBJECT')#todo return to previous mode
return result
def addControlsToBBones(armature, bbones = [], verbose=False):
"""add animator control bones to bendy bones
@param armature (str) name of armature object
@param bbones (list of str) optional names for bendy bones to add controls to. if none supplied it uses all bones
@param verbose (bool) whether to show debug prints
"""
assert armature
bones = bbones
if not bones:
bones = getAllDeformBones(armature) or []
for bone in bones: #["socket_dn.L"]
bb = BendyBoneAnimatorController(bone, armature, verbose=verbose)
bb.doIt()
#added support for artist provided animator control names
#fixed logic in auto computing animator control names
#some coding redesign moving some global methods that use same data to the bendy bone controller class
#fixed bug if bone had a parent using head_local tail_local
"""
import imp
testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/riggingTools/faceTools/addControlsToBendyBones.py") #change to path to python file
#testTool.addControlsToBBones("Armature")
#testTool.addControlsToBBones("Armature", bbones=["Bone", "Bone.001"], verbose=True)
#bb = testTool.BendyBoneAnimatorController("Bone","Armature") #name of bendybone, name of armature
#bb.doIt()
#print(testTool._getEndBonePositions("Bone","Armature"))
print(testTool._getEndBonePositions("socket_dn.L","Armature"))
print(testTool._getStartBonePositions("socket_dn.L","Armature"))
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment