3D Models in the iPhone

I'm thinking of moving my half-developed 2D game into 3D, and I thought I'd ask you guys what you all have done in terms of model loading. I've looked around a bit and it appears the options are OBJ or Collada, but I can't find a very good (complete) loader for either. Ideally the loader would be able to get all the polygons, textures, and UV detail into the game. I don't necessarily need animations, although I wouldn't object to having them.

With Cheetah3D, the .h output is as efficient as you can get, really. There are a couple issues I can think of off the top of my head:

- It doesn't output the texture names for the meshes, but that's certainly not hard to do.
- It doesn't do material groups, like you can with .obj. So you'll have to use textures for everything.
- It doesn't output animation (e.g. no skeletal animation).
- It's in code form, so you lose the [potential] added compression of [your own binary format] on disk, but that's really not a huge problem (no pun intended).

If you don't have a problem with those, then I can't think of a more simple and effective way to get 3D geometry into your games.

.obj would be the next option, which is certainly easy enough at first, but there are boatloads of issues with that if you want maximum flexibility. Beyond that are Collada and FBX. I don't know too much about Collada, but I use .obj and FBX myself and I can say it's not easy to learn how to use the FBX SDK (far more difficult than learning .obj). Plus with FBX on iPhone you will have to learn to extract all the info out of your files by preprocessing them into your own format before using it on iPhone, since I don't believe the FBX SDK will work on iPhone. I would consider FBX a somewhat advanced route, although you do get lots of power out of it, including cameras, lights and skeletal animation, among other things.

Here's a sample .h output from Cheetah3D for you to look at. I snipped out all the redundant data where you see "...":

Code:

// Headerfile *.h (generated by Cheetah3D)
//
// There are the following name conventions:
// NAME =name of the object in Cheetah3D. Caution!! Avoid giving two objects the same name
// NAME_vertex =float array which contains the vertex,normal and uvcoord data
// NAME_index =int array which contains the polygon index data
// NAME_vertexcount =number of vertices
// NAME_polygoncount =number of triangles
//
// The vertex data is saved in the following format:
// u0,v0,normalx0,normaly0,normalz0,x0,y0,z0
// u1,v1,normalx1,normaly1,normalz1,x1,y1,z1
// u2,v2,normalx2,normaly2,normalz2,x2,y2,z2
// ...
// You can draw the mesh the following way:
// glEnableClientState(GL_INDEX_ARRAY);
// glEnableClientState(GL_NORMAL_ARRAY);
// glEnableClientState(GL_VERTEX_ARRAY);
// glEnableClientState(GL_TEXTURE_COORD_ARRAY);
// glInterleavedArrays(GL_T2F_N3F_V3F,0,NAME_vertex);
// glDrawElements(GL_TRIANGLES,NAME_polygoncount*3,GL_UNSIGNED_INT,NAME_index)​;
//

That's pretty cool. Yeah I'd ideally have FBX loading in here, but I don't have much experience writing model loaders so I might avoid that for now. The trouble is that the artists are probably going to want cameras, lighting, and all that jazz, so I might need to end up implementing FBX anyway. Thanks for the help, however. If you've got a link or a place to get started for me to figure out how to get FBX in here, that would be much appreciated.

demonpants Wrote:That's pretty cool. Yeah I'd ideally have FBX loading in here, but I don't have much experience writing model loaders so I might avoid that for now. The trouble is that the artists are probably going to want cameras, lighting, and all that jazz, so I might need to end up implementing FBX anyway. Thanks for the help, however. If you've got a link or a place to get started for me to figure out how to get FBX in here, that would be much appreciated.

Like I mentioned, FBX is tough to learn, and if you don't have much prior experience with this stuff, that'll make it even tougher. The first thing to do is to go to autodesk and download the latest SDK. The next thing to do is sharpen up your obfuscated C++ reading skills. The documentation is very poorly written. The SDK header documentation is terrible and gets in the way of reading the headers. The comments are like:

getValue // get value

The written documentation seems to be prepared based upon the header documentation. So what I did was only refer to the docs when I had enough time to weed through it. Otherwise the only good tip I can offer is to refer 90% to the sample code, and in particular, the ViewScene sample [which comes with the SDK and builds on OS X]. Do be forewarned that it is also buggy (probably because they don't know how to use their own SDK ), but at least it works and offers all the hints you'll need to get it rolling on your own on OS X.

Sorry, I didn't bookmark some of the other stuff I stumbled across specifically for FBX. I think there were a few posts at gamedev.net. Google is your friend -- sort of, with FBX, since I didn't find much out there...

demonpants Wrote:Also, about how many polys are you usually able to support?

I can't recall exactly, since I haven't been working with 3d on iPhone for several months. It obviously depends on how much framerate you're willing to accept. I seem to recall maybe 12k triangles, lit and depth tested at like 18 or 19 FPS in the best case scenario, using mostly one texture. Maybe 4 thousand should be like maybe 25 fps? I'm pulling these numbers out of my dusty trunk though... It seemed to me to be about the same as what you could get away with on a Rage128 on a 300 MHz Blue & White G3 from 1999.

johncmurphy Wrote:How do you get this to work? The interleaved array seems to be a problem on the iPhone OpenGL ES implementation.

I haven't actually used the .h output from Cheetah3D on iPhone to be honest, since like I said, I use .obj or FBX (converted to my own format). For glDrawElements, using VBOs, I do something like this on iPhone (I just ripped it out of one of my model renderers and modified it a bit to read here, so it's pseudo-code-ish):

