Friday, December 12, 2008

NeHe Tutorials, Lesson 5 Ported to iPhone

Into the third dimension!



I've now ported NeHe OpenGL Lesson 5. This one's not exactly straightforward - my description of the difference will probably be longer than the original lesson 5. That's partially because of functionality that's not available on the iPhone, and partially because the iPhone is relatively resource-constrained device, so I implemented some optimizations that weren't strictly necessary, but which you should really use on the iPhone.

Here is the link for the ported Lesson 5 project.

Where do we even begin this time?

Defining & Drawing the Pyramid



First, let's talk about a basic fact of life. When we've been defining our vertices, we've been doing it using floating point numbers stored in GLfloat variables. Now, generally speaking, floating point numbers are slower than integers. Of course, with GPUs and FPUs it's not always that straightforward, but it's generally a good rule of thumb when programming, including programming for the iPhone.

We don't want to sacrifice precision, however. We want to define our vertices with floating point numbers, and then refer to them using integers. Take a look at this spiffy picture I drew (I know, can you believe I don't draw for a living?):

These are the vertices for the pyramid shape. Notice that we have a total of five vertices that make up this three-dimensional shape. However, we can't just feed those five vertices into OpenGL and expect it to know what to do with them. It doesn't work that way. We have to be a little more specific than that. We have to identify each of the faces or polygons that make up the shape.

For the pyramid, we're not going to draw the bottom, so let's not worry about that, but besides the bottom, a pyramid is made up of four triangular shapes, and we need to draw all four of them. So that's four triangles with three vertices each, for a total of twelve vertices that we need to feed to OpenGL to draw this pyramid. But, look at the illustration again: We only have five vertices total in the shape. That means, we're going to feed the same vertices into OpenGL multiple times, because the polygons or faces share vertices.

We could create a vertex array with all twelve points, and that would work fine. But it's inefficient, because we're making OpenGL toss around the same five vertices (made up of three floating point values each) multiple times. OpenGL is not going to look at the values and determine if they're the same - it's just going to use them to draw, and in the process, you're going to make it shuttle around and do calculations on a lot more floating point values than it needs to.

Instead of doing that, we're going to define a vertex array, just like before, and it's only going to have five values in it - one for each unique point in space that make up the polygon.
static const GLfloat pyramidVertices[] = { 
0.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, -1.0f
};
Then, we're going to feed that vertex array to OpenGL, again just like we did before. But, we're not going to use glDrawArrays() to tell it to draw the vertex array, as we've done before. Instead, we're going to define another array, but this one is not going to contain vertices, it's going to contain integer values holding index values that point into the vertex array that contains the five vertices. So, to refer to the top of the pyramid, instead of referring to it as {0.0f, 1.0f, 0.0f}, we are going to refer to it by that vertex's index within the vertex array.

This is a little confusing, however, because we're not going to refer to them using the index in the actual array, but rather we're going to pretend that each vertex in the vertex array is a distinct object, with its own index value, like so:
static const GLfloat pyramidVertices[] = { 
0.0f, 1.0f, 0.0f, // These three values are one vertex with index 0
-1.0f, -1.0f, 1.0f, // These three values are one vertex with index 1
1.0f, -1.0f, 1.0f, // These three values are one vertex with index 2
1.0f, -1.0f, -1.0f, // These three values are one vertex with index 3
-1.0f, -1.0f, -1.0f // These three values are one vertex with index 4
};
Or, to use our fancy illustration again, this is how we're going to think of these indices:

So, we'll create another array that actually defines each face of the pyramid (remember, we're not drawing the bottom). That array, can be made up of any of the OpenGL integer data types. I've chosen GLubyte because it's the smallest and I have less than 256 vertices in my array, so it will work just fine. Here is what that array looks like
static const GLubyte pyramidVertexFaces[] = { 
0, 1, 2, // Defines Front Face
0, 3, 2, // Defines Right Face
0, 3, 4, // Defines Back Face
0, 1, 4 // Defines Left Face
};
Pretty simple, right? You can see that we still have values repeated, but at least they're nice, small one-byte integer values that can be shuffled around quickly. Doing this also allows OpenGL to cache calculations that correspond to a specific vertex so that they don't have to be done multiple times.

For the pyramid, we're still using that color array to give us that fancy blended gradation, but we're going to optimize it. Colors can be defined using floating point value between 0.0 and 1.0. They can also be defined as an integer between 0 and 255. Because all of our colors can adequately be defined using one byte per color channel, this will be a tiny bit faster in OpenGL, and tiny bits faster can add up when you're programming on a mobile device like the iPhone. Here's the new version of the color array.
static const GLubyte triVertexColors[] = { 
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
0, 255, 0, 255,
0, 0, 255, 255
};
Why do we have more values this time, you might wonder? Well, we it's because we define a color for each element in the vertex array. When we refer to the vertex by its index value, OpenGL goes and grabs the matching color from this array, There are a total of five vertices, and we're assigning a color to each one. You want to the same number of colors in your color array that you have vertices in your vertex array.

Oh, here's another thing - in OpenGL, you can define colors with three values in a color array. Not, however, in OpenGL ES. You must use four values to define a color - you must provide the alpha value, even if it's always 1.0 or 255.

