1581 lines
		
	
	
		
			58 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			1581 lines
		
	
	
		
			58 KiB
		
	
	
	
		
			Python
		
	
	
#!/usr/bin/env python
 | 
						|
# ############################################
 | 
						|
# fbx to glTF2.0 converter
 | 
						|
# glTF spec : https://github.com/KhronosGroup/glTF/blob/master/specification/2.0
 | 
						|
# fbx version 2018.1.1
 | 
						|
# http://github.com/pissang/
 | 
						|
# ############################################
 | 
						|
import sys, struct, json, os.path, math, argparse, shutil
 | 
						|
 | 
						|
try:
 | 
						|
    from FbxCommon import *
 | 
						|
except ImportError:
 | 
						|
    import platform
 | 
						|
    msg = 'You need to copy the content in compatible subfolder under /lib/python<version> into your python install folder such as '
 | 
						|
    if platform.system() == 'Windows' or platform.system() == 'Microsoft':
 | 
						|
        msg += '"Python33/Lib/site-packages"'
 | 
						|
    elif platform.system() == 'Linux':
 | 
						|
        msg += '"/usr/local/lib/python3.3/site-packages"'
 | 
						|
    elif platform.system() == 'Darwin':
 | 
						|
        msg += '"/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages"'
 | 
						|
    msg += ' folder.'
 | 
						|
    print(msg)
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
lib_materials = []
 | 
						|
 | 
						|
lib_images = []
 | 
						|
lib_samplers = []
 | 
						|
lib_textures = []
 | 
						|
 | 
						|
# attributes, indices, anim_parameters will be merged in accessors
 | 
						|
lib_attributes_accessors = []
 | 
						|
lib_indices_accessors = []
 | 
						|
lib_animation_accessors = []
 | 
						|
lib_ibm_accessors = []
 | 
						|
lib_accessors = []
 | 
						|
 | 
						|
lib_buffer_views = []
 | 
						|
lib_buffers = []
 | 
						|
 | 
						|
lib_cameras = []
 | 
						|
lib_meshes = []
 | 
						|
 | 
						|
lib_nodes = []
 | 
						|
lib_scenes = []
 | 
						|
 | 
						|
lib_skins = []
 | 
						|
 | 
						|
lib_animations = []
 | 
						|
 | 
						|
# Only python 3 support bytearray ?
 | 
						|
# http://dabeaz.blogspot.jp/2010/01/few-useful-bytearray-tricks.html
 | 
						|
attributeBuffer = bytearray()
 | 
						|
indicesBuffer = bytearray()
 | 
						|
invBindMatricesBuffer = bytearray()
 | 
						|
animationBuffer = bytearray()
 | 
						|
 | 
						|
GL_RGBA = 0x1908
 | 
						|
 | 
						|
GL_BYTE = 5120
 | 
						|
GL_UNSIGNED_BYTE = 5121
 | 
						|
GL_SHORT = 5122
 | 
						|
GL_UNSIGNED_SHORT = 5123
 | 
						|
GL_UNSIGNED_INT = 5125
 | 
						|
GL_FLOAT = 5126
 | 
						|
 | 
						|
GL_TEXTURE_2D = 0x0DE1
 | 
						|
GL_TEXTURE_CUBE_MAP = 0x8513
 | 
						|
GL_REPEAT = 0x2901
 | 
						|
GL_CLAMP_TO_EDGE = 0x812F
 | 
						|
GL_NEAREST = 0x2600
 | 
						|
GL_LINEAR = 0x2601
 | 
						|
GL_NEAREST_MIPMAP_NEAREST = 0x2700
 | 
						|
GL_LINEAR_MIPMAP_NEAREST = 0x2701
 | 
						|
GL_NEAREST_MIPMAP_LINEAR = 0x2702
 | 
						|
GL_LINEAR_MIPMAP_LINEAR = 0x2703
 | 
						|
 | 
						|
GL_ARRAY_BUFFER = 0x8892
 | 
						|
GL_ELEMENT_ARRAY_BUFFER = 0x8893
 | 
						|
 | 
						|
ENV_QUANTIZE = False
 | 
						|
ENV_FLIP_V = True
 | 
						|
 | 
						|
 | 
						|
_id = 0
 | 
						|
def GetId():
 | 
						|
    global _id
 | 
						|
    _id = _id + 1
 | 
						|
    return _id
 | 
						|
 | 
						|
def ListFromM4(m):
 | 
						|
    return [m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]]
 | 
						|
 | 
						|
def MatGetOpacity(pMaterial):
 | 
						|
    lFactor = pMaterial.TransparencyFactor.Get()
 | 
						|
    lColor = pMaterial.TransparentColor.Get()
 | 
						|
 | 
						|
    return 1.0 - lFactor * (lColor[0] + lColor[1] + lColor[2]) / 3.0;
 | 
						|
 | 
						|
 | 
						|
def quantize(pList, pStride, pMin, pMax):
 | 
						|
    lRange = range(pStride)
 | 
						|
    lMultiplier = []
 | 
						|
    lDivider = []
 | 
						|
    # TODO dynamic precision? may lose info?
 | 
						|
    lPrecision = float(1e6)
 | 
						|
    for i in lRange:
 | 
						|
        pMax[i] = math.ceil(pMax[i] * lPrecision) / lPrecision;
 | 
						|
        pMin[i] = math.floor(pMin[i] * lPrecision) / lPrecision;
 | 
						|
        if pMax[i] == pMin[i]:
 | 
						|
            lMultiplier.append(0.0)
 | 
						|
            lDivider.append(0.0)
 | 
						|
        else:
 | 
						|
            lDividerTmp = (pMax[i] - pMin[i]) / 65535.0;
 | 
						|
            lDividerTmp = math.ceil(lDividerTmp * lPrecision) / lPrecision
 | 
						|
            lDivider.append(lDividerTmp)
 | 
						|
            lMultiplier.append(1.0 / lDividerTmp)
 | 
						|
 | 
						|
    lNewList = []
 | 
						|
    for item in pList:
 | 
						|
        if pStride == 1:
 | 
						|
            lNewList.append(int((item - pMin[0]) * lMultiplier[0]))
 | 
						|
        else:
 | 
						|
            lNewItem = []
 | 
						|
            for i in lRange:
 | 
						|
                lNewItem.append(int((item[i] - pMin[i]) * lMultiplier[i]))
 | 
						|
            lNewList.append(lNewItem)
 | 
						|
 | 
						|
    # TODO
 | 
						|
    if pStride == 1:
 | 
						|
        lDecodeMatrix = [
 | 
						|
            lDivider[0], 0,
 | 
						|
            pMin[0], 1
 | 
						|
        ]
 | 
						|
    elif pStride == 2:
 | 
						|
        lDecodeMatrix = [
 | 
						|
            lDivider[0], 0, 0,
 | 
						|
            0, lDivider[1], 0,
 | 
						|
            pMin[0], pMin[1], 1
 | 
						|
        ]
 | 
						|
    elif pStride == 3:
 | 
						|
        lDecodeMatrix = [
 | 
						|
            lDivider[0], 0, 0, 0,
 | 
						|
            0, lDivider[1], 0, 0,
 | 
						|
            0, 0, lDivider[2], 0,
 | 
						|
            pMin[0], pMin[1], pMin[2], 1
 | 
						|
        ]
 | 
						|
    elif pStride == 4:
 | 
						|
        lDecodeMatrix = [
 | 
						|
            lDivider[0], 0, 0, 0, 0,
 | 
						|
            0, lDivider[1], 0, 0, 0,
 | 
						|
            0, 0, lDivider[2], 0, 0,
 | 
						|
            0, 0, 0, lDivider[3], 0,
 | 
						|
            pMin[0], pMin[1], pMin[2], pMin[3], 1
 | 
						|
        ]
 | 
						|
 | 
						|
    return lNewList, lDecodeMatrix, pMin, pMax
 | 
						|
 | 
						|
 | 
						|
