Wednesday, April 8, 2009

Wavin' in the Breeze

There are a lot of OpenGL tutorials around the intertubes, but as an iPhone developer, you may have found it frustrating that so many of those tutorials use direct mode or display lists, neither of which are supported on the version of OpenGL ES that we have on the iPhone. A while back, I started porting the various NeHe tutorials to the iPhone, but only made it up to Lesson 6. Well, we're going to skip ahead to #11 right now.

You can find the source code at Google Code for this project in the iPhone Bits repository. You can check out a copy of the project by typing the following command in a terminal window:
svn checkout http://iphonebits.googlecode.com/svn/trunk/src/PirateFlag PirateFlag

Note: If you have problems getting it to run, trying using Debug instead of Run. I'm trying to figure out what's causing that now.

Actually, this isn't exactly a port of NeHe #11, it's more of a re-implementation. This is a common tutorial that's been done many times in many places including on NeHe and at ZeusCMD. Similar tutorials are also featured in some books like OpenGL Game Programming.

There's a video of this code in action right here.

This isn't a real physics simulation. All we do is create a grid of vertices and move the z-coordinate along a sine wave. It does look a little like a flag blowing in the breeze, though, and it's certainly less computationally expensive than a real physics simulation would be, especially the way we're going to do it here. There are two basic approaches that fake-flag tutorials usually take. One is to calculate the location along the sine wave every time you go to draw. This is a more accurate method in that you can be very precise and take the exact amount of time elapsed since the previous frame was drawn into account. The second way is less accurate but uses far less processing power. The second approach is to pre-calculate the position for every spot on a grid and use those grids as vertices. Then each frame, you assign each vertex its neighbors value, taking the position of the last column of vertices and assigning it to the first column. That way, you get a continuous sine way but only have to calculate the positions once. This method is imprecise, but fine for many purposes (say, for a flag on a flagpole used as a background element in a game).

The grid of vertices that will represent our flag is just a two-dimensional array of Vertex3D structs. Remember, Vertex3D is just a structs with three GLfloat members representing the three cartesian coordinates of the point (x, y, z). Here's how we declare the grid:

Vertex3D        flagVertices[FLAG_X_POINTS][FLAG_Y_POINTS];

And here's how we pre-calculate the initial values:

    for (int x = 0; x < FLAG_X_POINTS; x++)
{
for (int y = 0; y < FLAG_Y_POINTS; y++)
{
flagVertices[x][y].x = (GLfloat)x;
flagVertices[x][y].y = (GLfloat)y;

GLfloat yPoints = (GLfloat)FLAG_Y_POINTS+5;
GLfloat sinVal = ((GLfloat)x*yPoints / 360.0) * 2.0 * M_PI;
flagVertices[x][y].z = (GLfloat)sin(sinVal);
}

}

FLAG_X_POINTS and FLAG_Y_POINTS are pre-compiler macros that define the height and width of our grid. The project is currently set to a 36x20 grid, though you can play with those values if you want.

The yPoints variable is a fudge to make the sine wave loop nicely. I plan to revisit it at some point and fix the algorithm so it's not necessary, but sometimes you can get a result faster by trial end error, and that's what happened here. I kept adding one to the value used as the divisor until I got an even loop. Inelegant, but functional. If anyone feels like challenging themselves, please feel free to fix the algorithm so the fudge is not necessary. I do not ever mind having my code corrected.

Because we're going to map a texture to our flag, we need to use smooth shading (GL_SMOOTH), and we're going to add some lights to make things look a little more realistic. Once we get lights in the mix in OpenGL, then we need to calculate normals, and since we're using GL_SMOOTH, we need to calculate vertex normals for every vertex we use. If you need a refresher on normals in OpenGL, I have two previous blog postings on the topic here, and here.

