Thursday, July 12, 2012

How to set up OpenGL on iOS


OpenGL ES is a scaled-down version of the OpenGL API for 2D and 3D graphics programming on mobile devices. iOS supports version 1.1 and 2.0 of the API. Version 1.0 is more simple, version 2.0 is more powerful and flexible. For this particular example I'll be using version 2.0 to create a bare bones OpenGL app that does nothing but clear the screen with a particular color. There's a lot to it, and OpenGL does have a bit of a learning curve, but I think in the long run it's a rewarding thing to learn.

A complete Xcode project for this post can be found on github.

OK, let's go.

On iOS all OpenGL content is rendered to a special Core Animation layer called CAEAGLLayer. Our basic application will create a UIView subclass called GLView which will wrap a CAEAGLLayer. We do this by overriding UIView's layerClass method to specify that our view is backed by a CAEAGLLayer:

+ (Class)layerClass
{
    return [CAEAGLLayer class];
}

The CAEAGLLayer instance is managed for us by our parent. We can retrieve it via the layer property:

CAEAGLLayer *glLayer;
glLayer = (CAEAGLLayer *)self.layer;

Once we have a reference to our CAEAGLLayer we can configure it. By default the layer is transparent. We have to change that. If the layer is not opaque performance will suffer:

glLayer.opaque = YES;

Now we need a context. In OpenGL the context is used to store current state. The class we use for this on iOS is EAGLContext. When we initialize the context we tell it which API version we wish to use:

EAGLContext *glContext;
glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

We then make our context the current context so OpenGL will use it:

[EAGLContext setCurrentContext:glContext]

Next we need to ask OpenGL to create a renderbuffer for us. A renderbuffer is a chunk of memory where the rendered image for the current frame will be stored. To create one, we use the glGenRenderbuffers command:

GLuint renderbuffer;
glGenRenderbuffers(1, &renderbuffer);

Notice we passed in the address of a GLuint variable. This variable holds an identifier that we can use to refer to this particular renderbuffer.

Once we have a renderbuffer, we bind it to the GL_RENDERBUFFER target. All this means is that when we execute commands that involve the bound renderbuffer in some way, this particular renderbuffer will be used:

glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);

Now we need to allocate storage for the renderbuffer:

[glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:glLayer];

Notice how we didn't explicitly specify which renderbuffer to allocate storage for. Instead, we specified the GL_RENDERBUFFER target. This is a good example of how the OpenGL API is "stateful". OpenGL creates and manages a bunch of internal objects that we don't directly control. Instead, we use OpenGL commands to build up the current state, and then use other OpenGL commands to manipulate the current state. If we want to manipulate some other state, say if we wanted to work in another context or use a different renderbuffer, we would have to tell OpenGL to use this other state before executing commands that would manipulate it.

This is an important concept in OpenGL. When working with the API, we have to make sure that we're using the correct state. If I was managing multiple renderbuffers I would have to make sure I told OpenGL which one was bound to the GL_RENDERBUFFER target before executing commands that manipulate the currently bound renderbuffer. This simple example only has a single renderbuffer and a single context, but this is a fundamental aspect of OpenGL and important to keep in mind.

Now that we have our renderbuffer, we need a framebuffer. The framebuffer is another chunk of memory that is used when rendering the current frame:

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);

Now we bind the framebuffer to the GL_FRAMEBUFFER target so that framebuffer-related commands act up on it:

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

Then we attach the renderbuffer to the framebuffer:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);

Notice we specified the GL_COLOR_ATTACHMENT0 slot. This is the framebuffer's attachment point for a renderbuffer. Sometimes renderbuffers are called "color buffers" or "color renderbuffers" because they're basically just a color image.

Now OpenGL is initialized and ready to use. Let's fill our renderbuffer with a solid color and push it to the screen.

First, we set the clear color:

glClearColor(150.0/255.0, 200.0/255.0, 255.0/255.0, 1.0);

When setting a color we specify values for four channels: red, green, blue and alpha (transparency). Each can have a value between 0 and 1. Normally we're using a color mode that has 8 bits per channel, so there are 256 distinct values any channel can have. Specifying a value between 0 and 255 is easy, and a lot of tools for woking with color support this. To change these "human readable" values into the 0 to 1 range OpenGL expects, we simply divide.

Now that we've specified the clear color we can fill the currently bound renderbuffer with it:

glClear(GL_COLOR_BUFFER_BIT);

Finally, we present the contents of the renderbuffer to the screen:

[glContext presentRenderbuffer:GL_RENDERBUFFER];

Take a look at the Xcode project on github to see the full GLView class. Take a look at the AppDelegate code to see how we attach GLView as a subview of the window. Run it, and you should see a blue screen. :-)

That's it for now. Comments and questions welcome. Thanks!

No comments:

Post a Comment