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.



15 comments:

Jon Baer said...
This comment has been removed by the author.
papillon68 said...

Hi, thanks for taking your time writing this valuable obj file loader !
When will you be adding normals and texture coordinates ?

Vic said...

Just a brief thanks for spending the time and effort in sharing your work. Its a boon for budding iphone developers

tom said...

Yeah, I just jumped on the iphone bandwagon. Appreciate your work!

bam93 said...

Great work! Thanks for sharing.

Devesh said...

Hi....
When i have run this code in iPhone os 3.0 it is not working..

It shows only white screen on iphone simulator.

So what will be the problem ???

pyrofer said...

The white screen problem is likely a 2.0/3.0 issue. Compile and run it on the simulator for 2.0 devices and it will probably work.

LonelyNoMore said...

Thank you so much for your posts!!!

robborg said...

@pyrofer
I'm using XCode 3.2 / iPhoneOS 3.1 and also saw the white screen with the attached project (version 0.1).

Here are the solutions to issues I found to get this working "as advertised" with 3.1.

1. Library versions.
Fix target library versions in your target properties & project properties
2. XCode complaining about illegal "_" characters in "Wavefront_OBJ_Loader". You have to hack this - just changing the project name to no longer contain spaces doesn't work, even with clean rebuilds and restarts. Firstly in Target properties > General > Name - take out the spaces. Secondly open "project_dir/Wavefront OBJ Loader.xcodeproj/project.pbxproj". Search and replace "Wavefront_OBJ_Loader" and "Wavefront OBJ Loader" to "WavefrontOBJLoader". Do this with discretion!
3. In interface builder, remove the window from MainWindow.xib. (Notice how application delegate already creates a window, so the IB one is redundant).

Hope this helps. Perhaps the author could provide an 0.2 project with these fixes?

Thanks for the great work too!

dave morris said...

@robborg Thanks! Your suggested changes worked for me in the latest version of XCode 3.1.2. I also went ahead and added this to line 24-26 of OpenGLWavefrontObject.m to get rid of a deprecation warning:

NSStringEncoding encoding = NSUTF8StringEncoding;
NSError *error = nil;
NSString *objData = [NSString stringWithContentsOfFile:path encoding:encoding error:&error];

Has anyone experimented with getting a camera to orbit/pan/zoom around an object with touch input?

Dave

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

antywong said...

The rounded shape of speedy 30 features textile fake louis vuitton lining and leather trimmings with shiny Louis Vuitton Monogram ldylle Romance Encre golden brass. Sized at 11.8" x 8.3" x 6.7", the large capacity Hermes Original Python Birkin 30 Grey of this bag is enough for handbags review daily essentials; you can put bags wholesale everything into this city bag. It also fits for Hermes Clemence Jypsiere 34 Purple every occasion and perfectly goes with any outfits mfakng100910.

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels

louis vuitton spring summer 2010 collection said...

Discover Louis Vuitton collections online: luggage, handbags, wallets, shoes ...

Gus said...

Hi I´m wondering How you will export from blender an object. What options Are you using to import from blender to an .obj?

Thanks a lot.