Tuesday, May 5, 2009

Procedural Spheres in OpenGL ES

OpenGL ES does not have GLU, which is a utility library available in regular OpenGL. Among the many things that GLU provides is a bunch of methods for rendering primitive shapes like cylinders and spheres, which can be really handy. The math behind calculating these shapes procedurally can be a little mind-bending, especially if you're new to the whole 3D thing.

I'm currently working on Part 5 of the OpenGL ES from the Ground Up series, which is on materials. I wasn't happy with using the icosahedron that I'd been using for the first four installments of the series. The interplay between lights and materials on the icosahedron just doesn't show the specular component in a way that's obvious enough. The ideal shape for showing a specular hightlight is, of course, a sphere.

So, I set out to render a sphere in OpenGL ES, which turned out to be nowhere near as straightforward as I thought it might be. Right now, the code is a little rough around the edges, but it works, and allows you to specify a "resolution" in terms of the number of "slices" and "stacks" that make up the sphere. These basically just define how many vertices there are latitudinally and longitudinally. Think of an orange. If you slice it with a knife vertically, those are "slices". If we cut it horizontally, those are "stacks". Well, not exactly, unless your knife curves at just the right angle, but it's good enough analogy.

As you can see from this illustration, the more slices and stacks you have, the smoother your sphere looks. But, the more slices and stacks you have, the more processing power your sphere uses. The number of vertices increases exponentially as you increase the stack/slice count.

threespheres.jpg

Three spheres created with this code, one with 8 slices and stacks (left), one with 25 slices and stacks (middle) and one with 50 slices and stacks (right).


Right now, this code comes in the shape of a rather scary-looking (but well-commented and quite attractive looking) C-function that takes four handles, two pointers, two unsigned integers, and a float.

                                                            // =========================================================
void getSolidSphere(Vertex3D **triangleStripVertexHandle, // Will hold vertices to be drawn as a triangle strip.
// Calling code responsible for freeing if not NULL
Vector3D **triangleStripNormalHandle, // Will hold normals for vertices to be drawn as triangle
// strip. Calling code is responsible for freeing if
// not NULL
GLuint *triangleStripVertexCount, // On return, will hold the number of vertices contained in
// triangleStripVertices
// =========================================================
Vertex3D **triangleFanVertexHandle, // Will hold vertices to be drawn as a triangle fan. Calling
// code responsible for freeing if not NULL
Vector3D **triangleFanNormalHandle, // Will hold normals for vertices to be drawn as triangle
// strip. Calling code is responsible for freeing if
// not NULL
GLuint *triangleFanVertexCount, // On return, will hold the number of vertices contained in
// the triangleFanVertices
// =========================================================
GLfloat radius, // The radius of the circle to be drawn
GLuint slices, // The number of slices, determines vertical "resolution"
GLuint stacks // the number of stacks, determines horizontal "resolution"
// =========================================================
)


My plan is to wrap this all up in a nice Objective-C class at some point in the not-too-distance future so that it will be easier to use, but I probably won't get to that right away. So, since I figure there must be some other people out there who need spheres or are curious how they might be done, I'm posting a sample project with my rough sphere code that you can look at to see how it works.
Note: I've been advised by somebody much smarter on OpenGL than I am that per-vertex specular highlights "look like total ass" unless you have a super-dense mesh, which you can see in the left-most screenshot above. In general, this code is probably not well-suited for use in a game or application where real-time performance is crucial if you're using strong specular lighting. But it will work quite nicely for showing the effects of lighting and material settings in OpenGL ES, especially when run in the simulator where we have processing power to spare
The sphere is built out of a triangle strip and a triangle fan, so one handle you pass in gets populated by the method with the vertex data for the triangle strip, another with the vertex data for the triangle fan.

Since these two sets of vertices have to be submitted to OpenGL separately, it made sense not to combine them into one array. In order to work with lights and smooth shading, the function also calculates the normals for both of these arrays - which accounts for the other two handles.

The two pointers are to variables that, on return, will identify the number of vertices in the triangle strip vertex array and the triangle fan vertex array.

Finally, there's a GLfloat that's used to specify the size of the sphere, and two GLuints to specify the number of stacks and slices. You'll probably almost always want to use the same number of stacks and slices, but I kept them separate just in case.

To use this function, you need to declare some variables. In the sample projects, they are instance variables of the view controller:

    Vertex3D    *sphereTriangleStripVertices;
Vector3D *sphereTriangleStripNormals;
GLuint sphereTriangleStripVertexCount;

Vertex3D *sphereTriangleFanVertices;
Vector3D *sphereTriangleFanNormals;
GLuint sphereTriangleFanVertexCount;


These values get passed in like so:

    getSolidSphere(&sphereTriangleStripVertices, 
&sphereTriangleStripNormals,
&sphereTriangleStripVertexCount,
&sphereTriangleFanVertices,
&sphereTriangleFanNormals,
&sphereTriangleFanVertexCount,
1.0,
50,
50)
;


You do not have to allocate memory for the vertices or normals. BUT you do have to free the memory when you're done, like so:

    if(sphereTriangleStripVertices)
free(sphereTriangleStripVertices);
if (sphereTriangleStripNormals)
free(sphereTriangleStripNormals);

if (sphereTriangleFanVertices)
free(sphereTriangleFanVertices);
if (sphereTriangleFanNormals)
free(sphereTriangleFanNormals);

Finally, here's how you draw the sphere using the values returned from getSolidSphere():

    glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);

glVertexPointer(3, GL_FLOAT, 0, sphereTriangleFanVertices);
glNormalPointer(GL_FLOAT, 0, sphereTriangleFanNormals);
glDrawArrays(GL_TRIANGLE_FAN, 0, sphereTriangleFanVertexCount);

glVertexPointer(3, GL_FLOAT, 0, sphereTriangleStripVertices);
glNormalPointer(GL_FLOAT, 0, sphereTriangleStripNormals);
glDrawArrays(GL_TRIANGLE_STRIP, 0, sphereTriangleStripVertexCount);

glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);

That's all there is to it. I'll be using this code in the next OpenGL blog posting so we can talk about lighting and materials, though I probably won't get to that until after the weekend. I'm going away for a long weekend and my wife's not letting me bring my computer.

Let me warn again that this code is probably not a good solution for anything that needs to run fast in realtime on the phone with strong specular lighting. I'll probably add the ability to populate a texture coordinate array to this function at some point, which will make it more suitable to realtime programs. As always, use at your own risk, there are no warranties, yada yada. You know the drill.

No comments:

Post a Comment