#!/usr/bin/env python """md3import.py Import MD3 into Blender Copyright 2002 PhaethonH Permission granted to use, copy, distribute, and modify, provided this copyright notice remains intact. Software is provided as-is, with absolutely no warranty of any kind. """ # md3import.py version 0006 (2002.03.03) """ Versions of Blender 2.20-2.23 should not need anything modified. Older versions may need to rename "Blender210", get rid of "import GUI", set md3file accordingly, and modify the code further on down to use a straight call to md3import() instead of using the FileSelector widget. Newer versions (>2.23) might need complete overhaul. Development platform: CPU: AMD Athlon 1200MHz kernel: linux-x86 2.4.17 libc: GNU glibc-2.2.5 OS: Debian GNU/Linux sid (2002.02.26) python libs: python-2.1.2 Blender: creator-2.22-linux-glibc2.1.2-i386 """ """ As of January 2002, development of the Creator line is temporarily suspended until NaN gets rich enough, or something. There is an annoying bug in Blender 2.23 for linux-x86 that breaks file append (library load) -- a real showstopper for mapping. This bug is not present in 2.22, which, in turn, has a buggy Armature-IKA (which isn't "critical" for Quake modelling/animation, though probably useful for generating certain types of animations). My opinion: keep around as many different versions of Blender as feasible. Python API is something of a mess in 2.2x. The module "Blender" represents pre-2.0 API, a far more complete interface to Blender's capabilities. This module was overhauled in 2.00-2.10, and in effect became a different module with a different API, but still called "Blender". In 2.1x, this new Blender module was renamed "Blender210" and the old pre-2.0 module was reinstated as just "Blender". Furthermore, post-2.23 Blender [Publisher] will have an expanded Python Modelling/Animation API based on "Blender210", and probably won't be called "Blender210" by the time the Creator line is resurrected. I don't forsee a return of the Creator line until after Nov 2002, and am planning accordingly. In the meantime, I expect to use both of 2.22's Blender and Blender210 modules. The 210 module has a more natural, serialized interface for importing/exporting formats, but has incomplete interface to the rest of the Blender modelling facilities. The older module fills the vacancy. If you want to help push Blender development with some cash (and help revitalize the Creator line), you can go and pay for Blender Publisher (http://www.blender3d.com/). However, this MD3 script will probably break in Publisher, meaning you'll still be firing up Creator. Bit of a Catch-22. On the bright side, files made by Creator are natively understood by Publisher (this has been one shining attribute of Blender -- all versions have always been forward-compatible). Python 2.0 is not packaged with Debian sid. As a workaround, I have to specify the environment variable PYTHONPATH to include the newer python libs (e.g. /usr/lib/python2.1) before starting Blender. YMMV. I think it may be best to move as much of the MD3 import/export stuff into the general parsing module. """ """ TODO: * use current frame as initial frame, instead of overwriting 1 """ #Set this to where the base directory of unpacked data starts. pk3path="." #Modify this if needed (e.g. FileSelector doesn't work) md3file="railgun.md3" import string import os.path from os import F_OK, R_OK, W_OK, X_OK # The MD3 parsing module. import md3 # Targeted for Blender 2.22. Plan for later Python API mess. import Blender210 import GUI import Blender #Default scale factor is 1/64 MD3BLENDER_SCALE = md3.MD3_XYZ_SCALE def make_bounds_box (md3obj): """Create bounds box in the current scene and layer. This is the box used in Q3A for determining collision. Return value is undefined.""" ourscene = Blender210.getCurrentScene() thisobj = Blender210.Object("bounds") thismesh = Blender210.Mesh("bounds") Blender210.connect(thisobj, thismesh) Blender210.connect(ourscene, thisobj) Blender210.setCurrentFrame(1) #Create the box (first frame). minx = md3obj.frames[0].bounds[0][0] miny = md3obj.frames[0].bounds[0][1] minz = md3obj.frames[0].bounds[0][2] maxx = md3obj.frames[0].bounds[1][0] maxy = md3obj.frames[0].bounds[1][1] maxz = md3obj.frames[0].bounds[1][2] thismesh.enterEditMode() thismesh.addVertex(0,0,0,0,0,0) #Throw-away vertex thismesh.addVertex(minx, miny, minz, 0, 0, 0) thismesh.addVertex(minx, miny, maxz, 0, 0, 0) thismesh.addVertex(minx, maxy, minz, 0, 0, 0) thismesh.addVertex(minx, maxy, maxz, 0, 0, 0) thismesh.addVertex(maxx, miny, minz, 0, 0, 0) thismesh.addVertex(maxx, miny, maxz, 0, 0, 0) thismesh.addVertex(maxx, maxy, minz, 0, 0, 0) thismesh.addVertex(maxx, maxy, maxz, 0, 0, 0) """ 8 ________ 6 / /| / / | 4 /______2/ | | z| | | 7 . | .| x | . | / 5 |_______|/ y 3 1 """ thismesh.addFace(1, 2, 4, 3, 0, 0) thismesh.addFace(5, 6, 8, 7, 0, 0) thismesh.addFace(1, 2, 6, 5, 0, 0) thismesh.addFace(3, 4, 8, 7, 0, 0) thismesh.addFace(1, 3, 7, 5, 0, 0) thismesh.addFace(2, 4, 8, 6, 0, 0) ### Animation for i in xrange(0, len(md3obj.frames)): Blender210.setCurrentFrame(i+1) #Move the vertices around #Insert VertexKey IPO key del thismesh.vertices[0] #Remove throw-away vertex. thismesh.leaveEditMode() #Commit Blender210.setCurrentFrame(1) #Restore frame number. def make_tags (md3obj): """Tags are named points in the model space. These are naturally represented by Empty objects. Return value is undefined.""" ourscene = Blender210.getCurrentScene() for i in xrange(0, len(md3obj.tags)): # print " tag %d (%s) @ " % (i, md3obj.tags[i].name), md3obj.tags[i].origin, md3obj.tags[i].axis print " tag %d:" % i, md3obj.tags[i].name #This should be Empty object. thistag = Blender210.Object(md3obj.tags[i].name) Blender210.connect(ourscene, thistag) #Note: Quake3 uses left-hand geometry. x = md3obj.tags[i].origin[0] y = md3obj.tags[i].origin[1] z = md3obj.tags[i].origin[2] thistag.loc = tuple([[x, y, z]]) rx = md3obj.tags[i].axis[2][0] ry = md3obj.tags[i].axis[2][1] rz = md3obj.tags[i].axis[2][2] thistag.rot = tuple([[rx, ry, rz]]) def find_texfile (tname): """Find texture file named `tname' Search through paths if needed. Return value is undefined.""" retval = tname if os.path.exists(retval): print "TEX =", retval return retval p = pk3path + os.pathsep + os.getenv("PK3PATH", ".") paths = string.split(p, os.pathsep) for p in paths: retval = os.path.normpath(p + os.sep + tname) if os.path.exists(retval): print "TEX =", retval return retval print "TEX not found:", tname return tname def make_surfaces (md3obj): """What MD3 calls a "surface", Blender calls a "mesh". Return value is undefined.""" ourscene = Blender210.getCurrentScene() for i in xrange(0, len(md3obj.surfaces)): print " mesh %d," % i, md3obj.surfaces[i].name thisobj = Blender210.Object(md3obj.surfaces[i].name) thismesh = Blender210.Mesh(md3obj.surfaces[i].name) Blender210.connect(thisobj, thismesh) Blender210.connect(ourscene, thisobj) ### Vertices thismesh.enterEditMode() #Make a throw-away vertex 0, so that addFace doesn't mess up. thismesh.addVertex(0,0,0,0,0,0) for j in xrange(0, md3obj.surfaces[i].numVerts): # print " vertex %d = " % j, md3obj.surfaces[i].verts[0][j].xyz, md3obj.surfaces[i].verts[0][j].normal #Note: Quake3 uses left-hand geometry. vx = md3obj.surfaces[i].verts[0][j].xyz[0] * MD3BLENDER_SCALE vy = md3obj.surfaces[i].verts[0][j].xyz[1] * MD3BLENDER_SCALE vz = md3obj.surfaces[i].verts[0][j].xyz[2] * MD3BLENDER_SCALE nx = md3obj.surfaces[i].verts[0][j].normal[0] ny = md3obj.surfaces[i].verts[0][j].normal[1] nz = md3obj.surfaces[i].verts[0][j].normal[2] thismesh.addVertex(vx, vy, vz, nx, ny, nz) ### Polygon faces (triangles) for j in xrange(0, len(md3obj.surfaces[i].triangles)): # print " tri %d = " % j, md3obj.surfaces[i].triangles[j].indexes #Ensures vertex 0 won't occur, so addFace won't mess up. a = md3obj.surfaces[i].triangles[j].indexes[0] + 1 b = md3obj.surfaces[i].triangles[j].indexes[1] + 1 c = md3obj.surfaces[i].triangles[j].indexes[2] + 1 d = 0 # thismesh.addFace(a, b, c, d, 0, 0) thismesh.addFace(c, b, a, 0, 0, 0) #Remove throw-away vertex 0 now that triangles are done. del thismesh.vertices[0] thismesh.removeDoubles() thismesh.leaveEditMode() #Commit. Blender210.setCurrentFrame(1) #Reset animation frame to 1. # print "thismesh %d = " % i, dir(thismesh) return 1 def make_textures (md3obj): """Set up textures and texture coordinates. This is broken pretty badly in Blender 2.22, 2.23 """ ### Textures for i in xrange(0, len(md3obj.surfaces)): thismesh = Blender210.getMesh(md3obj.surfaces[i].name) #Blender210 only allows 1 texture per mesh. for j in xrange(0, len(md3obj.surfaces[i].shaders)): pass tname = find_texfile(md3obj.surfaces[i].shaders[0].name) thismesh.addTexture(tname) ### Texture coordinates for j in xrange(0, len(md3obj.surfaces[i].triangles)): # print " tri %d" % j a = md3obj.surfaces[i].triangles[j].indexes[0] b = md3obj.surfaces[i].triangles[j].indexes[1] c = md3obj.surfaces[i].triangles[j].indexes[2] d = 0 u1, v1 = md3obj.surfaces[i].texcoords[a-1].st u2, v2 = md3obj.surfaces[i].texcoords[b-1].st u3, v3 = md3obj.surfaces[i].texcoords[c-1].st # thismesh.addTexCoords(u1, v1, u2, v2, u3, v3, 0, 0) thismesh.addTexCoords(u3, v3, u2, v2, u1, v1, 0, 0) # thismesh.texcoords[j] = [[u3, v3, u2, v2, u1, v1, 0, 0]] Blender210.setCurrentFrame(1) def make_animation (md3obj): """Set up animation of object. Doable, but requires extensive use of module `Blender' """ ### Animate for i in xrange(0, len(md3obj.surfaces)): for framenum in xrange(0, md3obj.surfaces[i].numFrames): Blender210.setCurrentFrame(framenum + 1) #Move around all the vertices. for j in xrange(0, md3obj.surfaces[i].numVerts): vx = md3obj.surfaces[i].verts[0][j].xyz[0] * MD3BLENDER_SCALE vy = md3obj.surfaces[i].verts[0][j].xyz[1] * MD3BLENDER_SCALE vz = md3obj.surfaces[i].verts[0][j].xyz[2] * MD3BLENDER_SCALE nx = md3obj.surfaces[i].verts[0][j].normal[0] ny = md3obj.surfaces[i].verts[0][j].normal[1] nz = md3obj.surfaces[i].verts[0][j].normal[2] thismesh.vertices[j] = tuple([[vx, vy, vz]]) thismesh.normals[j] = tuple([[nx, ny, nz]]) #Insert VertexKey IPO key Blender210.setCurrentFrame(1) def md3_import (md3obj): """Import MD3 object into Blender. Parameter is a parsed MD3 object populated with data (from md3obj.parse()). Return value is undefined.""" print "importing MD3", md3obj.name, "..." # print "MODEL:" # print " NAME:", md3obj.name # print " #FRAMES:", len(md3obj.frames) # print " #EMPTIES:", len(md3obj.tags) # print " #MESHES:", len(md3obj.surfaces) #Warning: Quake3 uses left-hand geometry. Blender uses right-hand. # - MD3 surfaces, meshes per frame make_surfaces(md3obj) # - bounds box make_bounds_box(md3obj) # - MD3 tags make_tags(md3obj) # - Textures # make_textures(md3obj) # - Animation # make_animation(md3obj) print "...MD3 finished" def md3import (fname): """Import MD3 by filename into Blender.""" md3obj = md3.qmodel() f = open(fname, "rb") if f: if md3obj.parse(f): md3_import(md3obj) f.close() def FScallback (FSobj): """Callback handler for FileSelector widget.""" fname = FSobj.filename # print "Filename", fname md3import(fname) if __name__ == '__main__': FS = GUI.FileSelector() FS.activate(FScallback, FS) # md3import(md3file)