fbxtomdl is a complex example of something you can create using the qmdl module. The script, found below, glues together qmdl, the autodesk fbx module and the Python Image Library. The script works on an entire directory of files: the fbx files get compiled into a frames of a mdl file (in alphabetical order), and all the bmp, pcx and tga files get imported as skins. Obviously for this to work all of the fbx files need to be different poses of the same underlying geometry, and all the images need to have the same dimensions.

If you don’t have python installed or don’t want to fiddle with all the libraries, I’ve compiled the script into a executable:
The executable package is pretty large, since it includes a full python interpreter. My advice on how to use it is to extract all the contents to a known directory like c:\tools\fbxtomdl\. Then make a very simple batch file fbxtomdl.bat with the command line
You can then make a copy of this batch in every model-bundle directory, and just double-click it to rebuild your model.

To get the latest version of the script to work in Python, I recommend using the following combination of versions and libraries (or at least note that this combination is one that works):

    • Python 3.3 (32 bit version)
    • The 2015.1 version of the FBX Python Binding. Make sure you find the 32 bit version of the library for 3.3 once you extract it, then move it into the python33 site-packages folder.
    • The Pillow fork of the PIL. I built the executable version with Pillow 2.0.0 for Python 3.3.

The current source to the fbxtomdl script follows:

from FbxCommon import *
import sys
import struct
import os
import warnings
import argparse
import math
from qmdl.mdl import Mdl
from qmdl.palette import palette
from qmdl.helper import Helper
from PIL import Image, ImagePalette, TgaImagePlugin, PcxImagePlugin
class BadInput(Exception):
def append_skin(mdl, filename):
    if(mdl.skinwidth == 0):
        mdl.skinwidth, mdl.skinheight = im.size
    elif ((mdl.skinwidth, mdl.skinheight) != im.size):
        raise BadInput
    if im.mode != "P":
        im = im.convert("RGB")
        pim=Image.new("P", (mdl.skinwidth, mdl.skinheight))
        pim = im
    sk=Mdl.Skin(mdl.skinwidth, mdl.skinheight)
#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:
    return meshNodes
#returns the minimum and maximum xyz coordinate values over all given meshes
def get_mesh_extents(meshes):
    minXYZ = maxXYZ = None
    for mesh in meshes:
        if(minXYZ == None):
            minXYZ = mesh.BBoxMin.Get()
           minXYZ = tuple(map(min, minXYZ, mesh.BBoxMin.Get()))
        if(maxXYZ == None):
            maxXYZ = mesh.BBoxMax.Get()
           maxXYZ = tuple(map(max, maxXYZ, mesh.BBoxMax.get()))
    return (minXYZ, maxXYZ)
#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)
            for j in range(3):
                skinUV = Mdl.Vertex()
                meshUVIndex = mesh.GetTextureUVIndex(pNum, j)
                meshUV = meshUVs.GetDirectArray().GetAt(meshUVIndex)
                skinUV.u = math.floor(meshUV[0] * outMdl.skinwidth)
                skinUV.v = math.floor((1-meshUV[1]) * outMdl.skinheight)
            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)
                pos = tuple(map(lambda x,o,s: round((x-o)/s),
                coord.position = pos
    if(len(frame.vertices) != len(outMdl.vertices)):
        raise BadInput
#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
            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)
    converter = FbxGeometryConverter(sdkManager)
    converter.Triangulate(scene, True)
    load_mesh_pose(get_mesh_nodes(scene), outMdl)
    outMdl.frames[len(outMdl.frames)-1].name = filename[0:-4].encode('ascii')
def parse_ranges(range_string):
	ranges = list()
	for segment in range_string.split(","):
		numbers = segment.split("-")
		# note in the next line the indices are 0 and -1
		# this gets us the first and last number
		# it even works when list is length 1 and they are the same!
		ranges.append( (int(numbers[0]),int(numbers[-1])) )
	return ranges

def group_ranges(range_string, helper):
	ranges = parse_ranges(range_string)
	#because frame indices change as we group frames, need to process from back to front
	#check for overlapping ranges
	if any([former[0]<=latter[1] for former,latter in zip(ranges,ranges[1:])]):
		raise BadInput
	for range in ranges:
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")
parser.add_argument("-g", "--groups", default=None,
                    help="Ranges of frames to make grouped in format 1-6,33-40,50-53")
args = parser.parse_args()
#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")
#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"])]
for skin in skinFiles:
        append_skin(outMdl, skin)
    except BadInput:
        warnings.warn_explicit("Skin file '"+skin+"' had wrong dimensions, skipped",
if len(outMdl.skins) == 0:
    warnings.warn_explicit("No skin files loaded, dummy skin added",
    outMdl.skinwidth = 64
    outMdl.skinheight = 64
    skin = Mdl.Skin(64, 64)
    skin.pixels = b"abcdefgh"*8*64
# Need to get the extents of the scene before we can import any vertices
minXYZ, maxXYZ = calculate_extents(inputFiles)
outMdl.origin = tuple(minXYZ)
outMdl.scale = tuple(map(lambda x,y: 0.1 if x == y else (x-y)/255, maxXYZ, minXYZ))
#build the geometry from the first file in the list
LoadScene(sdkManager, scene, inputFiles[0])
converter = FbxGeometryConverter(sdkManager)
converter.Triangulate(scene, True)
load_mesh_triangles(get_mesh_nodes(scene), outMdl)
for filename in inputFiles:
        import_frame(filename, outMdl)
    except BadInput:
        warnings.warn_explicit("fbx file '"+filename+"' had wrong face count, skipped",
h = Helper()
h.mdl = outMdl
if(args.groups != None):
		group_ranges(args.groups, h)
	except BadInput:
		warnings.warn_explicit("couldn't parse groups string, skipped frame-grouping",
outMdl.flags = args.flags


2 thoughts on “fbxtomdl

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.