http://www.raywenderlich.com/3664/opengl-es-2-0-for-iphone-tutorial
OpenGL ES is the lowest-level API that you use to program 2D and 3D graphics on the iPhone.
If you’ve used other framework such as Cocos2D, Sparrow, Corona, or Unity, these are all built on top of OpenGL!
One of the reasons why programmers like to use the above frameworks rather than using OpenGL directly is because OpenGL is notoriously difficult to learn.
And that’s what this tutorial is for – to make the learning curve a little less steep for beginner OpenGL developers!
In this series, you’ll get hands-on experience with OpenGL ES 2.0 and will create a simple “Hello, World” app that displays some simple geometry.
In the process, you’ll learn the following:
Caveat: I am not an Open GL expert! I am learning this myself, and am writing tutorials as I go. If I make any boneheaded mistakes, feel free to chime in with corrections or insights! :]
Without further ado, let’s start learning OpenGL ES!
First things first – you should know that there are two different versions of OpenGL ES (1.0 and 2.0), and they are very different.
OpenGL ES 1.0 uses a fixed pipeline, which is a fancy way of saying you use built-in functions to set lights, vertexes, colors, cameras, and more.
OpenGL ES 2.0 uses a programmable pipeline, which is a fancy way of saying all those built-in functions go away, and you have to write everything yourself.
“OMG!” you may think, “well why would I ever want to use OpenGL ES 2.0 then, if it’s just extra work?!” Although it does add some extra work, with OpenGL ES 2.0 you make some really cool effects that wouldn’t be possible in OpenGL ES 1.0, such as this toon shader (via Imagination Technologies):
Or even these amazing lighting and shadow effects (via Fabien Sanglard):
Pretty cool eh?
OpenGL ES 2.0 is only available on the iPhone 3GS+, iPod Touch 3G+, and all iPads. But the percentage of people with these devices is in the majority now, so that’s what we’re going to focus on in this tutorial!
Although Xcode comes with an OpenGL ES project template, I think that’s confusing for beginners because you have to go through a lot of code you didn’t write yourself and try to understand how it works.
I think it’s easier if you write all the code from scratch, so you can understand how everything fits together – so that’s what we’re going to do here!
Start up Xcode, and go to File\New\New Project. Select iOS\Application\Window-based Application, and click Next. Name your project HelloOpenGL, click Next, choose a folder to save it in, and click Create.
Compile and run the app, and you should see a blank white screen:
The Window-based Application template is about as “from scratch” as you can get with project templates in Xcode. All it does is create a UIWindow and present it to the screen – no views, view controllers, or anything!
Let’s add a new view that we’ll use to contain the OpenGL content. Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter UIView for Subclass of, click Next, name the new class OpenGLView.m, and click Save.
Next, you’ll add a bunch of code to OpenGLView.m inside the @implementation to just color the screen green for now.
Add each step here bit by bit, and I’ll explain what each part does as we go.
1) Add required frameworks
The first step is to add the two frameworks you need to use OpenGL – OpenGLES.frameworks and QuartzCore.framework.
To add these frameworks in Xcode 4, click on your HelloOpenGL project in the Groups & Files tree, and select the HelloOpenGL target. Expand the Link Binary with Libraries section, click the + button, and select OpenGLES.framework. Repeat for QuartzCore.framework as well.
2) Modify OpenGLView.h
Modify OpenGLView.h to look like the following:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
@interface OpenGLView : UIView {
CAEAGLLayer* _eaglLayer;
EAGLContext* _context;
GLuint _colorRenderBuffer;
}
@end |
This imports the headers you need for OpenGL, and creates the instance variables that the methods you wrote earlier were using.
3) Set layer class to CAEAGLLayer
+ (Class)layerClass {
return [CAEAGLLayer class];
} |
To set up a view to display OpenGL content, you need to set it’s default layer to a special kind of layer called a CAEAGLLayer. The way you set the default layer is to simply overwrite the layerClass method, like you just did above.
4) Set layer to opaque
- (void)setupLayer {
_eaglLayer = (CAEAGLLayer*) self.layer;
_eaglLayer.opaque = YES;
} |
By default, CALayers are set to non-opaque (i.e. transparent). However, this is bad for performance reasons (especially with OpenGL), so it’s best to set this as opaque when possible.
5) Create OpenGL context
- (void)setupContext {
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
_context = [[EAGLContext alloc] initWithAPI:api];
if (!_context) {
NSLog(@"Failed to initialize OpenGLES 2.0 context");
exit(1);
}
if (![EAGLContext setCurrentContext:_context]) {
NSLog(@"Failed to set current OpenGL context");
exit(1);
}
} |
To do anything with OpenGL, you need to create an EAGLContext, and set the current context to the newly created context.
An EAGLContext manages all of the information iOS needs to draw with OpenGL. It’s similar to how you need a Core Graphics context to do anything with Core Graphics.
When you create a context, you specify what version of the API you want to use. Here, you specify that you want to use OpenGL ES 2.0. If it is not available (such as if the program was run on an iPhone 3G), the app would terminate.
6) Create a render buffer
- (void)setupRenderBuffer {
glGenRenderbuffers(1, &_colorRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
} |
The next step to use OpenGL is to create a render buffer, which is an OpenGL object that stores the rendered image to present to the screen.
Sometimes you’ll see a render buffer also referred to as a color buffer, because in essence it’s storing colors to display!
There are three steps to create a render buffer:
7) Create a frame buffer
- (void)setupFrameBuffer {
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, _colorRenderBuffer);
} |
A frame buffer is an OpenGL object that contains a render buffer, and some other buffers you’ll learn about later such as a depth buffer, stencil buffer, and accumulation buffer.
The first two steps for creating a frame buffer is very similar to creating a render buffer – it uses the glGen and glBind like you’ve seen before, just ending with “Framebuffer/s” instead of “Renderbuffer/s”.
The last function call (glFramebufferRenderbuffer) is new however. It lets you attach the render buffer you created earlier to the frame buffer’s GL_COLOR_ATTACHMENT0 slot.
8) Clear the screen
- (void)render {
glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
[_context presentRenderbuffer:GL_RENDERBUFFER];
} |
We’re trying to get something displaying on the screen as quickly as possible, so before dealing with vertexes, shaders, and the like, let’s just clear the entire screen to a particular color!
Let’s set it to the main color of this website, which is RGB 0, 104, 55. Notice you have to divide the color values by 255 (the max color value), because the color range for each component is from 0 to 1.
To accomplish this we have to take three steps here:
9) Wrapup code in OpenGLView.m
// Replace initWithFrame with this
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setupLayer];
[self setupContext];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self render];
}
return self;
}
// Replace dealloc method with this
- (void)dealloc
{
[_context release];
_context = nil;
[super dealloc];
} |
This is just some helper code to call all of the above methods, and clean up in dealloc.
10) Hook up OpenGLView to the App Delegate
Make the following changes to HelloOpenGLAppDelegate.h:
// At top of file
#import "OpenGLView.h"
// Inside @interface
OpenGLView* _glView;
// After @interface
@property (nonatomic, retain) IBOutlet OpenGLView *glView; |
And the following changes to HelloOpenGLAppDelegate.m:
// At top of file
@synthesize glView=_glView;
// At top of application:didFinishLaunchingWithOptions
CGRect screenBounds = [[UIScreen mainScreen] bounds];
self.glView = [[[OpenGLView alloc] initWithFrame:screenBounds] autorelease];
[self.window addSubview:_glView];
// In dealloc
[_glView release]; |
This simply creates a new instance of the OpenGLView at startup, and attaches it to the window.
And that’s it! Compile and run your project, and you should see a green screen drawn by OpenGL ES 2.0!
In OpenGL ES 2.0, to render any geometry to the scene, you have to create two tiny little programs called shaders.
Shaders are written in a C-like language called GLSL. Don’t worry too much about studying up on the reference at this point – you can get just by looking at the examples in this tutorial for now.
There are two types of shaders:
Like I said, the easiest way to understand these is to look at some examples. Let’s keep things nice and simple and write the simplest possible vertex and fragment shaders we can!
In Xcode, go to File\New\New File…, choose iOS\Other\Empty, and click Next. Name the new file SimpleVertex.glsl, and click Save.
Open up SimpleVertex.glsl and add the following code:
attribute vec4 Position; // 1
attribute vec4 SourceColor; // 2
varying vec4 DestinationColor; // 3
void main(void) { // 4
DestinationColor = SourceColor; // 5
gl_Position = Position; // 6
} |
Everything is new here, so let’s go over it line by line!
Huh? That last bit sounds confusing, but is actually pretty easy to understand if you see a picture:
So basically, you can specify a different color for each vertex, and it will make all the values in-between a neat gradient! You’ll see an example of that for yourself soon.
OK, that’s it for our simple vertex shader! Let’s add a simple fragment shader next.
Go to File\New\New File…, choose iOS\Other\Empty, and click Next. Name the new file SimpleFragment.glsl, and click Save.
Open up SimpleFragment.glsl and add the following code:
varying lowp vec4 DestinationColor; // 1
void main(void) { // 2
gl_FragColor = DestinationColor; // 3
} |
This is pretty short and sweet but let’s explain it line by line anyways:
Not so bad, eh? Now let’s add some code to get start using these in our app.
We’ve added the two shaders to our Xcode project as glsl files, but guess what – Xcode doesn’t do anything with them except copy them into our application bundle.
It’s the job our our app to compile and run these shaders – at runtime!
You might be surprised by this (it’s kinda weird to have an app compiling code on the fly!), but it’s set up this way so that the shader code isn’t dependent on any particular graphics chip, etc.
So let’s write a method we can use to compile these shaders. Open up OpenGLView.m and add the following method above initWithFrame:
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
// 1
NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName
ofType:@"glsl"];
NSError* error;
NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath
encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSLog(@"Error loading shader: %@", error.localizedDescription);
exit(1);
}
// 2
GLuint shaderHandle = glCreateShader(shaderType);
// 3
const char * shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = [shaderString length];
glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
// 4
glCompileShader(shaderHandle);
// 5
GLint compileSuccess;
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
return shaderHandle;
} |
OK, let’s go over how this works:
You can use this method to compile the vertex and fragmetn shaders, but there’s a few more steps – linking them together, telling OpenGL to actually use the program, and getting some pointers to the attribute slots where you’ll be passing in the input values to the shaders.
Add a new method to do this right below compileShader:
- (void)compileShaders {
// 1
GLuint vertexShader = [self compileShader:@"SimpleVertex"
withType:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShader:@"SimpleFragment"
withType:GL_FRAGMENT_SHADER];
// 2
GLuint programHandle = glCreateProgram();
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
glLinkProgram(programHandle);
// 3
GLint linkSuccess;
glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
// 4
glUseProgram(programHandle);
// 5
_positionSlot = glGetAttribLocation(programHandle, "Position");
_colorSlot = glGetAttribLocation(programHandle, "SourceColor");
glEnableVertexAttribArray(_positionSlot);
glEnableVertexAttribArray(_colorSlot);
} |
Here’s how each section works:
Two last steps. Add the following method call to initWithFrame right before calling render:
[self compileShaders]; |
And declare the instance variables for _colorSlot and _positionSlot inside the @interface in OpenGLView.h as follows:
GLuint _positionSlot;
GLuint _colorSlot; |
You can compile and run now, and if it displays a green screen (and does not quit with an error), it means that your vertex and fragment shaders compiled OK at runtime!
Of course, nothing looks differnet because we haven’t given the shaders any vertex geometry to render. So let’s take care of that next.
Let’s start things nice and simple by rendering a square to the screen. The square will be set up like the following:
When you render geometry with OpenGL, keep in mind that it can’t render squares – it can only render triangles. However we can create a square with two triangles as you can see in the picture above: one triangle with vertices (0, 1, 2), and one triangle with vertices (2, 3, 0).
One of the nice things about OpenGL ES 2.0 is you can keep your vertex data organized in whatever manner you like. Open up OpenGLView.m and create a plain old C-structure and a few arrays to keep track of our square information, as shown below:
typedef struct {
float Position[3];
float Color[4];
} Vertex;
const Vertex Vertices[] = {
{{1, -1, 0}, {1, 0, 0, 1}},
{{1, 1, 0}, {0, 1, 0, 1}},
{{-1, 1, 0}, {0, 0, 1, 1}},
{{-1, -1, 0}, {0, 0, 0, 1}}
};
const GLubyte Indices[] = {
0, 1, 2,
2, 3, 0
}; |
So basically we create:
We now have all the information we need, we just need to pass it to OpenGL!
The best way to send data to OpenGL is through something called Vertex Buffer Objects.
Basically these are OpenGL objects that store buffers of vertex data for you. You use a few function calls to send your data over to OpenGL-land.
There are two types of vertex buffer objects – one to keep track of the per-vertex data (like we have in the Vertices array), and one to keep track of the indices that make up triangles (like we have in the Indices array).
So add a method above initWithFrame to create these:
- (void)setupVBOs {
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
} |
You can see that it’s pretty simple. It uses a similar pattern that you’ve seen before – calls glGenBuffers to create a new Vertex Buffer object, glBindBuffer to tell OpenGL “hey when I say GL_ARRAY_BUFFER, I really mean vertexBuffer”, and glBufferData to send the data over to OpenGL-land.
And before we forget, add this to initWithFrame right before calling render:
[self setupVBOs]; |
We have all of the pieces in place, finally we can update the render method to draw our new vertex data to the screen, using our new shaders!
Replace render with the following:
- (void)render {
glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
// 1
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
// 2
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE,
sizeof(Vertex), 0);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (GLvoid*) (sizeof(float) * 3));
// 3
glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]),
GL_UNSIGNED_BYTE, 0);
[_context presentRenderbuffer:GL_RENDERBUFFER];
} |
This works as follows:
This is a particularly important function so let’s go over how it works carefully.
Ok back to the final part of the code listing!
This is also an important function so let’s discuss each parameter here as well.
Guess what – you’re done! Compile and run the app and you should see a pretty rectangle on the screen:
You may be wondering why this rectangle happens to fit perfectly on the screen. Well by default, OpenGL has the “camera” at (0,0,0), looking down the z-axis. The bottom left of the screen is mapped to (-1,-1), and the upper right of the screen is mapped to (1,1), so it “stretches” our square to fit the entire screen.
Obviously, in a real app you’ll want to have more control over how the “camera” behaves. So let’s talk about how you can do that with a projection transform!
To make objects appear 3D on a 2D screen, we need to apply a projection transform on the objects. Here’s a diagram that shows how this works:
Basically we have a “near” plane and a “far plane”, and the objects we want to display are in-between. The closer an object is to the “near” plane we scale it so it looks smaller, and the closer and object is to the “far” plane we scale it so it appears bigger. This mimics the way a human eye works.
Let’s see how we can modify our app to use a projection. Start by opening SimpleVertex.glsl, and make the following changes:
// Add right before the main
uniform mat4 Projection;
// Modify gl_Position line as follows
gl_Position = Projection * Position; |
Here we add a new input variable called Projection. Notice that instead of marking it as an attribute, we mark it as a uniform. This means that we just pass in a constant value for all vertices, rather than a per-vertex value.
Also note that the Projection is marked as a mat4 type. Mat4 stands for a 4×4 matrix. Matrix math is a big subject that we won’t really cover here, but for now just think of them as things you can use to scale, rotate, or translate vertices. We’ll pass in a matrix that moves our vertices around according to the Projection diagram above.
Next, we set the final position of the vertex to be the Projection multiplied by the Position. The way you apply a matrix transform on something is to multiply like we did here! Order does matter by the way.
Now you need to pass in the Projection matrix to your vertex shader. However, this involves some complicated linear algebra and sadly I have forgotten too much of that since my college days :[ But luckly you don’t need to understand it to use it, since this is well known stuff that many smart people have already solved!
Smart people like Bill Hollings, the author of Cocos3D. He’s written a full-featured 3D graphics framework that integrates nicely with Cocos2D (and maybe I’ll write a tutorial about it sometime!) But anyway, Cocos3D contains a nice Objective-C vector and matrix library that we can pull into this project quite easily.
I’ve put the Cocos3D Math Library files you need into a zip (and removed a few unnecessary dependencies from them), so go ahead and download them and drag the files into your project. Make sure “Copy items into destination group’s folder (if needed)” is checked, and click Finish.
Then add a new instance variable to OpenGLView.h inside the @interface:
GLuint _projectionUniform; |
And make the following changes to OpenGLView.m:
// Add to top of file
#import "CC3GLMatrix.h"
// Add to bottom of compileShaders
_projectionUniform = glGetUniformLocation(programHandle, "Projection");
// Add to render, right before the call to glViewport
CC3GLMatrix *projection = [CC3GLMatrix matrix];
float h = 4.0f * self.frame.size.height / self.frame.size.width;
[projection populateFromFrustumLeft:-2 andRight:2 andBottom:-h/2 andTop:h/2 andNear:4 andFar:10];
glUniformMatrix4fv(_projectionUniform, 1, 0, projection.glMatrix);
// Modify vertices so they are within projection near/far planes
const Vertex Vertices[] = {
{{1, -1, -7}, {1, 0, 0, 1}},
{{1, 1, -7}, {0, 1, 0, 1}},
{{-1, 1, -7}, {0, 0, 1, 1}},
{{-1, -1, -7}, {0, 0, 0, 1}}
}; |
Here we import the header for the math library, and call glGetUniformLocation to get the handle we need to set the Projection input variable in the vertex shader.
Then, we use the math library to create a projection matrix. It comes with a nice and easy to use function that lets you specify the coordinates to use for the left/right and top/bottom for the planes, and the near and far z-coordinates.
The way you pass that data to the vertex shader is through glUniformMatrix4fv, and the CC3GLMatrix class has a handy glMatrix method that converts them matrix into the array format OpenGL uses.
The last step is to tweak our vertices a bit so they fit within the near/far planes. This just sets the z-coordinate for each vertex to -7.
Compile and run the app, and now you should see the square looking like it’s in the distance a bit!
It was kind of annoying to have to manually go through our vertex array and move everything backwards by manually changing the z-values to -7.
That kind of thing is what matrix transforms are for! They allow you to move vertices around in space really easily.
So far our vertex shader modifies the positions of our vertices by applying the projection matrix to each vertex, so why not apply a transform/scale/rotation matrix as well? We’ll call this a “model-view” transform.
Let’s see how this works. Make the following changes to SimpleVertex.glsl:
// Add right after the Projection uniform
uniform mat4 Modelview;
// Modify the gl_Position line
gl_Position = Projection * Modelview * Position; |
You should understand this perfectly by now! We’re just adding another uniform input matrix to pass in, and applying the modelview matrix to the position.
Then add a new instance variable to OpenGLView.h:
GLuint _modelViewUniform; |
And make the following changes to OpenGLView.m:
// Add to end of compileShaders
_modelViewUniform = glGetUniformLocation(programHandle, "Modelview");
// Add to render, right before call to glViewport
CC3GLMatrix *modelView = [CC3GLMatrix matrix];
[modelView populateFromTranslation:CC3VectorMake(sin(CACurrentMediaTime()), 0, -7)];
glUniformMatrix4fv(_modelViewUniform, 1, 0, modelView.glMatrix);
// Revert vertices back to z-value 0
const Vertex Vertices[] = {
{{1, -1, 0}, {1, 0, 0, 1}},
{{1, 1, 0}, {0, 1, 0, 1}},
{{-1, 1, 0}, {0, 0, 1, 1}},
{{-1, -1, 0}, {0, 0, 0, 1}}
}; |
Here we get the reference to the model view uniform input variable, and use the Cocos3D math library to create a new matrix, and populate it with a translation.
The translation is -7 along the z-axis, so it fits within the near/far planes, and then something weird – the sin() of the current time?
If you think back to trig in high school, you might remember that sin() is a function that returns values from -1 to 1, and cycles every PI iterations (3.14). So if we pass the current time in, this will cycle between -1 and 1 every 3.14 seconds.
Compile and run the code, and you’ll see the square appear in the center properly (even though we set the z back to 0):
However, nothing moves! Well if you think about it, that makes sense because we’re only calling render one time (upon init)!
Let’s fix this by calling the render method every frame.
Ideally we would like to synchronize the time we render with OpenGL to the rate at which the screen refreshes.
Luckily, Apple provides an easy way for us to do this with CADisplayLink! It’s really easy to use so let’s just dive in. Make the following changes to OpenGLView.m:
// Add new method before init
- (void)setupDisplayLink {
CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
// Modify render method to take a parameter
- (void)render:(CADisplayLink*)displayLink {
// Remove call to render in initWithFrame and replace it with the following
[self setupDisplayLink]; |
That’s it! With this CADisplayLink will call your render method every frame, and it will update the transformation based on the sin() of the current time, so the box will move back and forth!
I don’t think this is quite cool enough. It would be even cooler if it rotated, then we’d feel like we’re really 3D!
Add another instance variable to OpenGLView.h:
float _currentRotation; |
Then add the following to OpenGLView.m, inside the render method, right after the call to populateFromTranslation:
_currentRotation += displayLink.duration * 90;
[modelView rotateBy:CC3VectorMake(_currentRotation, _currentRotation, 0)]; |
Here we add a variable called _currentRotation, that will increment by 90 degrees every second.
Next, we modify the model view matrix (which is currently a translation) to add a rotation as well. The rotation is along both the x and y axis, and 0 along the z axis.
Compile and run your code, and now you’ll see the square flip with a cool 3D effect!
And yet it’s still not cool enough! I’m sick of squares, time to move onto cubes.
It’s really easy, simply comment out the Vertices and Indices arrays and add new ones in their place:
const Vertex Vertices[] = {
{{1, -1, 0}, {1, 0, 0, 1}},
{{1, 1, 0}, {1, 0, 0, 1}},
{{-1, 1, 0}, {0, 1, 0, 1}},
{{-1, -1, 0}, {0, 1, 0, 1}},
{{1, -1, -1}, {1, 0, 0, 1}},
{{1, 1, -1}, {1, 0, 0, 1}},
{{-1, 1, -1}, {0, 1, 0, 1}},
{{-1, -1, -1}, {0, 1, 0, 1}}
};
const GLubyte Indices[] = {
// Front
0, 1, 2,
2, 3, 0,
// Back
4, 6, 5,
4, 7, 6,
// Left
2, 7, 3,
7, 6, 2,
// Right
0, 4, 1,
4, 1, 5,
// Top
6, 2, 1,
1, 6, 5,
// Bottom
0, 3, 7,
0, 7, 4
}; |
I got these vertices just by sketching out a cube on paper and figuring it out. I encourage you to try the same, it’s a good exercise!
Compile and run, and w00t we have a 3D cube… or do we?
It acts kinda like a cube, but doesn’t look quite right. Sometimes it seems like you can see the inside of the cube!
Luckily we can solve this pretty easily by enabling depth testing in OpenGL. With depth testing, OpenGL can keep track of the z-coordinate for each pixel it wants to draw, and only draw the vertex if there isn’t someting in front of it already.
Let’s add this in real quick. Add a new instance variable to OpenGLView.h:
GLuint _depthRenderBuffer; |
Then make the following modifications to OpenGLView.m:
// Add new method right after setupRenderBuffer
- (void)setupDepthBuffer {
glGenRenderbuffers(1, &_depthRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _depthRenderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, self.frame.size.width, self.frame.size.height);
}
// Add to end of setupFrameBuffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthRenderBuffer);
// In the render method, replace the call to glClear with the following
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
// Add to initWithFrame, right before call to setupRenderBuffer
[self setupDepthBuffer]; |
The setupDepthBuffer method creates a depth buffer, in a similar manner to creating a render/color buffer. However, note that it alloacates the storage using the glRenderbufferStorage method rather than the context’s renderBufferStorage method (which is a special case for the color render buffer used for the OpenGL view).
Then we call glFramebufferRenderbuffer to associate the new depth buffer with the render buffer. Remember how I said that the frame buffer stores lots of different kinds of buffers? Well here’s our first new one to add other than the color buffer.
In the render method, we clear the depth buffer on each update, and enable depth testing.
Compile and run your project, and now you have an app with a rotating cube, using OpenGL ES 2.0, and made completely from scratch! :D
Here is the sample project with all of the code from the above tutorial.
We’ve barely scratched the surface of OpenGL, but hopefully this helps give you guys a good grounding of the basics. Check out the Part 2 in the series, where we take things to the next level by adding some textures to our cube!
By the way, the reason I wrote this tutorial in the first place was because it was the winner of the tutorial poll on the sidebar from a few weeks back! Thank you to everyone who voted, and please be sure to keep voting in the sidebar and suggesting tutorials – we have a new tutorial vote every week!
If you have any questions, comments, or advice for a n00b OpenGL programmer like me, please join the forum discussion below!