Sunday, December 14, 2008

The Start of a WaveFront OBJ File Loader Class

One of the most frequent questions I've gotten about iPhone programming is how you load 3D models in from third-party programs. Most people who are new to OpenGL and/or new to iPhone programming assume that there is some built-in functionality to load models from file.

Nope. There isn't. You have to do it yourself.

I started writing a class that can load Wavefront OBJ 3D files and draw the contents in OpenGL ES. I chose this file format because it's a simple text-based format, which will make it easy to illustrate the basic idea. Now, there are a lot of features you might need in your programs that the OBJ file format can't handle, like animations, for example. The three objects used in this project are simple shapes exported from Blender, but the 3D program you use doesn't matter, as long as you remember not to export any quads or larger polygons. Most programs have an option to only export triangles, so make sure you use it. OpenGL ES doesn't like quads, remember. I suppose, we could make this class more robust by subdividing any larger polygons into triangles, but for now, I'm just going to assume the file has only triangles and make it incumbent on myself to only use files exported that way.



As you can see, the objects can be loaded and displayed. Currently, the vertex data is loaded into a vertex array and the polygon data is loaded into an array of faces. I have not done the normals or the texture coordinates yet or any surface work at all, but it's not a bad start for a Sunday afternoon.

Notice how flat the objects look, however. That's because, without the surface normals, OpenGL has no way to calculate how the light should bounce.

You can find the Xcode project right here.

Here's the basic approach I took. First, I defined two structs - one of which you might have seen in my post on surface normals.
typedef struct {
GLfloat x;
GLfloat y;
GLfloat z;
} Vertex3D, Vector3D, Rotation3D;

// A Face 3D contains three indices to vertices, generally faster to use...
typedef struct {
GLushort v1;
GLushort v2;
GLushort v3;
} Face3D;
I declare the interface of my new class like so:

@interface OpenGLWaveFrontObject : NSObject {
Vertex3D *vertices;
int numberOfFaces;
Face3D *faces;
Vertex3D currentPosition;
Rotation3D currentRotation;
}
@property Vertex3D currentPosition;
@property Rotation3D currentRotation;
- (id)initWithPath:(NSString *)path;
- (void)drawSelf;
@end
So, there's a pointer to a Vertex3D. When I load the data, I'll count the number of vertices there are in the file, and allocate a chunk of memory big enough to hold that many Vertex3D objects. This will mean we can refer to the first vertex as vertices[0], the second as vertices[1], etc. It also means that the pointer vertices can be passed directly in to glVertexPtr.

Yep, even though I'm declaring my own data structures to hold the data, because my structures contains the data that OpenGL needs, in a format that it understands and in the order it needs, I can just pass the pointer to it. Remember, the order is important. If had made the first item in my Vertex3D struct y instead of x, then it wouldn't have worked.

Also, remember that "skip" parameter I kept telling you to ignore? Well, if we had additional items in this struct, we could still pass the pointer to OpenGL by using that argument to tell it to skip the elements it doesn't need, in this case, the elements that aren't vertex data. We didn't do that here, but I thought I'd mention it so you'd know what that skip parameter was for.

Generally, I avoid doing using skip. There are situations where you can get a performance gain by packing multiple types of data into a single array and using that skip variable to tell OpenGL which ones to use. That is an optimization, and in my mind, shouldn't be used unless you have performance problems that need to be addressed. I don't like adding more complexity than is necessary, so I start with the simplest scenario - containing each type of data in its own array.

If this is a little confusing, just think of it this way:
Vertex3D vertices[5]
allocates exactly the same amount of memory as
GLfloat vertices[15]
Vertex3D contains three GLfloats, so five of them is the same as fifteen GLFloats. It's just a different way of organizing the same chunk of data. This allows us to refer to vertices using their x, y, and z values in our code without paying any performance price, because the compiled code will look the same. Sweet, huh?

We also have an instance variable to keep track of the number of faces that were loaded from the file. This will be used to drive the loop that calls glDrawElements().

The Face3D pointer will work exactly the same way that the Vertex3D pointer works - it's going to be the array that has indices to the vertices used in each of the triangles in the shape and we'll feed it into the glDrawElements() call. Again, the struct just gives us another way to organize that big chunk o' data that OpenGL needs. It makes it easier to deal with in our code, but doesn't change the actual data in any way.

We also have two more instance variables to keep track of the current position and current rotation of this object. Since we're going to make this object self-contained, so that it knows how to draw itself, it needs to know where it's located in the virtual world, and where it's facing.

Notice that we have two methods - one to initialize the object based on path to a file, and another that tells this object to daw itself. Let's look first at the init method. Now, this isn't done - we'll be doing more work down the line to load normals, texture coordinates, etc.. I didn't want to wait until it was all done to blog it, however, because it's going to be a little overwhelming at that point, with all the data we'll be pulling in. This post, we're focusing on vertices and faces only.