def CreateAccessorBuffer(pList, pType, pStride, pMinMax=False, pQuantize=False, pNormalize=False):
 | 
						|
    lGLTFAccessor = {}
 | 
						|
 | 
						|
    if pMinMax:
 | 
						|
        if len(pList) > 0:
 | 
						|
            if pStride == 1:
 | 
						|
                lMin = [pList[0]]
 | 
						|
                lMax = [pList[0]]
 | 
						|
            elif pStride == 16:
 | 
						|
                lMin = ListFromM4(pList[0])
 | 
						|
                lMax = ListFromM4(pList[0])
 | 
						|
            else:
 | 
						|
                lMin = list(pList[0])[:pStride]
 | 
						|
                lMax = list(pList[0])[:pStride]
 | 
						|
        else:
 | 
						|
            lMax = [0] * pStride
 | 
						|
            lMin = [0] * pStride
 | 
						|
        lRange = range(pStride)
 | 
						|
        for item in pList:
 | 
						|
            if pStride == 1:
 | 
						|
                for i in lRange:
 | 
						|
                    lMin[i] = min(lMin[i], item)
 | 
						|
                    lMax[i] = max(lMax[i], item)
 | 
						|
            else:
 | 
						|
                if pStride == 16:
 | 
						|
                    item = ListFromM4(item)
 | 
						|
                for i in lRange:
 | 
						|
                    lMin[i] = min(lMin[i], item[i])
 | 
						|
                    lMax[i] = max(lMax[i], item[i])
 | 
						|
 | 
						|
    if pQuantize and pType == 'f' and pStride <= 4:
 | 
						|
        pList, lDecodeMatrix, lDecodedMin, lDecodedMax = quantize(pList, pStride, lMin[0:], lMax[0:])
 | 
						|
        pType = 'H'
 | 
						|
        # https://github.com/KhronosGroup/glTF/blob/master/extensions/Vendor/WEB3D_quantized_attributes
 | 
						|
        lGLTFAccessor['extensions'] = {
 | 
						|
            'WEB3D_quantized_attributes': {
 | 
						|
                'decodedMin': lDecodedMin,
 | 
						|
                'decodedMax': lDecodedMax,
 | 
						|
                'decodeMatrix': lDecodeMatrix
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
    lPackType = '<' + pType * pStride
 | 
						|
    lData = []
 | 
						|
    #TODO: Other method to write binary buffer ?
 | 
						|
    for item in pList:
 | 
						|
        if pStride == 1:
 | 
						|
            lData.append(struct.pack(lPackType, item))
 | 
						|
        elif pStride == 2:
 | 
						|
            lData.append(struct.pack(lPackType, item[0], item[1]))
 | 
						|
        elif pStride == 3:
 | 
						|
            lData.append(struct.pack(lPackType, item[0], item[1], item[2]))
 | 
						|
        elif pStride == 4:
 | 
						|
            lData.append(struct.pack(lPackType, item[0], item[1], item[2], item[3]))
 | 
						|
        elif pStride == 16:
 | 
						|
            m = item
 | 
						|
            lData.append(struct.pack(lPackType, m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], m[3][3]))
 | 
						|
 | 
						|
    if pType == 'f':
 | 
						|
        lGLTFAccessor['componentType'] = GL_FLOAT
 | 
						|
    # Unsigned Int
 | 
						|
    elif pType == 'I':
 | 
						|
        lGLTFAccessor['componentType'] = GL_UNSIGNED_INT
 | 
						|
    # Unsigned Short
 | 
						|
    elif pType == 'H':
 | 
						|
        lGLTFAccessor['componentType'] = GL_UNSIGNED_SHORT
 | 
						|
    # Unsigned Byte
 | 
						|
    elif pType == 'B':
 | 
						|
        lGLTFAccessor['componentType'] = GL_UNSIGNED_BYTE
 | 
						|
 | 
						|
    if pStride == 1:
 | 
						|
        lGLTFAccessor['type'] = 'SCALAR'
 | 
						|
    elif pStride == 2:
 | 
						|
        lGLTFAccessor['type'] = 'VEC2'
 | 
						|
    elif pStride == 3:
 | 
						|
        lGLTFAccessor['type'] = 'VEC3'
 | 
						|
    elif pStride == 4:
 | 
						|
        lGLTFAccessor['type'] = 'VEC4'
 | 
						|
    elif pStride == 9:
 | 
						|
        lGLTFAccessor['type'] = 'MAT3'
 | 
						|
    elif pStride == 16:
 | 
						|
        lGLTFAccessor['type'] = 'MAT4'
 | 
						|
 | 
						|
    lGLTFAccessor['byteOffset'] = 0
 | 
						|
    lGLTFAccessor['count'] = len(pList)
 | 
						|
 | 
						|
    if pMinMax:
 | 
						|
        lGLTFAccessor['max'] = lMax
 | 
						|
        lGLTFAccessor['min'] = lMin
 | 
						|
 | 
						|
    if pNormalize:
 | 
						|
        lGLTFAccessor['normalized'] = True
 | 
						|
 | 
						|
    return b''.join(lData), lGLTFAccessor
 | 
						|
 | 
						|
def appendToBuffer(pType, pBuffer, pData, pObj):
 | 
						|
    lByteOffset = len(pBuffer)
 | 
						|
    if pType == 'f' or pType == 'I':
 | 
						|
        # should be a multiple of 4 for alignment
 | 
						|
        if lByteOffset % 4 == 2:
 | 
						|
            pBuffer.extend(b'\x00\x00')
 | 
						|
            lByteOffset += 2
 | 
						|
 | 
						|
    pObj['byteOffset'] = lByteOffset
 | 
						|
    pBuffer.extend(pData)
 | 
						|
 | 
						|
def CreateAttributeBuffer(pList, pType, pStride, pNormalize=False):
 | 
						|
    lData, lGLTFAttribute = CreateAccessorBuffer(pList, pType, pStride, True, ENV_QUANTIZE, pNormalize)
 | 
						|
    appendToBuffer(pType, attributeBuffer, lData, lGLTFAttribute)
 | 
						|
    idx = len(lib_accessors)
 | 
						|
    lib_attributes_accessors.append(lGLTFAttribute)
 | 
						|
    lib_accessors.append(lGLTFAttribute)
 | 
						|
    return idx
 | 
						|
 | 
						|
 | 
						|
def CreateIndicesBuffer(pList, pType):
 | 
						|
    # Sketchfab needs all accessor have min, max?
 | 
						|
    lData, lGLTFIndices = CreateAccessorBuffer(pList, pType, 1, True)
 | 
						|
    appendToBuffer(pType, indicesBuffer, lData, lGLTFIndices)
 | 
						|
    idx = len(lib_accessors)
 | 
						|
    lib_indices_accessors.append(lGLTFIndices)
 | 
						|
    lib_accessors.append(lGLTFIndices)
 | 
						|
    return idx
 | 
						|
 | 
						|
def CreateAnimationBuffer(pList, pType, pStride):
 | 
						|
    lData, lGLTFAnimSampler = CreateAccessorBuffer(pList, pType, pStride, True)
 | 
						|
 | 
						|
    # PENDING
 | 
						|
    # lAllSame = True
 | 
						|
    # for i in range(pStride):
 | 
						|
    #     if lGLTFAnimSampler['min'][i] != lGLTFAnimSampler['max'][i]:
 | 
						|
    #         lAllSame = False
 | 
						|
    # # Just ignore it.
 | 
						|
    # if lAllSame:
 | 
						|
    #     return -1
 | 
						|
 | 
						|
    appendToBuffer(pType, animationBuffer, lData, lGLTFAnimSampler)
 | 
						|
 | 
						|
    idx = len(lib_accessors)
 | 
						|
    lib_animation_accessors.append(lGLTFAnimSampler)
 | 
						|
    lib_accessors.append(lGLTFAnimSampler)
 | 
						|
    return idx
 | 
						|
 | 
						|
def CreateIBMBuffer(pList):
 | 
						|
    lData, lGLTFIBM = CreateAccessorBuffer(pList, 'f', 16, True)
 | 
						|
    appendToBuffer('f', invBindMatricesBuffer, lData, lGLTFIBM)
 | 
						|
    idx = len(lib_accessors)
 | 
						|
    lib_ibm_accessors.append(lGLTFIBM)
 | 
						|
    lib_accessors.append(lGLTFIBM)
 | 
						|
    return idx
 | 
						|
 | 
						|
 | 
						|
def CreateImage(pPath):
 | 
						|
    lImageIndices = [idx for idx in range(len(lib_images)) if lib_images[idx]['uri'] == pPath]
 | 
						|
    if len(lImageIndices):
 | 
						|
        return lImageIndices[0]
 | 
						|
 | 
						|
    lImageIdx = len(lib_images)
 | 
						|
    lib_images.append({
 | 
						|
        'uri' : pPath
 | 
						|
    })
 | 
						|
    return lImageIdx
 | 
						|
 | 
						|
def HashSampler(pTexture):
 | 
						|
    lHashStr = []
 | 
						|
    # Wrap S
 | 
						|
    lHashStr.append(str(pTexture.WrapModeU.Get()))
 | 
						|
    # Wrap T
 | 
						|
    lHashStr.append(str(pTexture.WrapModeV.Get()))
 | 
						|
    return ' '.join(lHashStr)
 | 
						|
 | 
						|
def ConvertWrapMode(pWrap):
 | 
						|
    if pWrap == FbxTexture.eRepeat:
 | 
						|
        return GL_REPEAT
 | 
						|
    elif pWrap == FbxTexture.eClamp:
 | 
						|
        return GL_CLAMP_TO_EDGE
 | 
						|
 | 
						|
_samplerHashMap = {}
 | 
						|
def CreateSampler(pTexture):
 | 
						|
    lHashKey = HashSampler(pTexture)
 | 
						|
    if lHashKey in _samplerHashMap:
 | 
						|
        return _samplerHashMap[lHashKey]
 | 
						|
    else:
 | 
						|
        lSamplerIdx = len(lib_samplers)
 | 
						|
        lib_samplers.append({
 | 
						|
            'wrapS' : ConvertWrapMode(pTexture.WrapModeU.Get()),
 | 
						|
            'wrapT' : ConvertWrapMode(pTexture.WrapModeV.Get()),
 | 
						|
            # Texture filter in fbx ?
 | 
						|
            'minFilter' : GL_LINEAR_MIPMAP_LINEAR,
 | 
						|
            'magFilter' : GL_LINEAR
 | 
						|
        })
 | 
						|
        _samplerHashMap[lHashKey] = lSamplerIdx
 | 
						|
        return lSamplerIdx
 | 
						|
 | 
						|
_textureHashMap = {}
 | 
						|
def CreateTexture(pProperty):
 | 
						|
    lTextureList = []
 | 
						|
 | 
						|
    lFileTextures = []
 | 
						|
    lLayeredTextureCount = pProperty.GetSrcObjectCount(FbxCriteria.ObjectType(FbxLayeredTexture.ClassId))
 | 
						|
 | 
						|
    lScaleU = 1
 | 
						|
    lScaleV = 1
 | 
						|
    lTranslationU = 0
 | 
						|
    lTranslationV = 0
 | 
						|
 | 
						|
    if lLayeredTextureCount > 0:
 | 
						|
        for i in range(lLayeredTextureCount):
 | 
						|
            lLayeredTexture = pProperty.GetSrcObject(FbxCriteria.ObjectType(FbxLayeredTexture.ClassId), i)
 | 
						|
            for j in range(lLayeredTexture.GetSrcObjectCount(FbxCriteria.ObjectType(FbxTexture.ClassId))):
 | 
						|
                lTexture = lLayeredTexture.GetSrcObject(FbxCriteria.ObjectType(FbxTexture.ClassId), j)
 | 
						|
                if lTexture and lTexture.__class__ == FbxFileTexture:
 | 
						|
                    lFileTextures.append(lTexture)
 | 
						|
    else:
 | 
						|
        lTextureCount = pProperty.GetSrcObjectCount(FbxCriteria.ObjectType(FbxTexture.ClassId))
 | 
						|
        for t in range(lTextureCount):
 | 
						|
            lTexture = pProperty.GetSrcObject(FbxCriteria.ObjectType(FbxTexture.ClassId), t)
 | 
						|
            if lTexture and lTexture.__class__ == FbxFileTexture:
 | 
						|
                lFileTextures.append(lTexture)
 | 
						|
 | 
						|
    for lTexture in lFileTextures:
 | 
						|
        try:
 | 
						|
            lTextureFileName = lTexture.GetFileName()
 | 
						|
        except UnicodeDecodeError:
 | 
						|
            print('Get texture file name error.')
 | 
						|
            continue
 | 
						|
        # TODO rotation
 | 
						|
        lScaleU = lTexture.GetScaleU()
 | 
						|
        lScaleV = lTexture.GetScaleV()
 | 
						|
        lTranslationU = lTexture.GetTranslationU()
 | 
						|
        lTranslationV = lTexture.GetTranslationV()
 | 
						|
 | 
						|
        lImageIdx = CreateImage(lTextureFileName)
 | 
						|
        lSamplerIdx = CreateSampler(lTexture)
 | 
						|
        lHashKey = (lImageIdx, lSamplerIdx)
 | 
						|
        if lHashKey in _textureHashMap:
 | 
						|
            lTextureList.append(_textureHashMap[lHashKey])
 | 
						|
        else:
 | 
						|
            lTextureIdx = len(lib_textures)
 | 
						|
            lib_textures.append({
 | 
						|
                'format' : GL_RGBA,
 | 
						|
                'internalFormat' : GL_RGBA,
 | 
						|
                'sampler' : lSamplerIdx,
 | 
						|
                'source' : lImageIdx,
 | 
						|
                'target' : GL_TEXTURE_2D
 | 
						|
            })
 | 
						|
            _textureHashMap[lHashKey] = lTextureIdx
 | 
						|
            lTextureList.append(lTextureIdx)
 | 
						|
    # PENDING Return the first texture ?
 | 
						|
    if len(lTextureList) > 0:
 | 
						|
        return lTextureList[0], lScaleU, lScaleV, lTranslationU, lTranslationV
 | 
						|
    else:
 | 
						|
        return None, lScaleU, lScaleV, lTranslationU, lTranslationV
 | 
						|
 | 
						|
def GetRoughnessFromExponentShininess(pShininess):
 | 
						|
    # PENDING Is max 1024?
 | 
						|
    lGlossiness = math.log(pShininess) / math.log(1024.0)
 | 
						|
    return min(max(1 - lGlossiness, 0), 1)
 | 
						|
 | 
						|
def GetMetalnessFromSpecular(pSpecular, pBaseColor):
 | 
						|
    # x = pSpecular[0]
 | 
						|
    # y = pBaseColor[0]
 | 
						|
    # a = 0.04
 | 
						|
    # b = x + y - 0.08
 | 
						|
    # c = 0.04 - x
 | 
						|
    # k = b * b - 4 * a * c
 | 
						|
    # if k >= 0:
 | 
						|
    #     return math.sqrt(k)
 | 
						|
    # return 0
 | 
						|
 | 
						|
    # PENDING
 | 
						|
    if pSpecular[0] > 0.5:
 | 
						|
        return 1
 | 
						|
    else:
 | 
						|
        return 0
 | 
						|
 | 
						|
def ScaleV3(v3, scale):
 | 
						|
    v3[0] *= scale
 | 
						|
    v3[1] *= scale
 | 
						|
    v3[2] *= scale
 | 
						|
 | 
						|
def ConvertToPBRMaterial(pMaterial):
 | 
						|
    lMaterialName = pMaterial.GetName()
 | 
						|
    lShading = str(pMaterial.ShadingModel.Get()).lower()
 | 
						|
 | 
						|
    lScaleU = 1
 | 
						|
    lScaleV = 1
 | 
						|
    lTranslationU = 0
 | 
						|
    lTranslationV = 0
 | 
						|
 | 
						|
    lGLTFMaterial = {
 | 
						|
        "name" : lMaterialName,
 | 
						|
        "pbrMetallicRoughness": {
 | 
						|
            "baseColorFactor": [1, 1, 1, 1],
 | 
						|
            "metallicFactor": 0,
 | 
						|
            "roughnessFactor": 1
 | 
						|
        }
 | 
						|
    }
 | 
						|
    lValues = lGLTFMaterial["pbrMetallicRoughness"]
 | 
						|
 | 
						|
    lMaterialIdx = len(lib_materials)
 | 
						|
 | 
						|
    lSpecularColor = [0, 0, 0]
 | 
						|
    # print(dir(pMaterial))
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'Emissive'):
 | 
						|
        lGLTFMaterial['emissiveFactor'] = list(pMaterial.Emissive.Get())
 | 
						|
        ScaleV3(lGLTFMaterial['emissiveFactor'], pMaterial.EmissiveFactor.Get())
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'TransparencyFactor'):
 | 
						|
        lTransparency = MatGetOpacity(pMaterial)
 | 
						|
        if lTransparency < 1:
 | 
						|
            lGLTFMaterial['alphaMode'] = 'BLEND'
 | 
						|
            lValues['baseColorFactor'][3] = lTransparency
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'Diffuse'):
 | 
						|
        if pMaterial.Diffuse.GetSrcObjectCount() > 0:
 | 
						|
            # TODO other textures ?
 | 
						|
            lTextureIdx, lScaleU, lScaleV, lTranslationU, lTranslationV = CreateTexture(pMaterial.Diffuse)
 | 
						|
            if not lTextureIdx == None:
 | 
						|
                lValues['baseColorTexture'] = {
 | 
						|
                    "index": lTextureIdx,
 | 
						|
                    "texCoord": 0
 | 
						|
                }
 | 
						|
        else:
 | 
						|
            lValues['baseColorFactor'][0:3] = list(pMaterial.Diffuse.Get())
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'Specular'):
 | 
						|
        lSpecularColor = list(pMaterial.Specular.Get())
 | 
						|
        ScaleV3(lSpecularColor, pMaterial.SpecularFactor.Get())
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'Bump'):
 | 
						|
        if pMaterial.Bump.GetSrcObjectCount() > 0:
 | 
						|
            lTextureIdx, lScaleU, lScaleV, lTranslationU, lTranslationV = CreateTexture(pMaterial.Bump)
 | 
						|
            if not lTextureIdx == None:
 | 
						|
                lGLTFMaterial['normalTexture'] = {
 | 
						|
                    "index": lTextureIdx,
 | 
						|
                    "texCoord": 0
 | 
						|
                }
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'NormalMap'):
 | 
						|
        if pMaterial.NormalMap.GetSrcObjectCount() > 0:
 | 
						|
            lTextureIdx, lScaleU, lScaleV, lTranslationU, lTranslationV = CreateTexture(pMaterial.NormalMap)
 | 
						|
            if not lTextureIdx == None:
 | 
						|
                lGLTFMaterial['normalTexture'] = {
 | 
						|
                    "index": lTextureIdx,
 | 
						|
                    "texCoord": 0
 | 
						|
                }
 | 
						|
 | 
						|
    if hasattr(pMaterial, 'NormalMShininessap'):
 | 
						|
        lValues['roughnessFactor'] = GetRoughnessFromExponentShininess(pMaterial.Shininess.Get())
 | 
						|
 | 
						|
    lib_materials.append(lGLTFMaterial)
 | 
						|
 | 
						|
    if lShading == 'unknown':
 | 
						|
        # Maybe shading of VRay
 | 
						|
        lProp = pMaterial.GetFirstProperty()
 | 
						|
        lCount = 0
 | 
						|
        while lProp:
 | 
						|
            lPropName = lProp.GetName()
 | 
						|
            if lPropName == '' or lCount >= 100:
 | 
						|
                break
 | 
						|
            # TODO texture
 | 
						|
            if lPropName == 'EmissiveColor':
 | 
						|
                # Need to cast to double3
 | 
						|
                # https://forums.autodesk.com/t5/fbx-forum/fbxproperty-get-in-2013-1-python/td-p/4243290
 | 
						|
                lGLTFMaterial['emissiveFactor'] = list(FbxPropertyDouble3(lProp).Get())
 | 
						|
            elif lPropName == 'DiffuseColor':
 | 
						|
                lValues['baseColorFactor'][0:3] = list(FbxPropertyDouble3(lProp).Get())
 | 
						|
            elif lPropName == 'SpecularColor':
 | 
						|
                lSpecularColor = list(FbxPropertyDouble3(lProp).Get())
 | 
						|
            elif lPropName == 'SpecularFactor':
 | 
						|
                ScaleV3(lSpecularColor, FbxPropertyDouble1(lProp).Get())
 | 
						|
            elif lPropName == 'ShininessExponent':
 | 
						|
                lValues['roughnessFactor'] = GetRoughnessFromExponentShininess(FbxPropertyDouble1(lProp).Get())
 | 
						|
 | 
						|
            lProp = pMaterial.GetNextProperty(lProp)
 | 
						|
            lCount += 1
 | 
						|
 | 
						|
    lValues['metallicFactor'] = GetMetalnessFromSpecular(lSpecularColor, lValues['baseColorFactor'][0:3])
 | 
						|
 | 
						|
    return lMaterialIdx, lScaleU, lScaleV, lTranslationU, lTranslationV
 | 
						|
 | 
						|
 | 
						|
