Skip to content
Snippets Groups Projects
export_fbx.py 114 KiB
Newer Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

# Script copyright (C) Campbell Barton

"""
This script is an exporter to the FBX file format.

http://wiki.blender.org/index.php/Scripts/Manual/Export/autodesk_fbx
"""

import os
import time
import math  # math.pi
import shutil  # for file copying

import bpy
from mathutils import Vector, Euler, Matrix

# XXX not used anymore, images are copied one at a time
def copy_images(dest_dir, textures):
    import shutil
    if not dest_dir.endswith(os.sep):
        dest_dir += os.sep

    image_paths = set()
    for tex in textures:
        image_paths.add(bpy.path.abspath(tex.filepath))

    # Now copy images
    copyCount = 0
    for image_path in image_paths:
        if Blender.sys.exists(image_path):
            # Make a name for the target path.
            dest_image_path = dest_dir + image_path.split('\\')[-1].split('/')[-1]
            if not Blender.sys.exists(dest_image_path):  # Image isnt already there
                print("\tCopying %r > %r" % (image_path, dest_image_path))
                try:
                    shutil.copy(image_path, dest_image_path)
                except:
                    print("\t\tWarning, file failed to copy, skipping.")

    print('\tCopied %d images' % copyCount)

# I guess FBX uses degrees instead of radians (Arystan).
# Call this function just before writing to FBX.
# 180 / math.pi == 57.295779513
def tuple_rad_to_deg(eul):
    return eul[0] * 57.295779513, eul[1] * 57.295779513, eul[2] * 57.295779513

# def strip_path(p):
# 	return p.split('\\')[-1].split('/')[-1]

# Used to add the scene name into the filepath without using odd chars
sane_name_mapping_ob = {}
sane_name_mapping_mat = {}
sane_name_mapping_tex = {}
sane_name_mapping_take = {}
sane_name_mapping_group = {}

# Make sure reserved names are not used
sane_name_mapping_ob['Scene'] = 'Scene_'
sane_name_mapping_ob['blend_root'] = 'blend_root_'

def increment_string(t):
    name = t
    num = ''
    while name and name[-1].isdigit():
        num = name[-1] + num
        name = name[:-1]
    if num:
        return '%s%d' % (name, int(num) + 1)
    else:
        return name + '_0'


# todo - Disallow the name 'Scene' and 'blend_root' - it will bugger things up.
def sane_name(data, dct):
    #if not data: return None

    if type(data) == tuple:  # materials are paired up with images
        data, other = data
        use_other = True
    else:
        other = None
        use_other = False

    name = data.name if data else None
    orig_name = name

    if other:
        orig_name_other = other.name
        name = '%s #%s' % (name, orig_name_other)
    else:
        orig_name_other = None

    # dont cache, only ever call once for each data type now,
    # so as to avoid namespace collision between types - like with objects <-> bones
    #try:		return dct[name]
    #except:		pass

    if not name:
        name = 'unnamed'  # blank string, ASKING FOR TROUBLE!
        name = bpy.path.clean_name(name)  # use our own
    while name in iter(dct.values()):
        name = increment_string(name)
    if use_other:  # even if other is None - orig_name_other will be a string or None
        dct[orig_name, orig_name_other] = name
    else:
        dct[orig_name] = name

    return name


def sane_obname(data):
    return sane_name(data, sane_name_mapping_ob)


def sane_matname(data):
    return sane_name(data, sane_name_mapping_mat)


def sane_texname(data):
    return sane_name(data, sane_name_mapping_tex)


def sane_takename(data):
    return sane_name(data, sane_name_mapping_take)


def sane_groupname(data):
    return sane_name(data, sane_name_mapping_group)

# def derived_paths(fname_orig, basepath, FORCE_CWD=False):
# 	'''
# 	fname_orig - blender path, can be relative
# 	basepath - fname_rel will be relative to this
# 	FORCE_CWD - dont use the basepath, just add a ./ to the filepath.
# 		use when we know the file will be in the basepath.
# 	'''
# 	fname = bpy.path.abspath(fname_orig)
# # 	fname = Blender.sys.expandpath(fname_orig)
# 	fname_strip = os.path.basename(fname)
# # 	fname_strip = strip_path(fname)
# 	if FORCE_CWD:
# 		fname_rel = '.' + os.sep + fname_strip
# 	else:
# 		fname_rel = bpy.path.relpath(fname, basepath)
# # 		fname_rel = Blender.sys.relpath(fname, basepath)
# 	if fname_rel.startswith('//'): fname_rel = '.' + os.sep + fname_rel[2:]
# 	return fname, fname_strip, fname_rel


