Work-in-progress fbxtomdl script

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.

Advertisements

One thought on “Work-in-progress fbxtomdl script

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s