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.
Note: Just owning a Mac and an iPhone/iPod/iPad does not mean that you can actually run your self written applications on your device. Unfortunately, to run the apps on a device, you have to be a registered Apple Developer which costs 99$ per year. If you don't want to spend so much money just to try it, you can still develop on the iOS Simulator until your game idea is polished enough to be sold on the AppStore :)
Note: What is OpenGL ES anyway? OpenGL is an standard which defines an API to access drawing and geometry rasterization functionality. On the desktop it has evolved to version 4.2 now, providing access to the most recent features of new graphic cards, a lot of which deal with shaders. The OpenGL Shading Language (GLSL) is used to take control over specific parts of the graphics pipeline. We will see that in later tutorials. The ES stands for embedded systems, thus this API is tailored to mobile phones, consoles, and anything that is not a whole PC. GL ES is currently at version 2.0, and is quite similar to desktop OpenGL without the really advanced functionality.
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.
Note:If you want to create your own OpenGL ES project later on, then you can do so using the project wizard. The structure of that will be a bit more confusing, but it has the same components. The draw method would then be yourProjectNameViewController::drawFrame(). The lesson code here is simply cleaned up and I separated our OpenGL code from the window.
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
#import <UIKit/UIKit.h>
//standard main method, looks like that for every iOS app..
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"Running app");
int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release];
return retVal;
}
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
#import <UIKit/UIKit.h>
//we want to create variables of these classes, but don't need their implementation yet,
//so we just tell the compiler that they exist - called forward declaration
@class EAGLView;
class Lesson;
//This is our delegate class. It handles all messages from the device's operating system
@interface Lesson01AppDelegate : NSObject <UIApplicationDelegate> {
@private
//we store a pointer to our lesson so we can delete it at program shutdown
Lesson *lesson;
}
//we configure these variables in the interface builder (IB), thus they have to be declared as IBOutlet
//properties get accessor methods by synthesizing them in the source file (.mm)
//in this window we will embed a view which acts as OpenGL context
@property (nonatomic, retain) IBOutlet UIWindow *window;
//our main window, covering the whole screen
@property (nonatomic, retain) IBOutlet EAGLView *glView;
@end
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
#import "Lesson01AppDelegate.h"
#import "EAGLView.h"
#include "Lesson01.h"
//now we implement all methods needed by our delegate
@implementation Lesson01AppDelegate
//
@synthesize window;
@synthesize glView;
//this method tells us, that our application has started and we can set up our OpenGL things,
//as the window is set up, and thus our glView is going to be displayed soon
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//for the first lesson we don't need a depth buffer as we're not drawing any geometry yet
[glView setDepthBufferNeeded:FALSE];
//we create our lesson which contains the OpenGL code
//(allocated with new -> has to be cleaned up with delete!)
lesson = new Lesson01();
//we tell our OpenGL view which lesson we want to use for rendering.
[glView setLesson:lesson];
return YES;
}
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.
- (void)applicationWillResignActive:(UIApplication *)application
{
//the app is going to be suspended to the background,
//so we should stop our render loop
[glView stopRenderLoop];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
//we could do something here when the application entered the background
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
//we could start preparing stuff for becoming active again
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
//we're on stage! so let's draw some nice stuff
[glView startRenderLoop];
}
- (void)applicationWillTerminate:(UIApplication *)application
{
//before shutdown we stop the render loop
[glView stopRenderLoop];
}
//dealloc is the destructor in Objective-C, so clean up all allocated things
- (void)dealloc
{
[window release];
[glView release];
delete lesson;
[super dealloc];
}
@end
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:
//forward declarations again
@class EAGLContext;
class Lesson;
// This class combines our OpenGL context (which is our access to all drawing functionality)
// with a UIView that can be displayed on the iOS device. It handles the creation and presentation
// of our drawing surface, as well as handling the render loop which allows for seamless animations.
@interface EAGLView : UIView {
@private
// The pixel dimensions of the CAEAGLLayer.
GLint framebufferWidth;
GLint framebufferHeight;
// These are the buffers we render to: the colorRenderbuffer will contain the color that we will
// finaly see on the screen, the depth renderbuffer has to be used if we want to make sure, that
// we always see only the closest object and not just the one that has been drawn most recently.
// The framebuffer is a collection of buffers to use together while rendering, here it is either
// just the color buffer, or color and depth renderbuffer.
GLuint defaultFramebuffer, colorRenderbuffer, depthRenderbuffer;
// The display link is used to create a render loop
CADisplayLink* displayLink;
// Do we need a depth buffer
BOOL useDepthBuffer;
// The pointer to the lesson which we're rendering
Lesson* lesson;
// Did we already initialize our lesson?
BOOL lessonIsInitialized;
}
// The OpenGL context as a property (has autogenerated getter and setter)
@property (nonatomic, retain) EAGLContext *context;
// Configuration setters
- (void) setDepthBufferNeeded:(BOOL)needed;
- (void) setLesson:(Lesson*)newLesson;
//if we want OpenGL to repaint with the screens refresh rate, we use this render loop
- (void) startRenderLoop;
- (void) stopRenderLoop;
@end
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.
Note: The name EAGLView is not mandatory, but very common in iOS GL ES programs, because the context is called EAGLContext. EAGL probably stands for "Embedded AGL" where AGL are Apple's OpenGL Extensions.
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:
#import <QuartzCore/QuartzCore.h>
#import "EAGLView.h"
#include "Lesson.h"
//declare private methods, so they can be used everywhere in this file
@interface EAGLView (PrivateMethods)
- (void)createFramebuffer;
- (void)deleteFramebuffer;
@end
//start the actual implementation of our view here
@implementation EAGLView
//generate getter and setter for the context
@synthesize context;
// We have to implement this method
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
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).
//our EAGLView is the view in our MainWindow which will be automatically loaded to be displayed.
//when the EAGLView gets loaded, it will be initialized by calling this method.
- (id)initWithCoder:(NSCoder*)coder
{
//call the init method of our parent view
self = [super initWithCoder:coder];
//now we create the core animation EAGL layer
if (!self) {
return nil;
}
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
//we don't want a transparent surface
eaglLayer.opaque = TRUE;
//here we configure the properties of our canvas, most important is the color depth RGBA8 !
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:FALSE], kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
nil];
//create an OpenGL ES 2 context
context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
//if this failed or we cannot set the context for some reason, quit
if (!context || ![EAGLContext setCurrentContext:context]) {
NSLog(@"Could not create context!");
[self release];
return nil;
}
//do we want to use a depth buffer?
//for 3D applications we usually do, so we'll set it to true by default
useDepthBuffer = FALSE;
//we did not initialize our lesson yet:
lessonIsInitialized = FALSE;
//default values for our OpenGL buffers
defaultFramebuffer = 0;
colorRenderbuffer = 0;
depthRenderbuffer = 0;
return self;
}
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!
//on iOS, all rendering goes into a renderbuffer,
//which is then copied to the window by "presenting" it.
//here we create it!
- (void)createFramebuffer
{
//this method assumes, that the context is valid and current, and that the default framebuffer has not been created yet!
//this works, because as soon as we call glGenFramebuffers the value will be > 0
assert(defaultFramebuffer == 0);
NSLog(@"EAGLView: creating Framebuffer");
// Create default framebuffer object and bind it
glGenFramebuffers(1, &defaultFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);
// Create color render buffer
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
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.
//get the storage from iOS so it can be displayed in the view
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
//get the frame's width and height
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &framebufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &framebufferHeight);
//attach this color buffer to our framebuffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);
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.
//our lesson needs to know the size of the renderbuffer so it can work with the right aspect ratio
if(lesson != NULL)
{
lesson->setRenderbufferSize(framebufferWidth, framebufferHeight);
}
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.
if(useDepthBuffer)
{
//create a depth renderbuffer
glGenRenderbuffers(1, &depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
//create the storage for the buffer, optimized for depth values, same size as the colorRenderbuffer
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, framebufferWidth, framebufferHeight);
//attach the depth buffer to our framebuffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);
}
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.
//check that our configuration of the framebuffer is valid
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
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 :)
//deleting the framebuffer and all the buffers it contains
- (void)deleteFramebuffer
{
//we need a valid and current context to access any OpenGL methods
if (context) {
[EAGLContext setCurrentContext:context];
//if the default framebuffer has been set, delete it.
if (defaultFramebuffer) {
glDeleteFramebuffers(1, &defaultFramebuffer);
defaultFramebuffer = 0;
}
//same for the renderbuffers, if they are set, delete them
if (colorRenderbuffer) {
glDeleteRenderbuffers(1, &colorRenderbuffer);
colorRenderbuffer = 0;
}
if (depthRenderbuffer) {
glDeleteRenderbuffers(1, &depthRenderbuffer);
depthRenderbuffer = 0;
}
}
}
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.
//this is where all the magic happens!
- (void)drawFrame
{
//we need a context for rendering
if (context != nil)
{
//make it the current context for rendering
[EAGLContext setCurrentContext:context];
//if our framebuffers have not been created yet, do that now!
if (!defaultFramebuffer)
[self createFramebuffer];
glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);
//we need a lesson to be able to render something
if(lesson != nil)
{
//check whether we have to initialize the lesson
if(lessonIsInitialized == FALSE)
{
lesson->init();
lessonIsInitialized = TRUE;
}
//perform the actual drawing!
lesson->draw();
}
//finally, get the color buffer we rendered to, and pass it to iOS
//so it can display our awesome results!
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];
}
else
NSLog(@"Context not set!");
}
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?
//our render loop just tells the iOS device that we want to keep refreshing our view all the time
- (void)startRenderLoop
{
//check whether the loop is already running
if(displayLink == nil)
{
//the display link specifies what to do when the screen has to be redrawn,
//here we use the selector (method) drawFrame
displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(drawFrame)];
//by adding the display link to the run loop our draw method will be called 60 times per second
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
NSLog(@"Starting Render Loop");
}
}
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?
//we have to be able to stop the render loop
- (void)stopRenderLoop
{
if (displayLink != nil) {
//if the display link is present, we invalidate it (so the loop stops)
[displayLink invalidate];
displayLink = nil;
NSLog(@"Stopping Render Loop");
}
}
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.
//setter methods, should be straightforward
- (void) setDepthBufferNeeded:(BOOL)needed
{
useDepthBuffer = needed;
}
- (void) setLesson:(Lesson*)newLesson
{
lesson = newLesson;
//if we set a new lesson, it is not yet initialized!
lessonIsInitialized = FALSE;
}
//As soon as the view is resized or new subviews are added, this method is called,
//apparently the framebuffers are invalid in this case so we delete them
//and have them recreated the next time we draw to them
- (void)layoutSubviews
{
[self deleteFramebuffer];
}
//cleanup our view
- (void)dealloc
{
[self deleteFramebuffer];
[context release];
[super dealloc];
}
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
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
//this is our general lesson class, providing the two most important methods init and draw
//which will be invoked by our EAGLView
class Lesson
{
public:
//constructor
Lesson();
//the destructor has always to virtual!
virtual ~Lesson();
//abstract methods init and draw have to be defined in derived classes
virtual void init() = 0;
virtual void draw() = 0;
//we need to know the size of our drawing canvas (called renderbuffer here),
//so this method just saves the parameters in the member variables
virtual void setRenderbufferSize(unsigned int width, unsigned int height);
//all protected stuff will be visible within derived classes, but from nowhere else
protected:
//fields for the renderbuffer size
unsigned int m_renderbufferWidth, m_renderbufferHeight;
};
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.
#include "Lesson.h"
//Lesson constructor, set default values
Lesson::Lesson():
m_renderbufferWidth(0),
m_renderbufferHeight(0)
{
}
//Lesson destructor
Lesson::~Lesson()
{
//cleanup here
}
//save the renderbuffer size in the member variables
void Lesson::setRenderbufferSize(unsigned int width, unsigned int height)
{
m_renderbufferWidth = width;
m_renderbufferHeight = height;
glViewport(0, 0, m_renderbufferWidth, m_renderbufferHeight);
}
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 :)
//We derive our current lesson class from the general lesson class
class Lesson01 : public Lesson
{
public:
//overwrite all important methods
Lesson01();
virtual ~Lesson01();
virtual void init();
virtual void draw();
};
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.
#include "Lesson01.h"
//lesson constructor
Lesson01::Lesson01()
{
//initialize values
}
//lesson destructor
Lesson01::~Lesson01()
{
//do cleanup
}
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.
//initializing all OpenGL related things
void Lesson01::init()
{
NSLog(@"Init..");
//set the color we use for clearing our colorRenderbuffer to red
glClearColor(1.0, 0.0, 0.0, 1.0);
}
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.
?
22
23
24
25
26
27
28
29
//drawing a frame
void Lesson01::draw()
{
//clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);
//everything should be red now! yay :)
}
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:
* Lesson01 source code
Forum for this lesson:
* http://www.gamedev.net/topic/613789-nehe-ios-lesson-01-qa/
Your Blog is excellent!! Your RSS FEED does NOT point to right address......
回复删除Sorry for that, I try to repair it, but it still did not work. I will fix it as soon as possible.
回复删除