Pages

Tuesday, May 5, 2020

naShapekeyUtilsAddon

Hi,
These are some tools i wrote for creating/editing shapekeys in blender.
please modify/use at your own risk.

Happy Scripting and Sketching!
Nate


naShapekeyUtilsAddon from Nathan Anozie on Vimeo.




#naShapekeyUtilAddOn.py
#modify use at your own risk
import bpy
import os
import bmesh

####add on portion
bl_info = {
    "name":"shapkey editing tools",
    "description":"tool to edit shapekeys",
    "category": "Object",
    "author":"Nathaniel Anozie",
    "blender":(2,79,0)
}

from bpy.types import(
    Operator,
    Panel,
    PropertyGroup
    )

from bpy.props import(
    StringProperty,
    PointerProperty
    )

class splitSymmetricShapeKeyOperator(Operator):
    """split selected shapekey.  first select single mesh with shapekey in object mode and highlight shapekey wish to split.
    """
    bl_idname = "obj.splitsymmetricshapekey" #needs to be all lowercase
    bl_label = "splitSymmetricShapeKey"
    bl_options = {"REGISTER"}

    def execute(self, context):
        splitSymmetricShapeKey(obj=context.selected_objects[0],removeSourceShapeKey = False, context = context)
        return {'FINISHED'}


class mirrorShapekeyTopologyOperator(Operator):
    """first select single mesh with shapekey in object mode.
    """
    bl_idname = "obj.mirrorshapekeytopology"
    bl_label = "mirrorShapekeyTopology"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        mirrorShapekeyTopology(obj=context.selected_objects[0],context=context)
        return {'FINISHED'}

class mergeShapekeysOperator(Operator):
    """merge selected meshes shapekeys into one mesh
    """
    bl_idname = "obj.mergeshapekeys"
    bl_label = "mergeShapekeys"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        mergeShapekeys(context=context)
        return {'FINISHED'}

class zeroKeyOnSelectedVerticesOperator(Operator):
    """set selected vertices to basis position
    """
    bl_idname = "obj.zerokeyonselectedvertices"
    bl_label = "zeroKeyOnSelectedVertices"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        zeroKeyOnSelectedVertices(context=context)
        return {'FINISHED'}

class putMeshesInRowOperator(Operator):
    """arrange selected meshes in row with a little gap
    """
    bl_idname = "obj.putmeshesinrow"
    bl_label = "putMeshesInRow"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        putMeshesInRow(context.selected_objects,gridWidth=1)
        return {'FINISHED'}

class importObjOperator(Operator):
    """import objs in a directory
    """
    bl_idname = "obj.importobj"
    bl_label = "importObj"
    bl_options = {"REGISTER"}
    
    def execute(self, context):
        path = context.scene.importobj_prop.importObjPath
        dirpath = bpy.path.abspath(path)
        print("importObjOperator dir path: %s" %(dirpath))
        importObj(dirpath)
        return {'FINISHED'}
        
class naShapekeyUtilPanel(Panel):
    bl_label = "naShapekeyUtil Panel"
    bl_space_type = "VIEW_3D" #needed for ops working properly
    bl_region_type = "UI"
    
    def draw(self, context):
        layout = self.layout
        layout.operator( "obj.splitsymmetricshapekey")
        layout.operator( "obj.mirrorshapekeytopology")
        layout.operator( "obj.mergeshapekeys")
        layout.operator( "obj.zerokeyonselectedvertices")
        layout.operator( "obj.putmeshesinrow")
        #for import obj
        layout.label(text = "import all obj in directory")
        layout.prop(context.scene.importobj_prop, "importObjPath")
        layout.operator( "obj.importobj")
        ##


class importObjProperties(PropertyGroup):
    importObjPath = StringProperty(
        name = "Browse Directory",
        description = "Pick directory with .obj files",
        maxlen = 200,
        subtype = 'FILE_PATH'
    )

