Before I start I wanted to explain why I think this is a classic exercise
If you can represent a mesh in OBJ text format you pretty much understand how the data polygonal data exists in the package you are using and you have enough skill to pack it into a data structure to put it to disk.
So here is what you should get a good understanding of:
- The classic Wavefront OBJ file format
- The the components of a polygon mesh
- Using the Python Commands to access the polygon information from the scene
- Writing data structures to disk
The Structure of a Wavefront OBJ
so we should start with the OBJ of a 1x1x1 cube
An OBJ is great at encoding the surface of an object
which is made up of:
- Position of the Surface: P
- Normal of the Surface: N
- Texture Values of the Surface: st
It is described by stating the values of the surface at discrete points in space along with some connectivity information of how those points are joined together in space as a mesh.
Using the connectivity information a small amount of discrete information can be interpolated continuously to form the surface.
The inner structure of an OBJ has lots of parts
- “g” 2 groups: of components belonging to one mesh
- “v” 8 vertices: 3d points, (x-pos,y-pos,z-pos) describing the Position of the surface
- “vt” 14 texture verticies: 2d points (u-pos,v-pos) describing the layout of the texture on the surface
- “vn” 24 vertex normals: 3d vectors (x-direction,y-direction,z-direction) describing the angle of the surface
- “f” 8 faces: each containing indecies to 4 vertices, 4 texture verticies and 4 vertex normals describing the connectivity of the positional, normal and texture information
A diagram of what is the difference between a face, vertex, vertex normal and a texture vertex, would be great here, but you will have to use your imagination
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | # This file uses centimeters as units for non-parametric coordinates. mtllib cube.mtl g default v -0.500000 -0.500000 0.500000 v 0.500000 -0.500000 0.500000 v -0.500000 0.500000 0.500000 v 0.500000 0.500000 0.500000 v -0.500000 0.500000 -0.500000 v 0.500000 0.500000 -0.500000 v -0.500000 -0.500000 -0.500000 v 0.500000 -0.500000 -0.500000 vt 0.375000 0.000000 vt 0.625000 0.000000 vt 0.375000 0.250000 vt 0.625000 0.250000 vt 0.375000 0.500000 vt 0.625000 0.500000 vt 0.375000 0.750000 vt 0.625000 0.750000 vt 0.375000 1.000000 vt 0.625000 1.000000 vt 0.875000 0.000000 vt 0.875000 0.250000 vt 0.125000 0.000000 vt 0.125000 0.250000 vn 0.000000 0.000000 1.000000 vn 0.000000 0.000000 1.000000 vn 0.000000 0.000000 1.000000 vn 0.000000 0.000000 1.000000 vn 0.000000 1.000000 0.000000 vn 0.000000 1.000000 0.000000 vn 0.000000 1.000000 0.000000 vn 0.000000 1.000000 0.000000 vn 0.000000 0.000000 -1.000000 vn 0.000000 0.000000 -1.000000 vn 0.000000 0.000000 -1.000000 vn 0.000000 0.000000 -1.000000 vn 0.000000 -1.000000 0.000000 vn 0.000000 -1.000000 0.000000 vn 0.000000 -1.000000 0.000000 vn 0.000000 -1.000000 0.000000 vn 1.000000 0.000000 0.000000 vn 1.000000 0.000000 0.000000 vn 1.000000 0.000000 0.000000 vn 1.000000 0.000000 0.000000 vn -1.000000 0.000000 0.000000 vn -1.000000 0.000000 0.000000 vn -1.000000 0.000000 0.000000 vn -1.000000 0.000000 0.000000 s off g pCube1 usemtl initialShadingGroup f 1/1/1 2/2/2 4/4/3 3/3/4 f 3/3/5 4/4/6 6/6/7 5/5/8 f 5/5/9 6/6/10 8/8/11 7/7/12 f 7/7/13 8/8/14 2/10/15 1/9/16 f 2/2/17 8/11/18 6/12/19 4/4/20 f 7/13/21 1/1/22 3/3/23 5/14/24 |
Feel free to read the Wavefront OBJ File format Specification that I found on the internet.
If you want a copy of this file, your copy and paste buffer will work a treat or you could open up Maya and save out a unit cube on the origin.
Its a unit cube on the origin, named pCube1, using initialShadingGroup as a material.
You can see the eight vertices are 0.5 units away from the origin in each three axes with the eight permuations of the signs of each axis. This makes the spacing between vertices 1 unit apart.
The vertex normals are unit in length facing either up, down, left, right, front or back, these normalised vectors are repeated a number of times as there are only 6 discrete values but 24 records.
The texture vertices make squares that are 0.25 units in texture space in an upside down T-layout, with some of the texture verticies being shared between texture faces.
The order in which the faces are created is a left hand rule where the order of the vertices specifies which is the inside of the face as indicated by the fingers and the direction of the face normal is specified by the thumb.
The same left hand rule applies to the texture verticies determining if the texture is mirrored or not.
Edges are implied as the edges between verticies that make up a face.
The number of edges in a face is determined by how many Point/Texture/Normal groups there are in a face
Lets explain that a little further with a single face OBJ
1 2 3 4 5 6 7 8 9 10 11 12 13 | mtllib cube.mtl g default v -0.5 -0.5 0.5 v 0.5 -0.5 0.5 v -0.5 0.5 0.5 v 0.5 0.5 0.5 vt 0 0 vt 1 0 vt 1 1 vt 0 1 vn 0 0 1 g zPlanePointFive f 1/1/1 2/2/1 4/3/1 3/4/1 |
This is a Unit sized face on the Z Plane at +0.5 units in Z
On line 13 we can see the single face definition, it creates a four sided face that goes in the following order
13 | f 1/1/1 2/2/1 4/3/1 3/4/1 |
v/vt/vn : vertex position/ vertex texture / vertex normal, repeated for the number of faces
The edge created from the last vertex back to the first is implied but not explicity defined.
The order is as follows:
for vertex position: 1,2,4,3
for vertex texture position: 1,2,3,4
for vertex normals direction: 1,1,1,1
For Position we can see the face formed on the Z Plane
- vert #1 : -x,-y (inital point no edge)
- vert #2 : +x,-y (edge #1, left to right)
- vert #4 : +x, +y (edge #2, up)
- vert #3 : -x, +y (edge #3, right to left)
- back to inital vert : (edge #4, down, return to initial point)
For texture it goes around 0-1 UV space, left to right, up, right to left and then down.
All of the faces recycle the same face normal, the only face normal, face normal number ONE!!1!
Now we understand the OBJ we understand how the components are connected in the OBJ, we just need to find out about the same info in Maya and we can write an OBJ exporter
Getting at the Geometric Data for Polygonal Object in Maya with Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | import maya def log(message,prefix="Debug",hush=False): if not hush: print("%s : %s " % (prefix,message)) def getData(shapeNode): vertexValues = [] vertNormalValues =[] textureValues =[] vertList = [] vertNormalList = [] vertTextureList = [] oldSelection = maya.cmds.ls(selection=True) maya.cmds.select(shapeNode) #Verts numVerts = maya.cmds.polyEvaluate(vertex=True) log("NumVerts : %s" % numVerts) vertexValues = [maya.cmds.pointPosition("%s.vtx[%d]" % (shapeNode,i)) for i in range(numVerts)] log("Verticies:" + str(vertexValues)) #Normals faceNormals=[] numFaceNormals = 0 for face in range(maya.cmds.polyEvaluate(face=True)): maya.cmds.select("%s.f[%d]" % (shapeNode,face)) vertexFaces = maya.cmds.polyListComponentConversion(fromFace=True,toVertexFace=True) vertexFaces= maya.cmds.filterExpand(vertexFaces,selectionMask=70,expand=True) faceNormals.append([]) for vertexFace in vertexFaces: vertNormalValues.append(maya.cmds.polyNormalPerVertex(vertexFace, query=True, xyz=True)) numFaceNormals += 1 faceNormals[-1].append(numFaceNormals ) log("Num Face Normals: " + str(numFaceNormals)) log("Face Normals: " + str(vertNormalValues)) #Texture Coordinates numTexVerts = maya.cmds.polyEvaluate(uvcoord=True) log("NumTexVerts: " + str(numTexVerts)) textureValues = [maya.cmds.getAttr("%s.uvpt[%d]" % (shapeNode,i)) for i in range(numTexVerts)] log("Texture Coordinates: " + str(textureValues)) #Faces numFaces = maya.cmds.polyEvaluate(face=True) log("NumFaces : %s" % numFaces) vnIter = 0 faceValues = [] for i in range(numFaces): log("Face %d of %d" % (i+1,numFaces)) maya.cmds.select("%s.f[%d]" % (shapeNode,i)) #Verts (v) faceVerts = maya.cmds.polyInfo(faceToVertex=True) #This is hacky and should be replaced with snazzy regex faceVerts = [int(fv)+1 for fv in faceVerts[0].split(":")[-1].strip().replace(" "," ").replace(" "," ").replace(" "," ").replace(" ",",").split(",")] log("v: " + str(faceVerts) ) vertList.append(faceVerts) #Normals (vn) maya.cmds.select("%s.f[%d]" % (shapeNode,i)) log("vn: " + str(faceNormals[i])) vertNormalList.append(faceNormals[i]) #Texture (vt) maya.cmds.select("%s.f[%d]" % (shapeNode,i)) tex = maya.cmds.polyListComponentConversion(fromFace=True,toUV=True) tex= maya.cmds.filterExpand(tex,selectionMask=35,expand=True) tex=[int(i.split("map")[-1].strip("[]")) +1 for i in tex] log("vt: " + str(tex)) #Order is incorrect, need to get in same order as vertex ordering tmpDict = {} for t in tex: maya.cmds.select("%s.map[%d]" % (shapeNode,t-1)) vertFromTex = maya.cmds.polyListComponentConversion(fromUV=True,toVertex=True) tmpDict[int(vertFromTex[0].split("[")[-1].split("]")[0]) + 1] = t orderedTex=[] for i in vertList[-1]: orderedTex.append(tmpDict[i]) vertTextureList.append(orderedTex) face = " ".join( ["%d/%d/%d" % (vertList[-1][i], vertTextureList[-1][i], vertNormalList[-1][i]) for i in range( len( vertTextureList[-1] ) ) ] ) faceValues.append(face) log("") log("f: " + face) log("--") maya.cmds.select(oldSelection) return {"v":vertexValues,"vn":vertNormalValues,"vt":textureValues,"f":faceValues,"g":shapeNode} print "GO!" maya.cmds.file(new=True,force=True) maya.cmds.polyCube(ch=True,o=True,w=1,h=1,d=1,cuv=4) dataDict = getData("pCubeShape1") |
OK so apart from the messy code structure there is not much to getting the Point/Vertex (v), Normal (vn), Texture/UV/st (vt) data from the scene:
These are the three commands that make it possible to get to the data you want:
- polyInfo
- polyEvaluate
- polyListComponentConversion
but we need to clean up the output with:
- filterExpand
As well as getting the data out from the scene using
- pointPosition: P
- polyNormalPerVertex: N
- getAttr: st/UV
Writing the OBJ data to Disk using Python File I/O
So to finish it off we simply replace the end of the code with the following:
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | #Continues from the def "getData" def writeData(dataDict): outString = "\ng default\n" for i in dataDict["v"]: log(str(i)) outString+= "v %f %f %f \n" % (i[0],i[1],i[2]) for i in dataDict["vt"]: log(str(i)) outString+= "vt %f %f \n" % (i[0][0],i[0][1]) for i in dataDict["vn"]: log(str(i)) outString+= "vn %f %f %f \n" % (i[0],i[1],i[2]) outString += "g %s\n" % dataDict["g"] for i in dataDict["f"]: log(str(i)) outString += "f %s\n" % i outString += "\n" log(outString) return outString print "GO!" maya.cmds.file(new=True,force=True) maya.cmds.polyCube(ch=True,o=True,w=1,h=1,d=10,cuv=4) fileLocation = "/Users/hodgefamily/out.obj" f = open(fileLocation,"w") data = getData("pCubeShape1") string = writeData(data) f.writelines(string) f.close() maya.cmds.file(new=True,force=True) maya.cmds.file(fileLocation,i=True,type="OBJ",rpr="out") print "STOP" |
A friend of mine took my code and cleaned up up Thanks Katrin, you can download it here Katrin's Source Code