Been a bit of recent discussion at func_ where people have been using the fbx format for models. I’ve bashed together a script which will hoover up a collection of fbx frames and convert them into an animated mdl file. It takes the geometry and the UVs from the first file in the series.

Installing it requires python 3.1 and a couple of libraries. The difficult one is the fbx python library from:

http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=WS73099cc142f48755-751de9951262947c01c-6dc7.htm,topicNumber=d0e8430

The fancy installer doesn’t really count for much, because it installs in “program files” and makes you manually copy the folder into your python directory.

The other library you need is qmdl from this very website, which is at least honest enough to provide you with a zip to install from manually.

Put the following python code into a file. You’ll need to tweak one line to create an ordered list of the files you want to use, the line is marked with #=====This is the line where the filesnames are set=====.

from FbxCommon import * import sys import struct from qmdl.mdl import Mdl #returns a list of all the mesh type nodes in the scene def get_mesh_nodes(scene): meshNodes = [] rootNode = scene.GetRootNode() if not rootNode: return meshNodes for i in range(rootNode.GetChildCount()): node = rootNode.GetChild(i) attributeType = (node.GetNodeAttribute().GetAttributeType()) if attributeType == FbxNodeAttribute.eMesh: meshNodes.append(node.GetMesh()) return meshNodes #returns the minimum and maximum xyz coordinate values over all given meshes def get_mesh_extents(meshes): #initialise min and max to the first coordinate of the first mesh minXYZ = maxXYZ = meshes[0].GetControlPoints()[0] for mesh in meshes: for i in range(mesh.GetControlPointsCount()): minXYZ = tuple(map(min, minXYZ, mesh.GetControlPoints()[i])) maxXYZ = tuple(map(max, maxXYZ, mesh.GetControlPoints()[i])) return (minXYZ, maxXYZ) def triangulate_meshes(meshes): triMeshes = [] converter = FbxGeometryConverter(sdkManager) for mesh in meshes: triMeshes.append(converter.TriangulateMesh(mesh)) return triMeshes #takes a list of triangulated meshes and loads the triangles into the model def load_mesh_triangles(meshes, outMdl): i = 0; for mesh in meshes: meshUVs = mesh.GetLayer(0).GetUVs() for pNum in range(mesh.GetPolygonCount()): tri = Mdl.Triangle() tri.vertices = (i, i+2, i+1) outMdl.triangles.append(tri) for j in range(3): skinUV = Mdl.Vertex() meshUVIndex = mesh.GetTextureUVIndex(pNum, j) meshUV = meshUVs.GetDirectArray().GetAt(meshUVIndex) skinUV.u = round(meshUV[0] * outMdl.skinwidth) skinUV.v = round((1-meshUV[1]) * outMdl.skinheight) outMdl.vertices.append(skinUV) i = i + 3 #appends a frame to the model with the pose from "meshes" def load_mesh_pose(meshes, outMdl): frame = Mdl.Frame(); for mesh in meshes: normals = mesh.GetLayer(0).GetNormals() controlPoints = mesh.GetControlPoints() for pNum in range(mesh.GetPolygonCount()): for j in range(3): coord = Mdl.Coord() vertIndex = mesh.GetPolygonVertex(pNum, j) normal = normals.GetDirectArray().GetAt(vertIndex) coord.encode(tuple(normal)) pos = tuple(map(lambda x,o,s: round((x-o)/s), (controlPoints[vertIndex][0], controlPoints[vertIndex][1], controlPoints[vertIndex][2]), outMdl.origin, outMdl.scale)) coord.position = pos frame.vertices.append(coord) outMdl.frames.append(frame) #calculates the maximum extents over all the given fbx filesnames def calculate_extents(filenames): meshes = [] for filename in filenames: LoadScene(sdkManager, scene, filename) meshes.extend(get_mesh_nodes(scene)) return get_mesh_extents(meshes) #creates a mdl frame from the fbx file in filename def import_frame(filename, outMdl): LoadScene(sdkManager, scene, filename) meshes = triangulate_meshes(get_mesh_nodes(scene)) load_mesh_pose(meshes, outMdl) #returns a hashtable of vertex index lists #the vertices in each list can be merged to one vertex safely def create_merge_lists(outMdl): #our initial lists are all the vertices coincident on the skinmap def uv_key(i): return struct.pack("2H", outMdl.vertices[i].u, outMdl.vertices[i].v) distinct_vertices = {} for i in range(len(outMdl.vertices)): distinct_vertices[uv_key(i)] = [] for i in range(len(outMdl.vertices)): distinct_vertices[uv_key(i)].append(i) #the lists never grow, we only learn that fewer vertices can merge #we start each frame by assigning our old lists numbers 1 to n #the key we create encodes this number followed by the coordinate from #the current frame #vertices get inserted on the same list if a) they were on the same list #last frame and b) they have the same position/normal this frame for frame in outMdl.frames: def frame_key(oldKey,vertex): c = frame.vertices[vertex] return struct.pack("l4B", oldKey, c.position[0], c.position[1], c.position[2], c.normal) lastKeys = list(distinct_vertices.keys()) new_vertices = {} for i in range(len(lastKeys)): for vertex in distinct_vertices[lastKeys[i]]: new_vertices[frame_key(i, vertex)] = [] for i in range(len(lastKeys)): for vertex in distinct_vertices[lastKeys[i]]: new_vertices[frame_key(i, vertex)].append(vertex) distinct_vertices = new_vertices return distinct_vertices #takes the dictionary of mergable vertex lists and returns a list which #maps existing vertices to merged vertex numbers (the merged vertices are #numbered by the position of the vertex list in the container) def invert_merge_lists(distinct_vertices, outMdl): distinct_keys = list(distinct_vertices.keys()) # create an empty list the length of the vertex list vertex_mapping = [None] * len(outMdl.vertices) for i in range(len(distinct_keys)): for vertex in distinct_vertices[distinct_keys[i]]: vertex_mapping[vertex] = i return vertex_mapping def merge_model_vertices(outMdl): mergeLists = create_merge_lists(outMdl) vertexMapping = invert_merge_lists(mergeLists, outMdl) newVertices = [] for vertexList in mergeLists.values(): newVertices.append(outMdl.vertices[vertexList[0]]) outMdl.vertices = newVertices for triangle in outMdl.triangles: triangle.vertices = tuple(vertexMapping[i] for i in triangle.vertices) for frame in outMdl.frames: newVertices = [] for vertexList in mergeLists.values(): newVertices.append(frame.vertices[vertexList[0]]) frame.vertices = newVertices #Prepare the FBX SDK. sdkManager, scene = InitializeSdkObjects() #=====This is the line where the filesnames are set===== inputFiles = ["rune{0:02d}.fbx".format(i) for i in range(6)] outMdl = Mdl() #need a skin before we can import UV coords outMdl.skinwidth = 64 outMdl.skinheight = 64 skin = Mdl.Skin(64, 64) skin.pixels = b"abcdefgh"*8*64 outMdl.skins.append(skin) # Need to get the extents of the scene before we can import any vertices meshes = get_mesh_nodes(scene) minXYZ, maxXYZ = calculate_extents(inputFiles) outMdl.origin = minXYZ[0:3] outMdl.scale = tuple(map(lambda x,y: (x-y)/255, maxXYZ,minXYZ))[0:3] #build the geometry from the first file in the list LoadScene(sdkManager, scene, inputFiles[0]) meshes = get_mesh_nodes(scene) triMeshes = triangulate_meshes(meshes) load_mesh_triangles(triMeshes, outMdl) for filename in inputFiles: import_frame(filename, outMdl) #weld the disjoint vertices we created initially merge_model_vertices(outMdl) with open("output.mdl", "wb") as outFile: outMdl.write(outFile)

Wow, quite a lot of code. It’s not been extensively tested, and it doesn’t really do any error checking if you feed it bad files. It also only creates a dummy skin to avoid needing another library for something that’s already hard to install. People who are brave and practised in python might want to look at the Python Image Library, and the console model‘s included python script for a how-to.

[…] ← Work-in-progress fbxtomdl script […]