So, in addition to an array of vertices, we need an array of vectors to hold our normals. Because we're calculating vertex normals, we need one vector for every vertex, so we need another array of the same size (vectors and vertices are represented by exactly the same data structure - in fact, my Vector3D is actually just #defined to a Vertex3D. Here's the array for the normals:

Vector3D        flagVertexNormals[FLAG_X_POINTS][FLAG_Y_POINTS];

Calculating normals can be fairly costly - that's why OpenGL asks you to provide them rather than calculating them itself. So, we're going to cheat just like we did with the sine wave. We're going to simply shift the vertices over one. Because the relationship of each vertex to all of its neighbors stays the same, we don't have to recalculate the normals every frame, we can just calculate them once and keep re-using them by shifting them over the same way we will with the vertices.

Pre-calculating the vertex normals is not exactly straightforward however, because I've decided to use GL_TRIANGLE_STRIPs in this example, which should give better performance. So, each row of our vertex grid (except the last) is going to be used to build a triangle strip, our vertex strips are going to look like this:



Triangle strips are very efficient - notice that we don't have to specify vertices more than once because OpenGL knows that triangles in a triangle strip share some vertices. To create the same shape using triangles would take twenty-four vertices (eight triangles by three vertices per triangle), as opposed to the ten it's taken us here. That's very cool, and whenever possible, you should use triangle strips (or triangle fans - a similar approach we'll discuss in a later blog posting) instead of triangles. It reduces the amount of geometry you need to submit to OpenGL in order to define a shape.

But, meshes made up of triangle strips are a bit of a pain when it comes to normals. If you remember: a vertex normal is the average of the surface normals for all the polygons that a vertex is used in. How many polygons is each vertex used in? Six, usually:



Usually. The outside strips on all four sides are special cases, and so are the four corners. Two of the corner vertices are only used in one triangle (the blue dot in the illustration below); the other two are used in two (the green dot in the illustration below). The rest of the triangles that make up the borders of the grid are each used in three triangles (like the red dot in the illustration below):



So, in our little grid here, we've got vertices that are used in six triangles, three triangles, two triangles, and one triangle. Yuck. Okay, this is going to be a little gnarly. Let's handle the typical scenario first - the non-edge, non-corner vertices which are shared by six triangles. We can loop through the rows and columns and calculate the vertex normals by calculating surface normals for the six triangles they are part of, we just have to skip the first and last row and column:

for (int x = 1; x < FLAG_X_POINTS-1; x++)
{
for (int y = 1; y < FLAG_Y_POINTS-1; y++)
{
Vertex3D vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][y], flagVertices[x-1][y], flagVertices[x][y-1]));
Vertex3D vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][y], flagVertices[x][y-1], flagVertices[x+1][y-1]));
Vertex3D vertex3 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][y], flagVertices[x+1][y-1], flagVertices[x+1][y]));
Vertex3D vertex4 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][y], flagVertices[x+1][y], flagVertices[x+1][y+1]));
Vertex3D vertex5 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][y], flagVertices[x][y+1], flagVertices[x-1][y+1]));
Vertex3D vertex6 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][y], flagVertices[x-1][y+1], flagVertices[x-1][y]));

flagVertexNormals[x][y].x = (vertex1.x + vertex2.x + vertex3.x + vertex4.x + vertex5.x + vertex6.x) / 6.0;
flagVertexNormals[x][y].y = (vertex1.y + vertex2.y + vertex3.y + vertex4.y + vertex5.y + vertex6.y) / 6.0;
flagVertexNormals[x][y].z = (vertex1.z + vertex2.z + vertex3.z + vertex4.z + vertex5.z + vertex6.z) / 6.0;
Vector3DNormalize(&flagVertexNormals[x][y]);
}

}

Okay, deep breath now, we'll get through this. Next, let's handle the top and bottom strips. We can do them in the same loop, we'll just ignore the first vertex when doing the top strip and the last vertex when doing the bottom, because those are special cases:

for (int x = 0; x < FLAG_X_POINTS; x++)
{
// Calculate for top strip
if (x > 0)
{
Vertex3D vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][0], flagVertices[x-1][1], flagVertices[x-1][0]));
Vertex3D vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][0], flagVertices[x][1], flagVertices[x-1][1]));
Vertex3D vertex3 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][0], flagVertices[x+1][0], flagVertices[x][1]));
flagVertexNormals[x][0].x = (vertex1.x + vertex2.x + vertex3.x) / 3.0;
flagVertexNormals[x][0].y = (vertex1.y + vertex2.y + vertex3.y) / 3.0;
flagVertexNormals[x][0].z = (vertex1.z + vertex2.z + vertex3.z) / 3.0;
Vector3DNormalize(&flagVertexNormals[x][0]);
}


// Calculate for bottom strip
if (x < FLAG_X_POINTS)
{
Vertex3D vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][FLAG_Y_POINTS-1], flagVertices[x-1][FLAG_Y_POINTS-1], flagVertices[x][FLAG_Y_POINTS-2]));
Vertex3D vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][FLAG_Y_POINTS-1], flagVertices[x][FLAG_Y_POINTS-2], flagVertices[x+1][FLAG_Y_POINTS-2]));
Vertex3D vertex3 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[x][FLAG_Y_POINTS-1], flagVertices[x+1][FLAG_Y_POINTS-2], flagVertices[x+1][FLAG_Y_POINTS-1]));
flagVertexNormals[x][FLAG_Y_POINTS-1].x = (vertex1.x + vertex2.x + vertex3.x) / 3.0;
flagVertexNormals[x][FLAG_Y_POINTS-1].y = (vertex1.y + vertex2.y + vertex3.y) / 3.0;
flagVertexNormals[x][FLAG_Y_POINTS-1].z = (vertex1.z + vertex2.z + vertex3.z) / 3.0;
Vector3DNormalize(&flagVertexNormals[x][FLAG_Y_POINTS-1]);
}

}

We do a similar thing for the left and right borders:

for (int y = 0; y < FLAG_Y_POINTS; y++)
{
if (y > 0)
{
Vertex3D vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[0][y], flagVertices[0][y-1], flagVertices[1][y-1]));
Vertex3D vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[0][y], flagVertices[1][y-1], flagVertices[1][y]));
Vertex3D vertex3 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[0][y], flagVertices[1][y], flagVertices[0][y+1]));
flagVertexNormals[0][y].x = (vertex1.x + vertex2.x + vertex3.x) / 3.0;
flagVertexNormals[0][y].y = (vertex1.y + vertex2.y + vertex3.y) / 3.0;
flagVertexNormals[0][y].z = (vertex1.z + vertex2.z + vertex3.z) / 3.0;
Vector3DNormalize(&flagVertexNormals[0][y]);
}

if (y < FLAG_Y_POINTS)
{
Vertex3D vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[FLAG_X_POINTS-1][y], flagVertices[FLAG_X_POINTS-2][y], flagVertices[FLAG_X_POINTS-1][y-1]));
Vertex3D vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[FLAG_X_POINTS-1][y], flagVertices[FLAG_X_POINTS-2][y+1], flagVertices[FLAG_X_POINTS-2][y]));
Vertex3D vertex3 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[FLAG_X_POINTS-1][y], flagVertices[FLAG_X_POINTS-1][y+1], flagVertices[FLAG_X_POINTS-2][y+1]));
flagVertexNormals[FLAG_X_POINTS-1][y].x = (vertex1.x + vertex2.x + vertex3.x) / 3.0;
flagVertexNormals[FLAG_X_POINTS-1][y].y = (vertex1.y + vertex2.y + vertex3.y) / 3.0;
flagVertexNormals[FLAG_X_POINTS-1][y].z = (vertex1.z + vertex2.z + vertex3.z) / 3.0;
Vector3DNormalize(&flagVertexNormals[FLAG_X_POINTS-1][y]);
}

}