def register():
    bpy.utils.register_class(splitSymmetricShapeKeyOperator)
    bpy.utils.register_class(mirrorShapekeyTopologyOperator)
    bpy.utils.register_class(mergeShapekeysOperator)
    bpy.utils.register_class(zeroKeyOnSelectedVerticesOperator)
    bpy.utils.register_class(putMeshesInRowOperator)
    bpy.utils.register_class(importObjOperator)
    bpy.utils.register_class(naShapekeyUtilPanel)
    
    bpy.utils.register_class(importObjProperties)
    bpy.types.Scene.importobj_prop = PointerProperty( type = importObjProperties )
    
def unregister():
    bpy.utils.unregister_class(splitSymmetricShapeKeyOperator)
    bpy.utils.unregister_class(mirrorShapekeyTopologyOperator)
    bpy.utils.unregister_class(mergeShapekeysOperator)
    bpy.utils.unregister_class(zeroKeyOnSelectedVerticesOperator)
    bpy.utils.unregister_class(putMeshesInRowOperator)
    bpy.utils.unregister_class(importObjOperator)
    bpy.utils.unregister_class(naShapekeyUtilPanel)
    
    bpy.utils.unregister_class(importObjProperties)
    del bpy.types.Scene.importobj_prop
    
if __name__ == "__main__":
    register()
####



def splitSymmetricShapeKey(obj = None, removeSourceShapeKey = False, context = None):
    """going from symmetric shape key to a .L and .R shapekey
    works in xz plane only.
    duplicate shapekey twice > on one shape key set -x side to basis vert position, on other set +x side to basis vert position
    option to remove source shapekey after split
    """
    
    def duplicateShapeKeyAtIndex(obj = None, sourceIndex = None, context = None):
        #returns index of created shapekey
        result = None
        obj.active_shape_key_index = sourceIndex
        obj.show_only_shape_key = True
        bpy.ops.object.shape_key_add(from_mix=True)
        result = context.object.active_shape_key_index
        return result


    #duplicate shape key twice
    #then zero out appropriate side of meshes
    sourceIndex = obj.active_shape_key_index
    sourceName = obj.data.shape_keys.key_blocks[sourceIndex].name
    leftIndex = duplicateShapeKeyAtIndex(obj,sourceIndex,context)
    rightIndex = duplicateShapeKeyAtIndex(obj,sourceIndex,context)
    
    obj.data.shape_keys.key_blocks[leftIndex].name = sourceName+'.L'
    obj.data.shape_keys.key_blocks[rightIndex].name = sourceName+'.R'
    
    context.object.active_shape_key_index = leftIndex
    zeroSelectedKeyInX(sign="-",includeCenter = False, context=context) #to avoid double transformation of center vertices

    bpy.context.object.active_shape_key_index = rightIndex
    zeroSelectedKeyInX(sign="+",includeCenter = True, context=context)
    
    #optionally delete source shape key
    if removeSourceShapeKey:
        obj.active_shape_key_index = sourceIndex
        bpy.ops.object.shape_key_remove(all=False)
        
        

    
    
