Saturday, October 19, 2019

naPaintBlendshapeAddOn.py for Blender


naPaintBlendshapeAddOn for Blender from Nathan Anozie on Vimeo.

Hi,
This is a tool to help modify shapekeys/blendshapes using painted weights. example used to paint out eyelids from brow down shape.
it works by first selecting blendshape then the default base mesh.
modify use at your own risk.

Happy Scripting! Sketching!

to install naPaintBlendshape.py needs to be in addon directory, and naPaintBlendshapeAddOn.py needs to be installed in blender via user preferences.


#naPaintBlendshape.py
#this is a tool to help modify shapekeys using painted weights. example used to paint out eyelids from brow down shape key
"""need imp to reload script
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/snippets/blendshapes')

import naPaintBlendshape as mod
import imp
imp.reload(mod)
bpy.app.debug=True #temporary
#first version for addon select shape then default mesh
mod.paintBSWeights(bpy.context)
mod.doIt(bpy.context)
mod.cleanUp(bpy.context)
"""

import bpy
import re

def doIt(context = None):
    """
    sculpt new blendshape using painted weights
    assumes painted mesh is selected
    it assumes a painted mesh is available with same name as shape mesh with _naPaint suffix
    
    """
    if not context.active_object:
        print("requires paint mesh selected")
        return
        
    paintMeshName = context.active_object.name
    
    if not bpy.data.objects.get(paintMeshName):
        print("cannot find paint mesh %s. first click paint weight button" %paintMeshName)
        return

    pattern = re.compile(".*naPnt_(.*)_naPnt_(.*)")
    if not pattern.match(paintMeshName):
        print("cannot find default and blendshape from painted mesh name")
        return
    patternGroups = pattern.match(paintMeshName).groups()
    if not patternGroups or len(patternGroups) != 2:
        print("cannot find default and blendshape from painted mesh name")
        return
        
    defaultMeshObj = bpy.data.objects[patternGroups[0]]
    bsMeshObj = bpy.data.objects[patternGroups[1]]
    
    print("using defaultMesh:%s reference shape:%s painted weights:%s" %(defaultMeshObj.name,bsMeshObj.name,paintMeshName) )

    weightsObj = bpy.data.objects[paintMeshName]
    newBS = weightsObj #maybe in future make another mesh for newBS
    wObj = weightsObj
    
    #get new position for vertex
    #set vertex position on newBS
    for i in range(len(wObj.data.vertices)):
        wt = wObj.data.vertices[i].groups[0].weight #assuming one vertex group on mesh
        posx = wt*bsMeshObj.data.vertices[i].co[0]+(1-wt)*defaultMeshObj.data.vertices[i].co[0]
        posy = wt*bsMeshObj.data.vertices[i].co[1]+(1-wt)*defaultMeshObj.data.vertices[i].co[1]
        posz = wt*bsMeshObj.data.vertices[i].co[2]+(1-wt)*defaultMeshObj.data.vertices[i].co[2]
        
        newBS.data.vertices[i].co[0] = posx
        newBS.data.vertices[i].co[1] = posy
        newBS.data.vertices[i].co[2] = posz

def paintBSWeights(context = None):
    """
    returns mesh to paint weights for blendshape
    assumes first selected is shape
    second selected is default mesh
    """
    #get selection
    selected = context.selected_objects
    if not len(selected) == 2:
        print("please select shape then default mesh")
        return
    defaultMeshObj = context.active_object
    selected.remove(defaultMeshObj)
    bsMeshObj = selected[0]
    ###    
    
    paintMeshName = 'naPnt_%s_naPnt_%s'%(defaultMeshObj.name,bsMeshObj.name)
    if bpy.data.objects.get(paintMeshName):
        print("painted mesh for selection already exists doing nothing")
        return
        
    weightsObj = duplicateMesh( context, defaultMeshObj)
    #rename it so we can keep track whether it was made already
    weightsObj.name = paintMeshName
    weightsObj.data.name = paintMeshName
    
    #bboxy = defaultMeshObj.dimensions.y
    #weightsObj.location = [0,-(bboxy+0.25*bboxy),0] #using y, and using bounding box to place new bs
    makeObjActive(context,weightsObj)
    
    #create vertex group for weights
    vgrp = weightsObj.vertex_groups.new(name='naPnt')
    verts = []
    for vert in weightsObj.data.vertices:
        verts.append(vert.index)
    vgrp.add(verts,0.0,'ADD') #by default non of blendshape used

    bpy.ops.paint.weight_paint_toggle()
    
    return weightsObj