def CreateSkin():
 | 
						|
    lSkinIdx = len(lib_skins)
 | 
						|
    # https://github.com/KhronosGroup/glTF/issues/100
 | 
						|
    lib_skins.append({
 | 
						|
        'joints' : [],
 | 
						|
    })
 | 
						|
 | 
						|
    return lSkinIdx
 | 
						|
 | 
						|
_defaultMaterialName = 'DEFAULT_MAT_'
 | 
						|
 | 
						|
def CreateDefaultMaterial(pScene):
 | 
						|
    lMat = FbxSurfacePhong.Create(pScene, _defaultMaterialName + str(len(lib_materials)))
 | 
						|
    return lMat
 | 
						|
 | 
						|
def ProcessUV(uv, scaleU, scaleV, translationU, translationV):
 | 
						|
    for i in range(len(uv)):
 | 
						|
        uv[i] = [
 | 
						|
            uv[i][0] * scaleU + translationU,
 | 
						|
            uv[i][1] * scaleV + translationV
 | 
						|
        ]
 | 
						|
        if ENV_FLIP_V:
 | 
						|
            # glTF2.0 don't flipY. So flip the uv.
 | 
						|
            uv[i][1] = 1.0 - uv[i][1]
 | 
						|
 | 
						|
def GetSkinningData(pMesh, pSkin, pClusters, pNode):
 | 
						|
    moreThanFourJoints = False
 | 
						|
    lMaxJointCount = 0
 | 
						|
    lControlPointsCount = pMesh.GetControlPointsCount()
 | 
						|
 | 
						|
    lWeights = []
 | 
						|
    lJoints = []
 | 
						|
    # Count joint number of each vertex
 | 
						|
    lJointCounts = []
 | 
						|
    for i in range(lControlPointsCount):
 | 
						|
        lWeights.append([0, 0, 0, 0])
 | 
						|
        # -1 can't used in UNSIGNED_SHORT
 | 
						|
        lJoints.append([0, 0, 0, 0])
 | 
						|
        lJointCounts.append(0)
 | 
						|
 | 
						|
    for i in range(pMesh.GetDeformerCount(FbxDeformer.eSkin)):
 | 
						|
        lDeformer = pMesh.GetDeformer(i, FbxDeformer.eSkin)
 | 
						|
 | 
						|
        for i2 in range(lDeformer.GetClusterCount()):
 | 
						|
            lCluster = lDeformer.GetCluster(i2)
 | 
						|
            lNode = lCluster.GetLink()
 | 
						|
            lJointIndex = -1
 | 
						|
            lNodeIdx = GetNodeIdx(lNode)
 | 
						|
            if not lNodeIdx in pSkin['joints']:
 | 
						|
                lJointIndex = len(pSkin['joints'])
 | 
						|
                pSkin['joints'].append(lNodeIdx)
 | 
						|
                pClusters[lNodeIdx] = lCluster
 | 
						|
            else:
 | 
						|
                lJointIndex = pSkin['joints'].index(lNodeIdx)
 | 
						|
 | 
						|
            lControlPointIndices = lCluster.GetControlPointIndices()
 | 
						|
            lControlPointWeights = lCluster.GetControlPointWeights()
 | 
						|
 | 
						|
            for i3 in range(lCluster.GetControlPointIndicesCount()):
 | 
						|
                lControlPointIndex = lControlPointIndices[i3]
 | 
						|
                lControlPointWeight = lControlPointWeights[i3]
 | 
						|
                lJointCount = lJointCounts[lControlPointIndex]
 | 
						|
 | 
						|
                # At most binding four joint per vertex
 | 
						|
                if lJointCount <= 3:
 | 
						|
                    # Joint index
 | 
						|
                    lJoints[lControlPointIndex][lJointCount] = lJointIndex
 | 
						|
                    lWeights[lControlPointIndex][lJointCount] = lControlPointWeight
 | 
						|
                else:
 | 
						|
                    moreThanFourJoints = True
 | 
						|
                    # More than four joints, replace joint of minimum Weight
 | 
						|
                    lMinW, lMinIdx = min((lWeights[lControlPointIndex][i], i) for i in range(len(lWeights[lControlPointIndex])))
 | 
						|
                    lJoints[lControlPointIndex][lMinIdx] = lJointIndex
 | 
						|
                    lWeights[lControlPointIndex][lMinIdx] = lControlPointWeight
 | 
						|
                    lMaxJointCount = max(lMaxJointCount, lJointIndex)
 | 
						|
                lJointCounts[lControlPointIndex] += 1
 | 
						|
    if moreThanFourJoints:
 | 
						|
        print('More than 4 joints (%d joints) bound to per vertex in %s. ' %(lMaxJointCount, pNode.GetName()))
 | 
						|
 | 
						|
    return lJoints, lWeights
 | 
						|
 | 
						|
