Skip to content

Instantly share code, notes, and snippets.

@nathanielanozie
Created May 6, 2020 04:04
Show Gist options
  • Save nathanielanozie/c7af2f341865e46cd2f69c122d3b16ed to your computer and use it in GitHub Desktop.
Save nathanielanozie/c7af2f341865e46cd2f69c122d3b16ed to your computer and use it in GitHub Desktop.
some shapekey tools for blender 2.79 addon
#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment