#!/usr/bin/env python
#Analyze md3, the 3D file format used in Id Software's Quake 3: Arena
# Copyright 2002  PhaethonH <phaethon@linux.ucla.edu>
#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()