def CreatePrimitiveRaw(matIndex, useTexcoords1=False, scaleU=1, scaleV=1,translationU=0, translationV=1):
 | 
						|
    return {
 | 
						|
        "normals": [],
 | 
						|
        "texcoords0": [],
 | 
						|
        "texcoords1": [],
 | 
						|
        "indices": [],
 | 
						|
        "positions": [],
 | 
						|
        "vertexColors": [],
 | 
						|
        "joints": [],
 | 
						|
        "weights": [],
 | 
						|
        "material": matIndex,
 | 
						|
        # Should use texcoord in layer2 if material is in layer2
 | 
						|
        # PENDING
 | 
						|
        "useTexcoords1": useTexcoords1,
 | 
						|
        "indicesMap": {},
 | 
						|
        "scaleU": scaleU,
 | 
						|
        "scaleV": scaleV,
 | 
						|
        "translationU": translationU,
 | 
						|
        "translationV": translationV
 | 
						|
    }
 | 
						|
 | 
						|
def GetVertexAttribute(pLayer, pControlPointIdx, pPolygonVertexIndex):
 | 
						|
    if pLayer.GetMappingMode() == FbxLayerElement.eByControlPoint:
 | 
						|
        if pLayer.GetReferenceMode() == FbxLayerElement.eDirect:
 | 
						|
            return pLayer.GetDirectArray().GetAt(pControlPointIdx)
 | 
						|
        elif pLayer.GetReferenceMode() == FbxLayerElement.eIndexToDirect:
 | 
						|
            return pLayer.GetDirectArray().GetAt(pLayer.GetIndexArray().GetAt(pControlPointIdx))
 | 
						|
    elif pLayer.GetMappingMode() == FbxLayerElement.eByPolygonVertex:
 | 
						|
        if pLayer.GetReferenceMode() == FbxLayerElement.eDirect:
 | 
						|
            return pLayer.GetDirectArray().GetAt(pPolygonVertexIndex)
 | 
						|
        elif pLayer.GetReferenceMode() == FbxLayerElement.eDirect or\
 | 
						|
            pLayer.GetReferenceMode() == FbxLayerElement.eIndexToDirect:
 | 
						|
            return pLayer.GetDirectArray().GetAt(pLayer.GetIndexArray().GetAt(pPolygonVertexIndex))
 | 
						|
    else:
 | 
						|
        pass
 | 
						|
        # Unknown
 | 
						|
 | 
						|
def ConvertMesh(pScene, pMesh, pNode, pSkin, pClusters):
 | 
						|
    lPrimitivesList = []
 | 
						|
    lWeights = []
 | 
						|
    lJoints = []
 | 
						|
 | 
						|
    lLayer = pMesh.GetLayer(0)
 | 
						|
    lLayer2 = pMesh.GetLayer(1)
 | 
						|
    lSecondMaterialLayer = None
 | 
						|
    if lLayer2:
 | 
						|
        lSecondMaterialLayer = lLayer2.GetMaterials()
 | 
						|
 | 
						|
    lNormalLayer = pMesh.GetElementNormal(0)
 | 
						|
    lVertexColorLayer = pMesh.GetElementVertexColor(0)
 | 
						|
    lUvLayer = pMesh.GetElementUV(0)
 | 
						|
    lUv2Layer = pMesh.GetElementUV(1)
 | 
						|
 | 
						|
    hasSkin = False
 | 
						|
    # Handle Skinning data
 | 
						|
    if (pMesh.GetDeformerCount(FbxDeformer.eSkin) > 0):
 | 
						|
        hasSkin = True
 | 
						|
        lJoints, lWeights = GetSkinningData(pMesh, pSkin, pClusters, pNode)
 | 
						|
    lPositions = pMesh.GetControlPoints()
 | 
						|
    # Prepare materials
 | 
						|
    lAllSameMaterial = True
 | 
						|
    lAllSameMaterialIndex = -1
 | 
						|
    for i in range(pMesh.GetElementMaterialCount()):
 | 
						|
        lMaterialLayer = pMesh.GetElementMaterial(i)
 | 
						|
        if not lMaterialLayer.GetMappingMode() == FbxLayerElement.eAllSame:
 | 
						|
            lIndexArray = lMaterialLayer.GetIndexArray()
 | 
						|
            for k in range(pMesh.GetPolygonCount()):
 | 
						|
                if not lIndexArray.GetAt(k) == lIndexArray.GetAt(0):
 | 
						|
                    lAllSameMaterial = False
 | 
						|
                    break
 | 
						|
 | 
						|
        if lAllSameMaterial:
 | 
						|
            lAllSameMaterialIndex = lMaterialLayer.GetIndexArray().GetAt(0)
 | 
						|
 | 
						|
    if lAllSameMaterial:
 | 
						|
        lMaterial = pNode.GetMaterial(lAllSameMaterialIndex)
 | 
						|
        if not lMaterial:
 | 
						|
            lMaterial = CreateDefaultMaterial(pScene)
 | 
						|
 | 
						|
        lTmpIndex, lScaleU, lScaleV, lTranslationU, lTranslationV = ConvertToPBRMaterial(lMaterial)
 | 
						|
        lPrimitivesList.append(CreatePrimitiveRaw(
 | 
						|
            lTmpIndex, False,
 | 
						|
            lScaleU, lScaleV, lTranslationU, lTranslationV
 | 
						|
        ))
 | 
						|
    else:
 | 
						|
        lMaterialIndices = [-1]*pMesh.GetPolygonCount()
 | 
						|
        lMaterialsPrimitivesMap = {}
 | 
						|
        lIsMaterialInSecondLayer = {}
 | 
						|
        for i in range(pMesh.GetElementMaterialCount()):
 | 
						|
            lMaterialLayer = pMesh.GetElementMaterial(i)
 | 
						|
            lIndexArray = lMaterialLayer.GetIndexArray()
 | 
						|
            lIsInSecondLayer = lMaterialLayer == lSecondMaterialLayer
 | 
						|
            if lMaterialLayer.GetMappingMode() == FbxLayerElement.eByPolygon:
 | 
						|
                for k in range(len(lMaterialIndices)):
 | 
						|
                    if lIndexArray.GetAt(k) >= 0:
 | 
						|
                        # index in top material layer will overwrite the bottom material layer
 | 
						|
                        lMaterialIndices[k] = lIndexArray.GetAt(k)
 | 
						|
                    lIsMaterialInSecondLayer[lIndexArray.GetAt(k)] = lIsInSecondLayer
 | 
						|
            elif lMaterialLayer.GetMappingMode() == FbxLayerElement.eAllSame:
 | 
						|
                lIdx = lIndexArray.GetAt(0)
 | 
						|
                if lIdx:
 | 
						|
                    if lIdx >= 0:
 | 
						|
                        for k in range(len(lMaterialIndices)):
 | 
						|
                            lMaterialIndices[k] = lIdx
 | 
						|
                lIsMaterialInSecondLayer[lIdx] = lIsInSecondLayer
 | 
						|
        for lIdx in lMaterialIndices:
 | 
						|
            if not lIdx in lMaterialsPrimitivesMap:
 | 
						|
                lMaterial = pNode.GetMaterial(lIdx)
 | 
						|
                if not lMaterial:
 | 
						|
                    lMaterial = CreateDefaultMaterial(pScene)
 | 
						|
                lGLTFMaterialIdx, lScaleU, lScaleV, lTranslationU, lTranslationV = ConvertToPBRMaterial(lMaterial)
 | 
						|
                lMaterialsPrimitivesMap[lIdx] = len(lPrimitivesList)
 | 
						|
                lPrimitivesList.append(CreatePrimitiveRaw(
 | 
						|
                    lGLTFMaterialIdx, lIsMaterialInSecondLayer[lIdx],
 | 
						|
                    lScaleU, lScaleV, lTranslationU, lTranslationV
 | 
						|
                ))
 | 
						|
 | 
						|
    range3 = range(3)
 | 
						|
    lVertexCount = 0
 | 
						|
 | 
						|
    lNeedHash = False
 | 
						|
    if lNormalLayer:
 | 
						|
        if lNormalLayer.GetMappingMode() == FbxLayerElement.eByPolygonVertex:
 | 
						|
            lNeedHash = True
 | 
						|
    if lVertexColorLayer:
 | 
						|
        if lVertexColorLayer.GetMappingMode() == FbxLayerElement.eByPolygonVertex:
 | 
						|
            lNeedHash = True
 | 
						|
    if lUvLayer:
 | 
						|
        if lUvLayer.GetMappingMode() == FbxLayerElement.eByPolygonVertex:
 | 
						|
            lNeedHash = True
 | 
						|
    if lUv2Layer:
 | 
						|
        if lUv2Layer.GetMappingMode() == FbxLayerElement.eByPolygonVertex:
 | 
						|
            lNeedHash = True
 | 
						|
 | 
						|
    for i in range(pMesh.GetPolygonCount()):
 | 
						|
        if lAllSameMaterial:
 | 
						|
            lPrimitive = lPrimitivesList[0]
 | 
						|
        else:
 | 
						|
            lMaterialIndex = lMaterialIndices[i]
 | 
						|
            lPrimitive = lPrimitivesList[lMaterialsPrimitivesMap[lMaterialIndex]]
 | 
						|
        # Mesh should be triangulated
 | 
						|
        for j in range3:
 | 
						|
            lControlPointIndex = pMesh.GetPolygonVertex(i, j)
 | 
						|
            if lNeedHash:
 | 
						|
                vertexKeyList = []
 | 
						|
                vertexKeyList += lPositions[lControlPointIndex]
 | 
						|
            if lNormalLayer:
 | 
						|
                lNormal = GetVertexAttribute(lNormalLayer, lControlPointIndex, lVertexCount)
 | 
						|
                if lNeedHash:
 | 
						|
                    vertexKeyList += lNormal
 | 
						|
            if lVertexColorLayer:
 | 
						|
                lVertexColor = GetVertexAttribute(lVertexColorLayer, lControlPointIndex, lVertexCount)
 | 
						|
                lVertexColor = [lVertexColor.mRed, lVertexColor.mGreen, lVertexColor.mBlue, lVertexColor.mAlpha]
 | 
						|
                lVertexColor = [round(i * 255) for i in lVertexColor]
 | 
						|
                if lNeedHash:
 | 
						|
                    vertexKeyList += lVertexColor
 | 
						|
            if lUvLayer:
 | 
						|
                # PENDING GetTextureUVIndex?
 | 
						|
                lUv = GetVertexAttribute(lUvLayer, lControlPointIndex, lVertexCount)
 | 
						|
                if lNeedHash:
 | 
						|
                    vertexKeyList += lUv
 | 
						|
            if lUv2Layer:
 | 
						|
                lUv2 = GetVertexAttribute(lUv2Layer, lControlPointIndex, lVertexCount)
 | 
						|
                if lNeedHash:
 | 
						|
                    vertexKeyList += lUv2
 | 
						|
 | 
						|
            lVertexCount += 1
 | 
						|
 | 
						|
            if lNeedHash:
 | 
						|
                vertexKey = tuple(vertexKeyList)
 | 
						|
            else:
 | 
						|
                vertexKey = lControlPointIndex
 | 
						|
 | 
						|
            if not vertexKey in lPrimitive['indicesMap']:
 | 
						|
                lIndex = len(lPrimitive['positions'])
 | 
						|
                lPrimitive['positions'].append(lPositions[lControlPointIndex])
 | 
						|
                if lNormalLayer and lNormal: # incase unsupported mapping mode returns none.
 | 
						|
                    lPrimitive['normals'].append(lNormal)
 | 
						|
                if lVertexColorLayer and lVertexColor: # incase unsupported mapping mode returns none.
 | 
						|
                    lPrimitive['vertexColors'].append(lVertexColor)
 | 
						|
                # PENDING
 | 
						|
                # Texcoord may be put in the second layer
 | 
						|
                if lPrimitive['useTexcoords1']:
 | 
						|
                    if lUv2Layer:
 | 
						|
                        if lUv2: # incase unsupported mapping mode returns none.
 | 
						|
                            lPrimitive['texcoords0'].append(lUv2)
 | 
						|
                    else:
 | 
						|
                        if lUv: # incase unsupported mapping mode returns none.
 | 
						|
                            lPrimitive['texcoords0'].append(lUv)
 | 
						|
                else:
 | 
						|
                    if lUvLayer:
 | 
						|
                        if lUv: # incase unsupported mapping mode returns none.
 | 
						|
                            lPrimitive['texcoords0'].append(lUv)
 | 
						|
                    if lUv2Layer:
 | 
						|
                        if lUv2: # incase unsupported mapping mode returns none.
 | 
						|
                            lPrimitive['texcoords1'].append(lUv2)
 | 
						|
                if hasSkin:
 | 
						|
                    lPrimitive['joints'].append(lJoints[lControlPointIndex])
 | 
						|
                    lPrimitive['weights'].append(lWeights[lControlPointIndex])
 | 
						|
 | 
						|
                lPrimitive['indicesMap'][vertexKey] = lIndex
 | 
						|
            else:
 | 
						|
                lIndex = lPrimitive['indicesMap'][vertexKey]
 | 
						|
 | 
						|
            lPrimitive['indices'].append(lIndex)
 | 
						|
 | 
						|
 | 
						|
    lGLTFPrimitivesList = []
 | 
						|
    for i in range(len(lPrimitivesList)):
 | 
						|
        lPrimitive = lPrimitivesList[i]
 | 
						|
        lGLTFPrimitive = {
 | 
						|
            'attributes': {
 | 
						|
                'POSITION': CreateAttributeBuffer(lPrimitive['positions'], 'f', 3)
 | 
						|
            },
 | 
						|
            'material': lPrimitive['material']
 | 
						|
        }
 | 
						|
        if len(lPrimitive['normals']) > 0:
 | 
						|
            lGLTFPrimitive['attributes']['NORMAL'] = CreateAttributeBuffer(lPrimitive['normals'], 'f', 3)
 | 
						|
        if len(lPrimitive['vertexColors']) > 0:
 | 
						|
            lGLTFPrimitive['attributes']['COLOR_0'] = CreateAttributeBuffer(lPrimitive['vertexColors'], 'B', 4, True)
 | 
						|
        if len(lPrimitive['texcoords0']) > 0:
 | 
						|
            ProcessUV(
 | 
						|
                lPrimitive['texcoords0'],
 | 
						|
                lPrimitive['scaleU'], lPrimitive['scaleV'],
 | 
						|
                lPrimitive['translationU'], lPrimitive['translationV']
 | 
						|
            )
 | 
						|
            lGLTFPrimitive['attributes']['TEXCOORD_0'] = CreateAttributeBuffer(lPrimitive['texcoords0'], 'f', 2)
 | 
						|
        if len(lPrimitive['texcoords1']) > 0:
 | 
						|
            ProcessUV(
 | 
						|
                lPrimitive['texcoords1'],
 | 
						|
                lPrimitive['scaleU'], lPrimitive['scaleV'],
 | 
						|
                lPrimitive['translationU'], lPrimitive['translationV']
 | 
						|
            )
 | 
						|
            lGLTFPrimitive['attributes']['TEXCOORD_1'] = CreateAttributeBuffer(lPrimitive['texcoords1'], 'f', 2)
 | 
						|
        if len(lPrimitive['joints']) > 0:
 | 
						|
            # PENDING UNSIGNED_SHORT will have bug.
 | 
						|
            lGLTFPrimitive['attributes']['JOINTS_0'] = CreateAttributeBuffer(lPrimitive['joints'], 'H', 4)
 | 
						|
            # TODO Seems most engines needs VEC4 weights.
 | 
						|
            lGLTFPrimitive['attributes']['WEIGHTS_0'] = CreateAttributeBuffer(lPrimitive['weights'], 'f', 4)
 | 
						|
 | 
						|
        if len(lPrimitive['positions']) >= 0xffff:
 | 
						|
            #Use unsigned int in element indices
 | 
						|
            lIndicesType = 'I'
 | 
						|
        else:
 | 
						|
            lIndicesType = 'H'
 | 
						|
        lGLTFPrimitive['indices'] = CreateIndicesBuffer(lPrimitive['indices'], lIndicesType)
 | 
						|
 | 
						|
        lGLTFPrimitivesList.append(lGLTFPrimitive)
 | 
						|
 | 
						|
    return lGLTFPrimitivesList
 | 
						|
 | 
						|