def mat4x4str(mat):
    return '%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f,%.15f' % tuple([f for v in mat for f in v])


# XXX not used
# duplicated in OBJ exporter
def getVertsFromGroup(me, group_index):
    ret = []

    for i, v in enumerate(me.vertices):
        for g in v.groups:
            if g.group == group_index:
                ret.append((i, g.weight))

        return ret

# ob must be OB_MESH
def BPyMesh_meshWeight2List(ob, me):
    ''' Takes a mesh and return its group names and a list of lists, one list per vertex.
    aligning the each vert list with the group names, each list contains float value for the weight.
    These 2 lists can be modified and then used with list2MeshWeight to apply the changes.
    '''

    # Clear the vert group.
    groupNames = [g.name for g in ob.vertex_groups]
    len_groupNames = len(groupNames)

    if not len_groupNames:
        # no verts? return a vert aligned empty list
        return [[] for i in range(len(me.vertices))], []
    else:
        vWeightList = [[0.0] * len_groupNames for i in range(len(me.vertices))]

    for i, v in enumerate(me.vertices):
        for g in v.groups:
            vWeightList[i][g.group] = g.weight

    return groupNames, vWeightList

def meshNormalizedWeights(ob, me):
    try:  # account for old bad BPyMesh
        groupNames, vWeightList = BPyMesh_meshWeight2List(ob, me)
    except:

    for i, vWeights in enumerate(vWeightList):
        tot = 0.0
        for w in vWeights:

        if tot:
            for j, w in enumerate(vWeights):

    return groupNames, vWeightList

header_comment = \
'''; FBX 6.1.0 project file
; Created by Blender FBX Exporter
; for support mail: ideasman42@gmail.com
; ----------------------------------------------------

'''