def mirrorShapekeyTopology(obj=None, context = None):
    """for getting all shapekeys have mirrored topology
    
    first copy out all shapekeys to a new mesh > each mesh no shapekeys > each mesh symmetric topology
    second, remove all shapekeys on mesh we wish to mirror > make its topology mirrored
    third, apply all created meshes as shapekeys on mesh
    
    note doesnt preserve drivers on shapkeys.
    """
    dupObjs = [] #list of tuples obj, shapekey name
    def makeMeshUsingShapekeyIndex( obj=None, index = 1,context=None ):
        #result mesh no shapekeys. its shape would have matched shapekey at index
        dupMeshObj = None
        bpy.ops.object.select_all(action='DESELECT')
        obj.select = True
        context.scene.objects.active = obj
        
        bpy.ops.object.duplicate()
        dupMeshObj = context.selected_objects[-1]
        
        #first remove all shape keys on dupped object
        #then transfer just the given index onto dupped object
        #finally remove the basis shape and then last the only shapekey to get duped object at pose of index
        
        bpy.ops.object.shape_key_remove(all=True)
        obj.active_shape_key_index = index
        
        bpy.ops.object.select_all(action='DESELECT')
        obj.select = True
        dupMeshObj.select = True
        context.scene.objects.active = dupMeshObj
        bpy.ops.object.shape_key_transfer()

        bpy.ops.object.select_all(action='DESELECT')
        dupMeshObj.select = True
        context.scene.objects.active = dupMeshObj
        dupMeshObj.active_shape_key_index = 0
        bpy.ops.object.shape_key_remove(all=False)
        bpy.ops.object.shape_key_remove(all=True)
        
        return dupMeshObj


    def getShapekeyName( geoName = None, dupObjsL = [] ):
        #dependent on data format tuples 
        for arg in dupObjsL:
            if arg[0].name == geoName:
                return arg[1]
        
    if not obj.data.shape_keys:
        return
           
    countShapeKeys = len(obj.data.shape_keys.key_blocks)
    for i in range(1,countShapeKeys):  
        dupObj = makeMeshUsingShapekeyIndex(obj,i,context)
        #make dupped object symmetric
        deleteHalfMesh(dupObj)
        makeMeshWhole(dupObj)
        dupObjs.append( (dupObj, obj.data.shape_keys.key_blocks[i].name) )
    
    #done duping all objects
    #remove all shapekeys from source object
    #make it mirrored
    #note not preserving drivers on shapekeys
    bpy.ops.object.select_all(action='DESELECT')
    obj.select = True
    context.scene.objects.active = obj
    bpy.ops.object.shape_key_remove(all=True)
    deleteHalfMesh(obj)
    makeMeshWhole(obj)    
    #
    #apply all of the duped objects as shapekeys
    bpy.ops.object.select_all(action='DESELECT')
    for dupTuple in dupObjs:
        dObj = dupTuple[0]
        dObj.select = True
    obj.select = True
    context.scene.objects.active = obj    
    bpy.ops.object.join_shapes()
    
    #fix names of shapekeys to match original
    for j in range(1,len(obj.data.shape_keys.key_blocks)):
        kblock = obj.data.shape_keys.key_blocks[j]
        n = getShapekeyName( kblock.name, dupObjs )
        kblock.name = n
        
    #cleanup
    bpy.ops.object.select_all(action='DESELECT')    
    for dupTuple in dupObjs:
        dObj = dupTuple[0]
        dObj.select = True
    context.scene.objects.active = dupObjs[0][0]
    bpy.ops.object.delete()
    
    #restore selection
    obj.select = True
    context.scene.objects.active = obj

def importObj(shapeDir = ''):
    """import all .obj in shapeDir, use obj names for blender geo names
    """
    
    if not os.path.exists(shapeDir):
        return
    
    #find all objs in folder
    objFileNames = []
    toNames = [] #names to use for meshes in blender
    
    #if its a folder get all objs in it
    if os.path.isdir(shapeDir):
        for fpath in os.listdir(shapeDir):
            ff,fext = os.path.splitext(fpath)
            if fext.lower() == ".obj":
                objFileNames.append(fpath)
                toNames.append(ff)
    else:
        #import just the single obj path
        fpath = shapeDir
        fffull,fext = os.path.splitext(fpath)
        if fext.lower() == ".obj":
            fpathshort = os.path.split(fffull)[-1]
            objFileNames.append(fpath)
            toNames.append(fpathshort)
                
    #import objs into blender
    for f,toName in zip(objFileNames,toNames):
        fileName = os.path.join(shapeDir,f)
        importedObj = bpy.ops.import_scene.obj(filepath = fileName,
                                                use_split_objects = False,
                                                use_split_groups = False)
        obj = bpy.context.selected_objects[0]
        obj.name = toName
        obj.data.name = toName


def putMeshesInRow(meshObjs = [], gridWidth = 1):
    """
    #position given mesh objects in grid > nice to use bounding box
    #input how far apart want mesh
    """
    meshes = [m for m in meshObjs if m.type == 'MESH']
    
    xpos = 0
    for mesh in meshes:
        mesh.location = (xpos,0,0)
        xdim = mesh.dimensions[0]
        xpos += xdim+gridWidth #move a whole width of object plus given x offset
        
        
    