def ConvertCamera(pCamera):
 | 
						|
    lGLTFCamera = {}
 | 
						|
 | 
						|
    if pCamera.ProjectionType.Get() == FbxCamera.ePerspective:
 | 
						|
        lGLTFCamera['type'] = 'perspective'
 | 
						|
        lGLTFCamera['perspective'] = {
 | 
						|
            "yfov": pCamera.FieldOfView.Get(),
 | 
						|
            "znear": pCamera.NearPlane.Get(),
 | 
						|
            "zfar": pCamera.FarPlane.Get()
 | 
						|
        }
 | 
						|
    elif pCamera.ProjectionType.Get() == FbxCamera.eOrthogonal:
 | 
						|
        lGLTFCamera['type'] = 'orthographic'
 | 
						|
        lGLTFCamera['orthographic'] = {
 | 
						|
            # PENDING
 | 
						|
            "xmag": pCamera.OrthoZoom.Get(),
 | 
						|
            "ymag": pCamera.OrthoZoom.Get(),
 | 
						|
            "znear": pCamera.NearPlane.Get(),
 | 
						|
            "zfar": pCamera.FarPlane.Get()
 | 
						|
        }
 | 
						|
 | 
						|
    lCameraIdx = len(lib_cameras)
 | 
						|
    lib_cameras.append(lGLTFCamera)
 | 
						|
    return lCameraIdx
 | 
						|
 | 
						|
def ConvertSceneNode(pScene, pNode, pPoseTime):
 | 
						|
    lGLTFNode = {}
 | 
						|
    lNodeName = pNode.GetName()
 | 
						|
    lGLTFNode['name'] = pNode.GetName()
 | 
						|
 | 
						|
    lib_nodes.append(lGLTFNode)
 | 
						|
 | 
						|
    # Transform matrix
 | 
						|
    lGLTFNode['matrix'] = ListFromM4(pNode.EvaluateLocalTransform(pPoseTime, FbxNode.eDestinationPivot))
 | 
						|
 | 
						|
    #PENDING : Triangulate and split all geometry not only the default one ?
 | 
						|
    #PENDING : Multiple node use the same mesh ?
 | 
						|
    lMesh = pNode.GetMesh()
 | 
						|
    # PENDING If invisible node will have all children invisible.
 | 
						|
    if pNode.GetVisibility() and lMesh:
 | 
						|
        lMeshKey = lNodeName
 | 
						|
        lMeshName = lMesh.GetName()
 | 
						|
        if lMeshName == '':
 | 
						|
            lMeshName = lMeshKey
 | 
						|
 | 
						|
        lGLTFMesh = {'name' : lMeshName, "primitives": []}
 | 
						|
 | 
						|
        # If any attribute of this node have skinning data
 | 
						|
        # (Mesh splitted by material may have multiple MeshAttribute in one node)
 | 
						|
        lHasSkin = lMesh.GetDeformerCount(FbxDeformer.eSkin) > 0
 | 
						|
        lGLTFSkin = None
 | 
						|
        lClusters = {}
 | 
						|
 | 
						|
        if lHasSkin:
 | 
						|
            lSkinIdx = CreateSkin()
 | 
						|
            lGLTFSkin = lib_skins[lSkinIdx]
 | 
						|
            lGLTFNode['skin'] = lSkinIdx
 | 
						|
 | 
						|
        if lMesh.GetLayer(0):
 | 
						|
            for i in range(pNode.GetNodeAttributeCount()):
 | 
						|
                lNodeAttribute = pNode.GetNodeAttributeByIndex(i)
 | 
						|
                if lNodeAttribute.GetAttributeType() == FbxNodeAttribute.eMesh:
 | 
						|
                    lGLTFMesh['primitives'] += ConvertMesh(pScene, lNodeAttribute, pNode, lGLTFSkin, lClusters)
 | 
						|
 | 
						|
            lMeshIdx = len(lib_meshes)
 | 
						|
            lib_meshes.append(lGLTFMesh)
 | 
						|
            lGLTFNode['mesh'] = lMeshIdx
 | 
						|
 | 
						|
        if lHasSkin:
 | 
						|
            lClusterGlobalInitMatrix = FbxAMatrix()
 | 
						|
            lReferenceGlobalInitMatrix = FbxAMatrix()
 | 
						|
 | 
						|
            lIBM = []
 | 
						|
            for i in range(len(lGLTFSkin['joints'])):
 | 
						|
                lJointIdx = lGLTFSkin['joints'][i]
 | 
						|
                lCluster = lClusters[lJointIdx]
 | 
						|
 | 
						|
                # Inverse Bind Pose Matrix
 | 
						|
                # Matrix of Mesh
 | 
						|
                lCluster.GetTransformMatrix(lReferenceGlobalInitMatrix)
 | 
						|
                # Matrix of Joint
 | 
						|
                lCluster.GetTransformLinkMatrix(lClusterGlobalInitMatrix)
 | 
						|
                # http://blog.csdn.net/bugrunner/article/details/7232291
 | 
						|
                # http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref__view_scene_2_draw_scene_8cxx_example_html
 | 
						|
                m = lClusterGlobalInitMatrix.Inverse() * lReferenceGlobalInitMatrix
 | 
						|
                lIBM.append(m)
 | 
						|
 | 
						|
            lGLTFSkin['inverseBindMatrices'] = CreateIBMBuffer(lIBM)
 | 
						|
 | 
						|
    elif pNode.GetCamera():
 | 
						|
        # Camera attribute
 | 
						|
        lCameraKey = ConvertCamera(pNode.GetCamera())
 | 
						|
        lGLTFNode['camera'] = lCameraKey
 | 
						|
 | 
						|
    if pNode.GetChildCount() > 0:
 | 
						|
        lGLTFNode['children'] = []
 | 
						|
        for i in range(pNode.GetChildCount()):
 | 
						|
            lChildNodeIdx = ConvertSceneNode(pScene, pNode.GetChild(i), pPoseTime)
 | 
						|
            if lChildNodeIdx >= 0:
 | 
						|
                lGLTFNode['children'].append(lChildNodeIdx)
 | 
						|
 | 
						|
    return GetNodeIdx(pNode)
 | 
						|
 | 
						|
def ConvertScene(pScene, pPoseTime):
 | 
						|
    lRoot = pScene.GetRootNode()
 | 
						|
 | 
						|
    lGLTFScene = {'nodes' : []}
 | 
						|
 | 
						|
    lSceneIdx = len(lib_scenes)
 | 
						|
    lib_scenes.append(lGLTFScene)
 | 
						|
 | 
						|
    for i in range(lRoot.GetChildCount()):
 | 
						|
        lNodeIdx = ConvertSceneNode(pScene, lRoot.GetChild(i), pPoseTime)
 | 
						|
        if lNodeIdx >= 0:
 | 
						|
            lGLTFScene['nodes'].append(lNodeIdx)
 | 
						|
 | 
						|
    return lSceneIdx
 | 
						|
 | 
						|
