Showing posts with label obj. Show all posts
Showing posts with label obj. Show all posts

Monday, September 13, 2010

Parsing Wavefront .obj using Python

Wavefront OBJ File Format Parsing in Python






I thought I would share some simple parsing information about the wavefront .OBJ file format using python. The thing I like about this format is that it is stored in plain text, and easy to use if you are writing simple 3D game engines, or just 3D modeling programs.

You can use this parser to load wavefront files using python, and possibly to view the wavefront obj file.

Overview of the wavefront .OBJ file format



Based on http://en.wikipedia.org/wiki/Obj

Basically, our approach is to go line by line through the file. If the line starts with a "v", we are dealing with a vertex. If the line starts with a "vt" then we are dealing with a texture coordinate (u, v, optionally w). "n" means normal. "f" means its a face index. These are a bit special, but not too difficult to grasp. Our exported models that are from blender will all have normal vectors, and texture coordinates (make sure you specify the texture coordinates in blender or there will be none). The "f" lines will look like this:


f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3


where "v1" is the vertex array index, "vt1" is the texture coordinate index, and "vn1" is the normals array index. This particular face is a triangle. I recommend storing triangles, and quads in 2 different arrays. They can both reference your vertex/texture coordinate/normals array.

Very important note before we begin



The index format in the .OBJ Wavefront file format is 1 based, not 0 based. Thus, we should subtract 1 from the actual number in order to get a 0 based index, and make it compatible with python lists or any array type.


#do the loading of the obj file
def load_obj(filename) :
V = [] #vertex
T = [] #texcoords
N = [] #normals
F = [] #face indexies

fh = open(filename)
for line in fh :
if line[0] == '#' : continue

line = line.strip().split(' ')
if line[0] == 'v' : #vertex
V.append(line[1:])
elif line[0] == 'vt' : #tex-coord
T.append(line[1:])
elif line[0] == 'vn' : #normal vector
N.append(line[1:])
elif line[0] == 'f' : #face
face = line[1:]
if len(face) != 4 :
print line
#raise Exception('not a quad!')
continue
for i in range(0, len(face)) :
face[i] = face[i].split('/')
# OBJ indexies are 1 based not 0 based hence the -1
# convert indexies to integer
for j in range(0, len(face[i])) : face[i][j] = int(face[i][j]) - 1
F.append(face)

return V, T, N, F


Please not this program will give you string representations. You should loop through these arrays again and convert them to the proper format (int, float, etc).

Please leave your comments

Wednesday, March 3, 2010

Wavefront Obj file format, opengl vertex arrays format, and uv texture coords

Loading Wavefront files using Python


Loading .obj files into numpy arrays, then using OpenGL to draw them



Note: for information on parsing wavefront .obj format click here

Our task here is to load wavefront .obj files into numpy arrays and use them in OpenGL, or pyOpenGL of course.

I am here to discuss something that has always bothered me in openGL. The fact that the vertex arrays and glDrawElements uses only 1 pointer to draw the elements, when in reality, it should use 2 (at least). This could be solved by writing special GLSL vertex shaders and using vertex textures, but that is overly complicated.

Understanding the problem:

The problem with indexing a vertex is the texture coordinate. If you are going to have UV islands, you will see that each vertex at that point, will have more than 1 texture coordinate. This means that there is no 1:1 mapping between a vertex, and a texture coordinate, and this breaks the 1 array indexing multiple verticies in an array, and also breaks the glDrawElements method.

The solution?

We basically unravel the arrays, reusing as many vertex:uv mappings as we can, but create new indexes when they differ.


Visual Examples:
 
v1 v2 v3
--- ---
| | | - 2 quads connected
--- ---
v4 v5 v6

v1 v2 v2 v3
--- ---
| | | | - UV coordinates NOT connected
--- ---
v4 v5 v5 v6


In this example may you see that v2 and v5 don't share UV coordinates, so they will need to have 4 indexies into our index array (because the index array indexes both UV coordinates and Vertex coordinates)

How will we take care of this?

What we are going to do is write a python parser for OBJ files, parse it into the traditional array structure that is laid out in the file. Then we are going to unravel it and create secondary arrays that will work with the glDrawElements.

Code:


from numpy import *
import random


#util to unravel
indexies = dict()
counter = -1
def get_index(key) :
global indexies, counter
if key not in indexies :
counter += 1
indexies[key] = counter
return [False, counter]
else :
return [True, indexies[key]]

#do the loading of the obj file
def load_obj(filename) :
V = [] #vertex
T = [] #texcoords
N = [] #normals
F = [] #face indexies

fh = open(filename)
for line in fh :
if line[0] == '#' : continue

line = line.strip().split(' ')
if line[0] == 'v' : #vertex
V.append(line[1:])
elif line[0] == 'vt' : #tex-coord
T.append(line[1:])
elif line[0] == 'vn' : #normal vector
N.append(line[1:])
elif line[0] == 'f' : #face
face = line[1:]
if len(face) != 4 :
print line
#raise Exception('not a quad!')
continue
for i in range(0, len(face)) :
face[i] = face[i].split('/')
# OBJ indexies are 1 based not 0 based hence the -1
# convert indexies to integer
for j in range(0, len(face[i])) : face[i][j] = int(face[i][j]) - 1
F.append(face)

#Now we lay out all the vertex/texcoord/normal data into a flat array
#and try to reuse as much as possible using a hash key

V2 = []
T2 = []
N2 = []
C2 = []
F2 = []

for face in F :
for index in face :
#print V[index[0]], T[index[1]], N[index[2]]
key = '%s%s%s%s%s' % (V[index[0]][0], V[index[0]][1], V[index[0]][2], T[index[1]][0], T[index[1]][1])
idx = get_index(key)

if not idx[0] :
V2.append([float(V[index[0]][0]), float(V[index[0]][1]), float(V[index[0]][2])])
T2.append([float(T[index[1]][0]), float(T[index[1]][1])])
N2.append([float(N[index[2]][0]), float(N[index[2]][1]), float(N[index[2]][2])])
C2.append([random.random(), random.random(), random.random()])

F2.append(idx[1])

print len(V) * 3 * 4, 'bytes compared to', len(V2) * 3 * 4, 'bytes'

#return numpy arrays
return [
array(V2, dtype=float32),
array(T2, dtype=float32),
array(N2, dtype=float32),
array(C2, dtype=float32),
array(F2, dtype=uint32)
]