And, finally, handle the four corners:

Vertex3D vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[0][FLAG_Y_POINTS-1], flagVertices[0][FLAG_Y_POINTS-2], flagVertices[1][FLAG_Y_POINTS-2]));
Vertex3D vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[0][FLAG_Y_POINTS-1], flagVertices[1][FLAG_Y_POINTS-2], flagVertices[1][FLAG_Y_POINTS-1]));
flagVertexNormals[0][FLAG_Y_POINTS-1].x = (vertex1.x + vertex2.x) / 2.0;
flagVertexNormals[0][FLAG_Y_POINTS-1].y = (vertex1.y + vertex2.y) / 2.0;
flagVertexNormals[0][FLAG_Y_POINTS-1].z = (vertex1.z + vertex2.z) / 2.0;
Vector3DNormalize(&flagVertexNormals[0][FLAG_Y_POINTS-1]);

vertex1 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[FLAG_X_POINTS-1][0], flagVertices[FLAG_X_POINTS-2][1], flagVertices[FLAG_X_POINTS-2][0]));
vertex2 = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[FLAG_X_POINTS-1][0], flagVertices[FLAG_X_POINTS-1][1], flagVertices[FLAG_X_POINTS-2][1]));
flagVertexNormals[FLAG_X_POINTS-1][0].x = (vertex1.x + vertex2.x) / 2.0;
flagVertexNormals[FLAG_X_POINTS-1][0].y = (vertex1.y + vertex2.y) / 2.0;
flagVertexNormals[FLAG_X_POINTS-1][0].z = (vertex1.z + vertex2.z) / 2.0;
Vector3DNormalize(&flagVertexNormals[FLAG_X_POINTS-1][0]);

// Finally, top left and bottom right corners are part of one, so no averaging or normalzing needed
flagVertexNormals[0][0] = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[0][0], flagVertices[1][0], flagVertices[0][1]));
flagVertexNormals[FLAG_X_POINTS-1][FLAG_Y_POINTS-1] = Triangle3DCalculateSurfaceNormal(Triangle3DMake(flagVertices[FLAG_X_POINTS-1][FLAG_Y_POINTS-1], flagVertices[FLAG_X_POINTS-2][FLAG_Y_POINTS-1], flagVertices[FLAG_X_POINTS-1][FLAG_Y_POINTS-2]));


When it comes time to draw in a few moments, we're going to submit the geometry to OpenGL once per triangle strip, so we need to allocate some memory to hold our vertex array, normal array, and texture coordinate array. Here are the variables that represent those objects - they are instance variables of my controller class:

   Vertex3D        *vertices; 
GLfloat *texCoords;
Vector3D *normals;
GLuint stripVertexCount;

And here's how we calculate the memory needed for each of these for one triangle strip:

    stripVertexCount = ((FLAG_Y_POINTS - 1) * 2);
vertices = calloc(stripVertexCount, sizeof(Vertex3D));
texCoords = calloc(stripVertexCount, sizeof(GLfloat) * 4);
normals = calloc(stripVertexCount, sizeof(Vector3D));

The reason that we use one less than the number of columns is that every time through the loop, we create triangles between one column and the next one, but we skip the last one because it will automatically get created the previous time through the loop (since each column creates triangles between itself and the next column). I know that's confusing, but look up at the first diagram - notice that there are ten vertices, but eight triangles? Try the math on that. There are five Y positions or columns in that diagram. If you subtract one from five, you get four. If you double four, you get eight, which is the number of triangles in that strip. Make sense?