def CreateAnimation(pName):
 | 
						|
    lAnimIdx = len(lib_animations)
 | 
						|
    lGLTFAnimation = {
 | 
						|
        'name': pName,
 | 
						|
        'channels' : [],
 | 
						|
        'samplers' : []
 | 
						|
    }
 | 
						|
 | 
						|
    return lAnimIdx, lGLTFAnimation
 | 
						|
 | 
						|
_samplerChannels = ['rotation', 'scale', 'translation']
 | 
						|
_timeSamplerHashMap = {}
 | 
						|
 | 
						|
def GetPropertyAnimationCurveTime(pAnimCurve):
 | 
						|
    lTimeSpan = FbxTimeSpan()
 | 
						|
    pAnimCurve.GetTimeInterval(lTimeSpan)
 | 
						|
    lStartTimeDouble = lTimeSpan.GetStart().GetSecondDouble()
 | 
						|
    lEndTimeDouble = lTimeSpan.GetStop().GetSecondDouble()
 | 
						|
    lDuration = lEndTimeDouble - lStartTimeDouble
 | 
						|
 | 
						|
    return lStartTimeDouble, lEndTimeDouble, lDuration
 | 
						|
 | 
						|
EPSILON = 1e-6
 | 
						|
def V3Same(a, b):
 | 
						|
    return abs(a[0] - b[0]) < EPSILON and abs(a[1] - b[1]) < EPSILON and abs(a[2] - b[2]) < EPSILON
 | 
						|
def V4Same(a, b):
 | 
						|
    return abs(a[0] - b[0]) < EPSILON and abs(a[1] - b[1]) < EPSILON and abs(a[2] - b[2]) < EPSILON and abs(a[3] - b[3]) < EPSILON
 | 
						|
def V3Middle(a, b):
 | 
						|
    return [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0, (a[2] + b[2]) / 2.0]
 | 
						|
def QuatSlerp(a, b, t):
 | 
						|
    [ax, ay, az, aw] = a
 | 
						|
    [bx, by, bz, bw] = b
 | 
						|
    ## calc cosine
 | 
						|
    cosom = ax * bx + ay * by + az * bz + aw * bw
 | 
						|
    ## adjust signs (if necessary)
 | 
						|
    if cosom < 0.0:
 | 
						|
        cosom = -cosom
 | 
						|
        bx = -bx
 | 
						|
        by = -by
 | 
						|
        bz = -bz
 | 
						|
        bw = -bw
 | 
						|
 | 
						|
    ## calculate coefficients
 | 
						|
    if 1.0 - cosom > 0.000001:
 | 
						|
        ## standard case (slerp)
 | 
						|
        omega = math.acos(cosom)
 | 
						|
        sinom = math.sin(omega)
 | 
						|
        scale0 = math.sin((1.0 - t) * omega) / float(sinom)
 | 
						|
        scale1 = math.sin(t * omega) / float(sinom)
 | 
						|
    else:
 | 
						|
        ## "from" and "to" quaternions are very close
 | 
						|
        ##  ... so we can do a linear interpolation
 | 
						|
        scale0 = 1.0 - t
 | 
						|
        scale1 = t
 | 
						|
    ## calculate final values
 | 
						|
    return [scale0 * ax + scale1 * bx, scale0 * ay + scale1 * by, scale0 * az + scale1 * bz, scale0 * aw + scale1 * bw]
 | 
						|
 | 
						|
def FitLinearInterpolation(pTime, pTranslationChannel, pRotationChannel, pScaleChannel):
 | 
						|
    lTranslationChannel = []
 | 
						|
    lRotationChannel = []
 | 
						|
    lScaleChannel = []
 | 
						|
    lTime = []
 | 
						|
    lHaveRotation = len(pRotationChannel) > 0
 | 
						|
    lHaveScale = len(pScaleChannel) > 0
 | 
						|
    lHaveTranslation = len(pTranslationChannel) > 0
 | 
						|
    if lHaveRotation:
 | 
						|
        lRotationChannel.append(pRotationChannel[0])
 | 
						|
    if lHaveScale:
 | 
						|
        lScaleChannel.append(pScaleChannel[0])
 | 
						|
    if lHaveTranslation:
 | 
						|
        lTranslationChannel.append(pTranslationChannel[0])
 | 
						|
    lTime.append(pTime[0])
 | 
						|
    for i in range(len(pTime)):
 | 
						|
        lLinearInterpolated = True
 | 
						|
        if i > 1:
 | 
						|
            if lHaveTranslation:
 | 
						|
                if not V3Same(V3Middle(pTranslationChannel[i - 2], pTranslationChannel[i]), pTranslationChannel[i - 1]):
 | 
						|
                    lLinearInterpolated = False
 | 
						|
            if lHaveScale and lLinearInterpolated:
 | 
						|
                if not V3Same(V3Middle(pScaleChannel[i - 2], pScaleChannel[i]), pScaleChannel[i - 1]):
 | 
						|
                    lLinearInterpolated = False
 | 
						|
            if lHaveRotation:
 | 
						|
                if not V4Same(QuatSlerp(pRotationChannel[i - 2], pRotationChannel[i], 0.5), pRotationChannel[i - 1]):
 | 
						|
                    lLinearInterpolated = False
 | 
						|
 | 
						|
        if not lLinearInterpolated:
 | 
						|
            if lHaveTranslation:
 | 
						|
                lTranslationChannel.append(pTranslationChannel[i - 1])
 | 
						|
            if lHaveRotation:
 | 
						|
                lRotationChannel.append(pRotationChannel[i - 1])
 | 
						|
            if lHaveScale:
 | 
						|
                lScaleChannel.append(pScaleChannel[i - 1])
 | 
						|
            lTime.append(pTime[i - 1])
 | 
						|
 | 
						|
    if len(pTime) > 1:
 | 
						|
        if lHaveRotation:
 | 
						|
            lRotationChannel.append(pRotationChannel[len(pRotationChannel) - 1])
 | 
						|
        if lHaveScale:
 | 
						|
            lScaleChannel.append(pScaleChannel[len(pScaleChannel) - 1])
 | 
						|
        if lHaveTranslation:
 | 
						|
            lTranslationChannel.append(pTranslationChannel[len(pTranslationChannel) - 1])
 | 
						|
 | 
						|
        lTime.append(pTime[len(pTime) - 1])
 | 
						|
 | 
						|
    return lTime, lTranslationChannel, lRotationChannel, lScaleChannel
 | 
						|
 | 
						|
 | 
						|
def ConvertNodeAnimation(pGLTFAnimation, pAnimLayer, pNode, pSampleRate, pStartTime, pDuration):
 | 
						|
    lNodeIdx = GetNodeIdx(pNode)
 | 
						|
 | 
						|
    curves = [
 | 
						|
        pNode.LclTranslation.GetCurve(pAnimLayer, 'X'),
 | 
						|
        pNode.LclTranslation.GetCurve(pAnimLayer, 'Y'),
 | 
						|
        pNode.LclTranslation.GetCurve(pAnimLayer, 'Z'),
 | 
						|
 | 
						|
        pNode.LclRotation.GetCurve(pAnimLayer, 'X'),
 | 
						|
        pNode.LclRotation.GetCurve(pAnimLayer, 'Y'),
 | 
						|
        pNode.LclRotation.GetCurve(pAnimLayer, 'Z'),
 | 
						|
 | 
						|
        pNode.LclScaling.GetCurve(pAnimLayer, 'X'),
 | 
						|
        pNode.LclScaling.GetCurve(pAnimLayer, 'Y'),
 | 
						|
        pNode.LclScaling.GetCurve(pAnimLayer, 'Z'),
 | 
						|
    ]
 | 
						|
 | 
						|
    lHaveTranslation = any(curves[0:3])
 | 
						|
    lHaveRotation = any(curves[3:6])
 | 
						|
    lHaveScaling = any(curves[6:9])
 | 
						|
 | 
						|
    # Curve time span may much smaller than stack local time span
 | 
						|
    # It can reduce a lot of space
 | 
						|
    # PENDING
 | 
						|
    lStartTimeDouble = 1000000
 | 
						|
    lDuration = 0
 | 
						|
    lEndTimeDouble = 0
 | 
						|
    for curve in curves:
 | 
						|
        if not curve == None:
 | 
						|
            lCurveStart, lCurveEnd, lCurveDuration = GetPropertyAnimationCurveTime(curve)
 | 
						|
            lStartTimeDouble = min(lCurveStart, lStartTimeDouble)
 | 
						|
            lEndTimeDouble = max(lCurveEnd, lEndTimeDouble)
 | 
						|
            lDuration = max(lCurveDuration, lDuration)
 | 
						|
 | 
						|
    lDuration = min(lDuration, pDuration)
 | 
						|
    lStartTimeDouble = max(lStartTimeDouble, pStartTime)
 | 
						|
 | 
						|
    if lDuration > 0:
 | 
						|
        lNumFrames = int(math.ceil(lDuration / float(pSampleRate)))
 | 
						|
 | 
						|
        lTime = FbxTime()
 | 
						|
 | 
						|
        lTimeChannel = []
 | 
						|
        lTranslationChannel = []
 | 
						|
        lRotationChannel = []
 | 
						|
        lScaleChannel = []
 | 
						|
 | 
						|
        lQuaternion = FbxQuaternion()
 | 
						|
        for i in range(lNumFrames):
 | 
						|
            lSecondDouble = min(lStartTimeDouble + pSampleRate * i, lEndTimeDouble)
 | 
						|
            lTime.SetSecondDouble(lSecondDouble)
 | 
						|
 | 
						|
            lTransform = pNode.EvaluateLocalTransform(lTime, FbxNode.eDestinationPivot)
 | 
						|
            lTranslation = lTransform.GetT()
 | 
						|
            lQuaternion = lTransform.GetQ()
 | 
						|
            lScale = lTransform.GetS()
 | 
						|
 | 
						|
            # Convert quaternion to axis angle
 | 
						|
            # PENDING. minus pStartTime or lStartTimeDouble?
 | 
						|
            lTimeChannel.append(lSecondDouble - pStartTime)
 | 
						|
 | 
						|
            if lHaveRotation:
 | 
						|
                lRotationChannel.append(list(lQuaternion))
 | 
						|
            if lHaveTranslation:
 | 
						|
                lTranslationChannel.append(list(lTranslation))
 | 
						|
            if lHaveScaling:
 | 
						|
                lScaleChannel.append(list(lScale))
 | 
						|
 | 
						|
        lTimeChannel, lTranslationChannel, lRotationChannel, lScaleChannel = FitLinearInterpolation(
 | 
						|
            lTimeChannel, lTranslationChannel, lRotationChannel, lScaleChannel
 | 
						|
        )
 | 
						|
 | 
						|
        # TODO Performance?
 | 
						|
        lTimeAccessorKey = tuple(lTimeChannel)
 | 
						|
        if not lTimeAccessorKey in _timeSamplerHashMap:
 | 
						|
            # TODO use ubyte.
 | 
						|
            _timeSamplerHashMap[lTimeAccessorKey] = CreateAnimationBuffer(lTimeChannel, 'f', 1)
 | 
						|
 | 
						|
        lSamplerAccessors = {
 | 
						|
            "time": _timeSamplerHashMap[lTimeAccessorKey]
 | 
						|
            # "time": CreateAnimationBuffer(lTimeChannel, 'f', 1)
 | 
						|
        }
 | 
						|
        if lHaveTranslation:
 | 
						|
            lAccessorIdx = CreateAnimationBuffer(lTranslationChannel, 'f', 3)
 | 
						|
            if lAccessorIdx >= 0:
 | 
						|
                lSamplerAccessors['translation'] = lAccessorIdx
 | 
						|
        if lHaveRotation:
 | 
						|
            lAccessorIdx = CreateAnimationBuffer(lRotationChannel, 'f', 4)
 | 
						|
            if lAccessorIdx >= 0:
 | 
						|
                lSamplerAccessors['rotation'] = lAccessorIdx
 | 
						|
        if lHaveScaling:
 | 
						|
            lAccessorIdx = CreateAnimationBuffer(lScaleChannel, 'f', 3)
 | 
						|
            if lAccessorIdx >= 0:
 | 
						|
                lSamplerAccessors['scale'] = lAccessorIdx
 | 
						|
 | 
						|
        #TODO Other interpolation methods
 | 
						|
        for path in _samplerChannels:
 | 
						|
            if path in lSamplerAccessors:
 | 
						|
                lSamplerIdx = len(pGLTFAnimation['samplers'])
 | 
						|
                pGLTFAnimation['samplers'].append({
 | 
						|
                    "input": lSamplerAccessors['time'],
 | 
						|
                    "interpolation": "LINEAR",
 | 
						|
                    "output": lSamplerAccessors[path]
 | 
						|
                })
 | 
						|
                pGLTFAnimation['channels'].append({
 | 
						|
                    "sampler" : lSamplerIdx,
 | 
						|
                    "target" : {
 | 
						|
                        "node": lNodeIdx,
 | 
						|
                        "path" : path
 | 
						|
                    }
 | 
						|
                })
 | 
						|
 | 
						|
    for i in range(pNode.GetChildCount()):
 | 
						|
        ConvertNodeAnimation(pGLTFAnimation, pAnimLayer, pNode.GetChild(i), pSampleRate, pStartTime, pDuration)
 | 
						|
 | 
						|
