Saturday, April 11, 2020

Learning to script with bones armatures in Blender Part I

Hi,
i'm learning about scripting with bones/armatures in blender 2.79 and wanted to share some exercises i created to improve learning more about scripting with armatures hope you find this helpful.

1.given armature draw bone at given position
2.given armature draw bone at given empty position
3.given armature draw bone with roll such that up is z
4. 1,2,3 creating new armature for each
5. draw a straight bone chain with any given number of bones — use fixed positions/ use fixed axis / create armature
6. given 2 bones parent one to the other with offset
7. 6, but use connected
8. given some bones add them to a given layer
9. given expected bones for biped arm automatically name them upArm.L, lowArm.L, hand.L
10. 9, but expect bones to be spine and name them spine1, spine2 … etc

notes creating an armature:
- need to create armature
- need to create object
- need to set objects data to armature
- need to link object to scene ( i think this may be different in higher blender versions)
- need to create edit_bone in armature in edit mode
- need to set tail of bone to a position to see bone in viewport

and here are some notes i created for solving the above exercises, as i'm still learning in this area there may be better ways to approach solving the exercises but here are some tips.

"""blender 2.79
import bpy
import sys
#example if wanted to test script without addon part. change to your path here
sys.path.append('/Users/Nathaniel/Documents/src_blender/python/introPipeForArtistsRigging')

import boneDoodles as mod
import imp
imp.reload(mod)

#for all examples below it assumes names exist in scene

#mod.drawBoneAtPosition( 'Armature',(0,0,0) )
#mod.drawBoneAtEmpty('Armature','Empty')
#mod.drawBoneAtPositionUpZ( 'Armature',(0,0,0) )
#mod.drawBoneAtPositionCreateArmature(pos = (0,0,0))
#mod.drawBoneAtEmptyCreateArmature(emptyName = 'Empty')
#mod.drawBoneChain( numBones = 2 )
#mod.parentBones( armatureName = 'Armature', 
                parentName = 'Bone', 
                childName = 'Bone.001', 
                useConnected = False )
#mod.parentBones( armatureName = 'Armature', 
                parentName = 'Bone', 
                childName = 'Bone.001', 
                useConnected = True )
#mod.putBoneInLayer( armatureName = 'Armature', 
                    boneNames = ['Bone'], 
                    layerIndex = 19 )                
 
#mod.nameBones(  armatureObj = bpy.data.objects['Armature'], 
                bones=['Bone','Bone.001','Bone.002'], 
                mode = 'ARM', 
                side = 'L') 
mod.nameBones(  armatureObj = bpy.data.objects['Armature'], 
                bones=['Bone','Bone.001','Bone.002'], 
                mode = 'SPINE', 
                side = 'L') 
"""

import bpy
import math

def drawBoneAtPosition(armatureName = '', pos = (0,0,0)):
    """1.given armature draw bone at given position
    #assume bone tail placed above head in z with 1 unit distance
    """
    curMode = bpy.context.object.mode
    
    #need to create bone
    #need to be in edit mode
    bpy.ops.object.mode_set(mode='EDIT')
    bone = bpy.data.armatures[armatureName].edit_bones.new('Bone') #name gets automatically incremented
    bone.head = pos
    bone.tail = (pos[0],pos[1],pos[2]+1) #z is offset by a bit
    
    #restore mode
    bpy.ops.object.mode_set(mode=curMode)


def drawBoneAtEmpty(armatureName = '', emptyName = ''):
    """#2.given armature draw bone at given empty position
    """
    
    #need armature to be only active object to go into edit mode to create bone
    #because empty doesnt have edit mode
    bpy.context.scene.objects.active = bpy.data.objects[armatureName]
    pos = bpy.data.objects[emptyName].location
    drawBoneAtPosition(armatureName = armatureName, pos = pos)
    
    
def drawBoneAtPositionUpZ(armatureName = '', pos = (0,0,0)):
    """3.given armature draw bone with roll such that up is z
    #assume bone tail placed besides head in x 
    """
    curMode = bpy.context.object.mode
    
    #need to create bone
    #need to be in edit mode
    bpy.ops.object.mode_set(mode='EDIT')
    bone = bpy.data.armatures[armatureName].edit_bones.new('Bone') #name gets automatically incremented
    bone.head = pos
    bone.tail = (pos[0]+1,pos[1],pos[2]) #x is offset by a bit
    
    #change roll so up is z
    bone.roll = -math.pi/2
    bone.roll = 0
    
    #restore mode
    bpy.ops.object.mode_set(mode=curMode)