The last thing we need to do in setup is to load the texture that we'll be mapping onto the flag. In this case, I've used a class I wrote for an earlier project to load a PVRTC-compressed texture. The iPhone has hardware support for PVRTC compression, so it's the best option in most cases, though I've heard reports from some people that the quality loss due to PVRTC compression makes it not suitable for all uses. In this case, it seems to work fine, so I've used it, though I also provide the uncompressed png of both textures in the project if you want to make changes, or just see if it looks any different without the compression.

    OpenGLTexture3D *theTexture = [[OpenGLTexture3D alloc] initWithFilename:@"not_a_pirate.pvr4" width:512.0 height:512.0];
self.flagTexture = theTexture;
[theTexture release];

Time to draw. In our drawView: method, which gets called on a timer to do the animation, we first clear the buffer. I've chosen to use a sky-blue color. We also set use glLoadIdentity() to cancel out any transformation that may be in place from previous drawing:

    glClearColor(0.68, 0.84, 0.90, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

Next, I rotate the flag 90° so that it fits better on the screen. I could also have used autorotation to do this, but decided it was easier to just do a rotation transform. I also use a translate transform to move the flag back away from the camera so we can see it.

    glRotatef(-90.0, 0.0, 0.0, 1.0);
glTranslatef(-17.5, -11.0, -35.0);

We need to make sure that some state is enabled so that we can use textures, coordinate arrays, vertex arrays, and normal arrays. In this project, we could have turned these on once in setupView: and just left them on, but it's good habit to wrap your code by turning on and off what you need, that way your code is more portable - you can drop this into some other OpenGL program and not worry about what is enabled elsewhere.

    glEnableClientState(GL_VERTEX_ARRAY);
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_TEXTURE);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);

Next, I bind my texture to make sure that it uses the correct texture when it draws. Again, in this case, there is only one loaded texture, so this is unnecessary, but for portability sake, it's a good idea to bind your texture before you use it. I also fall back to using the default binding (of no texture) if my texture object happens to be nil:

    if (flagTexture != nil)
[flagTexture bind];
else
[OpenGLTexture3D useDefaultTexture];

It's time to submit figure out the triangle strips and submit them to OpenGL:

    int vertexCounter = 0;
int texCoordCounter = 0;
int normalCounter = 0;
for (int x = 0; x < FLAG_X_POINTS-1; x++)
{
for (int y = 0; y < FLAG_Y_POINTS-1; y++)
{
vertices[vertexCounter++] = flagVertices[x][y];
vertices[vertexCounter++] = flagVertices[x+1][y];
normals[normalCounter++] = flagVertexNormals[x][y];
normals[normalCounter++] = flagVertexNormals[x+1][y];

// Calculate the texture coordinates for the two triangles
texCoords[texCoordCounter++] = (GLfloat)x * 1.0 / (GLfloat)(FLAG_X_POINTS);
texCoords[texCoordCounter++] = 1 - ((GLfloat)y * 1.0 / (GLfloat)(FLAG_Y_POINTS));
texCoords[texCoordCounter++] = (GLfloat)(x+1) * 1.0 / (GLfloat)(FLAG_X_POINTS);
texCoords[texCoordCounter++] = 1 - ((GLfloat)(y) * 1.0 / (GLfloat)(FLAG_Y_POINTS));
}


glVertexPointer(3, GL_FLOAT, 0, vertices);
glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
glNormalPointer(GL_FLOAT, 0, normals);
glDrawArrays(GL_TRIANGLE_STRIP, 0, stripVertexCount);
vertexCounter = 0;
texCoordCounter = 0;
normalCounter = 0;
}

There's a lot going on there. We're looping through the grid, creating triangle strips for each vertical row. We copy the vertices and the vertex normals from our pre-calculated array into the vertex array that we're going to submit to OpenGL. We keep re-using that same piece of memory for submitting every triangle strip, we just copy different data each time. You should be aware that there are more efficient ways of submitting geometry. We could submit a single array with all the vertices used (no duplicates) and then submit indices to the ones that make up each triangle strip. That's an optimization I'll show in a future blog posting, but I thought this code would be confusing enough without it.