def ConvertAnimation(pScene, pSampleRate, pStartTime, pDuration):
 | 
						|
    lRoot = pScene.GetRootNode()
 | 
						|
    for i in range(pScene.GetSrcObjectCount(FbxCriteria.ObjectType(FbxAnimStack.ClassId))):
 | 
						|
        lAnimStack = pScene.GetSrcObject(FbxCriteria.ObjectType(FbxAnimStack.ClassId), i)
 | 
						|
        lAnimIdx, lGLTFAnimation = CreateAnimation(lAnimStack.GetName())
 | 
						|
        for j in range(lAnimStack.GetSrcObjectCount(FbxCriteria.ObjectType(FbxAnimLayer.ClassId))):
 | 
						|
            lAnimLayer = lAnimStack.GetSrcObject(FbxCriteria.ObjectType(FbxAnimLayer.ClassId), j)
 | 
						|
            # for k in range(lRoot.GetChildCount()):
 | 
						|
            ConvertNodeAnimation(lGLTFAnimation, lAnimLayer, lRoot, pSampleRate, pStartTime, pDuration)
 | 
						|
        if len(lGLTFAnimation['samplers']) > 0:
 | 
						|
            lib_animations.append(lGLTFAnimation)
 | 
						|
 | 
						|
 | 
						|
def CreateBufferView(pBufferIdx, pBuffer, appendBufferData, lib, pByteOffset, target=GL_ARRAY_BUFFER):
 | 
						|
    if pByteOffset % 4 == 2:
 | 
						|
        pBuffer.extend(b'\x00\x00')
 | 
						|
        pByteOffset += 2
 | 
						|
 | 
						|
    pBuffer.extend(appendBufferData)
 | 
						|
    lBufferViewIdx = len(lib_buffer_views)
 | 
						|
    lBufferView = {
 | 
						|
        "buffer": pBufferIdx,
 | 
						|
        "byteLength": len(appendBufferData),
 | 
						|
        "byteOffset": pByteOffset,
 | 
						|
        # PENDING
 | 
						|
        # "byteStride": 0,
 | 
						|
        "target": target
 | 
						|
    }
 | 
						|
    lib_buffer_views.append(lBufferView)
 | 
						|
    for lAttrib in lib:
 | 
						|
        lAttrib['bufferView'] = lBufferViewIdx
 | 
						|
 | 
						|
    return lBufferView
 | 
						|
 | 
						|
 | 
						|
def CreateBufferViews(pBufferIdx, pBin):
 | 
						|
 | 
						|
    lByteOffset = CreateBufferView(pBufferIdx, pBin, attributeBuffer, lib_attributes_accessors, 0)['byteLength']
 | 
						|
 | 
						|
    if len(lib_ibm_accessors) > 0:
 | 
						|
        lByteOffset += CreateBufferView(pBufferIdx, pBin, invBindMatricesBuffer, lib_ibm_accessors, lByteOffset)['byteLength']
 | 
						|
 | 
						|
    if len(lib_animation_accessors) > 0:
 | 
						|
        lByteOffset += CreateBufferView(pBufferIdx, pBin, animationBuffer, lib_animation_accessors, lByteOffset)['byteLength']
 | 
						|
 | 
						|
    #When creating a Float32Array, which the offset must be multiple of 4
 | 
						|
    CreateBufferView(pBufferIdx, pBin, indicesBuffer, lib_indices_accessors, lByteOffset, GL_ELEMENT_ARRAY_BUFFER)
 | 
						|
 | 
						|
 | 
						|
# Start from -1 and ignore the root node
 | 
						|
_nodeCount = -1
 | 
						|
_nodeIdxMap = {}
 | 
						|
def PrepareSceneNode(pNode):
 | 
						|
    global _nodeCount
 | 
						|
    _nodeIdxMap[pNode.GetUniqueID()] = _nodeCount
 | 
						|
    _nodeCount = _nodeCount + 1
 | 
						|
 | 
						|
    for k in range(pNode.GetChildCount()):
 | 
						|
        PrepareSceneNode(pNode.GetChild(k))
 | 
						|
 | 
						|
# Each node can have two pivot context. The node's animation data can be converted from one pivot context to the other
 | 
						|
# Convert source pivot to destination with all zero pivot.
 | 
						|
# http://docs.autodesk.com/FBX/2013/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/class_fbx_node.html,topicNumber=cpp_ref_class_fbx_node_html
 | 
						|
