Hi,
i'm still working on this doodle but thought it may be useful to share as it has a little bit on scripting with shapekeys in blender. doodle was using blender 2.79. i tried to comment the doodle a bit to explain some of my thinking for the script. There are just two main functions an import and an export (the export does write shapekey data to disc). hope you find some of this helpful in learning python.
on the side i found it helpful just playing with blender to learn more about it. reading the info panel to see what operator called when do stuff in blender. and using python console to learn more about the data objects. i found it helpful jotting notes for the various areas in blender example for bones or shapekeys and jotting down a comment for what simple one liners from python console do. this is helpful for going back to when trying to write a simple script.
#
#tool doodle for exporting and importing shapekeys. currently only supporting meshes. tested in blender 2.79
#
#example
#exportShapeKey( mesh = 'Plane', shapes = ['Key 1'], dirPath = '/Users/Nathaniel/Documents/src_blender/python/naBlendShape/tmp' )
#importShapeKey( mesh = 'Plane', shapeFiles = ['shape_Key_1.json'], dirPath = '/Users/Nathaniel/Documents/src_blender/python/naBlendShape/tmp') #it should create the shape key if it doesnt exist
#
#modify use at your own risk
#
#last modified
#042721 -- working on initial release - worked on initial import
#042621 -- working on initial release - worked on initial export
"""form of shape key data written out (vertDelta is in order of all vertices)
{
'shape':{ 'shapeName':'Key 1', 'vertDelta':[(0,0,0),(0,.25,.35)...(0,0,0)]
}
}
#i think exporting delta instead of absolute position of shape verts will allow
#edit of basis shape after exporting shapes and have imported shapes use the newly sculpted basis in them
"""
import bpy
from mathutils import Vector
import json
import os
def exportShapeKey( mesh = '', shapes = [], dirPath = '' ):
"""mesh is the data.objects name, it can be different from data.objects.data name
shapes are the actual shapekey names wish to export
note it saves file names of form shape_shapeName where it replaces spaces with underscores
so exporting a 'Key 1' shape with an already saved shape_Key_1.json would overwrite that shape
dirPath is directory to save the shape json files
"""
if not os.path.exists(dirPath):
print('Requires an out directory that exists to write shapekey file')
return
if not mesh in bpy.data.objects:
print('Requires mesh: %s to exist in scene' %mesh)
return
if not bpy.data.objects[mesh].type == 'MESH':
print('Requires mesh type for %s' %mesh)
return
#require mesh to have shape keys
mesh_obj = bpy.data.objects[mesh].data
if mesh_obj.shape_keys is None:
print('requires mesh %s to have shape keys' %mesh)
return
#exit if no Basis shape key
basis_name = 'Basis'
if not basis_name in mesh_obj.shape_keys.key_blocks.keys():
print('requires a basis shape named Basis on mesh %s exiting' %mesh)
return
if not shapes:
print('Requires some shape names to export')
#start looping over shape keys to export
for shp in shapes:
shape_dict = {}
num_verts = 0
#check shp shapekey exists on mesh
if not shp in mesh_obj.shape_keys.key_blocks.keys():
print('could not find shapekey %s in mesh %s so skipping it' %(shp,mesh) )
continue
#todo: verify only upper and lower case letters and spaces and dots in shape key name
num_verts = len( mesh_obj.shape_keys.key_blocks[shp].data )
vert_delta_list = []
for i in range(num_verts):
vdelta = mesh_obj.shape_keys.key_blocks[basis_name].data[i].co - mesh_obj.shape_keys.key_blocks[shp].data[i].co
vert_delta_list.append( (vdelta.x, vdelta.y, vdelta.z) )
shape_dict['shape']={'shapeName':shp,'vertDelta':vert_delta_list}
#export this shapekey and continue to next
shp_filename = 'shape_'+shp.replace(' ','_')+'.json' #can tweak this to something different
shp_file = os.path.join(dirPath,shp_filename)
with open( shp_file, 'w' ) as f:
f.write( json.dumps(shape_dict) )
def importShapeKey( mesh = '', shapeFiles = [], dirPath = '' ):
"""look in directory dirPath for shape files. a shape file has vertex deltas from basis for a single shape key.
it adds .json to file names in shapeFiles if it isnt provided.
mesh is mesh data.objects name it needs to have at least a Basis shape.
dirPath is where to look for shapekey json files
"""
def _isShapeExists( mesh, shapeName ):
mesh_obj = bpy.data.objects[mesh].data
return shapeName in mesh_obj.shape_keys.key_blocks
def _sculptShape( mesh, shapeName, vertDelta ):
#sculpt shape using delta from basis
mesh_obj = bpy.data.objects[mesh].data
##go through each vertex using the basis and the delta stored on disc
for vid in range(num_mesh_verts):
basis_pos = mesh_obj.shape_keys.key_blocks['Basis'].data[vid].co
shp_pos = Vector( vertDelta[vid] ) #Vector so can subtract it
#move vertices to sculpt shape
mesh_obj.shape_keys.key_blocks[shapeName].data[vid].co = basis_pos - shp_pos
##check inputs
#assert dirPath exists
#assert mesh exists
#assert shapeFiles provided
#assert mesh has a shapekey
#check Basis shape exists on mesh
if not os.path.exists(dirPath):
print('Requires an out directory that exists to write shapekey file')
return
if not mesh in bpy.data.objects:
print('Requires mesh: %s to exist in scene' %mesh)
return
if not shapeFiles:
print('Requires shape file names to import')
return
mesh_obj = bpy.data.objects[mesh].data #bpy.data.meshes[mesh]
mesh_name = mesh_obj.name
if mesh_obj.shape_keys is None:
print("Requires at least a Basis shape on mesh %s" %mesh)
return
#exit if no Basis shape key on mesh
basis_name = 'Basis'
if not basis_name in mesh_obj.shape_keys.key_blocks.keys():
print('requires a basis shape named Basis on mesh %s exiting' %mesh)
return
#need to be in object mode to edit shape keys
bpy.ops.object.mode_set(mode="OBJECT")
#loop over shape files
for shp_file_name in shapeFiles:
shp_fname = shp_file_name+'.json' if not shp_file_name.endswith('.json') else shp_file_name
shp_file = os.path.join( dirPath, shp_fname )
if not os.path.exists(shp_file):
print('could not find shape file %s , skipping' %shp_file)
continue
#read shape from disc
shp_dict = {}
with open( shp_file, 'r' ) as f:
shp_dict = json.load(f)
shapeName = shp_dict['shape']['shapeName']
vertDelta = shp_dict['shape']['vertDelta']
#skip if saved shape has different topology as mesh
num_mesh_verts = len( mesh_obj.shape_keys.key_blocks['Basis'].data )
num_shp_file_verts = len(vertDelta)
if num_mesh_verts != num_shp_file_verts:
print('Requires shape stored on file %s to have same topology as mesh %s, skipping' %(shp_file,mesh) )
continue
if _isShapeExists( mesh, shapeName ):
#Edit existing shape
_sculptShape( mesh, shapeName, vertDelta )
else:
#Create a new shape
new_shp = bpy.data.objects[mesh].shape_key_add(shapeName)
_sculptShape( mesh, shapeName, vertDelta )
new_shp.interpolation = 'KEY_LINEAR'
"""some examples of usage of some of the methods when not using ui
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/naBlendShape')
import naShapekeyIO as mod
import imp
imp.reload(mod)
#bpy.app.debug=True #temporary
#mod.exportShapeKey( mesh = 'Plane', shapes = ['Key 1'], dirPath = '/Users/Nathaniel/Documents/src_blender/python/naBlendShape/tmp' )
#mod.importShapeKey( mesh = 'Plane', shapeFiles = ['shape_Key_1.json'], dirPath = '/Users/Nathaniel/Documents/src_blender/python/naBlendShape/tmp') #it should create the shape key if it doesnt exist
#if it does exist should overwrite only that shape key. not saving mesh in shape paths in case want to import onto a different named mesh
#with same topology
"""Happy Sketching!
Nate