def cleanUp(context = None):
    """
    removes vertex group created, cleans up name
    assumes painted mesh is selected
    """
    if not context.active_object:
        print("requires paint mesh selected")
        return
        
    obj = context.active_object

    removeVertexGroupsFromObj(obj)
    #bpy.ops.paint.weight_paint_toggle() #get out of paint mode
    #rename mesh
    simpleName = obj.name.split('naPnt_')[-1]+'_New'
    obj.name = simpleName
    obj.data.name = simpleName
    
def duplicateMesh(context, sourceObj=None):
    """
    return new mesh (ex: bpy.data.objects['Plane.001']) given sourceObj ex:bpy.data.objects['Plane'] and context bpy.context
    """
    if not sourceObj:
        return
    scn = context.scene
    srcObj = sourceObj #bpy.data.objects['Plane'] #change this
    
    newObj = srcObj.copy()
    newObj.data = srcObj.data.copy()
    newObj.animation_data_clear()
    scn.objects.link(newObj)
    
    return newObj
    
def makeObjActive(context,obj):
    bpy.ops.object.select_all(action='DESELECT')
    obj.select=True
    context.scene.objects.active = obj
    bpy.ops.object.mode_set(mode='OBJECT')
    
def removeVertexGroupsFromObj(obj):
    for vgroup in obj.vertex_groups:
        obj.vertex_groups.remove(vgroup)
    
#inspired by:
#https://blenderartists.org/t/batch-delete-vertex-groups-script/449881/7
#https://blender.stackexchange.com/questions/7412/how-to-rename-selected-objects-using-python
#https://blenderartists.org/t/how-to-test-if-an-object-exists/566990
#https://blender.stackexchange.com/questions/8459/get-blender-x-y-z-and-bounding-box-with-script
#https://blender.stackexchange.com/questions/109700/how-to-clean-weight-groups-by-script
#https://blender.stackexchange.com/questions/26852/python-move-object-on-local-axis
#https://blender.stackexchange.com/questions/45099/duplicating-a-mesh-object



#naPaintBlendshapeAddOn.py
#modify use at your own risk
bl_info = {
    "name":"tool to help modify shapekeys using painted weights",
    "category": "Object"
}

import bpy

from bpy.types import(
    Operator,
    Panel
    )

#assumes file lives in addons folder
import naPaintBlendshape
import imp
imp.reload(naPaintBlendshape)

class paintBSWeightsOperator(Operator):
    bl_idname = "obj.paintbsweights" #needs to be all lowercase
    bl_label = "1.paintBSWeights"
    bl_options = {"REGISTER"}

    def execute(self, context):
        naPaintBlendshape.paintBSWeights(context)
        return {'FINISHED'}
        
class doItOperator(Operator):
    bl_idname = "obj.doit"
    bl_label = "2.doIt"
    bl_options = {"REGISTER"}

    def execute(self, context):
        #self.report({'INFO'}, "context.selected_objects: %s" %context.selected_objects )
        naPaintBlendshape.doIt(context)
        return {'FINISHED'}
        
class cleanUpOperator(Operator):
    bl_idname = "obj.cleanup"
    bl_label = "3.cleanUp"
    bl_options = {"REGISTER"}

    def execute(self, context):
        naPaintBlendshape.cleanUp(context)
        return {'FINISHED'}
        
        
class naPaintBlendshapePanel(Panel):
    bl_label = "naPaintBlendshape Panel"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    
    def draw(self, context):
        layout = self.layout
        #layout.label(text = "Modeling tool")
        layout.operator( "obj.paintbsweights")
        layout.operator( "obj.doit")
        layout.operator( "obj.cleanup")
        
def register():
    bpy.utils.register_class(paintBSWeightsOperator)
    bpy.utils.register_class(doItOperator)
    bpy.utils.register_class(cleanUpOperator)
    bpy.utils.register_class(naPaintBlendshapePanel)
    
def unregister():
    bpy.utils.unregister_class(paintBSWeightsOperator)
    bpy.utils.unregister_class(doItOperator)
    bpy.utils.unregister_class(cleanUpOperator)
    bpy.utils.unregister_class(naPaintBlendshapePanel)

if __name__ == "__main__":
    register()