Sunday, June 7, 2009

OpenGL ES from the Ground Up Part 8: Interleaving Vertex Data

Technote 2230 makes many suggestions for improving the performance of your iPhone apps that use OpenGL ES. You're now far enough along in your understanding of OpenGL ES that you should read it. No, really. Go read it, I'll wait.

Okay, done? Under the section titled Optimizing Vertex Data, there's a somewhat cryptic recommendation to "submit strip-ordered indexed triangles with per vertex data interleaved". When Apple makes a recommendation, they usually have a good reason for it, so let's look at how we comply with this one.

First of all, let's look at what it means. Let's break it down:

Strip Ordered: In other words, if your model has adjacent triangles, submit them as triangle strips rather than submitting each triangle individually. We've talked about using triangle-strips in earlier installments, so you already know a little about doing that. It's not always possible to use triangle strips, but for a good many objects you will be able to, and whenever you can, you should because using triangle strips greatly decreases the amount of vertex data you have to push into OpenGL ES every frame.

indexed: This is also nothing new. We've been using vertex indices for a while now. Our spinning icosahedron uses them to create twenty faces with only twelve vertices. glDrawElements() draws based an indices rather than vertices.

Heck, we're doing great so far, aren't we? So far, we seem to be doing all the right things! Let's look at the last part of the recommendation, however:

with per vertex data interleaved: Okay, hmm.. What the hell does that mean?

Okay, time to test out your memory. Do you remember in several of the past installments when we time we talked about functions like glVertexPointer(), glNormalPointer(), glColorPointer(), or glTexCoordPointer ? In earlier installments, I told you not to worry about the parameter called stride and to just set it to 0.

Well, now you can start worrying about stride, because that's the key to interleaving your per vertex data.

Per Vertex Data

So, you might be wondering what "per vertex data" is and how you would interleave it.

You remember, of course, that in OpenGL ES we always pass geometry in using a vertex array, which is an array containing sets of three GLfloats that define the points that make up our objects. Along with that, we also sometimes specify other data. For example, if we use lighting and need vertex normals, we have to specify one normal per vertex in our normal array. if we use texture coordinates, we have to make sure that our texture coordinate array has one set of texture coordinates per vertex. And if we use a color array, we have to specify one color per vertex. Do you notice how I keep saying "per vertex"? Well, these types of data are what Apple is referring to when they say "per vertex data" in that Technote. It's anything that you pass as an array into OpenGL ES that supplies any kind of data that applies to the vertices in the vertex array.

Interleaving

Up until now in this series, we've created one array to hold the vertex data, and additional separate arrays to hold the normal data, color data, and/or texture coordinate data, like so:

separatearrays.png


What we're going learn how to do today is to smush all this data together into a single contiguous chunk of memory:

interleaved.png


Don't worry if you can't read the code in that illustration. When it becomes important, I'll give you the code listing again, that's just to illustrate the point that we're going to have all of our vertex data in a single glob of memory. What that's going to do is put all the data describing a single vertex together in one place in memory. That will allow OpenGL faster access to the information about each vertex. In today's installment, we're going to interleave vertices, normals, and color data, though the same exact technique would work for texture coordinates, or for just interleaving vertices and normals. In fact, in the accompanying Xcode Project, there are data structures defined to handle all three of those interleaving scenarios.

Defining a Vertex Node

In order for this to work, we need a new data structure. In order to interleave vertices, normals, and color data, we need a structure that looks like this:

typedef struct {
Vertex3D vertex;
Vector3D normal;
Color3D color;
}
ColoredVertexData3D;


Pretty straightforward, huh? You just create a struct with each piece of per-vertex data that we're using.

Next, of course, we need to populate our vertex data, so we need to combine those three static const arrays into a single one. Here's what the same icosahedron data looks like specified using a static array of our new datatype:

static const ColoredVertexData3D vertexData[] = {
{
{0, -0.525731, 0.850651}, // Vertex |
{0.000000, -0.417775, 0.675974}, // Normal | Vertex 0
{1.0, 0.0, 0.0, 1.0} // Color |
}
,
{
{0.850651, 0, 0.525731}, // Vertex |
{0.675973, 0.000000, 0.417775}, // Normal | Vertex 1
{1.0, 0.5, 0.0, 1.0} // Color |
}
,
{
{0.850651, 0, -0.525731}, // Vertex |
{0.675973, -0.000000, -0.417775}, // Normal | Vertex 2
{1.0, 1.0, 0.0, 1.0} // Color |
}
,
{
{-0.850651, 0, -0.525731}, // Vertex |
{-0.675973, 0.000000, -0.417775}, // Normal | Vertex 3
{0.5, 1.0, 0.0, 1.0} // Color |
}
,
{
{-0.850651, 0, 0.525731}, // Vertex |
{-0.675973, -0.000000, 0.417775}, // Normal | Vertex 4
{0.0, 1.0, 0.0, 1.0} // Color |
}
,
{
{-0.525731, 0.850651, 0}, // Vertex |
{-0.417775, 0.675974, 0.000000}, // Normal | Vertex 5
{0.0, 1.0, 0.5, 1.0} // Color |
}
,
{
{0.525731, 0.850651, 0}, // Vertex |
{0.417775, 0.675973, -0.000000}, // Normal | Vertex 6
{0.0, 1.0, 1.0, 1.0} // Color |
}
,
{
{0.525731, -0.850651, 0}, // Vertex |
{0.417775, -0.675974, 0.000000}, // Normal | Vertex 7
{0.0, 0.5, 1.0, 1.0} // Color |
}
,
{
{-0.525731, -0.850651, 0}, // Vertex |
{-0.417775, -0.675974, 0.000000}, // Normal | Vertex 8
{0.0, 0.0, 1.0, 1.0}, // Color |
}
,
{
{0, -0.525731, -0.850651}, // Vertex |
{0.000000, -0.417775, -0.675973}, // Normal | Vertex 9
{0.5, 0.0, 1.0, 1.0} // Color |
}
,
{
{0, 0.525731, -0.850651}, // Vertex |
{0.000000, 0.417775, -0.675974}, // Normal | Vertex 10
{1.0, 0.0, 1.0, 1.0} // Color |
}
,
{
{0, 0.525731, 0.850651}, // Vertex |
{0.000000, 0.417775, 0.675973}, // Normal | Vertex 11
{1.0, 0.0, 0.5, 1.0} // Color |
}

}
;


Here is how we pass the information into OpenGL. Instead of passing in the pointer to the appropriate array, we pass the address of the appropriate member of the first vertex in the array, and provide the size of that struct as the stride argument.

    glVertexPointer(3, GL_FLOAT, sizeof(ColoredVertexData3D), &vertexData[0].vertex);
glColorPointer(4, GL_FLOAT, sizeof(ColoredVertexData3D), &vertexData[0].color);
glNormalPointer(GL_FLOAT, sizeof(ColoredVertexData3D), &vertexData[0].normal);


The the last parameter in each of those calls a points to the data corresponding to the first vertex. So, for example, &vertexData[0].color points to the color information for the first vertex. The stride parameter identifies how many bytes of data need to be skipped before the same type of data for the next vertex can be found. That might make a little more sense if you look at this diagram (sorry, it's wide, you may have to expand your browser to see all of this one:

stridediagram.png


What could be easier, right? If you don't feel like typing it all in, you can download the interleaved version of the spinning icosahedron. I've also updated my OpenGL ES Xcode Template with these new data structures<.

We're still not using triangle strips, but merging triangles into triangle strips is going to have to be a subject for a future installment, because it's time to go meet some people at WWDC.

No comments:

Post a Comment