def mergeShapekeys(context = None):
    """
    1.given bunch of meshes with shape keys put all those shape keys on last selected object.
    last selection is target mesh
    """
    targetMesh = context.active_object
    sourceMeshes = [msh for msh in context.selected_objects if msh.name != targetMesh.name] 
    
    if len(sourceMeshes) == 0:
        print("please select 1 or more source meshes then last target mesh")
        return

    for sourceMesh in sourceMeshes:
        #selection
        bpy.ops.object.select_all(action='DESELECT')
        sourceMesh.select = True
        targetMesh.select = True
        context.scene.objects.active = targetMesh
        
        #changing active shapekey index to transfer shapes
        shapeKey = sourceMesh.data.shape_keys
        #if no shape key on source mesh skip it
        if not shapeKey:
            continue
        maxShapeKeyIndex = len(shapeKey.key_blocks) 
        for i in range(1,maxShapeKeyIndex):
            sourceMesh.active_shape_key_index = i
            bpy.ops.object.shape_key_transfer()
            targetMesh.show_only_shape_key = False
    
    #cleanup selection
    bpy.ops.object.select_all(action='DESELECT')
    targetMesh.select = True
    context.scene.objects.active = targetMesh
    
def zeroKeyOnSelectedVertices(context = None):
    """
    this zero out all selected vertices of shape key.
    """

    obj = context.active_object
    
    curMode = None
    if context.object:
        curMode = context.object.mode
        bpy.ops.object.mode_set(mode='OBJECT') #if something is selected go to object mode
    
    #assumes basis shape at index 0
    vertIndex = [v.index for v in obj.data.vertices if v.select]
    for i in vertIndex:
        basisx = obj.data.shape_keys.key_blocks[0].data[i].co.x
        basisy = obj.data.shape_keys.key_blocks[0].data[i].co.y
        basisz = obj.data.shape_keys.key_blocks[0].data[i].co.z
        #modify selected shape key
        activeIndex = obj.active_shape_key_index
        if activeIndex > 0:
            #zero out shape
            obj.data.shape_keys.key_blocks[activeIndex].data[i].co.x = basisx
            obj.data.shape_keys.key_blocks[activeIndex].data[i].co.y = basisy
            obj.data.shape_keys.key_blocks[activeIndex].data[i].co.z = basisz

    #restore mode
    if curMode:
        bpy.ops.object.mode_set(mode=curMode)





def zeroSelectedKeyInX(sign="+", includeCenter = False, context = None):
    """
    this zero out all vertices of shape key that are in direction of x axis.
    supports positive or negative x axis
    optionally zero out center vertices
    """
    def zeroShape(obj=None,vid=None):
        #zero shape on given vertex id
        basisx = obj.data.shape_keys.key_blocks[0].data[vid].co.x
        basisy = obj.data.shape_keys.key_blocks[0].data[vid].co.y
        basisz = obj.data.shape_keys.key_blocks[0].data[vid].co.z        
        activeIndex = obj.active_shape_key_index
        if activeIndex > 0:
            #zero out shape
            obj.data.shape_keys.key_blocks[activeIndex].data[vid].co.x = basisx
            obj.data.shape_keys.key_blocks[activeIndex].data[vid].co.y = basisy
            obj.data.shape_keys.key_blocks[activeIndex].data[vid].co.z = basisz


    obj = context.active_object
    
    #assumes basis shape at index 0
    verts = obj.data.vertices

    if sign == "+":
        for i in range(len(verts)):
            basisx = obj.data.shape_keys.key_blocks[0].data[i].co.x
            if includeCenter:
                if basisx >= 0:
                    zeroShape(obj,i)
            else:
                if basisx > 0:
                    zeroShape(obj,i)            
    else:
        for i in range(len(verts)):
            basisx = obj.data.shape_keys.key_blocks[0].data[i].co.x
            if includeCenter:
                if basisx <= 0:
                    zeroShape(obj,i)
            else:
                if basisx < 0:
                    zeroShape(obj,i)


def zeroAllKeys():
    """
    when first testing shapekeys. this zeros them all out again
    """
    obj = bpy.context.active_object
    allKeys = obj.data.shape_keys.key_blocks.keys()
    for key in allKeys:
        obj.data.shape_keys.key_blocks[key].value = 0