We also define a constant that identifies the number of vertices we're going to draw - this is the number of polygons or faces multiplied by the number of vertices in each polygon.

static const GLubyte triNumberOfIndices = 12
Now that we've got our vertex array, color array, and faces defined, we can draw. We're not going to use glDrawArrays(), we're going to use a new function called glDrawElements(). We will call this function once per polygon, which means we're not just feeding an array in and letting OpenGL do the rest, we're going to loop through our polygons and feed them, one at a time, to glDrawElements(). It's not as hard as it sounds, however. Here's how we do it:
 glVertexPointer(3, GL_FLOAT, 0, pyramidVertices); 
glColorPointer(4, GL_UNSIGNED_BYTE, 0, triVertexColors);
for(int i = 0; i < triNumberOfIndices; i += 3)
{
glDrawElements(GL_TRIANGLE_FAN, 3, GL_UNSIGNED_BYTE, &pyramidVertexFaces[i]);
}
Notice that we've changed our call to glColorPointer() to use GL_UNSIGNED_BYTE because we changed our color array to use GLubyte. Then we loop through our array of polygons, incrementing by three so that we only hit each triangle once, and we feed that triangle into the glDrawElements() call.

It's a little confusing (and it's not that easy to explain either), but it's really rather straight forward once you've wrapped your head around the basic idea. The glDrawElements() function knows that the values you're feeding are indices that point into the vertex array you provided to it earlier, and it draws a polygon based on the vertex indices that you feed it.

Our pyramid is made up of triangles (except the bottom, which we didn't draw), but what about the cube?

Defining and Drawing the Cube


Well, OpenGL ES doesn't support GL_QUAD. You can't draw squares in OpenGL ES. You can draw points, lines, line loops, triangles, and triangle fans, but not quads. So, how do we draw the cube, which is made up of six quadrangles?

In theory, it's easy enough. We just subdivide each square into two triangles. Draw a line from one corner to the opposite corner of any rectangle, and you've got two triangles. In fact, any polygon can be subdivided into triangles. So, instead of drawing six rectangles, we just have to draw twelve triangles. But, the theory is exactly the same as with the pyramid. We define all the vertices that make up the cube (eight of them) and then create a vertex array out of them. We then define, using those special, the twelve triangles that make up the cube. Here's what those data structures look like:
 static const GLfloat cubeVertices[] = { 
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f,-1.0f, 1.0f,
-1.0f,-1.0f, 1.0f,
-1.0f, 1.0f,-1.0f,
1.0f, 1.0f,-1.0f,
1.0f,-1.0f,-1.0f,
-1.0f,-1.0f,-1.0f
};
static const GLubyte cubeNumberOfIndices = 36;

const GLubyte cubeVertexFaces[] = {
0, 1, 5, // Half of top face
0, 5, 4, // Other half of top face

4, 6, 5, // Half of front face
4, 6, 7, // Other half of front face

0, 1, 2, // Half of back face
0, 3, 2, // Other half of back face

1, 2, 5, // Half of right face
2, 5, 6, // Other half of right face

0, 3, 4, // Half of left face
7, 4, 3, // Other half of left face

3, 6, 2, // Half of bottom face
6, 7, 3, // Other half of bottom face

};
Feeding them to OpenGL is exactly the same. One difference is that we're not creating gradations of color this time, but rather are drawing each side of the cube in a different color, so we define a color array, but we don't feed it to OpenGL as a color array, we use it in our loop to manually set the color. Here's the color array:
 const GLubyte cubeFaceColors[] = { 
0, 255, 0, 255,
255, 125, 0, 255,
255, 0, 0, 255,
255, 255, 0, 255,
0, 0, 255, 255,
255, 0, 255, 255
};
The only gotcha here is that we have six colors, for six sides, but we're drawing twelve triangles, so we have to remember to only increment the color index every other triangle. Here's the draw loop for the cube:
 glVertexPointer(3, GL_FLOAT, 0, cubeVertices);
int colorIndex = 0;
for(int i = 0; i < cubeNumberOfIndices; i += 3)
{
glColor4ub(cubeFaceColors[colorIndex], cubeFaceColors[colorIndex+1], cubeFaceColors[colorIndex+2], cubeFaceColors[colorIndex+3]);
int face = (i / 3.0);
if (face%2 != 0.0)
colorIndex+=4;

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, &cubeVertexFaces[i]);
}

Okay, I know this was a heavy posting. This posting is probably twice as long as the original NeHe lesson. That's the price to be paid for 3D on a mobile device like the iPhone. You can't rely on OpenGL ES to do for you all the things that OpenGL proper does for you. But, once you start to get the hang of it, it's really not that bad to work with.

I think I'm going to go off script after this posting. I'm not sure there's much value in going lesson-by-lesson through the next batch of NeHe tutorials... not until some of the more advanced topics. I do want to cover a couple of things, however - normals, and texturing, for example. So I think I'll take a break from NeHe for the next couple of postings and talk about what normals are and how they help in 3D drawing, and also show how to map textures to polygons. Then, maybe, I'll show how to load a 3D object created in a program like Maya or Blender, at least, once I've figured out how to do it myself.

No comments:

Post a Comment