Tuesday, April 27, 2021

doodle shapekey import export in blender

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