Here is the init method:
- (id)initWithPath:(NSString *)path
{
if ((self = [super init]))
{

NSString *objData = [NSString stringWithContentsOfFile:path];
NSUInteger vertexCount = 0, faceCount = 0;
// Iterate through file once to discover how many vertices, normals, and faces there are
NSArray *lines = [objData componentsSeparatedByString:@"\n"];
for (NSString * line in lines)
{
if ([line hasPrefix:@"v "])
vertexCount++;
else if ([line hasPrefix:@"f "])
faceCount++;
}
NSLog(@"Vertices: %d, Normals: %d, Faces: %d", vertexCount, faceCount);
vertices = malloc(sizeof(Vertex3D) * vertexCount);
faces = malloc(sizeof(Face3D) * faceCount);

// Reuse our count variables for second time through
vertexCount = 0;
faceCount = 0;
for (NSString * line in lines)
{
if ([line hasPrefix:@"v "])
{
NSString *lineTrunc = [line substringFromIndex:2];
NSArray *lineVertices = [lineTrunc componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
vertices[vertexCount].x = [[lineVertices objectAtIndex:0] floatValue];
vertices[vertexCount].y = [[lineVertices objectAtIndex:1] floatValue];
vertices[vertexCount].z = [[lineVertices objectAtIndex:2] floatValue];
// Ignore weight if it exists..
vertexCount++;
}
else if ([line hasPrefix:@"f "])
{
NSString *lineTrunc = [line substringFromIndex:2];
NSArray *faceIndexGroups = [lineTrunc componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// Unrolled loop, a little ugly but functional
/*
From the WaveFront OBJ specification:
o The first reference number is the geometric vertex.
o The second reference number is the texture vertex. It follows the first slash.
o The third reference number is the vertex normal. It follows the second slash.
*/
NSString *oneGroup = [faceIndexGroups objectAtIndex:0];
NSArray *groupParts = [oneGroup componentsSeparatedByString:@"/"];
faces[faceCount].v1 = [[groupParts objectAtIndex:kGroupIndexVertex] intValue]-1; // indices in file are 1-indexed, not 0 indexed
oneGroup = [faceIndexGroups objectAtIndex:1];
groupParts = [oneGroup componentsSeparatedByString:@"/"];
faces[faceCount].v2 = [[groupParts objectAtIndex:kGroupIndexVertex] intValue]-1;
oneGroup = [faceIndexGroups objectAtIndex:2];
groupParts = [oneGroup componentsSeparatedByString:@"/"];
faces[faceCount].v3 = [[groupParts objectAtIndex:kGroupIndexVertex] intValue]-1;
faceCount++;

}
}
numberOfFaces = faceCount;

}
return self;
}
It starts off simple enough by loading the object data into a string. Since this is a text-based file format, it is safe to load it into a string. Doing so sure makes our life easier thanks to all the great functionality from NSString. We declare two variables that will be used to keep track of the number of vertices in the file, and the number of polygons.

We actually iterate through the data twice. The first time through, all we do is count the vertices and faces in the file so we know how much memory to allocate. Once we know how many of each we have, we allocate a chunk of memory for each:
  vertices = malloc(sizeof(Vertex3D) * vertexCount);
faces = malloc(sizeof(Face3D) * faceCount);
Then we reset the count variables and start over looping through the file. This time, we parse out the vectors and face data and store them at the appropriate spots in our vertex array and face array. The only real gotcha here is that the indices in the OBJ file are 1-indexed, and C arrays are 0-indexed, but that's easily handled by subtracting one along the way. When we're all done looping, we save off the final face count in our instance variable numberOfFaces so we know how many triangles we have to draw.

If everything was successful, we return self like any self-respecting init method.

All that's left is to write the code to draw this vertex data. Again, this method will get a little more complex as we add normals and texture data. Here's what the draw method look like now:
- (void)drawSelf
{
// Save the current transformation by pushing it on the stack
glPushMatrix();

// Load the identity matrix to restore to origin
glLoadIdentity();

// Translate to the current position
glTranslatef(currentPosition.x, currentPosition.y, currentPosition.z);

// Rotate to the current rotation
glRotatef(currentRotation.x, 1.0, 0.0, 0.0);
glRotatef(currentRotation.y, 0.0, 1.0, 0.0);
glRotatef(currentPosition.z, 0.0, 0.0, 1.0);

// Enable and load the vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);

// Loop through faces and draw them
for (int i = 0; i < numberOfFaces; i++)
{
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, &faces[i]);
}

glDisableClientState(GL_VERTEX_ARRAY);

// Restore the current transformation by popping it off
glPopMatrix();
}
Not that much to it, is there. The first and last call do nothing more than save and restore the current transformation - that way, we can load identity, move and rotate as necessary, and then restore the transformation when we're don so that drawing done by other objects will work as expected.

We reset the transformation using glLoadIdentity(), and then use glTranslate() and glRotate() to place this object based on the current position and rotation as stored in our instance variables.

Because we don't know what other objects will be doing, we enable GL_VERTEX_ARRAY before we draw, and disable it when we're done. Then we call glVertexPointer() passing in vertices - see how easy life is thanks to that Vertex3D struct?

After that, we loop through our faces and pass them to glDrawElements() exactly as we did back in NeHe Lesson 05.

Now, to use this, it's pretty darn easy. First, of course, we need to create an object instance based on a file so to load the plane shape, we do this in our setupView method:
 NSString *path = [[NSBundle mainBundle] pathForResource:@"plane" ofType:@"obj"];
OpenGLWaveFrontObject *theObject = [[OpenGLWaveFrontObject alloc] initWithPath:path];
Vertex3D position;
position.z = -8.0;
position.y = 3.0;
position.x = 0.0;
theObject.currentPosition = position;
self.plane = theObject;
[theObject release];
This loads the object into memory and sets its initial position. The same process is used for the other shapes as well. Then, when we want to draw the shape in our drawView method, all we have to do is set the color (which won't be necessary when we get texturing and materials working) and then tell it to draw itself.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor4f(0.0, 0.5, 1.0, 1.0);
[plane drawSelf];
Pretty darn easy, isn't it? If you haven't, download the Xcode project and give it a try for yourself.

I'll be adding normals, textures, and face colors in the near future, but I hope this gets you familiar with the basic concept of model loading.

No comments:

Post a Comment