# @author zfedoran / http://github.com/zfedoran import os import sys import math import operator import re import json import types import shutil # ##################################################### # Globals # ##################################################### option_triangulate = True option_textures = True option_copy_textures = True option_prefix = True option_geometry = False option_forced_y_up = False option_default_camera = False option_default_light = False option_pretty_print = False converter = None inputFolder = "" outputFolder = "" # ##################################################### # Pretty Printing Hacks # ##################################################### # Force an array to be printed fully on a single line class NoIndent(object): def __init__(self, value, separator = ','): self.separator = separator self.value = value def encode(self): if not self.value: return None return '[ %s ]' % self.separator.join(str(f) for f in self.value) # Force an array into chunks rather than printing each element on a new line class ChunkedIndent(object): def __init__(self, value, chunk_size = 15, force_rounding = False): self.value = value self.size = chunk_size self.force_rounding = force_rounding def encode(self): # Turn the flat array into an array of arrays where each subarray is of # length chunk_size. Then string concat the values in the chunked # arrays, delimited with a ', ' and round the values finally append # '{CHUNK}' so that we can find the strings with regex later if not self.value: return None if self.force_rounding: return ['{CHUNK}%s' % ', '.join(str(round(f, 6)) for f in self.value[i:i+self.size]) for i in range(0, len(self.value), self.size)] else: return ['{CHUNK}%s' % ', '.join(str(f) for f in self.value[i:i+self.size]) for i in range(0, len(self.value), self.size)] # This custom encoder looks for instances of NoIndent or ChunkedIndent. # When it finds class CustomEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, NoIndent) or isinstance(obj, ChunkedIndent): return obj.encode() else: return json.JSONEncoder.default(self, obj) def executeRegexHacks(output_string): # turn strings of arrays into arrays (remove the double quotes) output_string = re.sub(':\s*\"(\[.*\])\"', r': \1', output_string) output_string = re.sub('(\n\s*)\"(\[.*\])\"', r'\1\2', output_string) output_string = re.sub('(\n\s*)\"{CHUNK}(.*)\"', r'\1\2', output_string) # replace '0metadata' with metadata output_string = re.sub('0metadata', r'metadata', output_string) # replace 'zchildren' with children output_string = re.sub('zchildren', r'children', output_string) # add an extra newline after '"children": {' output_string = re.sub('(children.*{\s*\n)', r'\1\n', output_string) # add an extra newline after '},' output_string = re.sub('},\s*\n', r'},\n\n', output_string) # add an extra newline after '\n\s*],' output_string = re.sub('(\n\s*)],\s*\n', r'\1],\n\n', output_string) return output_string # ##################################################### # Object Serializers # ##################################################### # FbxVector2 is not JSON serializable def serializeVector2(v, round_vector = False): # JSON does not support NaN or Inf if math.isnan(v[0]) or math.isinf(v[0]): v[0] = 0 if math.isnan(v[1]) or math.isinf(v[1]): v[1] = 0 if round_vector or option_pretty_print: v = (round(v[0], 5), round(v[1], 5)) if option_pretty_print: return NoIndent([v[0], v[1]], ', ') else: return [v[0], v[1]] # FbxVector3 is not JSON serializable def serializeVector3(v, round_vector = False): # JSON does not support NaN or Inf if math.isnan(v[0]) or math.isinf(v[0]): v[0] = 0 if math.isnan(v[1]) or math.isinf(v[1]): v[1] = 0 if math.isnan(v[2]) or math.isinf(v[2]): v[2] = 0 if round_vector or option_pretty_print: v = (round(v[0], 5), round(v[1], 5), round(v[2], 5)) if option_pretty_print: return NoIndent([v[0], v[1], v[2]], ', ') else: return [v[0], v[1], v[2]] # FbxVector4 is not JSON serializable def serializeVector4(v, round_vector = False): # JSON does not support NaN or Inf if math.isnan(v[0]) or math.isinf(v[0]): v[0] = 0 if math.isnan(v[1]) or math.isinf(v[1]): v[1] = 0 if math.isnan(v[2]) or math.isinf(v[2]): v[2] = 0 if math.isnan(v[3]) or math.isinf(v[3]): v[3] = 0 if round_vector or option_pretty_print: v = (round(v[0], 5), round(v[1], 5), round(v[2], 5), round(v[3], 5)) if option_pretty_print: return NoIndent([v[0], v[1], v[2], v[3]], ', ') else: return [v[0], v[1], v[2], v[3]] # ##################################################### # Helpers # ##################################################### def getRadians(v): return ((v[0]*math.pi)/180, (v[1]*math.pi)/180, (v[2]*math.pi)/180) def getHex(c): color = (int(c[0]*255) << 16) + (int(c[1]*255) << 8) + int(c[2]*255) return int(color) def setBit(value, position, on): if on: mask = 1 << position return (value | mask) else: mask = ~(1 << position) return (value & mask) def generate_uvs(uv_layers): layers = [] for uvs in uv_layers: tmp = [] for uv in uvs: tmp.append(uv[0]) tmp.append(uv[1]) if option_pretty_print: layer = ChunkedIndent(tmp) else: layer = tmp layers.append(layer) return layers # ##################################################### # Object Name Helpers # ##################################################### def hasUniqueName(o, class_id): scene = o.GetScene() object_name = o.GetName() object_id = o.GetUniqueID() object_count = scene.GetSrcObjectCount(class_id) for i in range(object_count): other = scene.GetSrcObject(class_id, i) other_id = other.GetUniqueID() other_name = other.GetName() if other_id == object_id: continue if other_name == object_name: return False return True def getObjectName(o, force_prefix = False): if not o: return "" object_name = o.GetName() object_id = o.GetUniqueID() if not force_prefix: force_prefix = not hasUniqueName(o, FbxNode.ClassId) prefix = "" if option_prefix or force_prefix: prefix = "Object_%s_" % object_id return prefix + object_name def getMaterialName(o, force_prefix = False): object_name = o.GetName() object_id = o.GetUniqueID() if not force_prefix: force_prefix = not hasUniqueName(o, FbxSurfaceMaterial.ClassId) prefix = "" if option_prefix or force_prefix: prefix = "Material_%s_" % object_id return prefix + object_name def getTextureName(t, force_prefix = False): if type(t) is FbxFileTexture: texture_file = t.GetFileName() texture_id = os.path.splitext(os.path.basename(texture_file))[0] else: texture_id = t.GetName() if texture_id == "_empty_": texture_id = "" prefix = "" if option_prefix or force_prefix: prefix = "Texture_%s_" % t.GetUniqueID() if len(texture_id) == 0: prefix = prefix[0:len(prefix)-1] return prefix + texture_id def getMtlTextureName(texture_name, texture_id, force_prefix = False): texture_name = os.path.splitext(texture_name)[0] prefix = "" if option_prefix or force_prefix: prefix = "Texture_%s_" % texture_id return prefix + texture_name def getPrefixedName(o, prefix): return (prefix + '_%s_') % o.GetUniqueID() + o.GetName() # ##################################################### # Triangulation # ##################################################### def triangulate_node_hierarchy(node): node_attribute = node.GetNodeAttribute(); if node_attribute: if node_attribute.GetAttributeType() == FbxNodeAttribute.eMesh or \ node_attribute.GetAttributeType() == FbxNodeAttribute.eNurbs or \ node_attribute.GetAttributeType() == FbxNodeAttribute.eNurbsSurface or \ node_attribute.GetAttributeType() == FbxNodeAttribute.ePatch: converter.TriangulateInPlace(node); child_count = node.GetChildCount() for i in range(child_count): triangulate_node_hierarchy(node.GetChild(i)) def triangulate_scene(scene): node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): triangulate_node_hierarchy(node.GetChild(i)) # ##################################################### # Generate Material Object # ##################################################### def generate_texture_bindings(material_property, material_params): # FBX to Three.js texture types binding_types = { "DiffuseColor": "map", "DiffuseFactor": "diffuseFactor", "EmissiveColor": "emissiveMap", "EmissiveFactor": "emissiveFactor", "AmbientColor": "lightMap", # "ambientMap", "AmbientFactor": "ambientFactor", "SpecularColor": "specularMap", "SpecularFactor": "specularFactor", "ShininessExponent": "shininessExponent", "NormalMap": "normalMap", "Bump": "bumpMap", "TransparentColor": "transparentMap", "TransparencyFactor": "transparentFactor", "ReflectionColor": "reflectionMap", "ReflectionFactor": "reflectionFactor", "DisplacementColor": "displacementMap", "VectorDisplacementColor": "vectorDisplacementMap" } if material_property.IsValid(): #Here we have to check if it's layeredtextures, or just textures: layered_texture_count = material_property.GetSrcObjectCount(FbxLayeredTexture.ClassId) if layered_texture_count > 0: for j in range(layered_texture_count): layered_texture = material_property.GetSrcObject(FbxLayeredTexture.ClassId, j) texture_count = layered_texture.GetSrcObjectCount(FbxTexture.ClassId) for k in range(texture_count): texture = layered_texture.GetSrcObject(FbxTexture.ClassId,k) if texture: texture_id = getTextureName(texture, True) material_params[binding_types[str(material_property.GetName())]] = texture_id else: # no layered texture simply get on the property texture_count = material_property.GetSrcObjectCount(FbxTexture.ClassId) for j in range(texture_count): texture = material_property.GetSrcObject(FbxTexture.ClassId,j) if texture: texture_id = getTextureName(texture, True) material_params[binding_types[str(material_property.GetName())]] = texture_id def generate_material_object(material): #Get the implementation to see if it's a hardware shader. implementation = GetImplementation(material, "ImplementationHLSL") implementation_type = "HLSL" if not implementation: implementation = GetImplementation(material, "ImplementationCGFX") implementation_type = "CGFX" output = None material_params = None material_type = None if implementation: print("Shader materials are not supported") elif material.GetClassId().Is(FbxSurfaceLambert.ClassId): ambient = getHex(material.Ambient.Get()) diffuse = getHex(material.Diffuse.Get()) emissive = getHex(material.Emissive.Get()) opacity = 1.0 - material.TransparencyFactor.Get() opacity = 1.0 if opacity == 0 else opacity opacity = opacity transparent = False reflectivity = 1 material_type = 'MeshBasicMaterial' # material_type = 'MeshLambertMaterial' material_params = { 'color' : diffuse, 'ambient' : ambient, 'emissive' : emissive, 'reflectivity' : reflectivity, 'transparent' : transparent, 'opacity' : opacity } elif material.GetClassId().Is(FbxSurfacePhong.ClassId): ambient = getHex(material.Ambient.Get()) diffuse = getHex(material.Diffuse.Get()) emissive = getHex(material.Emissive.Get()) specular = getHex(material.Specular.Get()) opacity = 1.0 - material.TransparencyFactor.Get() opacity = 1.0 if opacity == 0 else opacity opacity = opacity shininess = material.Shininess.Get() transparent = False reflectivity = 1 bumpScale = 1 material_type = 'MeshPhongMaterial' material_params = { 'color' : diffuse, 'ambient' : ambient, 'emissive' : emissive, 'specular' : specular, 'shininess' : shininess, 'bumpScale' : bumpScale, 'reflectivity' : reflectivity, 'transparent' : transparent, 'opacity' : opacity } else: print "Unknown type of Material", getMaterialName(material) # default to Lambert Material if the current Material type cannot be handeled if not material_type: ambient = getHex((0,0,0)) diffuse = getHex((0.5,0.5,0.5)) emissive = getHex((0,0,0)) opacity = 1 transparent = False reflectivity = 1 material_type = 'MeshLambertMaterial' material_params = { 'color' : diffuse, 'ambient' : ambient, 'emissive' : emissive, 'reflectivity' : reflectivity, 'transparent' : transparent, 'opacity' : opacity } if option_textures: texture_count = FbxLayerElement.sTypeTextureCount() for texture_index in range(texture_count): material_property = material.FindProperty(FbxLayerElement.sTextureChannelNames(texture_index)) generate_texture_bindings(material_property, material_params) material_params['wireframe'] = False material_params['wireframeLinewidth'] = 1 output = { 'type' : material_type, 'parameters' : material_params } return output def generate_proxy_material_object(node, material_names): material_type = 'MeshFaceMaterial' material_params = { 'materials' : material_names } output = { 'type' : material_type, 'parameters' : material_params } return output # ##################################################### # Find Scene Materials # ##################################################### def extract_materials_from_node(node, material_dict): name = node.GetName() mesh = node.GetNodeAttribute() node = None if mesh: node = mesh.GetNode() if node: material_count = node.GetMaterialCount() material_names = [] for l in range(mesh.GetLayerCount()): materials = mesh.GetLayer(l).GetMaterials() if materials: if materials.GetReferenceMode() == FbxLayerElement.eIndex: #Materials are in an undefined external table continue for i in range(material_count): material = node.GetMaterial(i) material_names.append(getMaterialName(material)) if material_count > 1: proxy_material = generate_proxy_material_object(node, material_names) proxy_name = getMaterialName(node, True) material_dict[proxy_name] = proxy_material def generate_materials_from_hierarchy(node, material_dict): if node.GetNodeAttribute() == None: pass else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eMesh: extract_materials_from_node(node, material_dict) for i in range(node.GetChildCount()): generate_materials_from_hierarchy(node.GetChild(i), material_dict) def generate_material_dict(scene): material_dict = {} # generate all materials for this scene material_count = scene.GetSrcObjectCount(FbxSurfaceMaterial.ClassId) for i in range(material_count): material = scene.GetSrcObject(FbxSurfaceMaterial.ClassId, i) material_object = generate_material_object(material) material_name = getMaterialName(material) material_dict[material_name] = material_object # generate material porxies # Three.js does not support meshs with multiple materials, however it does # support materials with multiple submaterials node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): generate_materials_from_hierarchy(node.GetChild(i), material_dict) return material_dict # ##################################################### # Generate Texture Object # ##################################################### def generate_texture_object(texture): #TODO: extract more texture properties wrap_u = texture.GetWrapModeU() wrap_v = texture.GetWrapModeV() offset = texture.GetUVTranslation() if type(texture) is FbxFileTexture: url = texture.GetFileName() else: url = getTextureName( texture ) #url = replace_inFolder2OutFolder( url ) #print( url ) index = url.rfind( '/' ) if index == -1: index = url.rfind( '\\' ) filename = url[ index+1 : len(url) ] output = { 'url': filename, 'fullpath': url, 'repeat': serializeVector2( (1,1) ), 'offset': serializeVector2( texture.GetUVTranslation() ), 'magFilter': 'LinearFilter', 'minFilter': 'LinearMipMapLinearFilter', 'anisotropy': True } return output # ##################################################### # Replace Texture input path to output # ##################################################### def replace_inFolder2OutFolder(url): folderIndex = url.find(inputFolder) if folderIndex != -1: url = url[ folderIndex+len(inputFolder): ] url = outputFolder + url return url # ##################################################### # Replace Texture output path to input # ##################################################### def replace_OutFolder2inFolder(url): folderIndex = url.find(outputFolder) if folderIndex != -1: url = url[ folderIndex+len(outputFolder): ] url = inputFolder + url return url # ##################################################### # Find Scene Textures # ##################################################### def extract_material_textures(material_property, texture_dict): if material_property.IsValid(): #Here we have to check if it's layeredtextures, or just textures: layered_texture_count = material_property.GetSrcObjectCount(FbxLayeredTexture.ClassId) if layered_texture_count > 0: for j in range(layered_texture_count): layered_texture = material_property.GetSrcObject(FbxLayeredTexture.ClassId, j) texture_count = layered_texture.GetSrcObjectCount(FbxTexture.ClassId) for k in range(texture_count): texture = layered_texture.GetSrcObject(FbxTexture.ClassId,k) if texture: texture_object = generate_texture_object(texture) texture_name = getTextureName( texture, True ) texture_dict[texture_name] = texture_object else: # no layered texture simply get on the property texture_count = material_property.GetSrcObjectCount(FbxTexture.ClassId) for j in range(texture_count): texture = material_property.GetSrcObject(FbxTexture.ClassId,j) if texture: texture_object = generate_texture_object(texture) texture_name = getTextureName( texture, True ) texture_dict[texture_name] = texture_object def extract_textures_from_node(node, texture_dict): name = node.GetName() mesh = node.GetNodeAttribute() #for all materials attached to this mesh material_count = mesh.GetNode().GetSrcObjectCount(FbxSurfaceMaterial.ClassId) for material_index in range(material_count): material = mesh.GetNode().GetSrcObject(FbxSurfaceMaterial.ClassId, material_index) #go through all the possible textures types if material: texture_count = FbxLayerElement.sTypeTextureCount() for texture_index in range(texture_count): material_property = material.FindProperty(FbxLayerElement.sTextureChannelNames(texture_index)) extract_material_textures(material_property, texture_dict) def generate_textures_from_hierarchy(node, texture_dict): if node.GetNodeAttribute() == None: pass else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eMesh: extract_textures_from_node(node, texture_dict) for i in range(node.GetChildCount()): generate_textures_from_hierarchy(node.GetChild(i), texture_dict) def generate_texture_dict(scene): if not option_textures: return {} texture_dict = {} node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): generate_textures_from_hierarchy(node.GetChild(i), texture_dict) return texture_dict # ##################################################### # Extract Fbx SDK Mesh Data # ##################################################### def extract_fbx_vertex_positions(mesh): control_points_count = mesh.GetControlPointsCount() control_points = mesh.GetControlPoints() positions = [] for i in range(control_points_count): tmp = control_points[i] tmp = [tmp[0], tmp[1], tmp[2]] positions.append(tmp) node = mesh.GetNode() if node: t = node.GeometricTranslation.Get() t = FbxVector4(t[0], t[1], t[2], 1) r = node.GeometricRotation.Get() r = FbxVector4(r[0], r[1], r[2], 1) s = node.GeometricScaling.Get() s = FbxVector4(s[0], s[1], s[2], 1) hasGeometricTransform = False if t[0] != 0 or t[1] != 0 or t[2] != 0 or \ r[0] != 0 or r[1] != 0 or r[2] != 0 or \ s[0] != 1 or s[1] != 1 or s[2] != 1: hasGeometricTransform = True if hasGeometricTransform: geo_transform = FbxMatrix(t,r,s) else: geo_transform = FbxMatrix() transform = None if option_geometry: # FbxMeshes are local to their node, we need the vertices in global space # when scene nodes are not exported transform = node.EvaluateGlobalTransform() transform = FbxMatrix(transform) * geo_transform elif hasGeometricTransform: transform = geo_transform if transform: for i in range(len(positions)): v = positions[i] position = FbxVector4(v[0], v[1], v[2]) position = transform.MultNormalize(position) positions[i] = [position[0], position[1], position[2]] return positions def extract_fbx_vertex_normals(mesh): # eNone The mapping is undetermined. # eByControlPoint There will be one mapping coordinate for each surface control point/vertex. # eByPolygonVertex There will be one mapping coordinate for each vertex, for every polygon of which it is a part. This means that a vertex will have as many mapping coordinates as polygons of which it is a part. # eByPolygon There can be only one mapping coordinate for the whole polygon. # eByEdge There will be one mapping coordinate for each unique edge in the mesh. This is meant to be used with smoothing layer elements. # eAllSame There can be only one mapping coordinate for the whole surface. layered_normal_indices = [] layered_normal_values = [] poly_count = mesh.GetPolygonCount() control_points = mesh.GetControlPoints() for l in range(mesh.GetLayerCount()): mesh_normals = mesh.GetLayer(l).GetNormals() if not mesh_normals: continue normals_array = mesh_normals.GetDirectArray() normals_count = normals_array.GetCount() if normals_count == 0: continue normal_indices = [] normal_values = [] # values for i in range(normals_count): normal = normals_array.GetAt(i) normal = [normal[0], normal[1], normal[2]] normal_values.append(normal) node = mesh.GetNode() if node: t = node.GeometricTranslation.Get() t = FbxVector4(t[0], t[1], t[2], 1) r = node.GeometricRotation.Get() r = FbxVector4(r[0], r[1], r[2], 1) s = node.GeometricScaling.Get() s = FbxVector4(s[0], s[1], s[2], 1) hasGeometricTransform = False if t[0] != 0 or t[1] != 0 or t[2] != 0 or \ r[0] != 0 or r[1] != 0 or r[2] != 0 or \ s[0] != 1 or s[1] != 1 or s[2] != 1: hasGeometricTransform = True if hasGeometricTransform: geo_transform = FbxMatrix(t,r,s) else: geo_transform = FbxMatrix() transform = None if option_geometry: # FbxMeshes are local to their node, we need the vertices in global space # when scene nodes are not exported transform = node.EvaluateGlobalTransform() transform = FbxMatrix(transform) * geo_transform elif hasGeometricTransform: transform = geo_transform if transform: t = FbxVector4(0,0,0,1) transform.SetRow(3, t) for i in range(len(normal_values)): n = normal_values[i] normal = FbxVector4(n[0], n[1], n[2]) normal = transform.MultNormalize(normal) normal.Normalize() normal = [normal[0], normal[1], normal[2]] normal_values[i] = normal # indices vertexId = 0 for p in range(poly_count): poly_size = mesh.GetPolygonSize(p) poly_normals = [] for v in range(poly_size): control_point_index = mesh.GetPolygonVertex(p, v) # mapping mode is by control points. The mesh should be smooth and soft. # we can get normals by retrieving each control point if mesh_normals.GetMappingMode() == FbxLayerElement.eByControlPoint: # reference mode is direct, the normal index is same as vertex index. # get normals by the index of control vertex if mesh_normals.GetReferenceMode() == FbxLayerElement.eDirect: poly_normals.append(control_point_index) elif mesh_normals.GetReferenceMode() == FbxLayerElement.eIndexToDirect: index = mesh_normals.GetIndexArray().GetAt(control_point_index) poly_normals.append(index) # mapping mode is by polygon-vertex. # we can get normals by retrieving polygon-vertex. elif mesh_normals.GetMappingMode() == FbxLayerElement.eByPolygonVertex: if mesh_normals.GetReferenceMode() == FbxLayerElement.eDirect: poly_normals.append(vertexId) elif mesh_normals.GetReferenceMode() == FbxLayerElement.eIndexToDirect: index = mesh_normals.GetIndexArray().GetAt(vertexId) poly_normals.append(index) elif mesh_normals.GetMappingMode() == FbxLayerElement.eByPolygon or \ mesh_normals.GetMappingMode() == FbxLayerElement.eAllSame or \ mesh_normals.GetMappingMode() == FbxLayerElement.eNone: print("unsupported normal mapping mode for polygon vertex") vertexId += 1 normal_indices.append(poly_normals) layered_normal_values.append(normal_values) layered_normal_indices.append(normal_indices) normal_values = [] normal_indices = [] # Three.js only supports one layer of normals if len(layered_normal_values) > 0: normal_values = layered_normal_values[0] normal_indices = layered_normal_indices[0] return normal_values, normal_indices def extract_fbx_vertex_colors(mesh): # eNone The mapping is undetermined. # eByControlPoint There will be one mapping coordinate for each surface control point/vertex. # eByPolygonVertex There will be one mapping coordinate for each vertex, for every polygon of which it is a part. This means that a vertex will have as many mapping coordinates as polygons of which it is a part. # eByPolygon There can be only one mapping coordinate for the whole polygon. # eByEdge There will be one mapping coordinate for each unique edge in the mesh. This is meant to be used with smoothing layer elements. # eAllSame There can be only one mapping coordinate for the whole surface. layered_color_indices = [] layered_color_values = [] poly_count = mesh.GetPolygonCount() control_points = mesh.GetControlPoints() for l in range(mesh.GetLayerCount()): mesh_colors = mesh.GetLayer(l).GetVertexColors() if not mesh_colors: continue colors_array = mesh_colors.GetDirectArray() colors_count = colors_array.GetCount() if colors_count == 0: continue color_indices = [] color_values = [] # values for i in range(colors_count): color = colors_array.GetAt(i) color = [color.mRed, color.mGreen, color.mBlue, color.mAlpha] color_values.append(color) # indices vertexId = 0 for p in range(poly_count): poly_size = mesh.GetPolygonSize(p) poly_colors = [] for v in range(poly_size): control_point_index = mesh.GetPolygonVertex(p, v) if mesh_colors.GetMappingMode() == FbxLayerElement.eByControlPoint: if mesh_colors.GetReferenceMode() == FbxLayerElement.eDirect: poly_colors.append(control_point_index) elif mesh_colors.GetReferenceMode() == FbxLayerElement.eIndexToDirect: index = mesh_colors.GetIndexArray().GetAt(control_point_index) poly_colors.append(index) elif mesh_colors.GetMappingMode() == FbxLayerElement.eByPolygonVertex: if mesh_colors.GetReferenceMode() == FbxLayerElement.eDirect: poly_colors.append(vertexId) elif mesh_colors.GetReferenceMode() == FbxLayerElement.eIndexToDirect: index = mesh_colors.GetIndexArray().GetAt(vertexId) poly_colors.append(index) elif mesh_colors.GetMappingMode() == FbxLayerElement.eByPolygon or \ mesh_colors.GetMappingMode() == FbxLayerElement.eAllSame or \ mesh_colors.GetMappingMode() == FbxLayerElement.eNone: print("unsupported color mapping mode for polygon vertex") vertexId += 1 color_indices.append(poly_colors) layered_color_indices.append( color_indices ) layered_color_values.append( color_values ) color_values = [] color_indices = [] # Three.js only supports one layer of colors if len(layered_color_values) > 0: color_values = layered_color_values[0] color_indices = layered_color_indices[0] ''' # The Fbx SDK defaults mesh.Color to (0.8, 0.8, 0.8) # This causes most models to receive incorrect vertex colors if len(color_values) == 0: color = mesh.Color.Get() color_values = [[color[0], color[1], color[2]]] color_indices = [] for p in range(poly_count): poly_size = mesh.GetPolygonSize(p) color_indices.append([0] * poly_size) ''' return color_values, color_indices def extract_fbx_vertex_uvs(mesh): # eNone The mapping is undetermined. # eByControlPoint There will be one mapping coordinate for each surface control point/vertex. # eByPolygonVertex There will be one mapping coordinate for each vertex, for every polygon of which it is a part. This means that a vertex will have as many mapping coordinates as polygons of which it is a part. # eByPolygon There can be only one mapping coordinate for the whole polygon. # eByEdge There will be one mapping coordinate for each unique edge in the mesh. This is meant to be used with smoothing layer elements. # eAllSame There can be only one mapping coordinate for the whole surface. layered_uv_indices = [] layered_uv_values = [] poly_count = mesh.GetPolygonCount() control_points = mesh.GetControlPoints() for l in range(mesh.GetLayerCount()): mesh_uvs = mesh.GetLayer(l).GetUVs() if not mesh_uvs: continue uvs_array = mesh_uvs.GetDirectArray() uvs_count = uvs_array.GetCount() if uvs_count == 0: continue uv_indices = [] uv_values = [] # values for i in range(uvs_count): uv = uvs_array.GetAt(i) uv = [uv[0], uv[1]] uv_values.append(uv) # indices vertexId = 0 for p in range(poly_count): poly_size = mesh.GetPolygonSize(p) poly_uvs = [] for v in range(poly_size): control_point_index = mesh.GetPolygonVertex(p, v) if mesh_uvs.GetMappingMode() == FbxLayerElement.eByControlPoint: if mesh_uvs.GetReferenceMode() == FbxLayerElement.eDirect: poly_uvs.append(control_point_index) elif mesh_uvs.GetReferenceMode() == FbxLayerElement.eIndexToDirect: index = mesh_uvs.GetIndexArray().GetAt(control_point_index) poly_uvs.append(index) elif mesh_uvs.GetMappingMode() == FbxLayerElement.eByPolygonVertex: uv_texture_index = mesh_uvs.GetIndexArray().GetAt(vertexId) if mesh_uvs.GetReferenceMode() == FbxLayerElement.eDirect or \ mesh_uvs.GetReferenceMode() == FbxLayerElement.eIndexToDirect: poly_uvs.append(uv_texture_index) elif mesh_uvs.GetMappingMode() == FbxLayerElement.eByPolygon or \ mesh_uvs.GetMappingMode() == FbxLayerElement.eAllSame or \ mesh_uvs.GetMappingMode() == FbxLayerElement.eNone: print("unsupported uv mapping mode for polygon vertex") vertexId += 1 uv_indices.append(poly_uvs) layered_uv_values.append(uv_values) layered_uv_indices.append(uv_indices) return layered_uv_values, layered_uv_indices # ##################################################### # Process Mesh Geometry # ##################################################### def generate_normal_key(normal): return (round(normal[0], 6), round(normal[1], 6), round(normal[2], 6)) def generate_color_key(color): return getHex(color) def generate_uv_key(uv): return (round(uv[0], 6), round(uv[1], 6)) def append_non_duplicate_uvs(source_uvs, dest_uvs, counts): source_layer_count = len(source_uvs) for layer_index in range(source_layer_count): dest_layer_count = len(dest_uvs) if dest_layer_count <= layer_index: dest_uv_layer = {} count = 0 dest_uvs.append(dest_uv_layer) counts.append(count) else: dest_uv_layer = dest_uvs[layer_index] count = counts[layer_index] source_uv_layer = source_uvs[layer_index] for uv in source_uv_layer: key = generate_uv_key(uv) if key not in dest_uv_layer: dest_uv_layer[key] = count count += 1 counts[layer_index] = count return counts def generate_unique_normals_dictionary(mesh_list): normals_dictionary = {} nnormals = 0 # Merge meshes, remove duplicate data for mesh in mesh_list: node = mesh.GetNode() normal_values, normal_indices = extract_fbx_vertex_normals(mesh) if len(normal_values) > 0: for normal in normal_values: key = generate_normal_key(normal) if key not in normals_dictionary: normals_dictionary[key] = nnormals nnormals += 1 return normals_dictionary def generate_unique_colors_dictionary(mesh_list): colors_dictionary = {} ncolors = 0 # Merge meshes, remove duplicate data for mesh in mesh_list: color_values, color_indices = extract_fbx_vertex_colors(mesh) if len(color_values) > 0: for color in color_values: key = generate_color_key(color) if key not in colors_dictionary: colors_dictionary[key] = ncolors ncolors += 1 return colors_dictionary def generate_unique_uvs_dictionary_layers(mesh_list): uvs_dictionary_layers = [] nuvs_list = [] # Merge meshes, remove duplicate data for mesh in mesh_list: uv_values, uv_indices = extract_fbx_vertex_uvs(mesh) if len(uv_values) > 0: nuvs_list = append_non_duplicate_uvs(uv_values, uvs_dictionary_layers, nuvs_list) return uvs_dictionary_layers def generate_normals_from_dictionary(normals_dictionary): normal_values = [] for key, index in sorted(normals_dictionary.items(), key = operator.itemgetter(1)): normal_values.append(key) return normal_values def generate_colors_from_dictionary(colors_dictionary): color_values = [] for key, index in sorted(colors_dictionary.items(), key = operator.itemgetter(1)): color_values.append(key) return color_values def generate_uvs_from_dictionary_layers(uvs_dictionary_layers): uv_values = [] for uvs_dictionary in uvs_dictionary_layers: uv_values_layer = [] for key, index in sorted(uvs_dictionary.items(), key = operator.itemgetter(1)): uv_values_layer.append(key) uv_values.append(uv_values_layer) return uv_values def generate_normal_indices_for_poly(poly_index, mesh_normal_values, mesh_normal_indices, normals_to_indices): if len(mesh_normal_indices) <= 0: return [] poly_normal_indices = mesh_normal_indices[poly_index] poly_size = len(poly_normal_indices) output_poly_normal_indices = [] for v in range(poly_size): normal_index = poly_normal_indices[v] normal_value = mesh_normal_values[normal_index] key = generate_normal_key(normal_value) output_index = normals_to_indices[key] output_poly_normal_indices.append(output_index) return output_poly_normal_indices def generate_color_indices_for_poly(poly_index, mesh_color_values, mesh_color_indices, colors_to_indices): if len(mesh_color_indices) <= 0: return [] poly_color_indices = mesh_color_indices[poly_index] poly_size = len(poly_color_indices) output_poly_color_indices = [] for v in range(poly_size): color_index = poly_color_indices[v] color_value = mesh_color_values[color_index] key = generate_color_key(color_value) output_index = colors_to_indices[key] output_poly_color_indices.append(output_index) return output_poly_color_indices def generate_uv_indices_for_poly(poly_index, mesh_uv_values, mesh_uv_indices, uvs_to_indices): if len(mesh_uv_indices) <= 0: return [] poly_uv_indices = mesh_uv_indices[poly_index] poly_size = len(poly_uv_indices) output_poly_uv_indices = [] for v in range(poly_size): uv_index = poly_uv_indices[v] uv_value = mesh_uv_values[uv_index] key = generate_uv_key(uv_value) output_index = uvs_to_indices[key] output_poly_uv_indices.append(output_index) return output_poly_uv_indices def process_mesh_vertices(mesh_list): vertex_offset = 0 vertex_offset_list = [0] vertices = [] for mesh in mesh_list: node = mesh.GetNode() mesh_vertices = extract_fbx_vertex_positions(mesh) vertices.extend(mesh_vertices[:]) vertex_offset += len(mesh_vertices) vertex_offset_list.append(vertex_offset) return vertices, vertex_offset_list def process_mesh_materials(mesh_list): material_offset = 0 material_offset_list = [0] materials_list = [] #TODO: remove duplicate mesh references for mesh in mesh_list: node = mesh.GetNode() material_count = node.GetMaterialCount() if material_count > 0: for l in range(mesh.GetLayerCount()): materials = mesh.GetLayer(l).GetMaterials() if materials: if materials.GetReferenceMode() == FbxLayerElement.eIndex: #Materials are in an undefined external table continue for i in range(material_count): material = node.GetMaterial(i) materials_list.append( material ) material_offset += material_count material_offset_list.append(material_offset) return materials_list, material_offset_list def process_mesh_polygons(mesh_list, normals_to_indices, colors_to_indices, uvs_to_indices_list, vertex_offset_list, material_offset_list): faces = [] for mesh_index in range(len(mesh_list)): mesh = mesh_list[mesh_index] flipWindingOrder = False node = mesh.GetNode() if node: local_scale = node.EvaluateLocalScaling() if local_scale[0] < 0 or local_scale[1] < 0 or local_scale[2] < 0: flipWindingOrder = True poly_count = mesh.GetPolygonCount() control_points = mesh.GetControlPoints() normal_values, normal_indices = extract_fbx_vertex_normals(mesh) color_values, color_indices = extract_fbx_vertex_colors(mesh) uv_values_layers, uv_indices_layers = extract_fbx_vertex_uvs(mesh) for poly_index in range(poly_count): poly_size = mesh.GetPolygonSize(poly_index) face_normals = generate_normal_indices_for_poly(poly_index, normal_values, normal_indices, normals_to_indices) face_colors = generate_color_indices_for_poly(poly_index, color_values, color_indices, colors_to_indices) face_uv_layers = [] for l in range(len(uv_indices_layers)): uv_values = uv_values_layers[l] uv_indices = uv_indices_layers[l] face_uv_indices = generate_uv_indices_for_poly(poly_index, uv_values, uv_indices, uvs_to_indices_list[l]) face_uv_layers.append(face_uv_indices) face_vertices = [] for vertex_index in range(poly_size): control_point_index = mesh.GetPolygonVertex(poly_index, vertex_index) face_vertices.append(control_point_index) #TODO: assign a default material to any mesh without one if len(material_offset_list) <= mesh_index: material_offset = 0 else: material_offset = material_offset_list[mesh_index] vertex_offset = vertex_offset_list[mesh_index] if poly_size > 4: new_face_normals = [] new_face_colors = [] new_face_uv_layers = [] for i in range(poly_size - 2): new_face_vertices = [face_vertices[0], face_vertices[i+1], face_vertices[i+2]] if len(face_normals): new_face_normals = [face_normals[0], face_normals[i+1], face_normals[i+2]] if len(face_colors): new_face_colors = [face_colors[0], face_colors[i+1], face_colors[i+2]] if len(face_uv_layers): new_face_uv_layers = [] for layer in face_uv_layers: new_face_uv_layers.append([layer[0], layer[i+1], layer[i+2]]) face = generate_mesh_face(mesh, poly_index, new_face_vertices, new_face_normals, new_face_colors, new_face_uv_layers, vertex_offset, material_offset, flipWindingOrder) faces.append(face) else: face = generate_mesh_face(mesh, poly_index, face_vertices, face_normals, face_colors, face_uv_layers, vertex_offset, material_offset, flipWindingOrder) faces.append(face) return faces def generate_mesh_face(mesh, polygon_index, vertex_indices, normals, colors, uv_layers, vertex_offset, material_offset, flipOrder): isTriangle = ( len(vertex_indices) == 3 ) nVertices = 3 if isTriangle else 4 hasMaterial = False for l in range(mesh.GetLayerCount()): materials = mesh.GetLayer(l).GetMaterials() if materials: hasMaterial = True break hasFaceUvs = False hasFaceVertexUvs = len(uv_layers) > 0 hasFaceNormals = False hasFaceVertexNormals = len(normals) > 0 hasFaceColors = False hasFaceVertexColors = len(colors) > 0 faceType = 0 faceType = setBit(faceType, 0, not isTriangle) faceType = setBit(faceType, 1, hasMaterial) faceType = setBit(faceType, 2, hasFaceUvs) faceType = setBit(faceType, 3, hasFaceVertexUvs) faceType = setBit(faceType, 4, hasFaceNormals) faceType = setBit(faceType, 5, hasFaceVertexNormals) faceType = setBit(faceType, 6, hasFaceColors) faceType = setBit(faceType, 7, hasFaceVertexColors) faceData = [] # order is important, must match order in JSONLoader # face type # vertex indices # material index # face uvs index # face vertex uvs indices # face color index # face vertex colors indices faceData.append(faceType) if flipOrder: if nVertices == 3: vertex_indices = [vertex_indices[0], vertex_indices[2], vertex_indices[1]] if hasFaceVertexNormals: normals = [normals[0], normals[2], normals[1]] if hasFaceVertexColors: colors = [colors[0], colors[2], colors[1]] if hasFaceVertexUvs: tmp = [] for polygon_uvs in uv_layers: tmp.append([polygon_uvs[0], polygon_uvs[2], polygon_uvs[1]]) uv_layers = tmp else: vertex_indices = [vertex_indices[0], vertex_indices[3], vertex_indices[2], vertex_indices[1]] if hasFaceVertexNormals: normals = [normals[0], normals[3], normals[2], normals[1]] if hasFaceVertexColors: colors = [colors[0], colors[3], colors[2], colors[1]] if hasFaceVertexUvs: tmp = [] for polygon_uvs in uv_layers: tmp.append([polygon_uvs[0], polygon_uvs[3], polygon_uvs[2], polygon_uvs[3]]) uv_layers = tmp for i in range(nVertices): index = vertex_indices[i] + vertex_offset faceData.append(index) if hasMaterial: material_id = 0 for l in range(mesh.GetLayerCount()): materials = mesh.GetLayer(l).GetMaterials() if materials: material_id = materials.GetIndexArray().GetAt(polygon_index) break material_id += material_offset faceData.append( material_id ) if hasFaceVertexUvs: for polygon_uvs in uv_layers: for i in range(nVertices): index = polygon_uvs[i] faceData.append(index) if hasFaceVertexNormals: for i in range(nVertices): index = normals[i] faceData.append(index) if hasFaceVertexColors: for i in range(nVertices): index = colors[i] faceData.append(index) return faceData # ##################################################### # Generate Mesh Object (for scene output format) # ##################################################### def generate_scene_output(node): mesh = node.GetNodeAttribute() # This is done in order to keep the scene output and non-scene output code DRY mesh_list = [ mesh ] # Extract the mesh data into arrays vertices, vertex_offsets = process_mesh_vertices(mesh_list) materials, material_offsets = process_mesh_materials(mesh_list) normals_to_indices = generate_unique_normals_dictionary(mesh_list) colors_to_indices = generate_unique_colors_dictionary(mesh_list) uvs_to_indices_list = generate_unique_uvs_dictionary_layers(mesh_list) normal_values = generate_normals_from_dictionary(normals_to_indices) color_values = generate_colors_from_dictionary(colors_to_indices) uv_values = generate_uvs_from_dictionary_layers(uvs_to_indices_list) # Generate mesh faces for the Three.js file format faces = process_mesh_polygons(mesh_list, normals_to_indices, colors_to_indices, uvs_to_indices_list, vertex_offsets, material_offsets) # Generate counts for uvs, vertices, normals, colors, and faces nuvs = [] for layer_index, uvs in enumerate(uv_values): nuvs.append(str(len(uvs))) nvertices = len(vertices) nnormals = len(normal_values) ncolors = len(color_values) nfaces = len(faces) # Flatten the arrays, currently they are in the form of [[0, 1, 2], [3, 4, 5], ...] vertices = [val for v in vertices for val in v] normal_values = [val for n in normal_values for val in n] color_values = [c for c in color_values] faces = [val for f in faces for val in f] uv_values = generate_uvs(uv_values) # Disable automatic json indenting when pretty printing for the arrays if option_pretty_print: nuvs = NoIndent(nuvs) vertices = ChunkedIndent(vertices, 15, True) normal_values = ChunkedIndent(normal_values, 15, True) color_values = ChunkedIndent(color_values, 15) faces = ChunkedIndent(faces, 30) metadata = { 'vertices' : nvertices, 'normals' : nnormals, 'colors' : ncolors, 'faces' : nfaces, 'uvs' : nuvs } output = { 'scale' : 1, 'materials' : [], 'vertices' : vertices, 'normals' : [] if nnormals <= 0 else normal_values, 'colors' : [] if ncolors <= 0 else color_values, 'uvs' : uv_values, 'faces' : faces } if option_pretty_print: output['0metadata'] = metadata else: output['metadata'] = metadata return output # ##################################################### # Generate Mesh Object (for non-scene output) # ##################################################### def generate_non_scene_output(scene): mesh_list = generate_mesh_list(scene) # Extract the mesh data into arrays vertices, vertex_offsets = process_mesh_vertices(mesh_list) materials, material_offsets = process_mesh_materials(mesh_list) normals_to_indices = generate_unique_normals_dictionary(mesh_list) colors_to_indices = generate_unique_colors_dictionary(mesh_list) uvs_to_indices_list = generate_unique_uvs_dictionary_layers(mesh_list) normal_values = generate_normals_from_dictionary(normals_to_indices) color_values = generate_colors_from_dictionary(colors_to_indices) uv_values = generate_uvs_from_dictionary_layers(uvs_to_indices_list) # Generate mesh faces for the Three.js file format faces = process_mesh_polygons(mesh_list, normals_to_indices, colors_to_indices, uvs_to_indices_list, vertex_offsets, material_offsets) # Generate counts for uvs, vertices, normals, colors, and faces nuvs = [] for layer_index, uvs in enumerate(uv_values): nuvs.append(str(len(uvs))) nvertices = len(vertices) nnormals = len(normal_values) ncolors = len(color_values) nfaces = len(faces) # Flatten the arrays, currently they are in the form of [[0, 1, 2], [3, 4, 5], ...] vertices = [val for v in vertices for val in v] normal_values = [val for n in normal_values for val in n] color_values = [c for c in color_values] faces = [val for f in faces for val in f] uv_values = generate_uvs(uv_values) # Disable json indenting when pretty printing for the arrays if option_pretty_print: nuvs = NoIndent(nuvs) vertices = NoIndent(vertices) normal_values = NoIndent(normal_values) color_values = NoIndent(color_values) faces = NoIndent(faces) metadata = { 'formatVersion' : 3, 'type' : 'geometry', 'generatedBy' : 'convert-to-threejs.py', 'vertices' : nvertices, 'normals' : nnormals, 'colors' : ncolors, 'faces' : nfaces, 'uvs' : nuvs } output = { 'scale' : 1, 'materials' : [], 'vertices' : vertices, 'normals' : [] if nnormals <= 0 else normal_values, 'colors' : [] if ncolors <= 0 else color_values, 'uvs' : uv_values, 'faces' : faces } if option_pretty_print: output['0metadata'] = metadata else: output['metadata'] = metadata return output def generate_mesh_list_from_hierarchy(node, mesh_list): if node.GetNodeAttribute() == None: pass else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eMesh or \ attribute_type == FbxNodeAttribute.eNurbs or \ attribute_type == FbxNodeAttribute.eNurbsSurface or \ attribute_type == FbxNodeAttribute.ePatch: if attribute_type != FbxNodeAttribute.eMesh: converter.TriangulateInPlace(node); mesh_list.append(node.GetNodeAttribute()) for i in range(node.GetChildCount()): generate_mesh_list_from_hierarchy(node.GetChild(i), mesh_list) def generate_mesh_list(scene): mesh_list = [] node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): generate_mesh_list_from_hierarchy(node.GetChild(i), mesh_list) return mesh_list # ##################################################### # Generate Embed Objects # ##################################################### def generate_embed_dict_from_hierarchy(node, embed_dict): if node.GetNodeAttribute() == None: pass else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eMesh or \ attribute_type == FbxNodeAttribute.eNurbs or \ attribute_type == FbxNodeAttribute.eNurbsSurface or \ attribute_type == FbxNodeAttribute.ePatch: if attribute_type != FbxNodeAttribute.eMesh: converter.TriangulateInPlace(node); embed_object = generate_scene_output(node) embed_name = getPrefixedName(node, 'Embed') embed_dict[embed_name] = embed_object for i in range(node.GetChildCount()): generate_embed_dict_from_hierarchy(node.GetChild(i), embed_dict) def generate_embed_dict(scene): embed_dict = {} node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): generate_embed_dict_from_hierarchy(node.GetChild(i), embed_dict) return embed_dict # ##################################################### # Generate Geometry Objects # ##################################################### def generate_geometry_object(node): output = { 'type' : 'embedded', 'id' : getPrefixedName( node, 'Embed' ) } return output def generate_geometry_dict_from_hierarchy(node, geometry_dict): if node.GetNodeAttribute() == None: pass else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eMesh: geometry_object = generate_geometry_object(node) geometry_name = getPrefixedName( node, 'Geometry' ) geometry_dict[geometry_name] = geometry_object for i in range(node.GetChildCount()): generate_geometry_dict_from_hierarchy(node.GetChild(i), geometry_dict) def generate_geometry_dict(scene): geometry_dict = {} node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): generate_geometry_dict_from_hierarchy(node.GetChild(i), geometry_dict) return geometry_dict # ##################################################### # Generate Light Node Objects # ##################################################### def generate_default_light(): direction = (1,1,1) color = (1,1,1) intensity = 80.0 output = { 'type': 'DirectionalLight', 'color': getHex(color), 'intensity': intensity/100.00, 'direction': serializeVector3( direction ), 'target': getObjectName( None ) } return output def generate_light_object(node): light = node.GetNodeAttribute() light_types = ["point", "directional", "spot", "area", "volume"] light_type = light_types[light.LightType.Get()] transform = node.EvaluateLocalTransform() position = transform.GetT() output = None if light_type == "directional": # Three.js directional lights emit light from a point in 3d space to a target node or the origin. # When there is no target, we need to take a point, one unit away from the origin, and move it # into the right location so that the origin acts like the target if node.GetTarget(): direction = position else: translation = FbxVector4(0,0,0,0) scale = FbxVector4(1,1,1,1) rotation = transform.GetR() matrix = FbxMatrix(translation, rotation, scale) direction = matrix.MultNormalize(FbxVector4(0,1,0,1)) output = { 'type': 'DirectionalLight', 'color': getHex(light.Color.Get()), 'intensity': light.Intensity.Get()/100.0, 'direction': serializeVector3( direction ), 'target': getObjectName( node.GetTarget() ) } elif light_type == "point": output = { 'type': 'PointLight', 'color': getHex(light.Color.Get()), 'intensity': light.Intensity.Get()/100.0, 'position': serializeVector3( position ), 'distance': light.FarAttenuationEnd.Get() } elif light_type == "spot": output = { 'type': 'SpotLight', 'color': getHex(light.Color.Get()), 'intensity': light.Intensity.Get()/100.0, 'position': serializeVector3( position ), 'distance': light.FarAttenuationEnd.Get(), 'angle': light.OuterAngle.Get()*math.pi/180, 'exponent': light.DecayType.Get(), 'target': getObjectName( node.GetTarget() ) } return output def generate_ambient_light(scene): scene_settings = scene.GetGlobalSettings() ambient_color = scene_settings.GetAmbientColor() ambient_color = (ambient_color.mRed, ambient_color.mGreen, ambient_color.mBlue) if ambient_color[0] == 0 and ambient_color[1] == 0 and ambient_color[2] == 0: return None output = { 'type': 'AmbientLight', 'color': getHex(ambient_color) } return output # ##################################################### # Generate Camera Node Objects # ##################################################### def generate_default_camera(): position = (100, 100, 100) near = 0.1 far = 1000 fov = 75 output = { 'type': 'PerspectiveCamera', 'fov': fov, 'near': near, 'far': far, 'position': serializeVector3( position ) } return output def generate_camera_object(node): camera = node.GetNodeAttribute() position = camera.Position.Get() projection_types = [ "perspective", "orthogonal" ] projection = projection_types[camera.ProjectionType.Get()] near = camera.NearPlane.Get() far = camera.FarPlane.Get() name = getObjectName( node ) output = {} if projection == "perspective": aspect = camera.PixelAspectRatio.Get() fov = camera.FieldOfView.Get() output = { 'type': 'PerspectiveCamera', 'fov': fov, 'aspect': aspect, 'near': near, 'far': far, 'position': serializeVector3( position ) } elif projection == "orthogonal": left = "" right = "" top = "" bottom = "" output = { 'type': 'PerspectiveCamera', 'left': left, 'right': right, 'top': top, 'bottom': bottom, 'near': near, 'far': far, 'position': serializeVector3( position ) } return output # ##################################################### # Generate Camera Names # ##################################################### def generate_camera_name_list_from_hierarchy(node, camera_list): if node.GetNodeAttribute() == None: pass else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eCamera: camera_string = getObjectName(node) camera_list.append(camera_string) for i in range(node.GetChildCount()): generate_camera_name_list_from_hierarchy(node.GetChild(i), camera_list) def generate_camera_name_list(scene): camera_list = [] node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): generate_camera_name_list_from_hierarchy(node.GetChild(i), camera_list) return camera_list # ##################################################### # Generate Mesh Node Object # ##################################################### def generate_mesh_object(node): mesh = node.GetNodeAttribute() transform = node.EvaluateLocalTransform() position = transform.GetT() scale = transform.GetS() rotation = getRadians(transform.GetR()) quaternion = transform.GetQ() material_count = node.GetMaterialCount() material_name = "" if material_count > 0: material_names = [] for l in range(mesh.GetLayerCount()): materials = mesh.GetLayer(l).GetMaterials() if materials: if materials.GetReferenceMode() == FbxLayerElement.eIndex: #Materials are in an undefined external table continue for i in range(material_count): material = node.GetMaterial(i) material_names.append( getMaterialName(material) ) if not material_count > 1 and not len(material_names) > 0: material_names.append('') #If this mesh has more than one material, use a proxy material material_name = getMaterialName( node, True) if material_count > 1 else material_names[0] output = { 'geometry': getPrefixedName( node, 'Geometry' ), 'material': material_name, 'position': serializeVector3( position ), 'quaternion': serializeVector4( quaternion ), 'scale': serializeVector3( scale ), 'visible': True, } return output # ##################################################### # Generate Node Object # ##################################################### def generate_object(node): node_types = ["Unknown", "Null", "Marker", "Skeleton", "Mesh", "Nurbs", "Patch", "Camera", "CameraStereo", "CameraSwitcher", "Light", "OpticalReference", "OpticalMarker", "NurbsCurve", "TrimNurbsSurface", "Boundary", "NurbsSurface", "Shape", "LODGroup", "SubDiv", "CachedEffect", "Line"] transform = node.EvaluateLocalTransform() position = transform.GetT() scale = transform.GetS() rotation = getRadians(transform.GetR()) quaternion = transform.GetQ() node_type = "" if node.GetNodeAttribute() == None: node_type = "Null" else: node_type = node_types[node.GetNodeAttribute().GetAttributeType()] name = getObjectName( node ) output = { 'fbx_type': node_type, 'position': serializeVector3( position ), 'quaternion': serializeVector4( quaternion ), 'scale': serializeVector3( scale ), 'visible': True } return output # ##################################################### # Parse Scene Node Objects # ##################################################### def generate_object_hierarchy(node, object_dict): object_count = 0 if node.GetNodeAttribute() == None: object_data = generate_object(node) else: attribute_type = (node.GetNodeAttribute().GetAttributeType()) if attribute_type == FbxNodeAttribute.eMesh: object_data = generate_mesh_object(node) elif attribute_type == FbxNodeAttribute.eLight: object_data = generate_light_object(node) elif attribute_type == FbxNodeAttribute.eCamera: object_data = generate_camera_object(node) else: object_data = generate_object(node) object_count += 1 object_name = getObjectName(node) object_children = {} for i in range(node.GetChildCount()): object_count += generate_object_hierarchy(node.GetChild(i), object_children) if node.GetChildCount() > 0: # Having 'children' above other attributes is hard to read. # We can send it to the bottom using the last letter of the alphabet 'z'. # This letter is removed from the final output. if option_pretty_print: object_data['zchildren'] = object_children else: object_data['children'] = object_children object_dict[object_name] = object_data return object_count def generate_scene_objects(scene): object_count = 0 object_dict = {} ambient_light = generate_ambient_light(scene) if ambient_light: object_dict['AmbientLight'] = ambient_light object_count += 1 if option_default_light: default_light = generate_default_light() object_dict['DefaultLight'] = default_light object_count += 1 if option_default_camera: default_camera = generate_default_camera() object_dict['DefaultCamera'] = default_camera object_count += 1 node = scene.GetRootNode() if node: for i in range(node.GetChildCount()): object_count += generate_object_hierarchy(node.GetChild(i), object_dict) return object_dict, object_count # ##################################################### # Generate Scene Output # ##################################################### def extract_scene(scene, filename): global_settings = scene.GetGlobalSettings() objects, nobjects = generate_scene_objects(scene) textures = generate_texture_dict(scene) materials = generate_material_dict(scene) geometries = generate_geometry_dict(scene) embeds = generate_embed_dict(scene) ntextures = len(textures) nmaterials = len(materials) ngeometries = len(geometries) position = serializeVector3( (0,0,0) ) rotation = serializeVector3( (0,0,0) ) scale = serializeVector3( (1,1,1) ) camera_names = generate_camera_name_list(scene) scene_settings = scene.GetGlobalSettings() # This does not seem to be any help here # global_settings.GetDefaultCamera() defcamera = camera_names[0] if len(camera_names) > 0 else "" if option_default_camera: defcamera = 'default_camera' metadata = { 'formatVersion': 3.2, 'type': 'scene', 'generatedBy': 'convert-to-threejs.py', 'objects': nobjects, 'geometries': ngeometries, 'materials': nmaterials, 'textures': ntextures } transform = { 'position' : position, 'rotation' : rotation, 'scale' : scale } defaults = { 'bgcolor' : 0, 'camera' : defcamera, 'fog' : '' } output = { 'objects': objects, 'geometries': geometries, 'materials': materials, 'textures': textures, 'embeds': embeds, 'transform': transform, 'defaults': defaults, } if option_pretty_print: output['0metadata'] = metadata else: output['metadata'] = metadata return output # ##################################################### # Generate Non-Scene Output # ##################################################### def extract_geometry(scene, filename): output = generate_non_scene_output(scene) return output # ##################################################### # File Helpers # ##################################################### def write_file(filepath, content): index = filepath.rfind('/') dir = filepath[0:index] #if not os.path.exists(dir): #os.makedirs(dir) out = open(filepath, "w") out.write(content.encode('utf8', 'replace')) out.close() def read_file(filepath): f = open(filepath) content = f.readlines() f.close() return content def copy_textures(textures): texture_dict = {} for key in textures: url = textures[key]['fullpath'] #src = replace_OutFolder2inFolder(url) #print( src ) #print( url ) if url in texture_dict: # texture has been copied continue if not os.path.exists(url): print("copy_texture error: we can't find this texture at " + url) continue try: index = url.rfind('/') if index == -1: index = url.rfind( '\\' ) filename = url[index+1:len(url)] saveFolder = "maps" saveFilename = saveFolder + "/" + filename #print( src ) #print( url ) #print( saveFilename ) if not os.path.exists(saveFolder): os.makedirs(saveFolder) shutil.copyfile(url, saveFilename) texture_dict[url] = True except IOError as e: print "I/O error({0}): {1} {2}".format(e.errno, e.strerror, url) def findFilesWithExt(directory, ext, include_path = True): ext = ext.lower() found = [] for root, dirs, files in os.walk(directory): for filename in files: current_ext = os.path.splitext(filename)[1].lower() if current_ext == ext: if include_path: found.append(os.path.join(root, filename)) else: found.append(filename) return found # ##################################################### # main # ##################################################### if __name__ == "__main__": from optparse import OptionParser try: from FbxCommon import * except ImportError: import platform msg = 'Could not locate the python FBX SDK!\n' msg += 'You need to copy the FBX SDK into your python install folder such as ' if platform.system() == 'Windows' or platform.system() == 'Microsoft': msg += '"Python26/Lib/site-packages"' elif platform.system() == 'Linux': msg += '"/usr/local/lib/python2.6/site-packages"' elif platform.system() == 'Darwin': msg += '"/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages"' msg += ' folder.' print(msg) sys.exit(1) usage = "Usage: %prog [source_file.fbx] [output_file.js] [options]" parser = OptionParser(usage=usage) parser.add_option('-t', '--triangulate', action='store_true', dest='triangulate', help="force quad geometry into triangles", default=False) parser.add_option('-x', '--ignore-textures', action='store_true', dest='notextures', help="don't include texture references in output file", default=False) parser.add_option('-n', '--no-texture-copy', action='store_true', dest='notexturecopy', help="don't copy texture files", default=False) parser.add_option('-u', '--force-prefix', action='store_true', dest='prefix', help="prefix all object names in output file to ensure uniqueness", default=False) parser.add_option('-f', '--flatten-scene', action='store_true', dest='geometry', help="merge all geometries and apply node transforms", default=False) parser.add_option('-y', '--force-y-up', action='store_true', dest='forceyup', help="ensure that the y axis shows up", default=False) parser.add_option('-c', '--add-camera', action='store_true', dest='defcamera', help="include default camera in output scene", default=False) parser.add_option('-l', '--add-light', action='store_true', dest='deflight', help="include default light in output scene", default=False) parser.add_option('-p', '--pretty-print', action='store_true', dest='pretty', help="prefix all object names in output file", default=False) (options, args) = parser.parse_args() option_triangulate = options.triangulate option_textures = True if not options.notextures else False option_copy_textures = True if not options.notexturecopy else False option_prefix = options.prefix option_geometry = options.geometry option_forced_y_up = options.forceyup option_default_camera = options.defcamera option_default_light = options.deflight option_pretty_print = options.pretty # Prepare the FBX SDK. sdk_manager, scene = InitializeSdkObjects() converter = FbxGeometryConverter(sdk_manager) # The converter takes an FBX file as an argument. if len(args) > 1: print("\nLoading file: %s" % args[0]) result = LoadScene(sdk_manager, scene, args[0]) else: result = False print("\nUsage: convert_fbx_to_threejs [source_file.fbx] [output_file.js]\n") if not result: print("\nAn error occurred while loading the file...") else: if option_triangulate: print("\nForcing geometry to triangles") triangulate_scene(scene) axis_system = FbxAxisSystem.MayaYUp if not option_forced_y_up: # According to asset's coordinate to convert scene upVector = scene.GetGlobalSettings().GetAxisSystem().GetUpVector(); if upVector[0] == 3: axis_system = FbxAxisSystem.MayaZUp axis_system.ConvertScene(scene) inputFolder = args[0].replace( "\\", "/" ); index = args[0].rfind( "/" ); inputFolder = inputFolder[:index] outputFolder = args[1].replace( "\\", "/" ); index = args[1].rfind( "/" ); outputFolder = outputFolder[:index] if option_geometry: output_content = extract_geometry(scene, os.path.basename(args[0])) else: output_content = extract_scene(scene, os.path.basename(args[0])) if option_pretty_print: output_string = json.dumps(output_content, indent=4, cls=CustomEncoder, separators=(',', ': '), sort_keys=True) output_string = executeRegexHacks(output_string) else: output_string = json.dumps(output_content, separators=(',', ': '), sort_keys=True) output_path = os.path.join(os.getcwd(), args[1]) write_file(output_path, output_string) if option_copy_textures: copy_textures( output_content['textures'] ) print("\nExported Three.js file to:\n%s\n" % output_path) # Destroy all objects created by the FBX SDK. sdk_manager.Destroy() sys.exit(0)