"""FBX to Three.js converter (based on FBX import example from Autodesk FBX SDK) Converts FBX scene file into Three.js scene format (one scene file and several ascii model files plus textures). ------------------------- How to use this converter ------------------------- python convert_fbx_three.py scene.fbx output_folder ------------ Dependencies ------------ Requires Autodesk FBX SDK Python bindings. If your platform is not included in "modules", full SDK can be downloaded from here: http://usa.autodesk.com/adsk/servlet/pc/index?siteID=123112&id=6837478 --------------------------- How to load generated scene --------------------------- -------- Features -------- - scene + models + materials + textures - multiple UV layers ------------------- Current limitations ------------------- - only very small subset from FBX is currently supported - static meshes - triangles and quads - max two UV layers - Lambert and Phong materials (partial: Three doesn't support emissive color) - no lights yet - no cameras yet (default one is created instead) ------ Author ------ AlteredQualia http://alteredqualia.com """ import os import sys import random import math import pprint import shutil import string import os.path # load platform specific binary modules platform_map = { "Windows" : { "folder": "win", "versions" : { 2 : "Python26_x86", 3 : "Python31_x86" } }, "Linux" : { "folder": "linux", "versions" : { 2 : "Python26_x86", 3 : "Python31_x86" } }, "Darwin" : { "folder": "mac", "versions" : { 2 : "Python26_x86", 3 : "Python31_x86" } } } import platform system = platform.system() version = sys.version_info[0] if system in platform_map: if version in platform_map[system]["versions"]: mod_path = os.path.join(sys.path[0], "modules", platform_map[system]["folder"], platform_map[system]["versions"][version]) print mod_path sys.path.append(mod_path) # ##################################################### # Configuration # ##################################################### DEFAULTS = { "bgcolor" : [0, 0, 0], "bgalpha" : 1.0, "camera" : { "name" : "default_camera", "type" : "perspective", "near" : 1, "far" : 10000, "fov" : 60, "aspect": 1.333, "position" : [0, 0, 10], "target" : [0, 0, 0] } } MATERIALS_IN_SCENE = True DEBUG_FBX_JSON = True # default colors for debugging (each material gets one distinct color): # white, red, green, blue, yellow, cyan, magenta COLORS = [0xeeeeee, 0xee0000, 0x00ee00, 0x0000ee, 0xeeee00, 0x00eeee, 0xee00ee] # ##################################################### # Templates - scene # ##################################################### TEMPLATE_SCENE_ASCII = u"""\ // Converted from: %(fname)s // Generated with FBX -> Three.js converter // http://github.com/alteredq/three.js/blob/master/utils/exporters/fbx/convert_fbx_three.py var url_base = %(url_base)s, url_models = url_base + "models/", url_textures = url_base + "textures/"; var scene = { %(sections)s "defaults" : { "bgcolor" : %(bgcolor)s, "bgalpha" : %(bgalpha)f, "camera" : %(defcamera)s } } postMessage( scene ); """ TEMPLATE_SECTION = """ "%s" : { %s }, """ TEMPLATE_OBJECT = """\ %(object_id)s : { "geometry" : %(geometry_id)s, "materials" : [ %(material_id)s ], "position" : %(position)s, "rotation" : %(rotation)s, "scale" : %(scale)s, "visible" : true }""" TEMPLATE_GEOMETRY = """\ %(geometry_id)s : { "type" : "ascii_mesh", "url" : url_models + %(model_file)s }""" TEMPLATE_TEXTURE = """\ %(texture_id)s : { "url": url_textures + %(texture_file)s }""" TEMPLATE_MATERIAL_SCENE = """\ %(material_id)s : { "type": %(type)s, "parameters": { %(parameters)s } }""" TEMPLATE_CAMERA_PERSPECTIVE = """\ %(camera_id)s : { "type" : "perspective", "fov" : %(fov)f, "aspect": %(aspect)f, "near" : %(near)f, "far" : %(far)f, "position": %(position)s, "target" : %(target)s }""" TEMPLATE_CAMERA_ORTHO = """\ %(camera_id)s: { "type" : "ortho", "left" : %(left)f, "right" : %(right)f, "top" : %(top)f, "bottom": %(bottom)f, "near" : %(near)f, "far" : %(far)f, "position": %(position)s, "target" : %(target)s }""" TEMPLATE_VEC3 = '[ %f, %f, %f ]' TEMPLATE_VEC2 = '[ %f, %f ]' TEMPLATE_STRING = '"%s"' TEMPLATE_HEX = "0x%06x" # ##################################################### # Templates - model # ##################################################### TEMPLATE_MODEL_ASCII = u"""\ // Converted from: %(fname)s // vertices: %(nvertex)d // faces: %(nface)d // materials: %(nmaterial)d // // Generated with FBX -> Three.js converter // http://github.com/alteredq/three.js/blob/master/utils/exporters/fbx/convert_fbx_three.py var model = { 'materials': [%(materials)s], 'normals': [%(normals)s], 'vertices': [%(vertices)s], 'uvs': [%(uvs)s], 'uvs2': [%(uvs2)s], 'triangles': [%(triangles)s], 'trianglesUvs': [%(trianglesUvs)s], 'trianglesNormals': [%(trianglesNormals)s], 'trianglesNormalsUvs': [%(trianglesNormalsUvs)s], 'quads': [%(quads)s], 'quadsUvs': [%(quadsUvs)s], 'quadsNormals': [%(quadsNormals)s], 'quadsNormalsUvs': [%(quadsNormalsUvs)s], 'end': (new Date).getTime() } postMessage( model ); """ TEMPLATE_VERTEX = "%f,%f,%f" TEMPLATE_UV_TRI = "%f,%f,%f,%f,%f,%f" TEMPLATE_UV_QUAD = "%f,%f,%f,%f,%f,%f,%f,%f" TEMPLATE_TRI = "%d,%d,%d,%d" TEMPLATE_QUAD = "%d,%d,%d,%d,%d" TEMPLATE_TRI_UV = "%d,%d,%d,%d,%d,%d,%d" TEMPLATE_QUAD_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_TRI_N = "%d,%d,%d,%d,%d,%d,%d" TEMPLATE_QUAD_N = "%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_TRI_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_QUAD_N_UV = "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d" TEMPLATE_N = "%f,%f,%f" TEMPLATE_UV = "%f,%f" # ##################################################### # Templates - misc # ##################################################### TEMPLATE_HTACCESS = """\ SetOutputFilter DEFLATE """ # ##################################################### # Parser - global settings # ##################################################### def extract_color(color): return [color.mRed, color.mGreen, color.mBlue] def extract_vec2(v): return [v[0], v[1]] def extract_vec3(v): return [v[0], v[1], v[2]] def extract_global_settings(settings): settings = { "ambient_color" : extract_color(settings.GetAmbientColor()), "default_camera": settings.GetDefaultCamera().Buffer() } return settings def extract_object_properties(object): info = {} property = object.GetFirstProperty() while property.IsValid(): name = property.GetName().Buffer() value = "UNIDENTIFIED" ptype = property.GetPropertyDataType().GetType() if ptype in [eBOOL1, eDOUBLE1, eINTEGER1, eDOUBLE4, eDOUBLE3, eFLOAT1]: value = property.Get() elif ptype == eSTRING: value = property.Get().Buffer() info[name] = { "label" : property.GetLabel().Buffer(), "type" : property.GetPropertyDataType().GetName(), "value" : value } property = object.GetNextProperty(property) return info def extract_node_generic_info(node): info = [] node_info = { "name" : node.GetName(), "type" : node.ClassId.GetFbxFileTypeName(), "properties" : extract_object_properties(node) } info.append(node_info) for i in range(node.GetChildCount()): info += extract_node_generic_info(node.GetChild(i)) return info def extract_generic_info(scene): info_list = [] root_node = scene.GetRootNode() for i in range(root_node.GetChildCount()): child = root_node.GetChild(i) info_list += extract_node_generic_info(child) info_dict = {} for item in info_list: name = item["name"] info_dict[name] = item["properties"] return info_dict # ##################################################### # Parser - hierarchy # ##################################################### def extract_node_hierarchy(node, depth): hierarchy = { "name" : node.GetName(), "depth" : depth, "children" : [] } for i in range(node.GetChildCount()): hierarchy["children"].append(extract_node_hierarchy(node.GetChild(i), depth + 1)) return hierarchy def extract_hierarchy(scene): root = scene.GetRootNode() hierarchy = { "name" : "root", "depth" : 0, "children" : [] } for i in range(root.GetChildCount()): hierarchy["children"].append(extract_node_hierarchy(root.GetChild(i), 1)) return hierarchy # ##################################################### # Parser - mesh - layers # ##################################################### def extract_layer_groups(layer, poly_count): group = [] polygroups = layer.GetPolygonGroups() if polygroups and \ polygroups.GetMappingMode() == KFbxLayerElement.eBY_POLYGON and \ polygroups.GetReferenceMode() == KFbxLayerElement.eINDEX: polygroups_index_array = polygroups.GetIndexArray() group = [] for p in range(poly_count): group_id = polygroups_index_array.GetAt(p) group.append(group_id) return group def extract_layer_materials(layer, poly_count, mesh): material_index_layer = [] materials = layer.GetMaterials() if materials: index_array = materials.GetIndexArray() index_array_count = index_array.GetCount() for i in range(index_array_count): material_index_layer.append(index_array.GetAt(i)) return material_index_layer def extract_layer_uvs(layer, poly_count, mesh): uv_values = [] uv_index_layer = [] uvs = layer.GetUVs() if uvs: uvs_array = uvs.GetDirectArray() uvs_count = uvs_array.GetCount() # values for i in range(uvs_count): uv = extract_vec2(uvs_array.GetAt(i)) uv_values.append(uv) # indices if uvs.GetMappingMode() == KFbxLayerElement.eBY_CONTROL_POINT \ and uvs.GetReferenceMode() == KFbxLayerElement.eDIRECT: for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): id = mesh.GetPolygonVertex(p, v) tmp.append(id) uv_index_layer.append(tmp) elif uvs.GetMappingMode() == KFbxLayerElement.eBY_CONTROL_POINT \ and uvs.GetReferenceMode() == KFbxLayerElement.eINDEX_TO_DIRECT: uvs_index_array = uvs.GetIndexArray() for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): control_point_index = mesh.GetPolygonVertex(p, v) id = uvs_index_array.GetAt(control_point_index) tmp.append(id) uv_index_layer.append(tmp) elif uvs.GetMappingMode() == KFbxLayerElement.eBY_POLYGON_VERTEX \ and ( uvs.GetReferenceMode() == KFbxLayerElement.eDIRECT \ or uvs.GetReferenceMode() == KFbxLayerElement.eINDEX_TO_DIRECT ): for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): id = mesh.GetTextureUVIndex(p, v) tmp.append(id) uv_index_layer.append(tmp) return uv_values, uv_index_layer def extract_layer_colors(layer, poly_count, mesh): color_values = [] color_index_layer = [] colors = layer.GetVertexColors() if colors: colors_array = colors.GetDirectArray() colors_count = colors_array.GetCount() # values tmp = [] for i in range(colors_count): color = extract_color(colors_array.GetAt(i)) color_values.append(color) # indices if colors.GetMappingMode() == KFbxLayerElement.eBY_CONTROL_POINT \ and colors.GetReferenceMode() == KFbxLayerElement.eDIRECT: for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): id = mesh.GetPolygonVertex(p, v) tmp.append(id) color_index_layer.append(tmp) elif colors.GetMappingMode() == KFbxLayerElement.eBY_CONTROL_POINT \ and colors.GetReferenceMode() == KFbxLayerElement.eINDEX_TO_DIRECT: for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): control_point_index = mesh.GetPolygonVertex(p, v) id = colors.GetIndexArray().GetAt(control_point_index) tmp.append(id) color_index_layer.append(tmp) elif colors.GetMappingMode() == KFbxLayerElement.eBY_POLYGON_VERTEX \ and colors.GetReferenceMode() == KFbxLayerElement.eDIRECT: vertex_id = 0 for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): tmp.append(vertex_id) vertex_id += 1 color_index_layer.append(tmp) elif colors.GetMappingMode() == KFbxLayerElement.eBY_POLYGON_VERTEX \ and colors.GetReferenceMode() == KFbxLayerElement.eINDEX_TO_DIRECT: colors_index_array = colors.GetIndexArray() vertex_id = 0 for p in range(poly_count): tmp = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): id = colors_index_array.GetAt(vertex_id) tmp.append(id) vertex_id += 1 color_index_layer.append(tmp) return color_values, color_index_layer # ##################################################### # Parser - mesh - polygons # ##################################################### def extract_polygons(mesh): poly_count = mesh.GetPolygonCount() layer_count = mesh.GetLayerCount() control_points = mesh.GetControlPoints() layers_groups = [] layers_uvs = [] layers_colors = [] indices_uv = [] indices_color = [] indices_vertex = [] indices_material = [] # per layer data for l in range(layer_count): layer = mesh.GetLayer(l) # groups group = extract_layer_groups(layer, poly_count) if group: groups_layers.append(group) # uvs uv_values, uv_index_layer = extract_layer_uvs(layer, poly_count, mesh) if uv_values: layers_uvs.append(uv_values) if uv_index_layer: indices_uv.append(uv_index_layer) # colors color_values, color_index_layer = extract_layer_colors(layer, poly_count, mesh) if color_values: layers_colors.append(color_values) if color_index_layer: indices_color.append(color_index_layer) # materials material_index_layer = extract_layer_materials(layer, poly_count, mesh) if material_index_layer: indices_material.append(material_index_layer) # single layer data for p in range(poly_count): face_vertex_index = [] polygon_size = mesh.GetPolygonSize(p) for v in range(polygon_size): control_point_index = mesh.GetPolygonVertex(p, v) face_vertex_index.append(control_point_index) indices_vertex.append(face_vertex_index) polygons = {} conditional_set(polygons, "indices_vertex", indices_vertex) conditional_set(polygons, "indices_uv", indices_uv) conditional_set(polygons, "indices_color", indices_color) conditional_set(polygons, "indices_material", indices_material) conditional_set(polygons, "layers_uvs", layers_uvs) conditional_set(polygons, "layers_colors", layers_colors) conditional_set(polygons, "layers_groups", layers_groups) return polygons def extract_control_points(mesh): control_points_count = mesh.GetControlPointsCount() control_points = mesh.GetControlPoints() layer_count = mesh.GetLayerCount() coordinates = [] layers_normals = [] for i in range(control_points_count): coordinates.append( extract_vec3( control_points[i] ) ) for layer in range(layer_count): normals = mesh.GetLayer(layer).GetNormals() if normals: znormals = [] for i in range(control_points_count): if normals.GetMappingMode() == KFbxLayerElement.eBY_CONTROL_POINT: if normals.GetReferenceMode() == KFbxLayerElement.eDIRECT: znormals.append( extract_vec3( normals.GetDirectArray().GetAt(i) ) ) if znormals: layers_normals.append( znormals ) points = {} conditional_set(points, "coordinates", coordinates) conditional_set(points, "normals", layers_normals) return points # ##################################################### # Parser - mesh - materials # ##################################################### def extract_materials(mesh): materials_layers = [] material_count = 0 layer_count = mesh.GetLayerCount() node = None if mesh: node = mesh.GetNode() if node: material_count = node.GetMaterialCount() for layer in range(layer_count): layer_materials = [] materials = mesh.GetLayer(layer).GetMaterials() if materials: if materials.GetReferenceMode() == KFbxLayerElement.eINDEX: #Materials are in an undefined external table continue if material_count > 0: for i in range(material_count): material = node.GetMaterial(i) zmaterial = { "name" : material.GetName() } # Get the implementation to see if it's a hardware shader. implementation = GetImplementation(material, "ImplementationHLSL") implemenation_type = "HLSL" if not implementation: implementation = GetImplementation(material, "ImplementationCGFX") implemenation_type = "CGFX" if implementation: # Now we have a hardware shader, let's read it zmaterial["hardware_shader_type"] = implemenation_type.Buffer() # Skipped parsing of shaders elif material.GetClassId().Is(KFbxSurfaceLambert.ClassId): ambient = material.GetAmbientColor() diffuse = material.GetDiffuseColor() emissive = material.GetEmissiveColor() zmaterial["ambient"] = [ambient.Get()[0], ambient.Get()[1], ambient.Get()[2]] zmaterial["diffuse"] = [diffuse.Get()[0], diffuse.Get()[1], diffuse.Get()[2]] zmaterial["emissive"] = [emissive.Get()[0], emissive.Get()[1], emissive.Get()[2]] zmaterial["opacity"] = material.GetTransparencyFactor().Get() elif (material.GetClassId().Is(KFbxSurfacePhong.ClassId)): ambient = material.GetAmbientColor() diffuse = material.GetDiffuseColor() emissive = material.GetEmissiveColor() specular = material.GetSpecularColor() zmaterial["ambient"] = [ambient.Get()[0], ambient.Get()[1], ambient.Get()[2]] zmaterial["diffuse"] = [diffuse.Get()[0], diffuse.Get()[1], diffuse.Get()[2]] zmaterial["emissive"] = [emissive.Get()[0], emissive.Get()[1], emissive.Get()[2]] zmaterial["specular"] = [specular.Get()[0], specular.Get()[1], specular.Get()[2]] zmaterial["opacity"] = material.GetTransparencyFactor().Get() zmaterial["shininess"] = material.GetShininess().Get() zmaterial["reflectivity"] = material.GetReflectionFactor().Get() zmaterial["shading_model"] = material.GetShadingModel().Get().Buffer() layer_materials.append(zmaterial) if layer_materials: materials_layers.append(layer_materials) return materials_layers # ##################################################### # Parser - mesh - textures # ##################################################### def extract_texture_info(texture, blend_mode): mapping_types = [ "Null", "Planar", "Spherical", "Cylindrical", "Box", "Face", "UV", "Environment" ] alpha_sources = [ "None", "RGB Intensity", "Black" ] blend_modes = [ "Translucent", "Add", "Modulate", "Modulate2" ] material_uses = [ "Model Material", "Default Material" ] texture_uses = [ "Standard", "Shadow Map", "Light Map", "Spherical Reflection Map", "Sphere Reflection Map" ] planar_mapping_normals = [ "X", "Y", "Z" ] info = { "name" : texture.GetName(), "filename" : texture.GetFileName(), "scale_u" : texture.GetScaleU(), "scale_v" : texture.GetScaleV(), "swap_uv" : texture.GetSwapUV(), "translation_u" : texture.GetTranslationU(), "translation_v" : texture.GetTranslationV(), "rotation_u" : texture.GetRotationU(), "rotation_v" : texture.GetRotationV(), "rotation_w" : texture.GetRotationW(), "mapping" : mapping_types[texture.GetMappingType()], "alpha_source" : alpha_sources[texture.GetAlphaSource()], "cropping_left" : texture.GetCroppingLeft(), "cropping_top" : texture.GetCroppingTop(), "cropping_right" : texture.GetCroppingRight(), "cropping_bottom" : texture.GetCroppingBottom(), "alpha" : texture.GetDefaultAlpha(), "material_use" : material_uses[texture.GetMaterialUse()], "texture_use" : texture_uses[texture.GetTextureUse()] } if texture.GetMappingType() == KFbxTexture.ePLANAR: info["planar_mapping_normal"] = planar_mapping_normals[texture.GetPlanarMappingNormal()] if blend_mode >= 0: info["blend_mode"] = blend_modes[blend_mode] return info def extract_texture_info_by_property(property, material_index): textures = [] if property.IsValid(): #Here we have to check if it's layeredtextures, or just textures: layered_texture_count = property.GetSrcObjectCount(KFbxLayeredTexture.ClassId) if layered_texture_count > 0: for j in range(layered_texture_count): ltexture = { "layered_texture" : j, "layers" : [] } layered_texture = property.GetSrcObject(KFbxLayeredTexture.ClassId, j) texture_count = layered_texture.GetSrcObjectCount(KFbxTexture.ClassId) for k in range(texture_count): ztexture = lLayeredTexture.GetSrcObject(KFbxTexture.ClassId, k) if ztexture: # NOTE the blend mode is ALWAYS on the LayeredTexture and NOT the one on the texture. # Why is that? because one texture can be shared on different layered textures and might # have different blend modes. blend_mode = layered_texture.GetTextureBlendMode(k) texture = { "material_index" : material_index, "texture_index" : k, "property_name" : property.GetName().Buffer(), "blend_mode" : blend_mode, "info" : extract_texture_info(ztexture, blend_mode) } ltexture["layers"].append(texture) textures.append(ltexture) # no layered texture simply get on the property else: texture_count = property.GetSrcObjectCount(KFbxTexture.ClassId) for j in range(texture_count): ztexture = property.GetSrcObject(KFbxTexture.ClassId, j) if ztexture: texture = { "material_index" : material_index, "info" : extract_texture_info(ztexture, -1) } textures.append(texture) return textures def extract_textures(mesh): textures = {} node = mesh.GetNode() materials_count = node.GetSrcObjectCount(KFbxSurfaceMaterial.ClassId) for material_index in range(materials_count): material = node.GetSrcObject(KFbxSurfaceMaterial.ClassId, material_index) #go through all the possible textures if material: for texture_index in range(KFbxLayerElement.LAYERELEMENT_TYPE_TEXTURE_COUNT): property = material.FindProperty(KFbxLayerElement.TEXTURE_CHANNEL_NAMES[texture_index]) texture = extract_texture_info_by_property(property, material_index) if texture: textures[property.GetName().Buffer()] = texture return textures def extract_material_mapping(mesh): return {} def extract_material_connections(mesh): return {} def extract_link(mesh): return {} def extract_shape(mesh): return {} def extract_mesh(node): mesh = node.GetNodeAttribute() zmesh = {} conditional_set(zmesh, "name", node.GetName()) conditional_set(zmesh, "shape", extract_shape(mesh)) conditional_set(zmesh, "link", extract_link(mesh)) conditional_set(zmesh, "control_points", extract_control_points(mesh)) conditional_set(zmesh, "faces", extract_polygons(mesh)) conditional_set(zmesh, "textures", extract_textures(mesh)) conditional_set(zmesh, "materials", extract_materials(mesh)) conditional_set(zmesh, "material_mapping", extract_material_mapping(mesh)) conditional_set(zmesh, "material_connections", extract_material_connections(mesh)) return zmesh # ##################################################### # Parser - nodes (todo) # ##################################################### def extract_marker(node): return {} def extract_nurb(node): return {} def extract_patch(node): return {} def extract_skeleton(node): return {} def extract_camera(node): return {} def extract_light(node): return {} # ##################################################### # Parser - nodes (generic) # ##################################################### def extract_target(node): if node.GetTarget(): return node.GetTarget().GetName() return "" def extract_transform(node): translation = node.GetGeometricTranslation(KFbxNode.eSOURCE_SET) rotation = node.GetGeometricRotation(KFbxNode.eSOURCE_SET) scale = node.GetGeometricScaling(KFbxNode.eSOURCE_SET) transform = { "translation": [ translation[0], translation[1], translation[2] ], "rotation" : [ rotation[0], rotation[1], rotation[2] ], "scale" : [ scale[0], scale[1], scale[2] ], } return transform def extract_transform_propagation(node): rotation_order = node.GetRotationOrder(KFbxNode.eSOURCE_SET) order_map = { eEULER_XYZ : "Euler XYZ", eEULER_XZY : "Euler XZY", eEULER_YZX : "Euler YZX", eEULER_YXZ : "Euler YXZ", eEULER_ZXY : "Euler ZXY", eEULER_ZYX : "Euler ZYX", eSPHERIC_XYZ:"Spheric XYZ" } order = "Euler XYZ" if rotation_order in order_map: order = order_map[rotation_order] # Use the Rotation space only for the limits # (keep using eEULER_XYZ for the rest) if node.GetUseRotationSpaceForLimitOnly(KFbxNode.eSOURCE_SET): only_limits = 1 else: only_limits = 0 inherit_type = node.GetTransformationInheritType() inherit_map = { eINHERIT_RrSs : "RrSs", eINHERIT_RSrs : "RSrs", eINHERIT_Rrs : "Rrs" } if inherit_type in inherit_map: inheritance = inherit_map[inherit_type] transform_propagation = { "rotation_order" : order, "only_limits" : only_limits, "inheritance" : inheritance } return transform_propagation def extract_pivots(node): return {} def extract_user_properties(node): return {} def extract_node_content(node): nodes = [] if node.GetNodeAttribute() == None: return "NULL" else: attribute_type = node.GetNodeAttribute().GetAttributeType() ztype = "undefined" data = {} type_map = { KFbxNodeAttribute.eMARKER : ["marker", extract_marker], KFbxNodeAttribute.eSKELETON : ["skeleton", extract_skeleton], KFbxNodeAttribute.eMESH : ["mesh", extract_mesh], KFbxNodeAttribute.eNURB : ["nurb", extract_nurb], KFbxNodeAttribute.ePATCH : ["patch", extract_patch], KFbxNodeAttribute.eCAMERA : ["camera", extract_camera], KFbxNodeAttribute.eLIGHT : ["light", extract_light] } if attribute_type in type_map: ztype = type_map[attribute_type][0] data = type_map[attribute_type][1](node) content = { } conditional_set(content, "type", ztype) conditional_set(content, "data", data) conditional_set(content, "target", extract_target(node)) conditional_set(content, "pivots", extract_pivots(node)) conditional_set(content, "transform", extract_transform(node)) conditional_set(content, "transform_propagation", extract_transform_propagation(node)) conditional_set(content, "user_properties", extract_user_properties(node)) nodes.append(content) for i in range(node.GetChildCount()): nodes += extract_node_content(node.GetChild(i)) return nodes def extract_nodes(scene): nodes = [] root = scene.GetRootNode() if root: for i in range(root.GetChildCount()): nodes += extract_node_content(root.GetChild(i)) return nodes def filter_mesh(item): return item["type"] == "mesh" def extract_meshes(scene): nodes = extract_nodes(scene) meshes = filter(filter_mesh, nodes) return meshes # ##################################################### # JSON extractors # ##################################################### def get_material_texture(material_index, textures, property): result = [t for t in textures.get(property, []) if t["material_index"] == material_index] return result def collect_textures(data): texture_set = set() for mesh in data["meshes"]: for texture_type in mesh["data"]["textures"]: for texture in mesh["data"]["textures"][texture_type]: texture_file = base_filename(texture["info"]["filename"]) texture_set.add(texture_file) return list(texture_set) # ##################################################### # Generator - model # ##################################################### def generate_vertex(v): return TEMPLATE_VERTEX % (v[0], v[1], v[2]) def generate_triangle(f): v = f['vertex'] return TEMPLATE_TRI % (v[0], v[1], v[2], f['material']) def generate_triangle_uv(f): v = f['vertex'] uv = f['uv'] return TEMPLATE_TRI_UV % (v[0], v[1], v[2], f['material'], uv[0], uv[1], uv[2]) def generate_triangle_n(f): v = f['vertex'] n = f['normal'] return TEMPLATE_TRI_N % (v[0], v[1], v[2], f['material'], n[0], n[1], n[2]) def generate_triangle_n_uv(f): v = f['vertex'] n = f['normal'] uv = f['uv'] return TEMPLATE_TRI_N_UV % (v[0], v[1], v[2], f['material'], n[0], n[1], n[2], uv[0], uv[1], uv[2]) def generate_quad(f): vi = f['vertex'] return TEMPLATE_QUAD % (vi[0], vi[1], vi[2], vi[3], f['material']) def generate_quad_uv(f): v = f['vertex'] uv = f['uv'] return TEMPLATE_QUAD_UV % (v[0], v[1], v[2], v[3], f['material'], uv[0], uv[1], uv[2], uv[3]) def generate_quad_n(f): v = f['vertex'] n = f['normal'] return TEMPLATE_QUAD_N % (v[0], v[1], v[2], v[3], f['material'], n[0], n[1], n[2], n[3]) def generate_quad_n_uv(f): v = f['vertex'] n = f['normal'] uv = f['uv'] return TEMPLATE_QUAD_N_UV % (v[0], v[1], v[2], v[3], f['material'], n[0], n[1], n[2], n[3], uv[0], uv[1], uv[2], uv[3]) def generate_normal(n): return TEMPLATE_N % (n[0], n[1], n[2]) def generate_uv(uv): return TEMPLATE_UV % (uv[0], uv[1]) # ##################################################### # Generator - scene # ##################################################### def generate_vec3(vec): return TEMPLATE_VEC3 % (vec[0], vec[1], vec[2]) def generate_vec2(vec): return TEMPLATE_VEC2 % (vec[0], vec[1]) def generate_hex(number): return TEMPLATE_HEX % number def generate_string(s): return TEMPLATE_STRING % s def generate_section(label, content): return TEMPLATE_SECTION % (label, content) def get_mesh_filename(mesh): object_id = mesh["data"]["name"] filename = "%s.js" % sanitize(object_id) return filename def generate_material_id_list(materials): chunks = [] for layer in materials: for material in layer: chunks.append(material["name"]) return ",".join(chunks) def generate_objects(data): chunks = [] for mesh in data["meshes"]: object_id = mesh["data"]["name"] geometry_id = "geo_%s" % object_id material_id = generate_material_id_list(mesh["data"]["materials"]) position = mesh["transform"]["translation"] rotation = mesh["transform"]["rotation"] scale = mesh["transform"]["scale"] # hunt for local transform if object_id in data["generic_info"]: gi = data["generic_info"][object_id] lt = gi.get("Lcl Translation", {}) local_translation = lt.get("value", [0,0,0]) position[0] += local_translation[0] position[1] += local_translation[1] position[2] += local_translation[2] object_string = TEMPLATE_OBJECT % { "object_id" : generate_string(object_id), "geometry_id" : generate_string(geometry_id), "material_id" : generate_string(material_id), "position" : generate_vec3(position), "rotation" : generate_vec3(rotation), "scale" : generate_vec3(scale) } chunks.append(object_string) return ",\n\n".join(chunks) def generate_geometries(data): chunks = [] for mesh in data["meshes"]: geometry_id = "geo_%s" % mesh["data"]["name"] model_filename = get_mesh_filename(mesh) geometry_string = TEMPLATE_GEOMETRY % { "geometry_id" : generate_string(geometry_id), "model_file" : generate_string(model_filename) } chunks.append(geometry_string) return ",\n\n".join(chunks) def generate_textures_scene(data): chunks = [] texture_set = set() for mesh in data["meshes"]: for texture_type in mesh["data"]["textures"]: for texture in mesh["data"]["textures"][texture_type]: texture_id = texture["info"]["name"] if texture_id not in texture_set: texture_set.add(texture_id) texture_file = base_filename(texture["info"]["filename"]) texture_string = TEMPLATE_TEXTURE % { "texture_id" : generate_string(texture_id), "texture_file" : generate_string(texture_file) } chunks.append(texture_string) return ",\n\n".join(chunks) def generate_materials_scene(data): chunks = [] type_map = { "Lambert" : "MeshLambertMaterial", "Phong" : "MeshPhongMaterial" } for mesh in data["meshes"]: for layer in mesh["data"]["materials"]: for material_index in range(len(layer)): material = layer[material_index] material_id = material["name"] shading = material["shading_model"] material_type = type_map.get(shading, "MeshBasicMaterial") parameters = "color: %s" % generate_hex(rgb2int(material["diffuse"])) if shading == "Phong": parameters += ", ambient: %s" % generate_hex(rgb2int(material["ambient"])) parameters += ", specular: %s" % generate_hex(rgb2int(material["specular"])) parameters += ", shininess: %f" % material["shininess"] # TODO: proper handling of textures colorMap = get_material_texture(material_index, mesh["data"]["textures"], "DiffuseColor") lightMap = get_material_texture(material_index, mesh["data"]["textures"], "AmbientColor") bumpMap = get_material_texture(material_index, mesh["data"]["textures"], "Bump") if map: parameters += ", map: %s" % generate_string(colorMap[0]["info"]["name"]) if lightMap: parameters += ", lightMap: %s" % generate_string(lightMap[0]["info"]["name"]) if bumpMap: parameters += ", bumpMap: %s" % generate_string(bumpMap[0]["info"]["name"]) material_string = TEMPLATE_MATERIAL_SCENE % { "material_id" : generate_string(material_id), "type" : generate_string(material_type), "parameters" : parameters } chunks.append(material_string) return ",\n\n".join(chunks) # TODO def generate_cameras(data): cameras = data.get("cameras", []) if not cameras: cameras.append(DEFAULTS["camera"]) chunks = [] for camera in cameras: if camera["type"] == "perspective": camera_string = TEMPLATE_CAMERA_PERSPECTIVE % { "camera_id" : generate_string(camera["name"]), "fov" : camera["fov"], "aspect" : camera["aspect"], "near" : camera["near"], "far" : camera["far"], "position" : generate_vec3(camera["position"]), "target" : generate_vec3(camera["target"]) } elif camera["type"] == "ortho": camera_string = TEMPLATE_CAMERA_ORTHO % { "camera_id" : generate_string(camera["name"]), "left" : camera["left"], "right" : camera["right"], "top" : camera["top"], "bottom" : camera["bottom"], "near" : camera["near"], "far" : camera["far"], "position" : generate_vec3(camera["position"]), "target" : generate_vec3(camera["target"]) } chunks.append(camera_string) return ",\n\n".join(chunks) def generate_lights(data): return "" def generate_ascii_scene(data): objects = generate_objects(data) geometries = generate_geometries(data) textures = generate_textures_scene(data) materials = generate_materials_scene(data) cameras = generate_cameras(data) lights = generate_lights(data) sections = [ ["objects", objects], ["geometries", geometries], ["textures", textures], ["materials", materials], ["cameras", cameras], ["lights", lights] ] chunks = [] for label, content in sections: if content: chunks.append(generate_section(label, content)) sections_string = "\n".join(chunks) default_camera = "default_camera" parameters = { "fname" : data["source_file"], "url_base" : generate_string(data["base_folder"]), "sections" : sections_string, "bgcolor" : generate_vec3(DEFAULTS["bgcolor"]), "bgalpha" : DEFAULTS["bgalpha"], "defcamera" : generate_string(default_camera) } text = TEMPLATE_SCENE_ASCII % parameters return text # ##################################################### # Generator - materials # ##################################################### def generate_color(i): """Generate hex color corresponding to integer. Colors should have well defined ordering. First N colors are hardcoded, then colors are random (must seed random number generator with deterministic value before getting colors). """ if i < len(COLORS): return "0x%06x" % COLORS[i] else: return "0x%06x" % int(0xffffff * random.random()) def value2string(v): if type(v)==str and v[0:2] != "0x": return '"%s"' % v return str(v) def generate_material_model(material, index): m = { 'DbgName' :generate_string(material["name"]), 'DbgIndex' :index, 'DbgColor' :generate_color(index), "shading" :generate_string(material["shading_model"]), "opacity" : material["opacity"] } if material["shading_model"] in ["Lambert", "Phong"]: m["colorAmbient"] = material["ambient"] m["colorDiffuse"] = material["diffuse"] m["col_emissive"] = material["emissive"] if material["shading_model"] in ["Phong"]: m["colorSpecular"] = material["specular"] m["shininess"] = material["shininess"] if not MATERIALS_IN_SCENE: conditional_set(m, "mapDiffuse", material.get("mapDiffuse", 0)) conditional_set(m, "mapLightmap", material.get("mapLightmap", 0)) mtl_raw = ",\n".join(['\t"%s" : %s' % (n, value2string(v)) for n,v in sorted(m.items())]) mtl_string = "\t{\n%s\n\t}" % mtl_raw return mtl_string # ##################################################### # Generator - models # ##################################################### def generate_ascii_model(data): materials = data["materials"] vertices = data["control_points"]["coordinates"] normals = [] if "normals" in data["control_points"]: normals = data["control_points"]["normals"][0] uvs = [] uvs2 = [] if "layers_uvs" in data["faces"]: n_uvs = len(data["faces"]["layers_uvs"]) if n_uvs > 0: uvs = data["faces"]["layers_uvs"][0] if n_uvs > 1: uvs2 = data["faces"]["layers_uvs"][1] triangles = [] trianglesUvs = [] trianglesNormals = [] trianglesNormalsUvs = [] quads = [] quadsUvs = [] quadsNormals = [] quadsNormalsUvs = [] indices_vertex = data["faces"]["indices_vertex"] for vi in range(len(indices_vertex)): vertex_index = indices_vertex[vi] material_index = 0 if vi < len(data["faces"]["indices_material"][0]): material_index = data["faces"]["indices_material"][0][vi] face = { 'vertex' : vertex_index, 'material' : material_index } if normals: face["normal"] = vertex_index if uvs and "indices_uv" in data["faces"]: indices_uv = data["faces"]["indices_uv"] face["uv"] = indices_uv[0][vi] if len(indices_uv) > 1: face["uv2"] = indices_uv[1][vi] if len(vertex_index) == 3: if normals: if uvs: where = trianglesNormalsUvs else: where = trianglesNormals else: if uvs: where = trianglesUvs else: where = triangles elif len(vertex_index) == 4: if normals: if uvs: where = quadsNormalsUvs else: where = quadsNormals else: if uvs: where = quadsUvs else: where = quads where.append(face) nvertex = len(vertices) nface = len(indices_vertex) nmaterial = 0 text = TEMPLATE_MODEL_ASCII % { "fname" : source_file, "nvertex" : nvertex, "nface" : nface, "nmaterial" : nmaterial, "materials" : "".join(generate_material_model(m, i) for i, m in enumerate(materials[0])), "normals" : ",".join(generate_normal(n) for n in normals), "vertices" : ",".join(generate_vertex(v) for v in vertices), "uvs" : ",".join(generate_uv(u) for u in uvs), "uvs2" : ",".join(generate_uv(u) for u in uvs2), "triangles" : ",".join(generate_triangle(f) for f in triangles), "trianglesUvs" : ",".join(generate_triangle_uv(f) for f in trianglesUvs), "trianglesNormals" : ",".join(generate_triangle_n(f) for f in trianglesNormals), "trianglesNormalsUvs": ",".join(generate_triangle_n_uv(f) for f in trianglesNormalsUvs), "quads" : ",".join(generate_quad(f) for f in quads), "quadsUvs" : ",".join(generate_quad_uv(f) for f in quadsUvs), "quadsNormals" : ",".join(generate_quad_n(f) for f in quadsNormals), "quadsNormalsUvs" : ",".join(generate_quad_n_uv(f) for f in quadsNormalsUvs) } return text # ##################################################### # Helpers # ##################################################### def sanitize(text): chunks = [] for ch in text: if ch in (string.ascii_letters + string.digits + "_."): chunks.append(ch) else: chunks.append("_") return "".join(chunks) def base_filename(path): return os.path.basename(path) def rgb2int(rgb): color = (int(rgb[0]*255) << 16) + (int(rgb[1]*255) << 8) + int(rgb[2]*255); return color def conditional_set(where, label, what): """Set dictionary property only if it exists""" if what: where[label] = what def dump_data(data): """Generate pretty printed view of data.""" chunks = [] pp = pprint.PrettyPrinter(indent=2, width=160) if type(data) == list: for d in data: chunks.append(pp.pformat(d)) elif type(data) == dict: chunks.append(pp.pformat(data)) return "\n\n".join(chunks) def ensure_folder_exist(foldername): """Create folder (with whole path) if it doesn't exist yet.""" if not os.access(foldername, os.R_OK|os.W_OK|os.X_OK): os.makedirs(foldername) def abort(message): print message sys.exit(1) def write_file(fname, content): out = open(fname, "w") out.write(content) out.close() def copy_files(textures, src_folder, dst_folder): for texture in textures: src_file = os.path.join(src_folder, texture) if os.path.isfile(src_file): shutil.copy(src_file, dst_folder) else: print "WARNING: couldn't find [%s]" % src_file # ##################################################### # Main # ##################################################### if __name__ == "__main__": try: from FbxCommon import * except ImportError: import platform msg = "" 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"' abort('You need to copy the content in compatible subfolder under /lib/python into your python install folder such as %s folder.' % msg) sdkManager, scene = InitializeSdkObjects() if len(sys.argv) < 3: abort("Usage: convert_fbx_three.py [scene.fbx] [scene_folder]") source_file = sys.argv[1] output_folder = sys.argv[2] junk, base_folder = os.path.split(os.path.normpath(output_folder)) result = LoadScene(sdkManager, scene, source_file) if not result: abort("An error occurred while loading the scene ...") random.seed(42) # to get well defined debug color order for materials ensure_folder_exist(output_folder) ensure_folder_exist(output_folder+"/models") ensure_folder_exist(output_folder+"/textures") meshes = extract_meshes(scene) generic_info = extract_generic_info(scene) scene_text = "" data = { "meshes" : meshes, "generic_info": generic_info, "source_file" : source_file, "base_folder" : base_folder+"/" } scene_text += generate_ascii_scene(data) if DEBUG_FBX_JSON: scene_text += "/*" + dump_data(meshes) + "\n\n\n" + dump_data(generic_info) + "*/" scene_file = os.path.join(output_folder, "scene.js") htaccess_file = os.path.join(output_folder, ".htaccess") write_file(scene_file, scene_text) write_file(htaccess_file, TEMPLATE_HTACCESS) for mesh in meshes: model_text = generate_ascii_model(mesh["data"]) model_file = os.path.join(output_folder, "models", get_mesh_filename(mesh)) write_file(model_file, model_text) textures_src_folder = os.path.dirname(source_file) textures_dst_folder = os.path.join(output_folder, "textures") copy_files(collect_textures(data), textures_src_folder, textures_dst_folder) # Destroy all objects created by the FBX SDK sdkManager.Destroy()