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:
http://www.quaketastic.com/files/tools/fbxtomdl-0.5.zip
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
c:\tools\fbxtomdl\fbxtomdl.exe
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):
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
if im.mode != "P":
im = im.convert("RGB")
pim=Image.new("P", (mdl.skinwidth, mdl.skinheight))
pim.putpalette(palette)
pim=im.quantize(palette=pim)
else:
pim = im
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):
minXYZ = maxXYZ = None
for mesh in meshes:
mesh.ComputeBBox()
if(minXYZ == None):
minXYZ = mesh.BBoxMin.Get()
else:
minXYZ = tuple(map(min, minXYZ, mesh.BBoxMin.Get()))
if(maxXYZ == None):
maxXYZ = mesh.BBoxMax.Get()
else:
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)
outMdl.triangles.append(tri)
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)
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)
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
ranges.sort(reverse=True)
#check for overlapping ranges
if any([former[0]<=latter[1] for former,latter in zip(ranges,ranges[1:])]):
raise BadInput
for range in ranges:
helper.group_frames(range[0],range[1],helper.mdl.frames[range[0]].name.decode('ascii'))
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()
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_explicit("Skin file '"+skin+"' had wrong dimensions, skipped",
UserWarning,"fbxtomdl.py",-1)
if len(outMdl.skins) == 0:
warnings.warn_explicit("No skin files loaded, dummy skin added",
UserWarning,"fbxtomdl.py",-1)
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
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:
try:
import_frame(filename, outMdl)
except BadInput:
warnings.warn_explicit("fbx file '"+filename+"' had wrong face count, skipped",
UserWarning,"fbxtomdl.py",-1)
h = Helper()
h.mdl = outMdl
h.merge_vertices()
if(args.groups != None):
try:
group_ranges(args.groups, h)
except BadInput:
warnings.warn_explicit("couldn't parse groups string, skipped frame-grouping",
UserWarning,"fbxtomdl.py",-1)
outMdl.flags = args.flags
h.save("output.mdl")
[…] out this week is a new edition of qmdl, and a corresponding update to fbxtomdl. The main change to fbxtomdl concerns loading indexed-palette skins: the indices are now […]
[…] 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 […]