Monday, September 4, 2023

import export of armature python class (tested Blender 2.79)

only handles bones and bone parenting. (tested in Blender 2.79 please modify use at your own risk)
import bpy
import os
import json
class ArmatureIO(object):
    """class to handle importing and exporting of an armature. only handles bones and bone parenting
    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')
    
    from naArmatureIOAddOn import ArmatureIO

    #usage:
    #export
    armatIO = ArmatureIO()
    armatIO.exportArmature(armature="Armature", export_dir = '/Users/Nathaniel/Documents/src_blender/python/snippets/tmp', file_name="bones.json")
    
    #import
    armatIO = ArmatureIO()
    armatIO.importArmature(file_path="/Users/Nathaniel/Documents/src_blender/python/snippets/tmp/bones.json")
    
    """
    def __init__(self):
        pass

    def exportArmature(self, armature = None, export_dir = '/Users/Nathaniel/Documents/src_blender/python/snippets/tmp', bones = [], file_name = ''):
        """
        @param armature - name for armature the data object name
        @param export_dir - directory in which to save armature json. if this is a full path to a file name it uses this instead of the file_name argument
        @param bones - optional list of bones to export. if empty it exports all bones in armature
        @param file_name - optional name for json file to save such as Armature.json. if it is not provided it uses the armature name
        """
        if not os.path.isdir(export_dir) and not os.path.isfile(export_dir) :
            print("could not find export path %s. enter one that exists" %export_dir)
            return
            
        if not armature in bpy.data.objects:
            print("requires an armature name. the data object name to exist")
            return
        
        dat_dict = {} #what we want to export
        
        #need to have armature selected
        self._selectOnlyThing(thing = armature)
        
        #compiling the data for armature
        bpy.ops.object.mode_set(mode='EDIT')
        bone_names = [ eb.name for eb in bpy.data.objects[armature].data.edit_bones] #default to all bones in armature
        if bones:
            bone_names = bones
        
        for bone in bone_names:
            edit_bone_data = {}
            pose_bone_data = {}
            
            #edit bone data
            #need to be in edit mode for getting parent - assumes armature is selected
            bpy.ops.object.mode_set(mode='EDIT')
            eb = bpy.data.objects[armature].data.edit_bones[bone]
            
            #adding edit bone attributes here
            edit_bone_data['head'] = tuple( eb.head )#(0,3,5)
            edit_bone_data['tail'] = tuple( eb.tail )
            edit_bone_data['roll'] = eb.roll #in radians
            edit_bone_data['parent'] = eb.parent.name if eb.parent else ''
            ##
            
            #pose bone data
            bpy.ops.object.mode_set(mode='POSE')
            pb = bpy.data.objects[armature].pose.bones[bone]
            
            #adding pose bone attributes here
            pose_bone_data['location'] = tuple( pb.location ) #(4,5,6)
            pose_bone_data['scale'] = tuple( pb.scale )
            pose_bone_data['rotation_mode'] = pb.rotation_mode
            pose_bone_data['rotation'] = tuple( pb.rotation_euler )
            if pb.rotation_mode == 'QUATERNION':
                pose_bone_data['rotation'] = tuple( pb.rotation_quaternion )
            pose_bone_data['custom_shape'] = pb.custom_shape.name if pb.custom_shape else '' #animator curve for bone - remember on import data bones should have show_wire to True if using curve shape for bone
            pose_bone_data['custom_shape_scale'] = pb.custom_shape_scale
            ##
            
            dat_dict[bone] = {'edit_bone_data':edit_bone_data, 'pose_bone_data':pose_bone_data}
        
        #exporting armature to json
        ##
        #if directory provided is a full path to a file name use it
        export_fullpath = ''
        if os.path.isfile(export_dir):
            export_fullpath = export_dir
        else:
            #use the file_name if it exists
            export_file_name = ''
            if file_name:
                export_file_name = file_name
            else:
                #use the armature name to figure out file name
                armature_edit = armature.replace(' ','_')    
                export_file_name = armature_edit+'.json'
                
            export_fullpath = os.path.join(export_dir,export_file_name)
    
        ##
        outDir = os.path.dirname(export_fullpath)
        if not os.path.exists(outDir):
            print('Requires an out directory that exists to write armature file %s' %outDir)
            return
        ##
        
        ##adding armature name
        output_dict = {}
        output_dict["armature"] = armature
        output_dict["bones"] = dat_dict
        ##
        
        print("exporting >>> %s to file name: %s" %(output_dict,export_fullpath) )
        
        with open(export_fullpath,"w") as outf:
            json.dump(output_dict,outf, indent=4)
        
    def _importMakeArmature(self, armatureName):
        """
        @param armatureName - str data object name for armature
        """
        if not armatureName:
            print("_importMakeArmature requires an armature name")
            return
            
        if armatureName in bpy.data.objects:
            return
        #make the armature
        arm_dat = bpy.data.armatures.new(armatureName)
        arm_obj = bpy.data.objects.new(armatureName, arm_dat)
        arm_obj.data = arm_dat
        scene = bpy.context.scene
        scene.objects.link(arm_obj) #specific to blender 2.79
        
    
    def importArmature(self, file_path=''):
        """
        @param file_path - str file path to import
        """
        if not os.path.exists(file_path):
            print("could not find %s skipping" %file_path)
            return
    
        info_dict = {}
        with open(file_path) as f:
            info_dict = json.load(f)
        print("read armature info>>",info_dict)
        
        #check if armature exists if it doesnt make an empty one
        armature = info_dict.get("armature")
        if not armature:
            print("using default armature name")
            armature = "na_default_armature" #temp for backwards compatibility
            
        if not armature in bpy.data.objects:
            print("making armature because it doesnt exist")
            self._importMakeArmature(armature)
        
        self.importArmatureFromDict( armature = armature, data_dict = info_dict.get("bones") )
    
    
    def importArmatureFromDict( self, armature = None, data_dict = None, use_bones = []):
        """
        @param armature - armature name - the data object name
        @param data_dict - see export for the format it is a dictionary with edit bone and pose bone information
        @param use_bones - when specified it limits import to only provide bone names
        """
        if not data_dict:
            print("requires data dictionary with bone information")
            return
    
        if not armature in bpy.data.objects:
            print("couldnt find armature - so making one")
            self._importMakeArmature(armature)
            
            
        dat_dict = {}
        dat_dict = data_dict
        print("using armature info>>",dat_dict)
        
    
        #ensure in edit mode of armature
        bpy.context.scene.objects.active = bpy.data.objects[armature]
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
            
        for bone, dat in dat_dict.items():
            
            #only import specified bones in input parameter
            if use_bones:
                if bone not in use_bones:
                    continue
            
            print(bone)
            edit_bone_data = dat.get('edit_bone_data') or None #assuming all keys exist
            head = edit_bone_data.get('head') or None
            tail = edit_bone_data.get('tail') or None
            roll = edit_bone_data.get('roll') or 0.0
            
            pose_bone_data = dat.get('pose_bone_data') or None
            location = pose_bone_data.get('location') or None
            scale = pose_bone_data.get('scale') or None
            rotation_mode = pose_bone_data.get('rotation_mode') or None
            rotation = pose_bone_data.get('rotation') or None        
            custom_shape = pose_bone_data.get('custom_shape') or '' 
            custom_shape_scale = pose_bone_data.get('custom_shape_scale') or 1.0
            
            bpy.ops.object.mode_set(mode='EDIT', toggle=False)
            bone_obj = None
            #if it exists already use it.
            if bone in bpy.data.objects[armature].data.edit_bones:
                bone_obj = bpy.data.objects[armature].data.edit_bones[bone]
            if not bone_obj:
                #make edit bone from scratch
                bone_obj = bpy.data.objects[armature].data.edit_bones.new(bone)
                
            #position edit bone
            bone_obj.head = head
            bone_obj.tail = tail
            bone_obj.roll = roll
            
            #position pose bone
            bpy.ops.object.mode_set(mode='POSE')
            pb = bpy.data.objects[armature].pose.bones[bone]
            pb.location = location
            pb.scale = scale
            pb.rotation_mode = rotation_mode
            if rotation_mode != 'QUATERNION':
                pb.rotation_euler = rotation
            else:
                pb.rotation_quaternion = rotation
            #if custom shape doesnt exist dont try to add it to bone
            if custom_shape in bpy.data.objects:
                pb.custom_shape = bpy.data.objects[custom_shape]
                pb.custom_shape_scale = custom_shape_scale
                bpy.data.objects[armature].data.bones[bone].show_wire = True #show wire
    
    
        #do the bone parenting at end so have all bones created
        #ensure in edit mode of armature
        bpy.context.scene.objects.active = bpy.data.objects[armature]
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
        
        for bone, dat in dat_dict.items():
            #only import specified bones in input parameter
            #if use_bones:
            #    if bone not in use_bones:
            #        continue
            
            #allowing any existing bone to be considered for parenting to support successive build workflow. like built jaw and head then later neck.
            if not bone in bpy.data.objects[armature].data.edit_bones:
                continue
                
            bone_obj = bpy.data.objects[armature].data.edit_bones[bone]
            edit_bone_data = dat.get('edit_bone_data') or None #assuming all keys exist
            parent = edit_bone_data.get('parent') or None 
            
            #skip parenting to parent if cannot find it in scene
            if not parent:
                continue
                
            if not parent in bpy.data.objects[armature].data.edit_bones:
                continue
                
            if parent:
                bone_obj.parent = bpy.data.objects[armature].data.edit_bones[parent]     
        ####


    def _selectOnlyThing(self, thing = None):
        #might need to be in object mode
        bpy.ops.object.mode_set(mode="OBJECT")
        if thing:
            thing_obj = bpy.data.objects.get(thing)
            if not thing_obj:
                print("coulndt find {0} to select".format(thing))
                return
            #make it only selection
            bpy.ops.object.select_all(action='DESELECT')
            thing_obj.select = True
            bpy.context.scene.objects.active = thing_obj 
Happy Scripting!