iOS Lesson 01 - Setting up GL ESHi all to the first tutorial in our new iOS series! PrefaceBefore you start with the tutorial you should know, that I'm fairly new to Objective-C and iOS, so if there's anything I'm doing wrong or that could be done better, let me know! I'll just use Objective-C to interact with the OS anyway, all the OpenGL related code will be in C++. I will try to explain most things as we come across, but I will assume you have some basic programming knowledge and understand the fundamental concepts of object oriented programming like classes and a class diagram like below. This code is working with iOS 4 and 5, so even compatible with the iPhone 4S and whatever you have out there. We are going to use XCode 4, so if you don't have that installed yet, go and grab it from the Mac AppStore! And if anybody read up to this point hoping he could develop iPhone Apps from Windows or Linux, I'm sorry to disappoint you, you can't.
And I know this first lesson looks scary, it is probably the most boring lesson in this series and we don't even see any cool stuff at the end.. :( Nevertheless, the lesson contains a whole lot of valuable information, and is important to understand the interactions between the different parts of our framework. So be sure to read through or skim over it before moving on, you don't need to understand everything as you can always come back later if you want to know the details. But in good old NeHe manner, I tried to explain every single bit of code! OverviewLet's first have a look at all the things and classes we need. As I said before, we'll have some Objective-C classes (with purple lines), and some C++ classes (in cyan). The starting point of the application is a main method, like in all C/C++ programs. From there we start a UIApplicationMain, which we'll configure with the InterfaceBuilder to be a UIWindow containing an EAGLView and using our Lesson01AppDelegate to process all events. The window is created by the UIApplicationMain automatically and will then display our view. The view contains our OpenGL context which you can think of as our access to a canvas where we can use OpenGL ES to draw to. Within the view, we will hook into the operating system's run loop, and redraw our frame as often as we can. This is required for later lessons with animation. The drawing itself is done by the draw method of a Lesson, or more precisely, of a Lesson01 object, because init() and draw() are abstract methods in Lesson. WalkthroughOkay that was pretty high level, so now let's look at how to this works step by step. Grab the code from here (or by cloning the git repository from http://code.google.com/p/nehe). Fire up XCode and open the project file Lesson01.xcodeproj.
You should see 3 folders in your project to the left: Lesson01, Frameworks and Products. Lesson01 contains all our code and has subfolders Supporting Files and Basecode. Frameworks contains all the Frameworks we intend to use, or which are needed by our project. Developers from other languages or operating systems call them libraries. Products finally lists all the applications that will be built, which is just our Lesson01.app right now. In our source code we will mainly work in the LessonXX class in the following lessons, and the AppDelegate changes every time to create an object of our current lesson instance. But in this first lesson we will have a detailed look at the Basecode and some of the Supporting Files. Let's approach the different files in the order they are traversed when the application is run. As we already mentioned, the birth of every program is it's main method. It is found in Lesson01/Supporting Files/main.m
The method is rather uninformative and looks pretty much the same for every iPhone app. The NSAutoreleasePool is needed for the Objective-C garbage collection, we print a log message, and then comes the important part: we hand over the control to the UIApplicationMain method and pass our application's parameters. As soon as control comes back, we release the garbare collection utility and end the program with the return value that came from the UIApplicationMain. The UIApplicationMain method runs, as the name implies, a user interface. In our project settings under Targets->Lesson01, tab Summary, our MainInterface is set to MainWindow. This tells our App, that we want it to display MainWindow.xib at startup. This file is under Supporting Files as well, and opens in the InterfaceBuilder. In the new sidebar left of the editor, you will see the Placeholders and Objects. Among the Objects is our Lesson01AppDelegate and a Window with an embedded View. If you've never done any GUI programming you could imagine this like opening your favourite Office text processor (=window), and open a document (=view). Now you can switch between documents(views) filled with content (here UI elements like buttons, text fields, or an OpenGL ES canvas) without closing the whole program. But to be able to see any content, you need an open document (view). The important thing to note about the setup of our window is not obvious unless you control-click (or right-click) Lesson01AppDelegate. There you see two defined outlets glView and window, which are connected to the View and the Window in the InterfaceBuilder. An outlet defines a slot, where a variable in the code of our app delegate contains a reference to the connected element in the InterfaceBuilder. Lesson01AppDelegateWhich brings us to the first important code file: Lesson01AppDelegate.h
In this Objective-C style class definition we first declare the interface in the header before the implementation of the methods follows in the corresponding source (.mm for Objective-C++) file. As also seen in the class diagram, it has member variables for the window, our glView, and the lesson which will be created when the AppDelegate is initialized. The important thing to note, is the definition of the properties as IBOutlets so they can be used in the InterfaceBuilder. An object of this class is automatically created when the application starts, because it is needed to handle the window's events. To be able to handle the window events, our AppDelegate implements the interface (or in Objective-C terminology: protocol) UIApplicationDelegate. This is done by adding it in < > brackets after the class name definition. The first event the window triggers after it has been created is didFinishLaunchingWithOptions. The code is in Lesson01AppDelegate.mm
As you can see, here we configure our glView and create the lesson instance. One important thing is that we defined the member variable lesson to be a pointer to a Lesson instance, but as Lesson01 is derived from Lesson(see class diagram), it offers the same interface and can be used as well. Also note, that we synthesize our two properties from the header, this auto-generates getter and setter method for the otherwise private fields. The remainder of Lesson01AppDelegate.mm deals with the other events that can be triggered from the window, and they are all related to becoming visible or being moved to the background. In the case of becoming visible, we tell our view to start refreshing repeatedly - and when we move to the background or close, we stop refreshing. Finally, if the AppDelegate gets disposed, its dealloc method is called where we clean up everything by freeing the memory that has been allocated. In Objective-C that is done with release, C++ pointers get created with new and deleted with delete.
Now let's finally move on to the classes involving OpenGL context creation and drawing! EAGLViewAs we already learned, the window and the delegate get created automatically if we run UIApplicationMain. When the window is created, it knows that it has to contain a view which is an EAGLView because we defined the corresponding outlet in our delegate. So the window automatically creates the view as it needs it to be displayed. Let's first look at the header EAGLView.h:
The class is derived from a UIView as it just specializes what this very view shall do. Being a subclass of a UIView allows us to overwrite several methods (or selectors in Objective-C), which we will se in the source file.
As we already mentioned, the EAGLView encapsulates our Open GL ES context. The OpenGL context can be seen as the permission to use OpenGL calls for drawing. It also keeps track of all the states we set like the current color or which image we're currently using as texture on our surfaces. The context is only useful in combination with a canvas where we can draw to. This canvas is implemented in a construct called a framebuffer. It can consist of several layers of buffers storing different information. The two layers that we usually need are a color renderbuffer (render because we are going to render to it), and a depth renderbuffer. The color renderbuffer stores information pretty similar to what is stored in images like JPEG, namely it stores a few bytes per color channel per pixel. This is what we will eventually see on the screen. The depth renderbuffer records for every pixel in our color buffer how far it is away from our screen, so that if we draw a house that is 10 units away and draw a person in front of it that is 5 units away, the person will definitely be in front of the house, no matter what is drawn first! This is called depth testing. The depth buffer is not meant to be displayed, though. After this explanation most of the member variables of our EAGLView class should make sense: we store the width and height of our framebuffer (the color and depth buffer have to be the same size!), and we store IDs of the buffers which act as names or references in OpenGL. Next is a CADisplayLink which allows us to hook into the system's run loop and request a redraw around 60 times a second. Then we have a switch to enable/disable the use of a depth buffer. As the buffer consumes valuable memory on the graphics chip, we should not set it up if we don't need it. And obviously we need a pointer to our lesson so that we can invoke our draw() method, and a flag whether we already initialized it. The context which we have talked about already for so long, is stored as an EAGLContext property. Last but not least we have 4 methods with pretty self explanatory names which are used by the AppDelegate. Before we can dive into the real code, EAGLView.mm begins with some initialization stuff:
We first add the private methods to the class definition. If we wouldn't do this, then we'd have to order the code in a way that a method has been defined before it is used. Not putting the declaration of the private methods into the header file has the advantage of keeping the header clean, although it does not really add to the readability of the source. Then we actually begin the implementation of our EAGLView, synthesize our context to get auto-generated getters and setters, and we overwrite the layerClass method of UIView. This needs to be done because our view does not behave like a standard UI element but it paints onto a CAEAGLLayer (CA for CoreAnimation).
When our view is initialized, the method initWithCoder is executed. That is when we start setting up our context. First, we initialize our superclass UIView and check that everything went fine. Then we create the CAEGLLayer from our View's layer and make it drawable. (Don't ask me about details of this part.. ;) ) The next step is where we finally create our OpenGL context, and we require it to be version 2 (available since iPhone 3GS and iPod Touch 3rd gen) by calling initWithAPI:kEAGLRenderingAPIOpenGLES2. If this was successfull and we're able to make the context current (visually speaking: take the brush in our hand), then we move on and set our member variables to default values. Remember the paragraph about framebuffers? Let's create our canvas which will serve as playground in all following lessons!
First we check that we don't have a frambuffer already. If that's not the case then we generate IDs for our framebuffer by calling OpenGL's method for this. This assures that the ID is unique, and every generated ID is greater than zero. After the ID is generated, we bind the framebuffer. OpenGL keeps track of the active object of most things, like the active framebuffer, the last recently st color, the active texture or active shader program, etc. This causes all following API calls with respect to a framebuffer, affect the currently bound framebuffer. Next we do the same thing for our color renderbuffer: we generate a name and bind it.
This color renderbuffer is special. We want the color that we draw to, to be used as the UIView's color. That's why we created the CAEAGLLayer earlier. Now, we need to grab the layer and use it as renderbuffer storage. This way, the color we draw ends up in the UIView without the need to copy the buffer contents again. That is done by calling the renderbufferStorage method of our context. The neat thing about this is, that we automatically have a buffer adjusted to the size of our view. The next two lines are just querying for the width and height of our framebuffer. glFramebufferRenderbuffer is very important. As we learned before, a framebuffer consists of several layers, called attachments. Here we tell OpenGL, that our currently bound framebuffer has a color buffer attached to it, namely our color renderbuffer. The index in GL_COLOR_ATTACHMENT_0 indicates that you can have several color attachments in one framebuffer, but that goes beyond the scope of this lesson.
As we just found out about the size of our actual render window, we need to pass this on to our lesson to make it actually render to the full screen size.
Given the case that we requested a depth buffer, then we perform pretty much the same steps as for the color renderbuffer, but this time we create the storage on our own by calling glRenderbufferStorage with details on which kind of data we store (DEPTH_COMPONENT16 is 16 bit per pixel) and how big the buffer is.
Finally we make sure that our framebuffer is ready to be rendered to. This is best practice with framebuffer objects (short FBOs) as there are many possibilities to go wrong :)
Everything we create has to be deleted again at some point. This is true for FBOs and their renderbuffers as well. As always we can only use OpenGL calls if we have a valid and current context, and we use glDeleteFramebuffers and glDeleteRenderbuffers for deletion.
Now let's look at the heart of our app, the drawFrame method. This is the method that is called every time a frame is rendered, invoked by our display link. Here we actually make sure that we have a context, that the framebuffer is created and bound, and the lesson is present and initialized. If all that is true, then we call lesson->draw()which will be the method we will mostly focus on in the following lessons. The last lines are pretty interesting. After having called lesson->draw() our renderbuffers now contain what we rendered. To actually tell the system that we have new information that shall be displayed, we bind the color buffer and ask our context to present it in [context presentRenderbuffer:GL_RENDERBUFFER]. We just mentioned the display link. Remember that we invoke startRenderLoop and stopRenderLoop from our AppDelegate to have the application refreshed periodically if it is active?
To begin updating our screen periodically, after making sure we haven't already set this up, we create a CADisplayLink for our screen, telling it what to do when the display needs to be redrawn. So we pass self as target and the selector drawFrame to the method displayLinkWithTarget. To actually force a refresh around 60 times per second, we need to add this displayLink to the system's runLoop, which is done with the next line. We get the system's runLoop by creating a NSRunLoop using its static method currentRunLoop and passing this to our displayLink's method addToRunLoop. Now we're actually rendering! But how do we stop this whenever our app moves to the background?
To stop the displayLink we just need to call its method invalidate. This automatically performs a clean up, so the pointer now points to some unitialized memory. That's why we set it to nil again, never ever leave pointers dangling! We're almost done with the class EAGLView, there are just two simple setters left to implement, a destructor (the dealloc method in Objective-C), and a callback method for a windowing event.
The two setters should be easy to understand. The callback layoutSubviews gets invoked whenever something about our UIView changes, so either it was resized or new subviews have been added to it. In this case we have to create our renderbuffers again because they become invalid through such an event. For this we just delete the buffers, as we know that drawFrame creates the framebuffer before rendering, if it is not present. The dealloc method cleans up everything that was created by it, namely the framebuffers and the context. For the framebuffers we use our delete method, the context has to be released as it is managed by the garbage collection. Finally we call our superclass' dealloc method, which is the UIView in this case. In Objective-C you have to invoke the dealloc method of the superclass manually, in C++ the destructor of the superclass is called automatically. LessonWe eventually end up in the class that plays the key role in all our following tutorials. A lesson is responsible for drawing every single frame, and for initializing our OpenGL stuff like loading images and shaders or sending geometry data to the graphic chip. See Basecode/Lesson.h
The first two lines include the OpenGL header files which define all the API. Then we define the class, which has a pretty simple interface. It has a constructor and a virtual destructor. In C++ every method that you are going to overwrite in derived classes has to be defined virtual in both, the super and the derived class. Remember that the constructors are never virtual, but the destructor should always be! The init and draw method define the interface for the core functionality. But as the class Lesson is just a generic interface, we don't want to implement this methods here but leave that to the derived classes (e.g. Lesson01). This is why the methods are not only virtual, but also set to zero. This is the C++ way of defining an abstract method, which implicitly makes the class Lesson abstract. An abstract class cannot be instanciated to prevent unallowed usage of these not implemented methods. We have two protected member variables for our renderbuffer size. Protected means they will be visible in derived classes, but nowhere else. We already saw in EAGLView::createFramebuffer that we called the method setRenderbufferSize to pass these values to the lesson, and here we receive this input. The implementation in Basecode/Lesson.mm is quite simple. The constructor does nothing but setting the size of the renderbuffer to zero, and the destructor does not need to clean up anything here as we haven't allocated any further memory from within the class.
The first interesting OpenGL call happens in setRenderbufferSize. After storing the width and height in our member variables, we call glViewport(int left, int bottom, int width, int height). With this we tell OpenGL which part of our screen we are drawing to. We specify that we want the whole screen by starting in the lower left and using the full width and height. Lesson01Now we know every piece of our application but the actual OpenGL part. This was necessary to get started, I promise from now on there will be less code and more explanations and images :)
For every lesson we will derive a new LessonXX class, putting in more and more features of OpenGL ES. This lesson's Lesson01.h is very simplistic, we just implement the necessary methods declared abstract in the superclass Lesson. In this lesson we will start with the simplest of all operations: clearing the screen. You can find them in Lesson01.mm. There we first define our constructor and destructor although they don't have to do anything yet.
When we initialize our lesson, we set the color we wish to use to overwrite everything that has been in there. Colors in OpenGL are usually specified as intensities for the color channels red, green, and blue (commonly known as RGB color). Each intensity is a floating point value between 0 (no intensity) and 1 (full intensity). Sometimes(e.g. in images like JPEG) the intensity is given as values between 0 and 255. This additional color model allows us to describe every color that a computer monitor can display. (Image taken from: http://en.wikipedia.org/wiki/File:RGB_farbwuerfel.jpg) So let's define our clearing color to plain red. This means we need full intensity in our red channel, and no intensity in green and blue.
Whats that? There's 4 parameters to glClearColor ? The last value specifies the alpha value (an RGBA color) which defines the opacity, which will be 1 for most surfaces. The alpha value can be used to mix the current and new color of each pixel (calld blending), but here we will set the value of each pixel to the RGBA-tupel and don't use blending, so actually the value does not matter. Now we also need to tell OpenGL that we want it to clear our color buffer. This is done with the command glClear(GL_COLOR_BUFFER_BIT). We will do this at the beginning of every frame, so put it into the draw() method.
Congratulations! You've just completed you first OpenGL application on iOS! Play around with the color a bit to make sure you understand the RGB color model, and next time we'll actually start drawing. Stay tuned! Carsten Downloads:Forum for this lesson: |
Support this Author
|