After populating the vertex and normal arrays, we calculate the texture coordinates. Remember: OpenGL texture coordinates are floating point values from 0.0 to 1.0 that represent where each vertex is in relation to the entire bitmap image being used as a texture. We use two texture coordinates per triangle (lower left and upper right), and we calculate these by multiplying the current row or texture by one over the number of rows or columns. Pretty easy, actually.
Note: I'm actually cheating here. The flag is a rectangle, but textures on the iPhone need to be square. Rather than adjust the texture coordinates to account for that difference, I just took a rectangle and resized it to a square in photoshop, knowing that the distortion would be offset when it got compressed back onto the flag. I'm not necessarily suggesting you should do it that way in your real projects.


Next, we disable the features we're using. We don't have to do this, but it's a good idea - there might be code somewhere else that needs to draw without a texture, for example, and it won't necessarily know to turn this off.

    glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_TEXTURE);
glDisableClientState(GL_NORMAL_ARRAY);
glDisable(GL_TEXTURE_2D);

And, finally, we move the values in the flag vertex and normal arrays over by one to make the flag wave.

    for (int y =0 ; y< FLAG_Y_POINTS - 1; y++)
{
GLfloat vertexWrap = flagVertices[FLAG_X_POINTS -1][y].z;
Vertex3D normalWrap = flagVertexNormals[FLAG_X_POINTS-1][y];
for (int x = FLAG_X_POINTS -1; x >= 0 - 1; x--)
{
flagVertices[x][y].z = flagVertices[x-1][y].z;
flagVertexNormals[x][y] = flagVertexNormals[x-1][y];
}

flagVertices[0][y].z = vertexWrap;
flagVertexNormals[0][y] = normalWrap;
}

Don't expect to grok it all just from reading this. Go grab the Xcode project and look at the code, run it, make changes to it, and just generally play with it until you're comfortable with what it's doing. And if you come up with better ways of doing something, let me know, I'll post your code changes if I agree that they are better.

OpenGL ES's lack of direct mode can be a bit of an impediment to learning OpenGL. On the other hand, direct mode is really inefficient, and the technique we use on the iPhone will work on other OpenGL platforms, so, in the long run, it's really not a bad platform to learn on.



17 comments:

Steve Hayman said...

Would love to try this but there seem to be some files missing when I do the svn checkout (no main.m, Info.plist, not_a_pirate.png and a few others)

Jeff LaMarche said...

Steve:

Really? Thanks for letting me know. I'll get it taken care of.

Jeff LaMarche said...

There, try now. Sorry about that.

jrock said...

seriously dude, your blog is the bees knees.

Jeff (composer) said...

Jeff: The calloc() calls should have the arguments flipped. It's supposed to be void *calloc(size_t count, size_t size). e.g., you should have:

vertices = calloc(stripVertexCount, sizeof(Vertex3D));

Also, to follow your own pattern, the normals calloc assignment should use sizeof(Vector3D) for the size argument.

I haven't looked over the code, but that jumped out at me. Cheers.

Jeff LaMarche said...

Jeff (Composer):

You are right, it does look like I flipped them, so I'll clean that up. I must admit that I hate calloc(). I like that it sets all the bytes to zero - makes it easier to debug certain problems in OpenGL - I just don't understand why they felt necessary to take two arguments instead of just taking one like malloc(). All calloc() does with the two arguments is multiply them together (so the flipped version works), but I will change it so that it is correct.

