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