def PrepareBakeTransform(pNode):
 | 
						|
    # http://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_C35D98CB_5148_4B46_82D1_51077D8970EE_htm
 | 
						|
    pNode.SetPivotState(FbxNode.eSourcePivot, FbxNode.ePivotActive)
 | 
						|
    pNode.SetPivotState(FbxNode.eDestinationPivot, FbxNode.ePivotActive)
 | 
						|
 | 
						|
    lZero = FbxVector4(0, 0, 0)
 | 
						|
    pNode.SetPostRotation(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetPreRotation(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetRotationOffset(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetScalingOffset(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetRotationPivot(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetScalingPivot(FbxNode.eDestinationPivot, lZero);
 | 
						|
 | 
						|
    pNode.SetGeometricTranslation(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetGeometricRotation(FbxNode.eDestinationPivot, lZero);
 | 
						|
    pNode.SetGeometricScaling(FbxNode.eDestinationPivot, FbxVector4(1, 1, 1));
 | 
						|
    # pNode.SetUseQuaternionForInterpolation(FbxNode.eDestinationPivot, pNode.GetUseQuaternionForInterpolation(FbxNode.eSourcePivot));
 | 
						|
 | 
						|
    for k in range(pNode.GetChildCount()):
 | 
						|
        PrepareBakeTransform(pNode.GetChild(k))
 | 
						|
 | 
						|
 | 
						|
def GetNodeIdx(pNode):
 | 
						|
    lId = pNode.GetUniqueID()
 | 
						|
    if not lId in _nodeIdxMap:
 | 
						|
        return -1
 | 
						|
    return _nodeIdxMap[lId]
 | 
						|
 | 
						|
 | 
						|
def FindFileInDir(pFileName, pDir):
 | 
						|
    for root, dirs, files in os.walk(pDir):
 | 
						|
        for file in files:
 | 
						|
            if file == pFileName:
 | 
						|
                return os.path.join(root, file)
 | 
						|
 | 
						|
 | 
						|
def CorrectImagesPaths(pFilePath):
 | 
						|
    lFileFullPath = os.path.join(os.getcwd(), pFilePath)
 | 
						|
    lFileExtension = pFilePath.rsplit('.', 1)[1].lower()
 | 
						|
    for lGLTFImage in lib_images:
 | 
						|
        lUri = lGLTFImage['uri']
 | 
						|
        lUri = lUri.replace(r'[\\\/]+', os.path.sep)
 | 
						|
        # FBX SDK extracts zip input files to temp folder, so use lGLTFImage uri instead to find temp folder
 | 
						|
        if lFileExtension == 'zip':
 | 
						|
            lFileDir = os.path.dirname(lGLTFImage['uri'])
 | 
						|
        else:
 | 
						|
            lFileDir = os.path.dirname(lFileFullPath)
 | 
						|
        lUri = FindFileInDir(os.path.basename(lUri), lFileDir)
 | 
						|
        if lUri:
 | 
						|
            lRelUri = os.path.relpath(lUri, lFileDir)
 | 
						|
            # If an alternative output directory is specified, copy all textures to output directory
 | 
						|
            if lOutputDirSpecified:
 | 
						|
                lOutputDir = os.path.dirname(args.output)
 | 
						|
                # If textures are in a dir and that dir does not yet exist, create it
 | 
						|
                lRelTextureDir = os.path.dirname(lRelUri)
 | 
						|
                lFullTextureDir = os.path.join(lOutputDir, lRelTextureDir)
 | 
						|
                if not os.path.exists(lFullTextureDir):
 | 
						|
                    os.makedirs(lFullTextureDir)
 | 
						|
                shutil.copyfile(lUri, os.path.join(lOutputDir, lRelUri))
 | 
						|
            if not lRelUri == lGLTFImage['uri']:
 | 
						|
                print('Changed texture file path from "' + lGLTFImage['uri'] + '" to "' + lRelUri + '"')
 | 
						|
            lGLTFImage['uri'] = lRelUri
 | 
						|
        else:
 | 
						|
            print("Can\'t find texture file in the folder, path: " + lGLTFImage['uri'])
 | 
						|
 | 
						|
 | 
						|
def EmbedImagesToBinary(pBuffer, pFilePath):
 | 
						|
    lFileFullPath = os.path.join(os.getcwd(), pFilePath)
 | 
						|
    lFileDir = os.path.dirname(lFileFullPath)
 | 
						|
    for lGLTFImage in lib_images:
 | 
						|
        lUri = lGLTFImage['uri']
 | 
						|
        lImgBytes = None
 | 
						|
 | 
						|
        if not os.path.isfile(lUri):
 | 
						|
            lUri = lUri.replace(r'[\\\/]+', os.path.sep)
 | 
						|
            lUri = FindFileInDir(os.path.basename(lUri), lFileDir)
 | 
						|
        try:
 | 
						|
            f = open(lUri, 'rb')
 | 
						|
            lImgBytes = f.read()
 | 
						|
        except:
 | 
						|
            print("Can\'t find texture file in the folder, path: " + lGLTFImage['uri'])
 | 
						|
 | 
						|
        if not lImgBytes:
 | 
						|
            continue
 | 
						|
 | 
						|
        lBufferViewIdx = len(lib_buffer_views)
 | 
						|
 | 
						|
        lGLTFImage['bufferView'] = lBufferViewIdx
 | 
						|
        del lGLTFImage['uri']
 | 
						|
 | 
						|
        lBufferView = {
 | 
						|
            'buffer': 0,
 | 
						|
            'byteLength': len(lImgBytes),
 | 
						|
            'byteOffset': len(pBuffer)
 | 
						|
            # TODO Mime type
 | 
						|
        }
 | 
						|
 | 
						|
        lib_buffer_views.append(lBufferView)
 | 
						|
 | 
						|
        pBuffer.extend(lImgBytes)
 | 
						|
        # 4-byte-aligned
 | 
						|
        lAlignedLen = (len(lImgBytes) + 3) & ~3
 | 
						|
        for i in range(lAlignedLen - len(lImgBytes)):
 | 
						|
            pBuffer.extend(b' ')
 | 
						|
 | 
						|
    return pBuffer
 | 
						|
 | 
						|
# FIXME
 | 
						|
# http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_fbxtime_8h_html
 | 
						|
TIME_INFINITY = FbxTime(0x7fffffffffffffff)
 | 
						|
 | 
						|
def Convert(
 | 
						|
    filePath,
 | 
						|
    ouptutFile = '',
 | 
						|
    excluded = [],
 | 
						|
    animFrameRate = 1.0 / 20.0,
 | 
						|
    startTime = 0,
 | 
						|
    duration = 1000,
 | 
						|
    poseTime = TIME_INFINITY,
 | 
						|
    beautify = False,
 | 
						|
    binary = False
 | 
						|
):
 | 
						|
    ignoreScene = 'scene' in excluded
 | 
						|
    ignoreAnimation = 'animation' in excluded
 | 
						|
    # Prepare the FBX SDK.
 | 
						|
    lSdkManager, lScene = InitializeSdkObjects()
 | 
						|
    fbxConverter = FbxGeometryConverter(lSdkManager)
 | 
						|
    # Load the scene.
 | 
						|
    lResult = LoadScene(lSdkManager, lScene, filePath)
 | 
						|
 | 
						|
    if not lResult:
 | 
						|
        print("\n\nAn error occurred while loading the scene...")
 | 
						|
    else:
 | 
						|
        lBasename, lExt = os.path.splitext(ouptutFile)
 | 
						|
 | 
						|
        # PENDING, if it will affect the conversion after.
 | 
						|
        FbxAxisSystem.OpenGL.ConvertScene(lScene)
 | 
						|
 | 
						|
        # Do it before SplitMeshesPerMaterial or the vertices of split mesh will be wrong.
 | 
						|
        PrepareBakeTransform(lScene.GetRootNode())
 | 
						|
        lScene.GetRootNode().ConvertPivotAnimationRecursive(None, FbxNode.eDestinationPivot, 60)
 | 
						|
 | 
						|
        # PENDING Triangulate before SplitMeshesPerMaterial or it will not work.
 | 
						|
        fbxConverter.Triangulate(lScene, True)
 | 
						|
 | 
						|
        # SplitMeshPerMaterial will fail if the mapped material is not per face (FbxLayerElement::eByPolygon) or if a material is multi-layered.
 | 
						|
        # http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_geometry_converter_html
 | 
						|
        # TODO May have bug
 | 
						|
        # if not fbxConverter.SplitMeshesPerMaterial(lScene, True):
 | 
						|
        #     print('SplitMeshesPerMaterial fail')
 | 
						|
 | 
						|
        PrepareSceneNode(lScene.GetRootNode())
 | 
						|
 | 
						|
        if not ignoreScene:
 | 
						|
            lSceneIdx = ConvertScene(lScene, poseTime)
 | 
						|
        if not ignoreAnimation:
 | 
						|
            ConvertAnimation(lScene, animFrameRate, startTime, duration)
 | 
						|
 | 
						|
        #Merge binary data and write to a binary file
 | 
						|
        lBin = bytearray()
 | 
						|
 | 
						|
        CreateBufferViews(0, lBin)
 | 
						|
 | 
						|
        if binary:
 | 
						|
            lBin = EmbedImagesToBinary(lBin, filePath)
 | 
						|
        else:
 | 
						|
            CorrectImagesPaths(filePath)
 | 
						|
 | 
						|
        lBufferName = lBasename + '.bin'
 | 
						|
        if binary:
 | 
						|
            lib_buffers.append({
 | 
						|
                'byteLength' : len(lBin)
 | 
						|
            })
 | 
						|
        else:
 | 
						|
            lib_buffers.append({
 | 
						|
                'byteLength' : len(lBin),
 | 
						|
                'uri' : os.path.basename(lBufferName)
 | 
						|
            })
 | 
						|
 | 
						|
        #Output json
 | 
						|
        lJSON = {
 | 
						|
            'asset': {
 | 
						|
                'generator': 'ClayGL - fbx2gltf',
 | 
						|
                'version': '2.0'
 | 
						|
            },
 | 
						|
            'accessors' : lib_accessors,
 | 
						|
            'bufferViews' : lib_buffer_views,
 | 
						|
            'buffers' : lib_buffers,
 | 
						|
            'nodes' : lib_nodes,
 | 
						|
            'scenes' : lib_scenes,
 | 
						|
            'meshes' : lib_meshes,
 | 
						|
        }
 | 
						|
        if len(lib_cameras) > 0:
 | 
						|
            lJSON['cameras'] = lib_cameras
 | 
						|
        if len(lib_skins) > 0:
 | 
						|
            lJSON['skins'] = lib_skins
 | 
						|
        if len(lib_materials) > 0:
 | 
						|
            lJSON['materials'] = lib_materials
 | 
						|
        if len(lib_images) > 0:
 | 
						|
            lJSON['images'] = lib_images
 | 
						|
        if len(lib_samplers) > 0:
 | 
						|
            lJSON['samplers'] = lib_samplers
 | 
						|
        if len(lib_textures) > 0:
 | 
						|
            lJSON['textures'] = lib_textures
 | 
						|
        if len(lib_animations) > 0:
 | 
						|
            lJSON['animations'] = lib_animations
 | 
						|
        #Default scene
 | 
						|
        if not ignoreScene:
 | 
						|
            lJSON['scene'] = lSceneIdx
 | 
						|
 | 
						|
        if binary:
 | 
						|
            lOutFile = open(ouptutFile, 'wb')
 | 
						|
            lJSONStr = json.dumps(lJSON, sort_keys = True, separators=(',', ':'))
 | 
						|
            lJSONBinary = bytearray(lJSONStr.encode(encoding='UTF-8'))
 | 
						|
            # 4-byte-aligned
 | 
						|
            lAlignedLen = (len(lJSONBinary) + 3) & ~3
 | 
						|
            for i in range(lAlignedLen - len(lJSONBinary)):
 | 
						|
                lJSONBinary.extend(b' ')
 | 
						|
 | 
						|
            lOut = bytearray()
 | 
						|
            lSize = 12 + 8 + len(lJSONBinary) + 8 + len(lBin)
 | 
						|
            # Magic number
 | 
						|
            lOut.extend(struct.pack('<I', 0x46546C67))
 | 
						|
            lOut.extend(struct.pack('<I', 2))
 | 
						|
            lOut.extend(struct.pack('<I', lSize))
 | 
						|
            lOut.extend(struct.pack('<I', len(lJSONBinary)))
 | 
						|
            lOut.extend(struct.pack('<I', 0x4E4F534A))
 | 
						|
            lOut += lJSONBinary
 | 
						|
            lOut.extend(struct.pack('<I', len(lBin)))
 | 
						|
            lOut.extend(struct.pack('<I', 0x004E4942))
 | 
						|
            lOut += lBin
 | 
						|
            lOutFile.write(lOut)
 | 
						|
            lOutFile.close()
 | 
						|
 | 
						|
        else:
 | 
						|
            lOutFile = open(ouptutFile, 'w')
 | 
						|
            lBinFile = open(lBasename + ".bin", 'wb')
 | 
						|
            lBinFile.write(lBin)
 | 
						|
            lBinFile.close()
 | 
						|
 | 
						|
            indent = None
 | 
						|
            seperator = ':'
 | 
						|
 | 
						|
            if beautify:
 | 
						|
                indent = 2
 | 
						|
                seperator = ': '
 | 
						|
            lOutFile.write(json.dumps(lJSON, indent = indent, sort_keys = True, separators=(',', seperator)))
 | 
						|
            lOutFile.close()
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
 | 
						|
    parser = argparse.ArgumentParser(description='FBX to glTF converter', add_help=True)
 | 
						|
    parser.add_argument('-e', '--exclude', type=str, default='', help="Data excluded. Can be: scene,animation")
 | 
						|
    parser.add_argument('-t', '--timerange', default='0,1000', type=str, help="Export animation time, in format 'startSecond,endSecond'")
 | 
						|
    parser.add_argument('-o', '--output', default='', type=str, help="Ouput glTF file path")
 | 
						|
    parser.add_argument('-f', '--framerate', default=20, type=float, help="Animation frame per second")
 | 
						|
    parser.add_argument('-p', '--pose', default=0, type=float, help="Start pose time")
 | 
						|
    parser.add_argument('-q', '--quantize', action='store_true', help="Quantize accessors with WEB3D_quantized_attributes extension")
 | 
						|
    parser.add_argument('-b', '--binary', action="store_true", help="Export glTF-binary")
 | 
						|
    parser.add_argument('--beautify', action="store_true", help="Beautify json output.")
 | 
						|
    parser.add_argument('--noflipv', action="store_true", help="If not flip v in texcoord.")
 | 
						|
    parser.add_argument('file')
 | 
						|
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    lStartTime = 0
 | 
						|
    lDuration = 1000
 | 
						|
    lTimeRange = args.timerange.split(',')
 | 
						|
    if lTimeRange[0]:
 | 
						|
        lStartTime = float(lTimeRange[0])
 | 
						|
    if lTimeRange[1]:
 | 
						|
        lDuration = float(lTimeRange[1])
 | 
						|
 | 
						|
    if not args.output:
 | 
						|
        lOutputDirSpecified = False
 | 
						|
        lBasename, lExt = os.path.splitext(args.file)
 | 
						|
        if args.binary:
 | 
						|
            args.output = lBasename + '.glb'
 | 
						|
        else:
 | 
						|
            args.output = lBasename + '.gltf'
 | 
						|
    else:
 | 
						|
        lOutputDirSpecified = True
 | 
						|
 | 
						|
    # PENDING Not use INFINITY poseTime or some joint transform without animation maybe not right.
 | 
						|
    lPoseTime = FbxTime()
 | 
						|
    lPoseTime.SetSecondDouble(float(args.pose))
 | 
						|
 | 
						|
    excluded = args.exclude.split(',')
 | 
						|
 | 
						|
    ENV_QUANTIZE = args.quantize
 | 
						|
    ENV_FLIP_V = not args.noflipv
 | 
						|
 | 
						|
    Convert(
 | 
						|
        args.file,
 | 
						|
        args.output,
 | 
						|
        excluded,
 | 
						|
        1.0 / float(args.framerate),
 | 
						|
        lStartTime,
 | 
						|
        lDuration,
 | 
						|
        lPoseTime,
 | 
						|
        args.beautify,
 | 
						|
        args.binary
 | 
						|
    )
 |