#4. 1,2,3 creating new armature for each
def drawBoneAtPositionCreateArmature(pos = (0,0,0)):
    """4.1.create armature draw bone at given position
    #assume bone tail placed above head in z with 1 unit distance
    """
    curMode = None
    if bpy.context.object:
        curMode = bpy.context.object.mode
        bpy.ops.object.mode_set(mode='OBJECT') #if something is selected go to object mode
    
    #create armature, armature object, link to scene
    amt = None
    amt = bpy.data.armatures.new('Armature') #name end up with may be different if its already used
    amtObj = bpy.data.objects.new('Armature',amt)
    bpy.context.scene.objects.link(amtObj)
    
    #need to create bone
    #need to be in edit mode
    bpy.context.scene.objects.active = amtObj #select armature
    bpy.ops.object.mode_set(mode='EDIT')
    bone = amt.edit_bones.new('Bone') #name gets automatically incremented
    bone.head = pos
    bone.tail = (pos[0],pos[1],pos[2]+1) #z is offset by a bit
    
    #restore mode
    if curMode:
        bpy.ops.object.mode_set(mode=curMode)


def drawBoneAtEmptyCreateArmature( emptyName = ''):
    """#4.2.create armature draw bone at given empty position
    """
    pos = bpy.data.objects[emptyName].location
    drawBoneAtPositionCreateArmature( pos = pos)
    
    
    
def drawBoneChain( numBones = 2 ):
    """#5. draw a straight bone chain with any given number of bones, use fixed positions, use fixed axis create armature
    no parenting
    """
    
    curMode = None
    if bpy.context.object:
        curMode = bpy.context.object.mode
        bpy.ops.object.mode_set(mode='OBJECT') #if something is selected go to object mode
    
    #create armature, armature object, link to scene
    amt = None
    amt = bpy.data.armatures.new('Armature') #name end up with may be different if its already used
    amtObj = bpy.data.objects.new('Armature',amt)
    bpy.context.scene.objects.link(amtObj)
    
    #loop creating bones
    #need to be in edit mode
    bpy.context.scene.objects.active = amtObj #select armature
    bpy.ops.object.mode_set(mode='EDIT')
    pos = (0,0,0)
    for i in range(0,numBones):
        pos = (0,0,i)
        bone = amt.edit_bones.new('Bone') #name gets automatically incremented
        bone.head = pos
        bone.tail = (pos[0],pos[1],pos[2]+1) #z is offset by a bit
    
    #restore mode
    if curMode:
        bpy.ops.object.mode_set(mode=curMode)
        
        
        

def parentBones( armatureName = None, 
                parentName = None, 
                childName = None, 
                useConnected = False ):
    """
    #6. given 2 bones parent one to the other with offset.
    assumes all inputs exist
    #7. 6, but use connected
    """
    #i think getting armature from object.data is more efficient
    amtObj = bpy.data.objects[armatureName]
    bpy.context.scene.objects.active = amtObj
    bpy.ops.object.mode_set(mode='EDIT')
    amt = amtObj.data
    amt.edit_bones[childName].parent = amt.edit_bones[parentName]  
    
    if useConnected:
        amt.edit_bones[childName].use_connect = True
        

def putBoneInLayer( armatureName = None, 
                    boneNames = [], 
                    layerIndex = 31 ):
    """
    #8. given some bones add them to a given layer
    add them to given layer
    assumes armature and bones exist.
    need to be in armature icon to see different layers
    """
    amtObj = bpy.data.objects[armatureName]
    amt = amtObj.data
    for bone in boneNames:
        amt.bones[bone].layers[layerIndex] = True
        #set all other layers to false
        numLayers = len(amt.bones[bone].layers)
        for j in range(0,numLayers):
            if j != layerIndex:
                amt.bones[bone].layers[j] = False
    
    

def nameBones(  armatureObj = None, 
                bones=[], 
                mode = 'ARM', 
                side = 'L'):
    """
    #9. given expected bones for biped arm automatically name them upArm.L, lowArm.L, hand.L
    #10. 9, but expect bones to be spine and name them spine1, spine2 etc
    mode 'ARM' or 'SPINE'
    bones bone names
    assumes all input exist
    """
    amt = armatureObj.data
    
    for i in range(0,len(bones)):
        if bones[i] not in amt.bones:
            print("couldnt find bone %s" %(bones[i]))
            return
    
    if mode == 'ARM':
        if len(bones) != 3:
            return
        amt.bones[ bones[0] ].name = 'upArm.%s' %(side)
        amt.bones[ bones[1] ].name = 'lowArm.%s' %(side)
        amt.bones[ bones[2] ].name = 'hand.%s' %(side)
    
    elif mode == 'SPINE':
        for i in range(0,len(bones)):
            amt.bones[ bones[i] ].name = 'spine%s' %(i+1) 
 
 

Happy Scripting and Sketching!
Nate

inspired by:
Sybren Stüvel's "Scripting for Artists" talk