Was on road trip this weekend, so didn't get a lot done, here is what I have:
__author__ = ["Paul Zirkle AKA Keless"]
__version__ = '0.1'
__url__ = ["www.blender.org","http://xreal.sourceforge.net","http://www.katsbits.com"]
# referenced export_md3_12.py by Xembie
"""
Name: 'Quake Model 5 (.md5mesh/.md5anim)'
Blender: 253
Group: 'Export'
Tooltip: 'Export a Md5 mesh/anim'
"""
import bpy,struct,math,os,time
MAX_QPATH = 64
MD5_COMMANDLINE = "created by Blender 2.53 with export_md5.py (v0.1) by Paul Zirkle"
MD5_VERSION = 10
class Md5Joint_t:
name = "" #string
parent = -1 #int
parentName = "" #string
pos = [] #float3 as vector3
orient = [] #float3 as quaternion3 (w is computed from xyz upon loading)
def __init__(self):
self.pos = [0.0, 0.0, 0.0]
self.orient = [0.0, 0.0, 0.0]
def GetSize(self):
return struct.calcsize(self.binaryFormat)
#serialize
def Save(self, file):
strOut = ' "%s" %d ( %f %f %f ) ( %f %f %f ) // %s ' % \
(self.name, parent, self.pos[0], self.pos[1], self.pos[2], \
self.orient[0], self.orient[1], self.orient[2], self.parentName )
#strOut = ' "' + self.name + '" ' + parent \
# + ' ( ' + self.pos[0] + ' ' + self.pos[1] + ' ' + self.pos[2] + ' )' \
# + ' ( ' + self.orient[0] + ' ' + self.orient[1] + ' ' + self.orient[2] + ' ) ' \
# + ' // ' + self.parentName
file.write(strOut)
#vertex texture and bone weight data
class Md5Vertex_t:
u = 0.0 #float
v = 0.0 #float
boneWeight = 0 #int
boneCount = 0 #int
def __init__(self):
self.u = 0.0
self.v = 0.0
def GetSize(self):
return struct.calcsize(self.binaryFormat)
#serialize
def Save(self, file, index):
strOut = ' vert %d ( %f %f ) %d %d ' % \
( index, self.u, self.v, self.boneWeight, self.boneCount)
#strOut = ' vert ' + index \
# + ' ( ' + self.u + ' ' + self.v + ' ) ' \
# + self.boneWeight + ' ' + self.boneCount
file.write(strOut + '\n')
#triangle index data
class Md5Triangle_t:
indices = [] #int[3] vertex indicies
def __init__(self):
self.indices = [ 0, 0, 0 ]
def GetSize(self):
return struct.calcsize(self.binaryFormat)
#serialize
def Save(self, file, index):
strOut = ' tri %d %d %d %d ' % \
( index, self.indices[0], self.indices[1], self.indices[2] )
#strOut = ' tri ' + index + ' ' \
# + self.indicies[0] + ' ' \
# + self.indicies[1] + ' ' \
# + self.indicies[2]
file.write(strOut + '\n')
#weighted vertex data
class Md5Weight_t:
jointIdx = 0 #int
bias = 0.0 #float
pos = [] #float[3] vertex
def __init__(self):
self.pos = [ 0.0, 0.0, 0.0 ]
def GetSize(self):
return struct.calcsize(self.binaryFormat)
#serialize
def Save(self, file, index):
strOut = ' weight %d %d %f ( %f %f %f ) ' % \
( index, self.jointIdx, self.bias, self.pos[0], self.pos[1], self.pos[2] )
#strOut = ' weight ' + index + ' ' \
# + self.jointIdx + ' ' + self.bias \
# + ' ( ' + self.pos[0] + ' ' + self.pos[1] + ' ' + self.pos[2] + ' ) '
file.write(strOut + '\n')
#holds verts, tris, and weights
class Md5Mesh:
name = ""
shader = "" #string referencing skin file (dont appent ext)
verts = [] # list of Md5Vertex_t
tris = [] # list of Md5Triangle_t
weights = [] # list of Md5Weight_t
def __init__(self):
self.numVerts = 0
self.numTris = 0
self.numWeights = 0
#serialize
def Save(self, file):
strOut = 'mesh { \n'
strOut += ' // meshes: ' + self.name + '\n'
strOut += ' shader "' + self.shader + '" \n'
strOut += '\n'
strOut += ' numVerts ' + str( len(self.verts) ) + '\n'
file.write(strOut)
for vi in range(len(self.verts)):
self.verts[vi].Save(file, vi)
strOut = '\n'
strOut += ' numtris ' + str( len(self.tris) ) + '\n'
file.write(strOut)
for ti in range(len(self.tris)):
self.tris[ti].Save(file, ti)
strOut = '\n'
strOut += ' numweights ' + str( len(self.weights) ) + '\n'
file.write(strOut)
for wi in range(len(self.weights)):
self.weights[wi].Save(file, wi)
strOut = '\n'
strOut += '} \n\n'
file.write(strOut)
class md5meshFileObject:
ver = 0 # should be "MD5Version 10"
commandLine = "" # ignored, so insert blatant self-promotion
skeleton = [] # list of Md5Joint_t
meshes = [] # list of Md5Mesh
def __init__(self):
self.ver = MD5_VERSION
self.commandLine = MD5_COMMANDLINE
def Save(self, file):
strOut = 'MD5Version ' + str(self.ver) + '\n'
strOut += 'commandline "' + self.commandLine + '" \n'
strOut += '\n'
strOut += 'numJoints ' + str( len(self.skeleton) ) + '\n'
strOut += 'numMeshes ' + str( len(self.meshes) ) + '\n'
strOut += '\n'
strOut += 'joints { \n'
file.write(strOut)
for joint in self.skeleton:
joint.Save(file)
strOut = '} \n'
strOut += '\n'
file.write(strOut)
for mesh in self.meshes:
mesh.Save(file)
def message(log,msg):
if log:
log.write(msg + "\n")
else:
print(msg)
class md5Settings:
def __init__(self, savepath, name, logtype, scale=1.0, offsetx=0.0, offsety=0.0, offsetz=0.0):
self.savepath = savepath
self.name = name
self.logtype = logtype
self.scale = scale
self.offsetx = offsetx
self.offsety = offsety
self.offsetz = offsetz
#serialize
def save_md5(settings):
starttime = time.clock() #start timer
newlogpath = os.path.splitext(settings.savepath)[0] + ".log"
if settings.logtype == "append":
log = open(newlogpath,"a")
elif settings.logtype == "overwrite":
log = open(newlogpath,"w")
else:
log = 0
message(log,"######################BEGIN######################")
message(log,"Exporting selected objects...")
bpy.ops.object.mode_set(mode='OBJECT')
md5 = md5meshFileObject()
for obj in bpy.context.selected_objects:
if obj.type == 'MESH':
bpy.context.scene.set_frame(bpy.context.scene.frame_start) #set timeline to 0 so we get clean mesh
nobj = obj.create_mesh(bpy.context.scene,True,'PREVIEW') #make a copy of the pure mesh
md5mesh = Md5Mesh()
md5mesh.name = obj.name
md5.meshes.append( md5mesh )
try:
md5mesh.shader = obj["md5shader"] #pull from custom property
except:
md5mesh.shader = ""
#we will store a list of unique vertex indicies here as we iterate the mesh's faces
vertlist = []
for f,face in enumerate(nobj.faces): #iterate each triangle face
md5tri = Md5Triangle_t()
if len(face.verts) != 3:
message(log,"WARNING: Skipping a non-triangle face in object " + obj.name)
continue
for v,vert_index in enumerate(face.verts):
uv_u = round(nobj.active_uv_texture.data[f].uv[v][0],5)
uv_v = round(nobj.active_uv_texture.data[f].uv[v][1],5)
match = 0 #boolean, true if this is a non-unique vertex
match_index = 0 #int, index of vertex if already listed
for i,vi in enumerate(vertlist):
if vi == vert_index:
if md5mesh.verts[i].u == uv_u and md5mesh.verts[i].v == uv_v:
match = 1
match_index = i
#ERROR: this isnt right, it assumes the same number of weights and verts, which is a lie!
if match == 0: #found new unique vertex
vertlist.append(vert_index)
md5tri.indices[v] = md5mesh.numVerts
md5vert = Md5Vertex_t()
md5vert.u = uv_u
md5vert.v = uv_v
#TODO: md5vert.boneWeight
#TODO: md5vert.boneCount
md5weight = Md5Weight_t()
#TODO: md5weight.jointIdx
#TODO: md5weight.bias
md5weight.pos = nobj.verts[vert_index].co * obj.matrix_world
md5weight.pos[0] = round(((md5weight.pos[0] + obj.matrix_world[3][0]) * settings.scale) + settings.offsetx,5)
md5weight.pos[1] = round(((md5weight.pos[0] + obj.matrix_world[3][1]) * settings.scale) + settings.offsety,5)
md5weight.pos[2] = round(((md5weight.pos[0] + obj.matrix_world[3][2]) * settings.scale) + settings.offsetz,5)
md5mesh.verts.append(md5vert)
md5mesh.weights.append(md5weight)
md5mesh.numVerts += 1
else: #found matched vertex
md5tri.indices[v] = match_index
md5mesh.tris.append(md5tri)
md5mesh.numTris += 1
#end for f,face in enumerate(nobj.face)
bpy.data.meshes.remove(nobj) #clean up temporary copy
#TODO: now read in reference skeleton
elif obj.type == 'EMPTY':
message(log, "skip empty obj " + obj.name)
if bpy.context.selected_objects:
file = open(settings.savepath + ".md5mesh", "w") #removed "wb" -- ascii, so dont open in binary mode
md5.Save(file)
file.close()
#TODO: save md5anim
message(log, "MD5 saved to " + settings.savepath)
elapsedtime = round(time.clock() - starttime,5)
message(log, "Elapsed " + str(elapsedtime) + " seconds")
else:
message(log, "Select an object to export!")
if log:
print("Logged to", newlogpath)
log.close()
#now create the UI registration interface
from bpy.props import *
class ExportMD5(bpy.types.Operator):
'''Export to Quake Model 5 (.md5mesh)'''
bl_idname = "export.md5"
bl_label = 'Export MD5'
logenum = [("console","Console","log to console"),
("append","Append","append to log file"),
("overwrite","Overwrite","overwrite log file")]
filepath = StringProperty(subtype = 'FILE_PATH',name="File Path", description="Filepath for exporting", maxlen= 1024, default= "")
md5name = StringProperty(name="MD5 Name", description="MD5 header name / skin path (64 bytes)",maxlen=64,default="")
md5logtype = EnumProperty(name="Save log", items=logenum, description="File logging options",default = 'console')
md5scale = FloatProperty(name="Scale", description="Scale all objects from world origin (0,0,0)",default=1.0,precision=5)
md5offsetx = FloatProperty(name="Offset X", description="Transition scene along x axis",default=0.0,precision=5)
md5offsety = FloatProperty(name="Offset Y", description="Transition scene along y axis",default=0.0,precision=5)
md5offsetz = FloatProperty(name="Offset Z", description="Transition scene along z axis",default=0.0,precision=5)
def execute(self, context):
settings = md5Settings(savepath = self.properties.filepath,
name = self.properties.md5name,
logtype = self.properties.md5logtype,
scale = self.properties.md5scale,
offsetx = self.properties.md5offsetx,
offsety = self.properties.md5offsety,
offsetz = self.properties.md5offsetz)
save_md5(settings)
return {'FINISHED'}
def invoke(self, context, event):
wm = context.manager
wm.add_fileselect(self)
return {'RUNNING_MODAL'}
def poll(self, context):
return context.active_object != None
def menu_func(self, context):
newpath = os.path.splitext(bpy.context.main.filepath)[0] + ".md5"
self.layout.operator(ExportMD5.bl_idname, text="Quake Model 5 (.md5)", icon='BLENDER').filepath = newpath
def register():
bpy.types.register(ExportMD5)
bpy.types.INFO_MT_file_export.append(menu_func)
def unregister():
bpy.types.unregister(ExportMD5)
bpy.types.INFO_MT_file_export.remove(menu_func)
if __name__ == "__main__":
register()
When used with the simple start up cube in blender (after CTRL-T to subdivide it into triangles), I get this result:
MD5Version 10
commandline "created by Blender 2.53 with export_md5.py (v0.1) by Paul Zirkle"
numJoints 0
numMeshes 1
joints {
}
mesh {
// meshes: Cube
shader ""
numVerts 20
vert 0 ( 0.000000 0.000000 ) 0 0
vert 1 ( 1.000000 0.000000 ) 0 0
vert 2 ( 1.000000 1.000000 ) 0 0
vert 3 ( 0.000000 1.000000 ) 0 0
vert 4 ( 0.000000 0.000000 ) 0 0
vert 5 ( 1.000000 0.000000 ) 0 0
vert 6 ( 1.000000 1.000000 ) 0 0
vert 7 ( 0.000000 1.000000 ) 0 0
vert 8 ( 0.000000 0.000000 ) 0 0
vert 9 ( 1.000000 0.000000 ) 0 0
vert 10 ( 0.000000 1.000000 ) 0 0
vert 11 ( 1.000000 1.000000 ) 0 0
vert 12 ( 0.000000 0.000000 ) 0 0
vert 13 ( 1.000000 0.000000 ) 0 0
vert 14 ( 0.000000 1.000000 ) 0 0
vert 15 ( 1.000000 1.000000 ) 0 0
vert 16 ( 1.000000 0.000000 ) 0 0
vert 17 ( 0.000000 1.000000 ) 0 0
vert 18 ( 1.000000 0.000000 ) 0 0
vert 19 ( 1.000000 1.000000 ) 0 0
numtris 12
tri 0 0 1 2
tri 1 0 2 3
tri 2 4 5 6
tri 3 4 6 7
tri 4 8 9 10
tri 5 9 11 10
tri 6 12 13 14
tri 7 13 15 14
tri 8 0 16 17
tri 9 16 11 17
tri 10 12 18 19
tri 11 12 19 7
numweights 20
weight 0 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 1 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 2 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 3 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 4 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 5 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 6 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 7 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 8 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 9 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 10 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 11 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 12 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 13 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 14 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 15 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 16 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
weight 17 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 18 0 0.000000 ( 1.000000 1.000000 1.000000 )
weight 19 0 0.000000 ( -1.000000 -1.000000 -1.000000 )
}
I don't have any code to pull the skeleton yet (and that sample doesnt have bones anyway) but there is a problem besides that: the num weights and num verts are exactly the same. I tried to use analogies from the original md3 exporter when writing this, but they dont quite work out. I'm not really sure what I should be pulling the "weights" and "verts" from (though I think the "tris" section is correct).
Part of the problem being the MD5 naming scheme is terrible since "weights" has actual XYZ vertex data, and "verts" has UV texcoords data.