Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1581 lines (1347 sloc) 57.5 KB
#!/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
)