Any graphics library is a little hard to handle when first getting started -there are so many different concepts to get a grasp on. This tutorial is here to start you down the path of using OpenGL ES for the iPhone. Creating OpenGL based applications in games involve a little bit more ground work to get things rolling, but on the plus side we can remove Interface Builder from the equation entirely. If you haven't built anything for the iPhone before I recommend you check out our beginner's tutorial for building iPhone applications - it will give you good starting point.
The application being built here is going to just get our feet wet. We are going to start out by drawing a triangle and then moving it around a little bit. Nothing too fancy, there is plenty of information to cover to get this working and lots of new concepts to take in. You can check out a quick video of the application below.
The first step in this grand adventure is creating the project, go ahead and open XCode, go to File > New Project, choose "OpenGL ES Application", and press the "Choose" button in the bottom right (referenced below). Pick a location for your project and give it a name.
Before we actually start writing code we are going to do a little bit of clean up. We are going to get rid of the main window nib file because we don't really want to use Interface Builder. This file is MainWindow.xib under the Resources folder. The second piece of this is updating our property list file to remove the entry that tells the application to open MainWindow.xib as the window. Screens of these below.
Updating the property list.
Since we removed the the main window nib we need to update our code to load the main window from our application delegate, in my case OpenGLBasicsAppDelegate. To do this we have to update main.m, where we need to update our call to UIApplicationMain. The forth argument is usually nil, we need to change that to the name of our application delegate @"OpenGLBasicsAppDelegate". This leaves the file with the following code.
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, @"OpenGLBasicsAppDelegate");
[pool release];
return retVal;
}
The window needs to be created still, this is going to be done in our application delegate and is all done in applicationDidFinishLaunching. We build the window and our OpenGL view, EAGLView, then add the view to the window and make the window visible. The updated function for the application delegate, OpenGLBasicAppDelegate, is below.
//No need for nib
window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
glView = [[EAGLView alloc] initWithFrame:window.bounds];
[window addSubview:glView];
[window makeKeyAndVisible];
glView.animationInterval = 1.0 / 60.0;
[glView startAnimation];
}
Ok, down to business, let's get some graphics on the screen. The rest of our work is going to be done in the EAGLView files, starting with the header, EAGLView.h. The default code for this file has pretty much all the stuff to get things rolling, but we want break out our setup and need to add an instance variable for the point on the screen where a touch happens. The header code ends up looking like the following.
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES1/gl.h>
#import <OpenGLES/ES1/glext.h>
@interface EAGLView : UIView {
@private
GLint backingWidth;
GLint backingHeight;
EAGLContext *context;
GLuint viewRenderbuffer, viewFramebuffer;
GLuint depthRenderbuffer;
NSTimer *animationTimer;
NSTimeInterval animationInterval;
CGPoint touchLocation;
}
@property NSTimeInterval animationInterval;
- (void)startAnimation;
- (void)stopAnimation;
- (void)drawView;
- (void)setupView;
@end
The other variables above represent the buffer size (size of the output screen) and the graphics context that is drawn to. Also included are two buffers, we have two because we double buffer the output (meaning we show one buffer on the output screen while we draw to the other and then swap what is displayed to make the update to what is shown very quick and seamless). A depth buffer is also created handle the 3rd dimension, which we won't really use in this tutorial but we will enable. Finally, a couple of timing variables. In the method list below we only really care about drawView which is where we handle drawing to the screen - as the name implies.
Moving to the implmentation file, you will see a lot of code has already been added. This code is here to make our life easier and to get us up and running quickly. Before we even get to the method implementation we need to updated the define statement to turn depth buffering on. To make our life a little easier we are also going to define a quick statement to convert degrees to radians.
#define DEGREES_TO_RADIANS(__ANGLE) ((__ANGLE) / 180.0 * M_PI)
Now we can start looking at the method implementation. One of the first methods you should come across is initWithCoder, this is no longer going to be used because we are not creating the view from a nib file. We are, however, using the initWithFrame to create the view in the application delegate. To handle this change we need to update the method name and the super init call. Snippet of code below shows the updated pieces.
Replace:
if ((self = [super initWithCoder:coder])) {
with:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
We need to now setup the view for drawing. To do this we create the implementation for the method we added to our header, - (void)setupView. The entire code for this method is below, I will dig into it right after.
const GLfloat zNear = 0.1, zFar = 1000.0, fieldOfView = 60.0;
GLfloat size;
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
size = zNear * tanf(DEGREES_TO_RADIANS(fieldOfView) / 2.0);
//Grab the size of the screen
CGRect rect = self.bounds;
glFrustumf(-size, size,
-size / (rect.size.width / rect.size.height),
size / (rect.size.width / rect.size.height),
zNear, zFar);
glViewport(0, 0, rect.size.width, rect.size.height);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//Initialize touchLocation to 0, 0
touchLocation = CGPointMake(160, 240);
}
The first thing to notice is that we are using GLfloat instead of float, this is due to the fact that OpenGL is written for many platforms and the size of primitive data types (int, double, float, etc...) vary across platforms, but the OpenGL versions don't. Therefore, each platform has to define all of the OpenGL data types to the correct size - for more info on the data types check out the references below. Next, we call glEnable method to enable depth testing, which tests which objects are in front to display (for 3D). This isn't absolutely necessary for this tutorial because we are working in 2D but it is good to know. All of the values passed into this method are going to be constants, that tell OpenGL to enable certain features.
After our call to glEnable we call glMatrixMode to set which matrix stack we are modifying. The main idea here is there are there matrix modes, GL_PROJECTION, GL_MODELVIEW, and GL_TEXTURE. The first GL_PROJECTION basically means we are modifying the camera and where it is looking, the second GL_MODELVIEW means we are updating the scene or how the models are displayed, the last GL_TEXTURE has to do with textures of course. We then setup the view frustum, glFrustumf which is the area the camera is looking at, this is where we use our defined constants. For the sake of this tutorial I am not going to go deeply into the math of setting up the frustum, but basically you set how close and far you are looking at and size of the area. We also have to set the bounds of our view using glViewport.
Setting the matrix mode to GL_MODELVIEW is the next task. This allows us to initialize our scene. We first call glLoadIdentity to basically reset everything back to 0, 0, 0. The identity matrix gives us a starting point to make transformations to scene. All that is left for setting up the scene is clear the view with a color using glClearColor, we pass in an RGBA (red, green, blue, alpha) value - one note is that the values passed in should be between 0 and 1. The final task for the setup is initializing our touch point to the middle of the screen (160, 240).
We now need to make sure to call this method, which is done inside of initWithFrame, just tack it in the bottom, inside the if ((self = [super initWithFrame:frame])) statement.
To get the drawing started remove everything currently in drawView. We are first going to setup our double buffering. To do this we set our drawing context, bind our frame buffer, draw stuff (which we aren't doing quite yet), bind our rendering buffer, and finally present the rendered buffer.
//setting up the draw content
[EAGLContext setCurrentContext:context];
glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
//Draw stuff
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
//show the render buffer
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
We can now build our triangle, which is created from three points in space or vertices. We simply define these points in an array where each triplet of values is a point. this is going to be done at the top of our method. The drawing starts by clearing everything, which is done using glClear and passing set of constants which can be "or"ed together. I passed in GL_COLOR_BUFFER_BIT to clear back to our original clear color and GL_DEPTH_BUFFER_BIT to empty our the depth buffer.
Our drawing always starts out with a call to glLoadIdentity. To setup our vertices for the triangle we call glVertexPointer and pass in the number of points, type of number, the stride (pretty much keep it 0, and a pointer to our array of points. To actually draw the vertices on the screen we need to do two calls, glEnableClientState and glDrawArrays. The first call we pass in GL_VERTEX_ARRAY to enable the capability to give OpenGL an array of verticies. The second call actually draws them to the frame buffer. glDrawArrays takes three arguments the first is the draw mode, which determines how the verticies in the array are drawn, the second is first index, and last is the number of verticies to draw.
Let's take a quick look the draw mode parameter of glDrawArrays. There are multiple modes, ones for drawing points, lines, and triangles. For this tutorial all we are worried about is the triangle modes, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, and GL_TRIANGLES. You can see how these three are distinguished below. In the figure (a) is a triangle strip (GL_TRIANGLE_STRIP), (b) is a triangle fan (GL_TRIANGLE_FAN), and (c) is single triangles (GL_TRIANGLES). For our simple output we just need to use a single triangle. The numbers in the diagram correspond to the vertex in the array.
Putting the above code in place leaves us with a drawView method that looks like the following.
const GLfloat triangleVertices[] = {
0.0, 1.0, -6.0, // top center
-1.0, -1.0, -6.0, // bottom left
1.0, -1.0, -6.0 // bottom right
};
//setting up the draw content
[EAGLContext setCurrentContext:context];
glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
//Draw stuff
//clear the back color back to our original color and depth buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//reset matrix to identity
glLoadIdentity();
//set the format and location for vertices
glVertexPointer(3, GL_FLOAT, 0, triangleVertices);
//set the opengl state
glEnableClientState(GL_VERTEX_ARRAY);
//draw the triangles
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
//show the render buffer
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
So with this code in place we should actually be able to run and see some results. As a matter of fact we should have a white triangle sitting in the middle of our screen, it should look like the boring picture below.
To spice it up a little we are going to add some color. The code to handle this is very close to the drawing code we just put in. We need to add another array to hold the RGBA values for each vertex/point in our triangle, where each set of 4 values is a color. We use glColorPointer to set the color array, the method takes the same parameters as glVertexPointer. Lastly we need to enable colors in our engine with glEnableClientState with GL_COLOR_ARRAY as the parameter. This gives us an updated drawView method.
const GLfloat triangleVertices[] = {
0.0, 1.0, -6.0, // top center
-1.0, -1.0, -6.0, // bottom left
1.0, -1.0, -6.0 // bottom right
};
const GLfloat triangleColors[] = {
1.0, 0.0, 0.0, 1.0, // red
0.0, 1.0, 0.0, 1.0, // green
0.0, 0.0, 1.0, 1.0, // blue
};
//setting up the draw content
[EAGLContext setCurrentContext:context];
glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
//Draw stuff
//clear the back color back to our original color and depth buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//reset matrix to identity
glLoadIdentity();
//set the format and location for verticies
glVertexPointer(3, GL_FLOAT, 0, triangleVertices);
//set the opengl state
glEnableClientState(GL_VERTEX_ARRAY);
//set the format and location for colors
glColorPointer(4, GL_FLOAT, 0, triangleColors);
//set the opengl state
glEnableClientState(GL_COLOR_ARRAY);
//draw the triangles
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
//show the render buffer
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
The final thing we are going to do is make our triangle move around with touches to the screen. This involves using the UIResponder method touchesBegan and grabbing the location of the first touch (or any touch in our case). The location is then stored in our instance variable touchLocation. We add this method to out view implementation file and it looks something like this.
{
UITouch *touch = [touches anyObject];
touchLocation = [touch locationInView:self];
}
To get the triangle to move we have to add one more piece to our drawView method. Right after we load the identity matrix we are going to calculate the position on the screen from the current touch position. Now you may think that this is no big deal, but actually if we wanted to be really accurate it takes a little bit of work. The reason this isn't a simple task is because we are moving from a 2D plane (our screen) to a 3D scene. There is also a little bit of work to modify the values to switch between coordinate systems. The screen coordinates have 2 dimensions (x, y) from the upper left corner of the screen while our 3D scene has 3 axes (x, y, z) which start in the middle of the screen - OpenGL coordinates can be seen on the right. So with a little bit of math we turn our screen coordinate into a scene coordinate. Lastly, we move or translate our triangle to that position using glTranslatef, passing in the x, y, and z positions to move the triangle to. I used -1 for the z just to show you that you can use the z axes also, play around with the z position and remember -z is going into the screen. That completes our drawView method, if you run the simulator now you should have a triangle that moves to the position that you touch (roughly).
const GLfloat triangleVertices[] = {
0.0, 1.0, -6.0, // top center
-1.0, -1.0, -6.0, // bottom left
1.0, -1.0, -6.0 // bottom right
};
const GLfloat triangleColors[] = {
1.0, 0.0, 0.0, 1.0, // red
0.0, 1.0, 0.0, 1.0, // green
0.0, 0.0, 1.0, 1.0, // blue
};
//setting up the draw content
[EAGLContext setCurrentContext:context];
glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
//Draw stuff
//clear the back color back to our original color and depth buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//reset matrix to identity
glLoadIdentity();
//rough approximation of screen to current 3D space
GLfloat x = (touchLocation.x - 160.0) / 38.0;
GLfloat y = (240.0 - touchLocation.y) / 38.0;
//translate the triangle
glTranslatef(x, y, -1.0);
//set the format and location for verticies
glVertexPointer(3, GL_FLOAT, 0, triangleVertices);
//set the opengl state
glEnableClientState(GL_VERTEX_ARRAY);
//set the format and location for colors
glColorPointer(4, GL_FLOAT, 0, triangleColors);
//set the opengl state
glEnableClientState(GL_COLOR_ARRAY);
//draw the triangles
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
//show the render buffer
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
That is it for this tutorial on getting started with OpenGL ES for the iPhone. There are a lot of concepts that we just covered and really this just touches the tip of the iceberg. I hope you found something useful and feel free to download the source code for this example. If you have any questions please post them in the comments or in the forums.
09/15/2009 - 04:39
Very useful post!! thanks mate!!
Add Comment
[language] [/language]
Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]
See here for supported languages.
Javascript must be enabled to submit anonymous comments - or you can login.