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.



24 comments:

Nico said...

The number of vertices increases polynomially with stack/slice count, not exponentially.

Jeff LaMarche said...

Nico:

Okay, yeah, sine the end-cap only increases by powers of 2, and the triangle strip increases as powers of 3, you're right, this is a polynomial increase.

I don't think I'll change the original posting - the people who care will probably read the comments anyway. "Polynomial increase" has a broader definition and doesn't necessarily imply a certain speed of increase in the vertex count. Most people understand that exponential growth is bad, since it means every increase at least doubles the vertex count, which is the important thing for people to realize - that there is a significant cost for every increase in resolution.

So, you're right, but I'm going to sacrifice accuracy for clarity here and hope it doesn't bother the purists too much :)

alexsaves said...

Hey I'm looking forward to your texture-coordinate array enhancement! I could use this. Trying to do an OpenGL planet.

George said...

Have you tried this on the device? The sphere looks more like a pancake.

Kim(Bob) said...

I found your blog a couple of days ago. Since I am looking for drawing a sphere using with glDrawArrrays or glDrawElements.
it looks work by your illustation. But what i
found from your code. The variable of nsign was not assigned any value. I think this
value will get 0 or 0.0. So the rest of your
location z will get only 0.0. Am i right ?

GLfloat nsign;
...

triangleFanVertices[0].z = nsign * radius;
...
z = nsign * cos(drho);

Sorry, I can't try with your code.

Jon said...

Thanks for this article. it appears that the sphere is flattened when running in the 3.0 sim but 2.2.1 is fine...

Reuven said...

I, too, don't get a sphere. I get a pancake. Any idea what's wrong?

tah70r said...

Very instructive. I ran it in the 2.2.1 Debug simulator. It looked fine (with the caveats stated in your article).
I ran it on an iPod Touch 2.2.1 and commented out the rotation(GLViewController.m, line 123). On the device it looks like a cone. There is a vertex at the closest point of the sphere in relation to the user viewing it.
Is this normal? I've seen postings elsewhere that state that the simulator does no justice to the actual device, so one must develop solely for the device when it comes to graphics.
Good blog btw.

Ryan Booker said...

@Kim(Bob)

Initialise nsign to 1.0f and it will work on SDK 3.x

marc said...

hi Jeff,

I have posted an article on alternative way of creating sphere procedurally using single triangle strip on my blog. Hopefully my explanation on how to create the sphere are useful to others.


cheers

Wahab said...

Hello,

For some reason, all I get is a white screen with nothing on it when I run the project in the simulator!

transmitterloc said...

To fix the white out screen problem delete the line

window = [[UIWindow alloc] initWithFrame:rect];

in applicationDidFinishLaunching

transmitterloc said...

PS - its because the window is allocated in the mainwindow.xib

Jonh5kult said...

@Ryan Booker

Thanks for the trick.

mario said...

Jeff,

The sample project works on 2.0 and 2.2.1 OS but doesn't work on 3.0 and above.

rogerboesch said...

One of the best OpenGL Blog for the iPhone in web. Thanks a lot for all that. Can you explain the math to integrate texturing?

Chris said...

I have successfully used gluSphere from Mike Gorchak's GLU ES port ( http://code.google.com/p/glues/ ). Well actually I got the glues source files from Javier Baez's PanoramaGL project ( http://www.codeproject.com/KB/openGL/panoramagl.aspx ). These are slightly different versions of the same files.

LukeSkyWalker said...

As indicated in the comments Chris. I use the library GLU ES (http://code.google.com/p/glues/) in my project called PanoramaGL. With this library you can use the function gluSphere among others.
Within PanoramaGL code, is a folder where the library GLU ES for iPhone (code.google.com/p/panoramagl/).

Good luck

Javier

John said...

buy viagra
viagra online
generic viagra

Marco Josue said...

thanks for all!!
retirement homes in costa rica

Bill Kidder said...

Init nsign = 1.0 fixes the pancake problem.

Edwin said...

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

Jo Codegirl said...

Why are the end-caps necessary? It seems like the code only draws one end-cap anyway.

Jeff LaMarche said...

Jo Codegirl:

Good eye! They're not. I published a later version that removed the end caps, which drops it to a single draw call.