Man, I am so unfamiliar with 3D. I decided to go with Jeff LaMarche's Wavefront loader, which seems okay, but a lot of weird stuff is happening with it, like the models are sometimes semi-transparent and a lot of other stuff like that. I'm downloading the latest version of his loader, hopefully that will fix a couple problems. Even so my only real capability here is to setup the 3D environment and then just call [model load:@"URL"] and [model draw].

All right, turns out that (in my opinion) Jeff LaMarche's OBJ loader is incredibly obfuscated and its UV texture mapping does not work. Awesome. I've spent the last two days trying to understand what is going on with it and I remain confused.

The main gap in my understanding is glDrawElements. It seems like there is a lot of mystical voodoo with this function - and naturally the OpenGL manual page on it just doesn't help. So,. here is my main question:

When you load up and OBJ, first you'll end up with a big list of vertices, then with a big list of texture coordinates, then a big list of vertex normals. That's all clear and easy. After you've got those 3 things loaded in, you'll begin loading groups and faces. The logic behind the groups is negligible at this point - let's just not worry about them. The faces themselves contain 3 values separated by a slash (/). These values point to the vertex index / texcoord index / normal index. You'll end up with (if you export all triangles, which you should) a triplet of 3 of these per face. This is because each one represents one of the vertices in a triangle face.

So, in the end, say you have a 2D triangle. Your OBJ might be something like:

v -1.0 1.0 0.0
v 1.0 1.0 0.0
v 0.5 0.5 0.0

vt 0.0 0.0
vt 1.0 1.0
vt 0.5 0.5

vn 0.0 1.0 0.0
vn 0.0 1.0 0.0
vn 0.0 1.0 0.0

f 1/1/1 2/2/2 3/3/3

Voila. Who knows what that would look like but you get the idea, and so do I. The Wavefront specs are very simple. So say we have that triangle, we load it in, we end up with all the data in 3 arrays - *vertices, *texcoords, and *normals. Basically they are just the values we already had from the file, so they will all have a length of 3 times how many elements each has (i.e. verices has length 9, texcoords has length 6, etc).

Now say we want to draw out these arrays, along with a texture that we've already bound. We want to make sure that the texture coordinates match the value loaded from the OBJ file, however, so we need to make sure those texture coordinates end up in there correctly.

To do all this, we're going to draw it with glDrawElements. In order to do that, we need to create another array, this one will contain the face mappings. So, let's call it *faces. Now this is where I'm confused. How exactly does this work? I've looked at plenty of examples and there seems to be a lot of weirdness that isn't clear. Before I call glDrawElements I set pointers with glVertexPointer, glTexCoordPointer, and glNormalPointer.

so we have

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, faces);

where *faces would have some sort of values like {0, 1, 2}, which is fine, but this is hardly as much information as we had in our OBJ file. Here I am only able to reference one specific array - the vertices - but I can't also specify where I'm getting the texture coordinates and the normals from. With the above example this {0, 1, 2} would probably work just fine.

But what if I had:

f 1/2/1 3/1/2 2/3/3

That obviously isn't going to match correctly. It doesn't seem like I am able to get the right information from a faces array of just {0,1,2} - in fact it would be totally off. Also consider if I have 100 faces and they don't have any sort of sequential referencing - like one might reference vertex 2 and another might references vertex 2 40 faces later... I obviously can't reorder by vertices to make it work, because then later faces would be messed up.

A quick response, I found a few other things on Google, and what I seemed to discover is that *vertices, *texcoords, and *normals needs to be parallel. That is...

If faces[5] is {8,3,5} and the matching value for this face that was in my Wavefront file was 2/8/5 8/5/4 3/5/6 then basically I need have vertices[8] be the 2nd v in the OBJ, texcoords[8] to be the 8th vt in the OBJ, and normals[8] to be the 5th vn in the OBJ, and so on.

Essentially this comes down to the fact that I will need to duplicate a lot of data in order to make everything parallel. Is this correct? If so, lame! Is that actually faster than simply moving all the glPointers to the proper array each time? i.e.

I don't know how correct that is but you can see the general idea. Basically I could store all those index values that I get from the OBJ file into some Face objects, then just use those values to change the positions of the pointers every time, then I'll always just need the next three values from those indices.

The downside there is that obviously I'll have a ton of extra GL calls, and I guess wasting a ton of memory creating a bunch of duplicates will be better than wasting processor constantly moving those pointers around... but it's much more of a pain.

He he, I've got a model loader working on my iphone. I preprocess an entire obj from Cheetah and create binary arrays useful with glDrawArrays. I put the array data into a sqlite file along with the parsed mtl data and images. Of course, you have to encode/decode any binary data but its working for me. There is no issue like a universal binary on a ppc vs intel mac with the endian stuff.

Hm, so how do you use glDrawArrays to do it? Mind just showing the little snippet that has the drawing portion? Or could you just explain what you did in terms of using duplicate coordinates? I was already thinking that I would write my own binary format because OBJ is so big and unwieldy, but to do that I need to load it in correctly the first time anyway. :-)

I got it all working using glDrawArrays. The secret? Stay away from wacky code when you're doing something that's not even that difficult. I ended up just rewriting the loader and it worked pretty swiftly. Oddly enough I had to flip the texture upside down - but otherwise it seems okay.