Standalone fbxtomdl

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)
Advertisements

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