Created
May 6, 2020 04:04
-
-
Save nathanielanozie/c7af2f341865e46cd2f69c122d3b16ed to your computer and use it in GitHub Desktop.
some shapekey tools for blender 2.79 addon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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