You're also right on the Vertex3D vs. Vector3D for the normals array. Won't make any difference in the actual working of the code (Vector3D is #defined to Vertex3D), but it's sloppy, so I'll change it.

It'll take me a few minutes to correct it and get the post fixed.
Thanks!

Jeff LaMarche said...

Fixed! Thanks again.

Jonathan said...

Hi Jeff,

This is fantastic. Your original Ne-He tutorial ports and OBJ loader got me off and running with OpenGL ES.

A couple of things I noticed:

Compressed (PVRTC) texture files might need to be square, but textures in general do not (something I only discovered recently). Dimensions need to be a power of two but 4x8 or 64x512 works. This is possible if the source is a JPEG or PNG.

Re: Optimization -- GlDrawArrays is still "immediate mode" in that you are transferring all of your geometry to VRAM each frame. Not a critical bottleneck on the simulator- but it's very important on the device.

Minimizing the number of arrays (and thus the number of API calls) is important - but it will only get you so far. Readers will likely discover (as I have) that this approach for displaying geometry breaks down very fast for high-polygon applications. RAM and video RAM on the iPhone may be physically shared, but they are distinct and separate.

Per your introduction, were you to use this effect to render a flag in a game, you would likely have to procedurally calculate the geometry for a set of frames (which the CPU can do in a blink), load all of these these into vRAM up front using a large interleaved vertex buffer object or two, then cycle through the frames to achieve the desired animated effect. This saves your memory bandwidth (and a good number of CPU cycles) for geometry that *must* be generated and loaded to VRAM on the fly. It would be an interesting exercise in optimization to see how many flags you can get displaying and animating simultaneously with each approach (for varying degrees of detail/poly counts).

Cheers.

Mark Johnson said...

Awesome Jeff. I've been using your Obj loader code. Beginners looking for 3D tools (me!) Cheetah 3D is on sale for $99 and it is fantastic. No more sitting in front of Blender wondering how to draw a triangle.

Libero Spagnolini said...

Hi Jeff, a quick question: I was wondering if you managed to get two side lighting working on the IPhone. Strangely it seems to work only in the simulator but not on the real device...keep up the good work! Cheers

Jeff LaMarche said...

Jonathan:

That's interesting about the textures. I could swear the documentation and WWDC presentation both said to always use square textures on the iPhone. I wonder if there's any performance hit for not doing so, or if somebody just applied the rules for PVRTC to everything, or if I'm just mis-remembering (possible)?

Second, glDrawArrays() is not "immediate mode". Immediate mode is when you glBegin() and glEnd. It is true that glDrawArrays() has additional overhead over, say VBOs, but that doesn't make it "immediate mode". Immediate mode is defined as "a simple way of sending data to the card piece by piece, i.e. sending vertices one by one." If you're submitting your geometry as a vertex array, it is by definition, not immediate mode, and couldn't be, because OpenGL ES specifically excludes the portions of OpenGL that are considered "immediate mode"

Doesn't mean it's the best or most efficient way to accomplish this (it's definitely not - it's an un-optimized port of an example used to each concepts), but it isn't immediate mode.

I'm curious how large the objects you're loading are. I've loaded objects with several thousand vertices (stored as static const arrays), run on the device and drawn using glDrawElements() and glDrawArrays() without noticeable slowdowns, so you must be talking some very complex models.

Daniel said...

Hi
Thisis a great article! I'm a beginner and like to use your code for understanding basic openGL priciples. I downloaded the code, but starting in the simulator just shows a white screen. If a end the app, I get a glimpse of the flag as the application shuts down. So I guess the flag is somehow "hidden" behind a view. I tested with iPhone OS 3.1.2. Is it possible that it's not proper working because of the newer OS version?

Daniel

Kallewoof said...

Daniel: comment out "window = [[UIWindow alloc] initWithFrame:rect];" from Pirate_FlagAppDelegate.m and the white window and/or crash at startup goes away.

Edwin said...

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

JeansPilot said...

JeansPilot offers the chance to buy a large variety of men’s and women’s jeans clothing from the world famous Italian Brands.
Online jeans clothing store looks for original fashion clothing sales and clearances of worldwide known designers. We participate in fashion auctions to get the lowest possible price for Top quality Clothes, Shoes and Accessories.
Buy Jeans

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

Archana said...

I would like to bend the texture so that it has a curvature. Can you tell me how to do this?