#!/usr/bin/env python #Analyze md3, the 3D file format used in Id Software's Quake 3: Arena # Copyright 2002 PhaethonH #Permission granted to copy, use, distribute and modify provided this #copyright notice remains intact. Software comes as-is, with no warranty of #any kind. # #C header information from Q3A_ToolSource by Id Software. #MD3.PY version 0004 (2002.03.07) import sys, struct, string, math from types import * MAX_QPATH = 64 ### qfiles.h stuff ## MD3 # magic numbers MD3_IDENT = "IDP3" MD3_VERSION = 15 # limits MD3_MAX_LODS = 4 MD3_MAX_TRIANGLES = 8192 MD3_MAX_VERTS = 4096 MD3_MAX_SHADERS = 256 MD3_MAX_FRAMES = 1024 MD3_MAX_SURFACES = 32 MD3_MAX_TAGS = 16 # vertex scales MD3_XYZ_SCALE = (1.0/64) #md3Frame_t: # vec3_t bounds[2] # vec3_t localOrigin # float radius # char name[16] #md3Tag_t: # char name[MAX_QPATH] # vec3_t origin # vec3_t axis[3] #md3Surface_t: # int ident # char name[MAX_QPATH] # int flags # int numFrames # int numShaders # int numVerts # int numTriangles # int ofsTriangles # int ofsShaders # int ofsSt # int ofsXyzNormals # int ofsEnd #md3Shader_t: # char name[MAX_QPATH] # int shaderIndex #md3Triangle_t: # int indexes[3] #md3St_t: # float st[2] #md3XyzNormal_t: # short xyz[3] # short normal #md3Header_t: # int ident # int version # char name[MAX_QPATH] # int flags # int numFrames # int numTags # int numSurfaces # int numSkins # int ofsFrames # int ofsTags # int ofsSurfaces # int ofsEnd ## MD4 MD4_IDENT = "IDP4" MD4_VERSION = 1 MD4_MAX_BONES = 128 #md4Weight_t: # int boneIndex # float boneWeight #md4Vertex_t: # vec3_t vertex # vec3_t normal # float texCoords[2] # int numWeights # md4Weight_t *weights #list of #md4Triangle_t: # int indexes[3] #md4Surface_t: # int ident # char name[MAX_QPATH] # char shader[MAX_QPATH] # int shaderIndex # int ofsHeader # int numVerts # int ofsVerts # int numTriangles # int ofsTriangles # int numBoneReferences # int ofsBoneReferences # int ofsEnd #md4Bone_t: # float matrix[3][4] #md4Frame_t: # vec3_t bounds[2] # vec3_t localOrigin # float radius # char name[16] # md4Bone_t *bones //list of #md4LOD_t: # int numSurfaces # int ofsSurfaces # int ofsEnd #md4Header_t: # int ident # int version # char name[MAX_QPATH] # int numFrames # int numBones # int ofsFrames # int numLODs # int ofsLODs # int ofsEnd def asciiz (s): n = 0 while (ord(s[n]) != 0): n = n + 1 return s[0:n] #various relevant data structures (heterogenous aggregates). class md3Frame: """MD3 Frame object - bounding box md3Frame_t vec3_t bounds[2] -> 12 * 2 = 24 vec3_t localOrigin -> 12 * 1 = 12 float radius -> 4 * 1 = 4 char name[16] -> 16 * 1 = 16 56""" binfmt = "<3f3f3ff16s" bounds = [] localOrigin = [] radius = 0.0 name = "" def __init__ (self): self.bounds = [[0.0, 0.0, 0.0]] * 2 self.localOrigin = [0.0, 0.0, 0.0] self.name = "" def parse (self, F): """Parse one MD3 Frame object from current location of supplied file object.""" x = F.read(struct.calcsize(self.binfmt)) a = struct.unpack(self.binfmt, x) self.bounds = [a[0:3], a[3:6]] self.localOrigin = a[6:9] self.radius = a[9] self.name = asciiz(a[10]) return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sBounds:" % prefix, self.bounds[0], self.bounds[1] print "%slocalOrigin:" % prefix, self.localOrigin print "%sradius:" % prefix, self.radius print "%sname:" % prefix, self.name class md3Tag: """MD3 Tag object. md3Tag_t: char name[MAX_QPATH] -> 64 * 1 = 64 vec3_t origin -> 12 * 1 = 12 vec3_t axis[3] -> 12 * 3 = 36 112""" binfmt = "%ds3f3f3f3f" % MAX_QPATH name = "" origin = [] axis = [] def __init__ (self): self.origin = [0.0, 0.0, 0.0] self.axis = [[0.0, 0.0, 0.0]] * 3 def parse (self, F): """Parse one MD3 Tag object from current location of supplied file object.""" x = F.read(struct.calcsize(self.binfmt)) a = struct.unpack(self.binfmt, x) self.name = asciiz(a[0]) self.origin = a[1:4] self.axis = [a[4:7], a[7:10], a[10:13]] return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sname:" % prefix, self.name print "%sorigin:" % prefix, self.origin print "%saxes:" % prefix, self.axis class md3Shader: """MD3 Shader object. md3Shader_t: char name[MAX_QPATH] -> 64 * 1 = 64 int shaderIndex -> 1 * 4 = 4 68""" binfmt = "<%dsi" % MAX_QPATH name = "" shaderIndex = 0 def __init__ (self): self.name = "" def parse (self, F): """Parse one MD3 Shader object from current location of supplied file object.""" x = F.read(struct.calcsize(self.binfmt)) a = struct.unpack(self.binfmt, x) self.name = asciiz(a[0]) self.shaderIndex = a[1] return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sshader #%d = %s" % (prefix, self.shaderIndex, self.name) class md3Triangle: """MD3 Triangle object. Set of three vertices. md3Triangle_t: int indexes[3] -> 3 * 4 = 12 12""" binfmt = "<3i" indexes = [] def __init__ (self): self.indexes = [0, 0, 0] def parse (self, F): """Parse one MD3 Triangle object from current location of supplied file object.""" x = F.read(struct.calcsize(self.binfmt)) self.indexes = struct.unpack(self.binfmt, x) return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sTriangle" % prefix, self.indexes #presumably meaning s-t (2-dimensional) coordinates, # which probably would have been x-y coordinates # if it weren't for the fact that 3D models are already using x-y-z. class md3St: """MD3 Texture Coordinates objects. TexCoord index matches its corresponding vertex's index (i.e. texcoords[5] specifics for verts[5]) md3St_t: float st[2] -> 2 * 4 = 8 8""" binfmt = "<2f" st = [] def __init__ (self): self.st = [0.0] * 2 def parse (self, F): """Parse one Texture Coordinate object from current location of supplied file object.""" x = F.read(struct.calcsize(self.binfmt)) self.st = struct.unpack(self.binfmt, x) return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sTexCoord" % prefix, self.st class md3XyzNormal: """MD3 Normal object. 3D coordinate and (encoded) vertex normal. md3XyzNormal_t: short xyz[3] -> 3 * 2 = 6 short normal -> 1 * 2 = 2 8""" binfmt = "3hh" xyz = [] normal = 0 def __init__ (self): self.xyz = [0] * 3 def decode (self, latlng): """Decode 16-bit latitude-longitude value into a normal vector.""" #Code ripped from q3toosl/q3map/misc_model.c: # // decode the lat/lng normal to a 3 float normal # lat = ( xyz->normal >> 8 ) & 0xff; # lng = ( xyz->normal & 0xff ); # lat *= Q_PI/128; # lng *= Q_PI/128; # # temp[0] = cos(lat) * sin(lng); # temp[1] = sin(lat) * sin(lng); # temp[2] = cos(lng); lat = (latlng >> 8) & 0xFF; lng = (latlng) & 0xFF; lat *= math.pi/128; lng *= math.pi/128; x = math.cos(lat) * math.sin(lng) y = math.sin(lat) * math.sin(lng) z = math.cos(lng) retval = [ x, y, z ] return retval def encode (self, normal): """Encode a normal vector into a 16-bit latitude-longitude value.""" x, y, z = normal lng = math.acos(z) lat = math.acos(x / math.sin(lng)) retval = ((lat & 0xFF) << 8) | (lng & 0xFF) return retval def parse (self, F): """Parse one MD3 Vertex object from current location of supplied file object.""" x = F.read(struct.calcsize(self.binfmt)) a = struct.unpack(self.binfmt, x) self.xyz = a[0:3] self.normal = self.decode(a[3]) return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sVertex" % prefix, self.xyz, self.normal class md3Surface: """MD3 Surface object. md3Surface_t: int ident -> 1 * 4 = 4 char name[MAX_QPATH] -> 64 * 1 = 64 int flags -> 1 * 4 = 4 int numFrames -> 1 * 4 = 4 int numShaders -> 1 * 4 = 4 int numVerts -> 1 * 4 = 4 int numTriangles -> 1 * 4 = 4 int ofsTriangles -> 1 * 4 = 4 int ofsShaders -> 1 * 4 = 4 int ofsSt -> 1 * 4 = 4 int ofsXyzNormals -> 1 * 4 = 4 int ofsEnd -> 1 * 4 = 4 108""" binfmt = "<4s%ds10i" % MAX_QPATH ident = 0 name = "" flags = 0 frames = [] shaders = [] verts = [] triangles = [] texcoords = [] ofsTriangles = 0 ofsShaders = 0 ofsSt = 0 ofsXyzNormals = 0 ofsEnd = 0 def __init__ (self): self.name = "" self.frames = [] self.shaders = [] self.verts = [] self.triangles = [] self.texcoords = [] def parse (self, F): """Parse one md3 surface from current location of supplied file object.""" surfpt = F.tell() x = F.read(struct.calcsize(self.binfmt)) a = struct.unpack(self.binfmt, x) self.ident = a[0] self.name = asciiz(a[1]) self.flags = a[2] self.numFrames = a[3] # Extend shaders list for appropriate number of blanks to be filled later. for j in xrange(0, a[4]): self.shaders.append(md3Shader()) self.numVerts = a[5] for j in xrange(0, a[6]): self.triangles.append(md3Triangle()) for j in xrange(0, self.numVerts): self.texcoords.append(md3St()); for j in xrange(0, self.numFrames): self.verts.append([]) for k in xrange(0, self.numVerts): self.verts[j].append(md3XyzNormal()) self.ofsTriangles = a[7] self.ofsShaders = a[8] self.ofsSt = a[9] self.ofsXyzNormals = a[10] self.ofsEnd = a[11] # Actual reading. F.seek(surfpt + self.ofsShaders, 0) for j in xrange(0, len(self.shaders)): self.shaders[j].parse(F) F.seek(surfpt + self.ofsTriangles, 0) for j in xrange(0, len(self.triangles)): self.triangles[j].parse(F) F.seek(surfpt + self.ofsSt, 0) for j in xrange(0, len(self.texcoords)): self.texcoords[j].parse(F) F.seek(surfpt + self.ofsXyzNormals, 0) for j in xrange(0, self.numFrames): for k in xrange(0, self.numVerts): self.verts[j][k].parse(F) F.seek(surfpt + self.ofsEnd) return self def dump (self, *args): prefix = "" if len(args): prefix = args[0] print "%sident:" % prefix, self.ident print "%sname:" % prefix, self.name print "%sflags:" % prefix, self.flags print "%sframes:" % prefix, self.numFrames print "%svertices:" % prefix, self.numVerts print "%stexs:" % prefix, len(self.texcoords) print "%sshaders:" % prefix, len(self.shaders) for j in xrange(0, len(self.shaders)): print "%s Shader %d = %s" % (prefix, j, self.shaders[j].name) for j in xrange(0, self.numFrames): for k in xrange(0, self.numVerts): print "%s Vertex %d/%d =" % (prefix, j, k), self.verts[j][k].xyz, self.verts[j][k].normal for j in xrange(0, len(self.triangles)): print "%s Triangle %d =" % (prefix, j), self.triangles[j].indexes for j in xrange(0, len(self.texcoords)): print "%s Texcoord %d =" % (prefix, j), self.texcoords[j].st class md3: """MD3 Object. md3Header_t: int ident -> 1 * 4 = 4 int version -> 1 * 4 = 4 char name[MAX_QPATH] -> 64 * 1 = 64 int flags -> 1 * 4 = 4 int numFrames -> 1 * 4 = 4 int numTags -> 1 * 4 = 4 int numSurfaces -> 1 * 4 = 4 int numSkins -> 1 * 4 = 4 int ofsFrames -> 1 * 4 = 4 int ofsTags -> 1 * 4 = 4 int ofsSurfaces -> 1 * 4 = 4 int ofsEnd -> 1 * 4 = 4 108""" binfmt = "<4sI%ds9I" % MAX_QPATH # magic numbers MAGIC = "IDP3" VERSION = 15 # limits MAX_LODS = 4 MAX_TRIANGLES = 8192 MAX_VERTS = 4096 MAX_SHADERS = 256 MAX_FRAMES = 1024 MAX_SURFACES = 32 MAX_TAGS = 16 # vertex scales XYZ_SCALE = (1.0/64) ident = 0 version = 0 name = "" flags = 0 frames = [] tags = [] surfaces = [] skins = [] ofsFrames = 0 ofsTags = 0 ofsSurfaces = 0 ofsEnd = 0 def __init__ (self): self.frames = [] self.tags = [] self.surfaces = [] self.skins = [] def parse (self, F): """Parse one MD3 header object from current location of supplied file object.""" # Read header information. x = F.read(struct.calcsize(self.binfmt)) a = struct.unpack(self.binfmt, x) self.ident = a[0] self.version = a[1] self.name = asciiz(a[2]) self.flags = a[3] for i in xrange(0, a[4]): self.frames.append(md3Frame()) for i in xrange(0, a[5]): self.tags.append(md3Tag()) for i in xrange(0, a[6]): self.surfaces.append(md3Surface()) for i in xrange(0, a[7]): self.skins.append([None]); self.ofsFrames, self.ofsTags, self.ofsSurfaces, self.ofsEOF = a[8:12] # Read Frames. F.seek(self.ofsFrames, 0) #XXX: Bad idea? for i in xrange(0, len(self.frames)): self.frames[i].parse(F) # Read Tags. F.seek(self.ofsTags, 0) for i in xrange(0, len(self.tags)): self.tags[i].parse(F) # Read Surfaces. nextpt = self.ofsSurfaces F.seek(self.ofsSurfaces, 0) for i in xrange(0, len(self.surfaces)): F.seek(nextpt) self.surfaces[i].parse(F) nextpt = nextpt + self.surfaces[i].ofsEnd return self def dump (self, *args): """Debugging dump.""" prefix = "" if len(args): prefix = args[0] print "%sMODEL" % prefix print "%s MAGIC:" % prefix, self.ident print "%s VERSION:" % prefix, self.version print "%s NAME:" % prefix, self.name print "%s FLAGS:" % prefix, self.flags print "%s #FRAMES:" % prefix, len(self.frames) print "%s #TAGS (EMPTIES):" % prefix, len(self.tags) print "%s #SURFACES (MESHES):" % prefix, len(self.surfaces) print "%s #SKINS (TEXTURES):" % prefix, len(self.skins) print "%s FRAMES @" % prefix, self.ofsFrames print "%s TAGS @" % prefix, self.ofsTags print "%s SURFACES @" % prefix, self.ofsSurfaces print "%s EOF @" % prefix, self.ofsEOF for i in xrange(0, len(self.frames)): print "%s Frame" % prefix, i self.frames[i].dump(prefix+" ") for i in xrange(0, len(self.tags)): print "%s Tag" % prefix, i self.tags[i].dump(prefix+" ") for i in xrange(0, len(self.surfaces)): print "%s Surface" % prefix, i self.surfaces[i].dump(prefix+" ") class qmodel (md3): """Quake Model Object.""" ident = 0 version = 0 def __init__ (self): pass def parse (self, F): """Read model data from file object. Returns 0 if file object not recognized, non-zero otherwise.""" x = F.read(4) if (x == md3.MAGIC): F.seek(-4, 1) md3.parse(self, F) return self else: return 0 if __name__ == '__main__': md3obj = qmodel() f = sys.stdin md3obj.parse(f) f.close() md3obj.dump()