# This func can be called with just the filepath
def save(operator, context, filepath="",
        EXP_MESH=True,
        EXP_MESH_APPLY_MOD=True,
        EXP_ARMATURE=True,
        EXP_LAMP=True,
        EXP_CAMERA=True,
        EXP_EMPTY=True,
        EXP_IMAGE_COPY=False,
        ANIM_ENABLE=True,
        ANIM_OPTIMIZE=True,
        ANIM_OPTIMIZE_PRECISSION=6,
        ANIM_ACTION_ALL=False,
        BATCH_ENABLE=False,
        BATCH_GROUP=True,
        BATCH_FILE_PREFIX='',
    mtx_x90 = Matrix.Rotation(math.pi / 2.0, 3, 'X')  # used
    mtx4_z90 = Matrix.Rotation(math.pi / 2.0, 4, 'Z')

    if GLOBAL_MATRIX is None:
        GLOBAL_MATRIX = Matrix()

    if bpy.ops.object.mode_set.poll():
        bpy.ops.object.mode_set(mode='OBJECT')

    # ----------------- Batch support!
    if BATCH_ENABLE:
        fbxpath = filepath

        # get the path component of filepath
        tmp_exists = bpy.utils.exists(fbxpath)

        if tmp_exists != 2:  # a file, we want a path
            fbxpath = os.path.dirname(fbxpath)
# 			while fbxpath and fbxpath[-1] not in ('/', '\\'):
# 				fbxpath = fbxpath[:-1]
            if not fbxpath:
                # XXX
                print('Error%t|Directory does not exist!')
# 				Draw.PupMenu('Error%t|Directory does not exist!')
                return

            tmp_exists = bpy.utils.exists(fbxpath)

        if tmp_exists != 2:
            # XXX
            print('Error%t|Directory does not exist!')
# 			Draw.PupMenu('Error%t|Directory does not exist!')
            return

        if not fbxpath.endswith(os.sep):
            fbxpath += os.sep
        del tmp_exists

        if BATCH_GROUP:
            data_seq = bpy.data.groups
        else:
            data_seq = bpy.data.scenes

        # call this function within a loop with BATCH_ENABLE == False
        orig_sce = context.scene

        new_fbxpath = fbxpath  # own dir option modifies, we need to keep an original
        for data in data_seq:  # scene or group
            newname = BATCH_FILE_PREFIX + bpy.path.clean_name(data.name)

            if BATCH_OWN_DIR:
                new_fbxpath = fbxpath + newname + os.sep
                # path may already exist
                # TODO - might exist but be a file. unlikely but should probably account for it.

                if bpy.utils.exists(new_fbxpath) == 0:
# 				if Blender.sys.exists(new_fbxpath) == 0:
                    os.mkdir(new_fbxpath)

            filepath = new_fbxpath + newname + '.fbx'

            print('\nBatch exporting %s as...\n\t%r' % (data, filepath))

            # XXX don't know what to do with this, probably do the same? (Arystan)
                # group, so objects update properly, add a dummy scene.
                scene = bpy.data.scenes.new()
                bpy.data.scenes.active = scene
                for ob_base in data.objects:
                    scene.objects.link(ob_base)

                scene.update(1)

                # TODO - BUMMER! Armatures not in the group wont animate the mesh

                data_seq.active = data

            # Call self with modified args
            # Dont pass batch options since we already usedt them
            write(filepath, data.objects,
                context,
                False,
                EXP_MESH,
                EXP_MESH_APPLY_MOD,
                EXP_ARMATURE,
                EXP_LAMP,
                EXP_CAMERA,
                EXP_EMPTY,
                EXP_IMAGE_COPY,
                GLOBAL_MATRIX,
                ANIM_ENABLE,
                ANIM_OPTIMIZE,
                ANIM_OPTIMIZE_PRECISSION,
                ANIM_ACTION_ALL
            )

            if BATCH_GROUP:
                # remove temp group scene
                bpy.data.scenes.unlink(scene)

        bpy.data.scenes.active = orig_sce

        return  # so the script wont run after we have batch exported.

    # end batch support

    # Use this for working out paths relative to the export location
    basepath = os.path.dirname(filepath) or '.'
    basepath += os.sep
# 	basepath = Blender.sys.dirname(filepath)

    # ----------------------------------------------
    # storage classes
    class my_bone_class(object):
        __slots__ = ("blenName",
                     "blenBone",
                     "blenMeshes",
                     "restMatrix",
                     "parent",
                     "blenName",
                     "fbxName",
                     "fbxArm",
                     "__pose_bone",
                     "__anim_poselist")

        def __init__(self, blenBone, fbxArm):
            # This is so 2 armatures dont have naming conflicts since FBX bones use object namespace
            self.fbxName = sane_obname(blenBone)

            self.blenName = blenBone.name
            self.blenBone = blenBone
            self.blenMeshes = {}					# fbxMeshObName : mesh
            self.fbxArm = fbxArm
            self.restMatrix = blenBone.matrix_local
# 			self.restMatrix =		blenBone.matrix['ARMATURESPACE']

            # not used yet
            # self.restMatrixInv =	self.restMatrix.copy().invert()
            # self.restMatrixLocal =	None # set later, need parent matrix

            pose = fbxArm.blenObject.pose
            self.__pose_bone = pose.bones[self.blenName]

            # store a list if matricies here, (poseMatrix, head, tail)
            # {frame:posematrix, frame:posematrix, ...}
            self.__anim_poselist = {}

        '''
        def calcRestMatrixLocal(self):
            if self.parent:
                self.restMatrixLocal = self.restMatrix * self.parent.restMatrix.copy().invert()
            else:
                self.restMatrixLocal = self.restMatrix.copy()
        '''
        def setPoseFrame(self, f):
            # cache pose info here, frame must be set beforehand

            # Didnt end up needing head or tail, if we do - here it is.
            '''
            self.__anim_poselist[f] = (\
                self.__pose_bone.poseMatrix.copy(),\
                self.__pose_bone.head.copy(),\
                self.__pose_bone.tail.copy() )
            self.__anim_poselist[f] = self.__pose_bone.matrix.copy()
        def getPoseMatrix(self, f):  # ----------------------------------------------
            return self.__anim_poselist[f]
        '''
        def getPoseHead(self, f):
            #return self.__pose_bone.head.copy()
            return self.__anim_poselist[f][1].copy()
        def getPoseTail(self, f):
            #return self.__pose_bone.tail.copy()
            return self.__anim_poselist[f][2].copy()
        '''
        # end

        def getAnimParRelMatrix(self, frame):
            #arm_mat = self.fbxArm.matrixWorld
            #arm_mat = self.fbxArm.parRelMatrix()
            if not self.parent:
                #return mtx4_z90 * (self.getPoseMatrix(frame) * arm_mat) # dont apply arm matrix anymore
                return self.getPoseMatrix(frame) * mtx4_z90
            else:
                #return (mtx4_z90 * ((self.getPoseMatrix(frame) * arm_mat)))  *  (mtx4_z90 * (self.parent.getPoseMatrix(frame) * arm_mat)).invert()
                return (self.parent.getPoseMatrix(frame) * mtx4_z90).invert() * ((self.getPoseMatrix(frame)) * mtx4_z90)

        # we need thes because cameras and lights modified rotations
        def getAnimParRelMatrixRot(self, frame):
            return self.getAnimParRelMatrix(frame)

        def flushAnimData(self):
            self.__anim_poselist.clear()

    class my_object_generic(object):
        __slots__ = ("fbxName",
                     "blenObject",
                     "blenData",
                     "origData",
                     "blenTextures",
                     "blenMaterials",
                     "blenMaterialList",
                     "blenAction",
                     "blenActionList",
                     "fbxGroupNames",
                     "fbxParent",
                     "fbxBoneParent",
                     "fbxBones",
                     "fbxArm",
                     "matrixWorld",
                     "__anim_poselist",
                     )

        # Other settings can be applied for each type - mesh, armature etc.
        def __init__(self, ob, matrixWorld=None):
            self.fbxName = sane_obname(ob)
            self.blenObject = ob
            self.fbxGroupNames = []
            self.fbxParent = None  # set later on IF the parent is in the selection.
            if matrixWorld:
                self.matrixWorld = GLOBAL_MATRIX * matrixWorld
            else:
                self.matrixWorld = GLOBAL_MATRIX * ob.matrix_world

            self.__anim_poselist = {}  # we should only access this

        def parRelMatrix(self):
            if self.fbxParent:
                return self.fbxParent.matrixWorld.copy().invert() * self.matrixWorld
            else:
                return self.matrixWorld

        def setPoseFrame(self, f, fake=False):
            if fake:
                # annoying, have to clear GLOBAL_MATRIX
                self.__anim_poselist[f] = self.matrixWorld * GLOBAL_MATRIX.copy().invert()
                self.__anim_poselist[f] = self.blenObject.matrix_world.copy()

        def getAnimParRelMatrix(self, frame):
            if self.fbxParent:
                #return (self.__anim_poselist[frame] * self.fbxParent.__anim_poselist[frame].copy().invert() ) * GLOBAL_MATRIX
                return (GLOBAL_MATRIX * self.fbxParent.__anim_poselist[frame]).invert() * (GLOBAL_MATRIX * self.__anim_poselist[frame])
            else:
                return GLOBAL_MATRIX * self.__anim_poselist[frame]

        def getAnimParRelMatrixRot(self, frame):
            obj_type = self.blenObject.type
            if self.fbxParent:
                matrix_rot = ((GLOBAL_MATRIX * self.fbxParent.__anim_poselist[frame]).invert() * (GLOBAL_MATRIX * self.__anim_poselist[frame])).rotation_part()
            else:
                matrix_rot = (GLOBAL_MATRIX * self.__anim_poselist[frame]).rotation_part()

            # Lamps need to be rotated
                matrix_rot = matrix_rot * mtx_x90
                y = Vector((0.0, 1.0, 0.0)) * matrix_rot
                matrix_rot = Matrix.Rotation(math.pi / 2.0, 3, y) * matrix_rot

            return matrix_rot

    # ----------------------------------------------

    print('\nFBX export starting... %r' % filepath)
    start_time = time.clock()
    try:
        file = open(filepath, 'w', encoding='utf8')
    except:
        return False

    scene = context.scene
    world = scene.world

    # ---------------------------- Write the header first
    file.write(header_comment)
        curtime = time.localtime()[0:6]
    else:
    #
    file.write(\
'''FBXHeaderExtension:  {
    FBXHeaderVersion: 1003
    FBXVersion: 6100
    CreationTimeStamp:  {
        Version: 1000
        Year: %.4i
        Month: %.2i
        Day: %.2i
        Hour: %.2i
        Minute: %.2i
        Second: %.2i
        Millisecond: 0
    }
    Creator: "FBX SDK/FBX Plugins build 20070228"
    OtherFlags:  {
        FlagPLE: 0
    }
}''' % (curtime))

    file.write('\nCreationTime: "%.4i-%.2i-%.2i %.2i:%.2i:%.2i:000"' % curtime)
    file.write('\nCreator: "Blender version %s"' % bpy.app.version_string)

    pose_items = []  # list of (fbxName, matrix) to write pose data for, easier to collect allong the way

    # --------------- funcs for exporting
    def object_tx(ob, loc, matrix, matrix_mod=None):
        '''
        Matrix mod is so armature objects can modify their bone matricies
        '''
        if isinstance(ob, bpy.types.Bone):
# 		if isinstance(ob, Blender.Types.BoneType):

            # we know we have a matrix
            # matrix = mtx4_z90 * (ob.matrix['ARMATURESPACE'] * matrix_mod)
            matrix = ob.matrix_local * mtx4_z90  # dont apply armature matrix anymore
# 			matrix = mtx4_z90 * ob.matrix['ARMATURESPACE'] # dont apply armature matrix anymore

            parent = ob.parent
            if parent:
                #par_matrix = mtx4_z90 * (parent.matrix['ARMATURESPACE'] * matrix_mod)
                par_matrix = parent.matrix_local * mtx4_z90  # dont apply armature matrix anymore
# 				par_matrix = mtx4_z90 * parent.matrix['ARMATURESPACE'] # dont apply armature matrix anymore
                matrix = par_matrix.copy().invert() * matrix

            loc, rot, scale = matrix.decompose()
            matrix_rot = rot.to_matrix()
            rot = tuple(rot.to_euler())  # quat -> euler
            scale = tuple(scale)
        else:
            # This is bad because we need the parent relative matrix from the fbx parent (if we have one), dont use anymore
            #if ob and not matrix: matrix = ob.matrix_world * GLOBAL_MATRIX
            if ob and not matrix:
                raise Exception("error: this should never happen!")

            matrix_rot = matrix
            #if matrix:
            #	matrix = matrix_scale * matrix

            if matrix:
                loc, rot, scale = matrix.decompose()
                matrix_rot = rot.to_matrix()

                # Lamps need to be rotated
                    matrix_rot = matrix_rot * mtx_x90
                elif ob and ob.type == 'CAMERA':
                    y = Vector((0.0, 1.0, 0.0)) * matrix_rot
                    matrix_rot = Matrix.Rotation(math.pi / 2.0, 3, y) * matrix_rot
                # else do nothing.

                loc = tuple(loc)
                rot = tuple(matrix_rot.to_euler())
                scale = tuple(scale)
            else:
                if not loc:
                    loc = 0.0, 0.0, 0.0
                scale = 1.0, 1.0, 1.0
                rot = 0.0, 0.0, 0.0

        return loc, rot, scale, matrix, matrix_rot

    def write_object_tx(ob, loc, matrix, matrix_mod=None):
        '''
        We have loc to set the location if non blender objects that have a location

        matrix_mod is only used for bones at the moment
        '''
        loc, rot, scale, matrix, matrix_rot = object_tx(ob, loc, matrix, matrix_mod)

        file.write('\n\t\t\tProperty: "Lcl Translation", "Lcl Translation", "A+",%.15f,%.15f,%.15f' % loc)
        file.write('\n\t\t\tProperty: "Lcl Rotation", "Lcl Rotation", "A+",%.15f,%.15f,%.15f' % tuple_rad_to_deg(rot))
# 		file.write('\n\t\t\tProperty: "Lcl Rotation", "Lcl Rotation", "A+",%.15f,%.15f,%.15f' % rot)
        file.write('\n\t\t\tProperty: "Lcl Scaling", "Lcl Scaling", "A+",%.15f,%.15f,%.15f' % scale)
        return loc, rot, scale, matrix, matrix_rot

    def write_object_props(ob=None, loc=None, matrix=None, matrix_mod=None):
        # if the type is 0 its an empty otherwise its a mesh
        # only difference at the moment is one has a color
        file.write('''
        Properties60:  {
            Property: "QuaternionInterpolate", "bool", "",0
            Property: "Visibility", "Visibility", "A+",1''')

        loc, rot, scale, matrix, matrix_rot = write_object_tx(ob, loc, matrix, matrix_mod)

        # Rotation order, note, for FBX files Iv loaded normal order is 1
        # setting to zero.
        # eEULER_XYZ = 0
        # eEULER_XZY
        # eEULER_YZX
        # eEULER_YXZ
        # eEULER_ZXY
        # eEULER_ZYX
Loading
Loading full blame...