def removeDigitShapeKeys():
    """noticed in fbx import from zbrush extra shape keys with digits at end
    this removes those shape keys
    """
    obj = bpy.context.active_object
    
    allKeys = obj.data.shape_keys.key_blocks.keys()
    keysEndDigit = [x for x in allKeys if x[-1].isdigit()]
    #print(keysEndDigit)
    
    for shape in keysEndDigit:
        index = obj.data.shape_keys.key_blocks.keys().index(shape)
        obj.active_shape_key_index = index
        bpy.ops.object.shape_key_remove()


def makeMeshWhole(obj):
    """default mirror from +x to -x of selected mesh, assumes no mirror modifiers on mesh to start
    """
    bpy.ops.object.modifier_add(type='MIRROR')
    bpy.ops.object.modifier_apply(modifier='Mirror') 
    
    
def deleteHalfMesh(obj):
    """
    default deletes -x side of selected mesh. standalone need give it bpy.context.object for selected object
    """
    #get current position
    curpos = ()
    curpos = (obj.location.x,obj.location.y,obj.location.z)
    
    #put obj at origin
    setLocation(obj,(0,0,0))
    
    selectedObj = obj #bpy.context.selected_objects[0]
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all( action='DESELECT')
    bpy.ops.mesh.select_mode(type='FACE')
    
    
    bm = bmesh.from_edit_mesh(selectedObj.data)
    for face in bm.faces:
        faceWorldPos = selectedObj.matrix_world*face.calc_center_median() #calc_center_median same as face.center using obj.data.polygons
        #[0] > 0.0 would be delete right half of mesh
        #[1] < 0.0 would be delete in y direction
        if faceWorldPos[0] < 0.0:
            face.select = True
            
    bm.select_flush(True)
    bpy.ops.mesh.delete(type='FACE')
    bpy.ops.object.mode_set(mode='OBJECT')
            
    #restore location
    setLocation(obj,curpos)
    
def setLocation(obj,pos):
    """ does location only. pos tuple (2.3,0,0) """
    obj.location.x = pos[0]
    obj.location.y = pos[1]
    obj.location.z = pos[2]



"""TODO 
-going from a .L shapekey to a .R shapekey
try to use blenders mirror shapekey ops
-simple animation of shapekeys
"""



"""
import bpy
import sys
sys.path.append("/users/Nathaniel/Documents/src_blender/python/naBlendShape")
import naShapekeyUtilAddOn as mod
import imp
imp.reload(mod)

#mod.importObj('/Users/Nathaniel/Documents/src_blender/python/snippets/pipeTools')
"""


"""for making a control
made two bone armature. parented one bone to other.
created cube,circle shapes. in pose mode added shapes to bones.
translating parent bone moves child without changing its local transforms so they
can be used for driving blendshape.
(changed roll bone 180 so up was positive down negative)
"""

#inspired by
#https://blender.stackexchange.com/questions/1412/efficient-way-to-get-selected-vertices-via-python-without-iterating-over-the-en
#https://blender.stackexchange.com/questions/111661/creating-shape-keys-using-python
#https://blenderartists.org/t/delete-shape-key-by-name-via-python/521762/3
#https://stackoverflow.com/questions/14471177/python-check-if-the-last-characters-in-a-string-are-numbers
#https://stackoverflow.com/questions/3964681/find-all-files-in-a-directory-with-extension-txt-in-python
#https://stackoverflow.com/questions/541390/extracting-extension-from-filename-in-python
#https://stackoverflow.com/questions/8933237/how-to-find-if-directory-exists-in-python
#https://blender.stackexchange.com/questions/18035/code-inside-function-not-working-as-it-should
#https://blender.stackexchange.com/questions/43820/how-to-use-the-file-browsers-with-importhelper-execute-function
#https://blender.stackexchange.com/questions/42654/ui-how-to-add-a-file-browser-to-a-panel
#https://blender.stackexchange.com/questions/23258/trouble-file-stringproperty-subtype-file-path