Game Editing > Scripts & Support

[MD5] EXPORT script for Blender 2.6x (OPEN)

(1/46) > >>

keless:
IMPORTANT: When posting problems, errors or support requests please reference the exact version of Blender used along with which script - not all versions, or combinations of either (Blender release candidates included) are supported so this information helps track issues.
To download the latest MD5 export script from KatsBits click here.
Generic MD5 installation and usage instructions are condensed here for convenience.

* For Blender 2.63 [note #1]
* For Blender 2.62 [note #2]
* For Blender 2.59 [note #3]
* For Blender 2.57 (OK for 2.60, 2.61)
* For Blender 2.54 & 2.55NOTES
Depending on the script used there may be issues with bone bounding-boxes. This does not have any negative bearing on the functionality of any exported models, unless, more sophisticated properties need to be used, i.e. using bone and their respective bound boxes for collision or collision-articulation (AF) which might be better suited to using external meshes or other system of mesh simplification and demarcation.

1) Re: 2.63. The MD5 export script for 2.63 has been found to work with the following versions of Blender; 2.67, 2.68a, 2.69, 2.70, 2.72. However, be sure to double-check support before use in any live projects (install Blender to a unique folder per version and test).

2) Re: 2.62. Script works correctly in Blender 2.62 ONLY.

3) Re: 2.59. Script works correctly in Blender 2.59 ONLY

MD5 Models, Multiple Textures/Materials

MD5 & Multiple Meshes (Blender)



--- Quote from: kat on September 09, 2010, 03:06:08 AM ---Sadly not yet... I've been onto der_ton to see if he's had a chance to address this yet but so far he's not been able to do anything due to real life getting in the way. I think a number of these 'old' scripts are going to need the community to come up with the goods as the original authors of them have either lost interest and gone MIA or have other more pressing responsibilities - wife/baby/cat/goldfish [delete as appropriate].

--- End quote ---

I've recently added an MD5 loader and render support in my game engine, but I hate animating in old Blender 2.4x so it looks like I need to roll up my sleeves and write a 2.5x exporter. I'm somewhat familiar with the MD5 format now (plus its a human-readable format) but I'm not familiar with writing Blender exporters (or python for that matter, but no biggie I'm a professional).

Is anyone familiar with writing Blender 2.5x exporters that could give me an overview of that side of things before I delve in (someone did an Md3 exporter recently)? Alternatively, I'll bite the bullet and search for some tutorials.

kat:
The MD3 was a bit of a 'hack' job from what the author told me. He did copy/paste some sections from the old ones to save time (why re-invent the wheel), but pretty much rewrote the thing. I'm having trouble getting Blenders website atm but there is supposed to be a knowledge base there for script authors looking for info on updating to 2.5.

http://www.blender.org/development/
The actual docs for Blender are here - http://wiki.blender.org/index.php/BlenderDev/Blender2.5

keless:
Thanks Kat.

With those, plus a python Quick Guide and Xembie's md3 exporter as reference, I've started down the yellow brick road. I have to diverge from the md3 script as it seems to be a binary format, whereas md5 is ascii (unless I'm reading the python code incorrectly)

Beginning to flesh out my data structs, can any python lovers remark if I'm messing things up?

--- Code: ---__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_VER_TAG = "MD5Version"
MD5_VERSION = 10

class Md5Joint_t:
name = "" #string
parent = -1 #int
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, parentName):
strOut = '    "' + self.name + '" ' + parent \
+ ' ( ' + self.pos[0] + ' ' + self.pos[1] + ' ' + self.pos[2] + ' )' \
+ ' ( ' + self.orient[0] + ' ' + self.orient[1] + ' ' + self.orient[2] + ' ) ' \
+ '    // ' + parentName
file.write(strOut)

class Md5Vertex_t:
u
v
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 ' + index \
+ ' ( ' + self.u + ' ' + self.u + ' ) ' \
+ self.boneWeight + ' ' + self.boneCount
file.write(strOut)

class Md5Triangle_t:
indices = [] #int[3] vertex indicies

__init__(self):
self.indices = [ 0, 0, 0 ]

def GetSize(self):
return struct.calcsize(self.binaryFormat)

#serialize
def Save(self, file, index):
strOut = '    tri ' + index + ' ' \
+ self.indicies[0] + ' ' \
+ self.indicies[1] + ' ' \
+ self.indicies[2]
file.write(strOut)
--- End code ---



as reference, here are some of the relevant data structure outputs snipped from an md5 file:

--- Code: --- "Body2" 1 ( 0 0.0000023712 39.7816314697 ) ( -0.7071067095 0 0 ) // Body
vert 4 ( 0.5554450154 0.2438279986 ) 13 4
tri 1 0 1 3
--- End code ---

TIA

kat:
Not sure if this will be of use here but might be a good idea to keep an eye on rich_is_bored's ASE topic, there's an interesting problem cropping up with that script to do with the latest builds of Blender.

And yes you're right, MD3 is compiled binary format. Although MD5 is 'ascii', a couple of games - Quake Wars and Wolfenstein - made use of a binary version of the format; iirc you just output a normal MD5's and then run them through the game editing tools to produce the binary versions.

keless:
Was on road trip this weekend, so didn't get a lot done, here is what I have:

--- Code: ---__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()
   
--- End code ---

When used with the simple start up cube in blender (after CTRL-T to subdivide it into triangles), I get this result:

--- Code: ---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 )

}


--- End code ---

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.

Navigation

[0] Message Index

[#] Next page

Go to full version