Please note! fbxtomdl has proven to be so popular that it’s got a page dedicated to it. Please visit https://tomeofpreach.wordpress.com/qmdl/fbxtomdl/ for the latest version instead!
Following on from last week’s script for converting fbx to mdl format, here’s a bundle with lots of extra features, like skin importing, flags, bug fixes etc. Download it from
http://www.quaketastic.com/upload/files/tools/fbxtomdl.zip
There’s instructions to use it in the zip, and the -h command switch summarises the command options. You need all the files in the zip to live in the same directory as the executable. I’d recommend unpacking the whole lot to a mdlconv directory, then for each model you create make a new subdirectory of mdlconv i.e. v_axe. You then call fbxtomdl from the mdlconv directory with “-d v_axe”.
The sourcecode for the script is posted below, but hidden for your protection. I believe the only way to get all the libraries to work is to use python 3.1 – and to do that I had to install argparse, the fbx library, and the Python Imaging Library. The PIL was a really tough one, it took building the package from scratch and fixing compiler errors along the way. Still, if you want to modify the algorithm or do fancy things like framegroups read on…
from FbxCommon import *
import sys
import struct
import os
import warnings
import argparse
from qmdl.mdl import Mdl
from qmdl.palette import palette
from PIL import Image, ImagePalette, TgaImagePlugin, PcxImagePlugin
class BadInput(Exception):
pass
def append_skin(mdl, filename):
im=Image.open(filename)
if(mdl.skinwidth == 0):
mdl.skinwidth, mdl.skinheight = im.size
elif ((mdl.skinwidth, mdl.skinheight) != im.size):
raise BadInput
im=im.convert("RGB")
pim=Image.new("P", (mdl.skinwidth, mdl.skinheight))
pim.putpalette(palette)
pim=im.quantize(palette=pim)
sk=Mdl.Skin(mdl.skinwidth, mdl.skinheight)
sk.pixels=pim.tostring()
mdl.skins.append(sk)
#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 & loads the triangles & UVs 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)
if(len(frame.vertices) != len(outMdl.vertices)):
raise BadInput
outMdl.frames.append(frame)
#calculates the maximum extents over all the given fbx filesnames
def calculate_extents(filenames):
minXYZ, maxXYZ = None, None
for filename in filenames:
LoadScene(sdkManager, scene, filename)
newMinXYZ, newMaxXYZ = get_mesh_extents(get_mesh_nodes(scene))
if(minXYZ == None):
minXYZ, maxXYZ = newMinXYZ, newMaxXYZ
else:
minXYZ = tuple(map(min, minXYZ, newMinXYZ))
maxXYZ = tuple(map(max, maxXYZ, newMaxXYZ))
return minXYZ, maxXYZ
#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)
outMdl.frames[len(outMdl.frames)-1].name = filename[0:-4]
#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
parser = argparse.ArgumentParser(description="Loads a directory of fbx files \
and skin files, and builds a .mdl file from them. \
Supports bmp, pcx and tga skins")
parser.add_argument("-f", "--flags", type=int, default=0,
help="Sets the flags on the model by number")
parser.add_argument("-d", "--dir", default=".",
help="Specifies the directory to load files from")
args = parser.parse_args()
os.chdir(args.dir)
#Prepare the FBX SDK.
sdkManager, scene = InitializeSdkObjects()
outMdl = Mdl()
inputFiles = [f for f in os.listdir(".")
if os.path.isfile(f) and f[-4:]==".fbx"]
if len(inputFiles) == 0:
sys.exit("no fbx found in this directory")
inputFiles.sort()
#need a skin before we can import UV coords
skinFiles = [f for f in os.listdir(".")
if os.path.isfile(f) and (f[-4:] in [".tga",".bmp",".pcx"])]
skinFiles.sort()
for skin in skinFiles:
try:
append_skin(outMdl, skin)
except BadInput:
warnings.warn("Skin file '"+skin+"' had wrong dimensions, skipped")
if len(outMdl.skins) == 0:
warnings.warn("No skin files loaded, dummy skin added")
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:
try:
import_frame(filename, outMdl)
except BadInput:
warnings.warn("fbx file '"+filename+"' had wrong face count, skipped")
#weld the disjoint vertices we created initially
merge_model_vertices(outMdl)
outMdl.flags = args.flags
with open("output.mdl", "wb") as outFile:
outMdl.write(outFile)