[ jimmyzhouj 翻譯] Nehe iOS OpenGL ES 2.0教程
引子: 最近要學習iOS 上的OpenGL ES的內容,在互聯網上找了一些教程來看。發現關於OpenGL ES2.0的教程不多。想起了知名的Nehe OpenGL 教程,就上nehe.gamedev.net上找了找,發現他們有一個用於移動設備的OpenGL ES教程,並且是基於OpenGL ES 2.0的。現在還只有兩篇教程,我先把這兩篇翻譯出來,以供大家學習。有什麼問題需要交流的,可以聯系我: [email protected]。 免責聲明:本教程翻譯原稿均來自互聯網,僅供學習交流之用,請勿進行商業傳播。同時,轉載時不要移除本聲明。如產生任何糾紛,均與本博客所有人和發表該翻譯稿之人無任何關系。謝謝合作! IOS Lesson 01 – 設置GL ES 原文鏈接地址:http://nehe.gamedev.net/tutorial/ios_lesson_01__setting_up_gl_es/44001/ 大家好,現在開始我們新的iOS OpenGL ES系列教程! 前言 當我們開始這個教程的時候,你該知道,我對Objective-C和iOS是一個新手,所以如果有什麼地方我弄錯了,或者什麼地方可以用更好的方法來實現,請告訴我。我只是在與OS交互的時候才使用Objective-C,所有和OpenGL相關的代碼都是用C++實現的。我會盡量解釋我們看到的所有地方,但我也假設你已經有基本的編程知識,並且懂得面向對象編程的基本概念。 教程附帶的代碼可以在iOS4,iOS5上工作,所以就算是在iPhone 4S上也能正常運行。 我們准備使用XCode4,如果你還沒有安裝的話,請到MAC APPStore上下載並安裝它。如果有人希望能夠在Windows或者Linux機器上開發iPhone App,我只能遺憾的告訴你不能這麼做。 我們知道,第一課看起來很恐怖,它可能是這一系列教程中最無聊的一課,到本節課結束也看不到什麼很酷的東西… :( 不過,這節課包含了很多有價值的內容,並且理解程序框架的不同部分之間的交互是非常重要的。所以你需要把它通讀一遍,你不需要理解所有的內容,當你之後想要知道細節的話,可以返回來再仔細看。按照 NeHe的的好習慣,我會盡量詳細解釋每一行代碼! 概要 讓我們先看看需要使用到的類。就像我之前說的,我們需要一些Objective-C的類紫色框)和一些C++的類青色框)。
和所有的C/C++程序一樣,應用程序的起點是main方法。我們執行UIApplicationMain,用InterfaceBuilder配置一個包含了EAGLView的UIWindow,並且用Lesson01AppDelegate去處理所有的事件。Window是UIApplicationMain自動創建的,可以顯示我們的view。View包含了我們的OpenGL conext,可以通過這個context訪問到用OpenGL ES來繪畫的canvas。
我們要鉤到操作系統的run loop中去,盡可能頻繁的重繪frame。在之後關於動畫的課程中這是必須的。繪畫動作是在Lesson對象中的draw方法中實現的,或者更精確的說,是在Lesson01對象中實現的,因為在類Lesson中,init)和draw)都是虛函數。 簡單浏覽 我們開始一步一步實現。你可以從這裡(http://code.google.com/p/nehe/downloads/list) 獲取代碼,然後打開項目文件Lesson01.xcodeproj。注意:你之後創建你自己的OpenGL ES 項目的時候,你可以使用project wizard。生成的項目的結構有一點復雜,不過總體構成是一樣的。Draw方法會在yourProjectNameViewController:drawFrame()裡。我們的代碼只是簡單的將他們都清除了,並且將我們的OpenGL代碼和window隔離開了而已。
你會在項目左側看到3個文件夾:Lesson01,Frameworks和Products。Lesson01還包含了2個子目錄Supporting Files和Basecode,Lesson01包括了我們所有的代碼。Frameworks包含了所有我們要用的或者項目需要使用的frameworks。從其他語言或者操作系統轉過來的開發者可以叫它們是libraries。Products列出類所有要生成的applications,現在就只有一個Lesson01.app。 在我們之後的課程的代碼中,我們主要和LessonXX 類打交道,每次AppDelegate都會生成一個當前課程的實例。在第一課,我們先詳細看一看Basecode和Supporting Files。 讓我們按照代碼執行時的訪問順序來看看這些文件。就像我之前指出的一樣,所有程序都是從main方法開始執行的。它在Lesson01/Supporting Files/main.m中。
這個方法在每個iPhone app裡都差不多是一樣的。NSAutoreleasePool是Objective-C的垃圾收集系統所需要的,我們打印一條log,然後是重要的部分:將應用程序的參數傳給UIApplicationMain方法,然後將控制權也轉給它。當最後程序的控制權轉移回來的時候,我們釋放garbage collection utility,用UIApplicationMain給出的返回值來結束程序。 就像它的名字一樣,UIApplicationMain方法運行一個用戶界面。在我們項目的設定中,選中Targets->Lesson01, 選Summary選項卡,MainInterface設置為MainWindow。這告訴App,程序啟動的時候,我們要顯示MainWindow.xib。這個文件也在Supporting Files目錄下,可以在InterfaceBuilder中打開。 打開MainWindow.xib,在編輯器的左邊欄,你可以看到Placeholder和Objects。在Objects下,有Lesson01AppDelegate和一個內嵌了View的Window。如果你沒有做過任何GUI編程,你可以認為這個就像你最喜歡的Office軟件(=window),打開了一個文檔(=view)。現在你可以在這些文檔(=views,包含了UI元素比如buttons,text fields或者OpenGL ES canvas)中切換,而不需要關閉整個程序。不過你需要有一個打開的文檔(view)才能看到其中的內容。
- #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;
- }
當你control-click(或者右鍵)Lesson01AppDelegate的時候,你會看見2個定義好的outlets:glView和window,它們分別關聯到InterfaceBuilder中的View和Window。一個Outlet可以讓app delegate代碼中的一個變量包含了它所關聯的InterfaceBuilder中元素的引用。
Lesson01AppDelegate 我們來看第一個重要的代碼文件:Lesson01AppDelegate.h
在Objective-C風格的類定義中,我們首先在頭文件聲明了interface,之後在相應的源文件(.mm)中使用implementation實現方法。從上面的代碼中還可以看出,有一些成員變量,window, glView, lesson,這些變量會在AppDelegate初始化的時候被創建。要注意的是,properties定義為IBOutlet後,才能夠被InterfaceBuilder使用。由於要處理窗口的事件,當程序開始運行的時候,會自動創建一個AppDelegate的對象實例。為了能處理窗口事件,AppDelegate實現了接口(或者叫protocol) UIApplicationDelegate。Protocol添加在類名的定義後,用<>符號包起來。 窗口創建後,觸發的第一個事件是didFinishLauchingWithOptions。代碼如下,在Lesson01AppDelegate.mm中。
- #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
首先,我們配置了glView,然後創建了一個lesson對象的實例。要注意的是,我們定義的成員變量lesson是指向Lesson對象的指針,但是由於Lesson01是Lesson的子類,所以它們提供了相同的接口,可以這麼使用。 還要注意,代碼中synthesize了頭文件定義的兩個property,這會自動生產getter和setter方法。 Lesson01AppDelegate.mm中的其余代碼部分處理了窗口觸發的其他的事件,這些事件都和應用變為可見,或者成為後台程序有關。當變為可見時,我們讓view周期性的刷新。當應用被移到後台或者關閉後,停止刷新。最後,當AppDelegate被釋放時,它的dealloc方法被調用,我們在這個方法裡釋放之前分配的內存。在Objective-C裡用release釋放,在 C++裡,用new來創建,用delete來刪除。
- #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;
- }
現在讓我們來看包含OpenGL context 的創建和負責繪畫的類。 EAGLView 正如我們所知,當運行UIApplicationMain時,會自動創建window和delegate。因為我們在delegate中定義了對應的outlet,當Window被創建時,它知道需要一個EAGLView作為Window所包含的View。所以Window會自動創建一個view來顯示。我們先看EAGLView.h
- - (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
類EAGLView來源於UIView。作為UIView的子類,我們可以重寫(overwrite)一些方法(在Objective-C中叫selector),下面我們會在源文件中看見。
- //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
注解:並不是強制必須用EAGLView這個名字,但在iOS GL ES程序中通常都用它,因為context被稱為EAGLContext。EAGL可能代表”Embedded AGL”,AGL是APPLE的OPENGL擴展。
正如我們所見,EAGLView封裝了OpenGL ES context。OpenGL context被認為是允許使用OpenGL調用來繪畫的許可。它跟蹤記錄了我們設定的所有狀態,比如說當前的顏色,當前哪一幅圖片被用來做紋理。Context要和canvas我們可以繪畫的地方)配合起來使用。Canvas通過一種叫framebuffer的構造來實現。framebuffer由存儲不同信息的多層buffer組成。我們常用到的兩個層是color renderbuffer和depth renderbuffer。Color renderbuffer裡面存儲了每個像素點的每個color channel,就像JPEG圖像一樣。這是最終顯示在屏幕上的內容。The depth renderbuffer記錄了color buffer中的每個像素點離屏幕的距離。如果我們畫了一座距離屏幕10單位遠的房子和一個距離屏幕5單位遠的人,則不管先畫的是房子還是人,人總是顯示在房子的前面。這被稱為是深度測試。Depth buffer內容不會被顯示。
EAGLView類的大部分成員變量如下:我們存儲了framebuffer的寬度和高度(顏色緩存和深度緩存尺寸也和這一樣),我們還保存的各個緩存的ID,用來作為在OpenGl中使用時的名字。 下面是CADisplayLink成員,它允許我們進入系統的主循環,並請求按一秒約60次的頻率重繪。我們還有一個開關用於啟用/禁用深度緩存。因為緩存很消耗顯示芯片的寶貴內存,如果不需要深度的話,我們應該禁用深度緩存。 我們還需要一個指針指向我們的lesson對象,這樣就可以調用draw()方法了。還需要一個標志來表示是否已經初始化了這個lesson對象。 之前一直在說的context被保存為EAGLContext property. 還有4個方法是AppDelegate要使用的,這4個方法的名字已經很好的解釋了它們的功能。 下面我們先看看EAGLView.mm中初始化的部分。
首先我們給類增加了幾個私有方法的聲明。如果不這麼做的話,我們就必須把這幾個方法的實現代碼放在調用它們的代碼的前面。為了保持頭文件的簡潔,我們不把聲明放到頭文件中,不過我們現在這麼做也沒有真正增加源碼的可讀性。
- #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];
- }
接下來我們開始實現EAGLView,合成(synthesize) context以自動生成getter和setter方法。然後需要重寫UIView的layerClass方法,因為現在我們用的view不是標准的UI元素,而是要畫到一個CAEAGL層上(CA指 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;
- }
首先檢查是否還沒有framebuffer。如果還沒有,調用OpenGL的方法來產生一個framebuffer對應的ID,這樣產生的ID可以確保是唯一的,並且ID總是大於0。得到ID後,我們將framebuffer和ID綁定。OpenGL會跟蹤活動對象的大部分事情,比如活動的framebuffer,最後設定的顏色,活動的紋理或者活動著色器程序(shader program)等等。這導致下面所有關於framebuffer的API都影響當前綁定的framebuffer。 下面對color renderbuffer做同樣的工作,產生一個ID並且綁定。
- //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);
這個color renderbuffer是特殊的。我們希望我們繪畫的顏色可以被用於UIView的顏色,這就是為什麼之前我們要創建 CAEAGLLayer。現在我們需要得到layer,將它用於renderbuffer存儲。這樣做,我們不需要再一次拷貝緩存中的內容就可以將設置的顏色顯示在UIView上。 這要通過調用context的 renderbufferStorage方法來實現。函數實現的很精巧,可以自動得到一個和view的尺寸相適應的buffer。再下面兩行用來查詢framebuffer的寬度和高度。
- //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);
glFramebufferRenderbuffer是非常重要的。一個framebuffer由多個layer組成,這些layer被稱為attachments。這裡我們告訴OpenGL,當前綁定的framebuffer要附加一個名字為colorrenderbuffer的color buffer。GL_COLOR_ATTACHMENT_0表示一個framebuffer可以有幾個color attachments,不過這個內容超過了本節課程的范圍了。
我們剛剛知道了實際的渲染窗口的大小,需要將這個參數傳遞給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);
- }
當需要深度緩存的時候,我們可以像之前顏色緩存那樣實現。這一次需要通過調用glRenderbufferStorage方法自己創建存儲,需要提供一些參數,如存儲何種類型的數據(DEPTH_COMPOMENT16是每個像素16位)和buffer需要多大。
- 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);
- }
最後我們需要確認framebuffer已經准備好了。因為有很多種可能會出錯的情況,所以最好對每一個framebuffer對象(短FBO)都執行檢查。
- //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));
- }
在某個時間點,我們創建的所有東西都需要被刪除掉,FBOs和renderbuffers也不例外。我們有一個有效的且是當前的context的話,就可以使用OpenGL函數了。通過調用glDeleteFramebuffers和glDeleteRenderbuffers來刪除。
- //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;
- }
- }
- }
現在是整個app中最關鍵的部分,drawFrame方法。當我們的display link觸發,每次frame被渲染的時候,都會調用這個方法。首先我們要確保有一個context,framebuffer已經創建並且綁定了,lesson對象已經創建並且初始化了。如果前面幾條都實現了,我們調用lesson->draw()方法,這個方法是後面的課程著力聚焦的地方。最後幾行很有趣。調用完lesson->draw()後,renderbuffers已經有需要渲染的內容了。為了告訴系統有新內容要顯示,需要綁定color buffer並且要求context將它呈現出來,調用的方法是[context presentRenderbuffer:GL_RENDERBUFFER。 我們剛剛提到了display link。還記得我們從AppDelegate中調用startRenderLoop和stopRenderLoop,使得當應用在活動時,可以周期性的刷新。
- //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!");
- }
為了開始周期性的更新屏幕,當確定沒有設置好displayLink時,創建一個屏幕的CADisplayLink,並且告訴它重繪屏幕的時候它需要做什麼。我們將target設為self,選擇selector drawFrame傳遞給方法displayLinkWithTarget。為了得到大約每秒60次左右的刷新率,需要將displayLink添加到系統的runloop裡去。代碼的下一行實現這個功能,我們調用NSRunLoop 的靜態方法currentRunloop來的到系統的runLoop,然後將它傳遞給displayLink的addToRunLoop方法。 現在我們可以渲染了!但是,當app進入後台的時候,如何停止渲染呢?
- //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");
- }
- }
為了停止displayLink,我們只需要調用方法invalidate即可。它自動執行清理工作。方法執行完畢後,指針指向的是未初始化的內存。我們需要把指針指向nil,千萬不要讓指針成為野指針! 我們差不多將EAGLView分析完畢了,只剩下兩個簡單的setter方法,一個析構函數(在Objective-C中叫dealloc方法),和一個窗口事件的回調函數。
- //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");
- }
- }
兩個setter方法很容易理解。當UIView發生變化時,比如UIView的尺寸變了,或者有新的添加進來了新的subview,回調函數layoutSubviews就會被觸發。在回調函數裡,我們只需要刪除framebuffer就可以了,因為drawFrame方法中,如果在渲染時發現framebuffer不存在的話,會生成一個新的framebuffer。
- //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];
- }
Dealloc方法清除我們創建的一切,即framebuffers和context。我們調用deleteFramebuffer來清除framebuffer。我們用release方法來釋放context,因為它是通過garbage collection來管理的。最後我們調用父類的dealloc方法,在這裡父類是UIView。在Objective-C裡需要手動調用父類的dealloc方法,而在C++裡父類的析構函數會被自動調用。
Lesson 我們終於研究到了將在以後的課程中起到重要作用的類了。Lesson負責繪制每一個簡單frame,它還初始化一些OpenGL的東西,比如加載圖片,加載shaders,或者向顯示芯片傳送幾何數據。看Basecode/Lesson.h
前兩行代碼包含了OpenGL頭文件,這些頭文件定義了所有的API。然後定義了類,它的接口很簡單。它有一個構造函數和一個虛析構函數。在C++裡,所有在派生類中要重寫的函數,必須在父類和派生類中都定義為虛函數。記住,構造函數不能是虛函數,但是析構函數一定是虛函數。 init方法和draw方法定義了類接口的核心功能。因為Lesson只是一個通用接口,我們不希望在這裡實現這兩個方法,而是打算放在Lesson的派生類(比如Lesson01)中實現。這就是為什麼這兩個函數不但是虛函數,還被設為0的原因。在C++中這樣做暗示了這個類是抽象類。抽象類是不能實例化的,這樣就避免了沒有實現的函數被非法調用。 還有兩個protected的成員變量,用於記錄renderbuffer的尺寸。protected指的是它們只在在當前類或者其派生類中可見,其他地方則不可見。我們在EAGLView中的createFramebuffer中調用了setRenderbufferSize方法將參數傳給lesson,在這裡我們接收輸入的參數。 Basecode/Lesson.mm的實現是相當簡單的。構造函數只是將renderbuffer的尺寸設為0,析構函數什麼清理也不用做,因為在這裡我們沒有分配任何存儲空間。
- #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;
- };
在setRenderbuffSize函數裡有了一個有趣的OpenGL函數調用。在把寬度和高度保存到成員變量後,我們調用了glViewport(int left, int bottom, int width, int height)。通過這個函數,我們告訴OpenGL我們打算繪制到屏幕的哪一部分。通過把起始點設為左下角,使用全部的寬度和高度,我們指明了需要用到整個屏幕。 Lesson01 現在我們已經知道了應用程序的每一小部分,除了實際繪圖的OpenGL部分。現在該開始了解了,我希望從現在起可以有較少的代碼和較多的解釋和圖片:)
- #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);
- }
每一課我們都會派生出一個新的LessonXX的類,逐步加入越來越多的OpenGl特性。這節課的Lesson.h非常簡單,我們只是實現了在超類Lesson中定義的那些抽象方法。 這節課我們開始最簡單的操作:清除屏幕。代碼在Lesson01.mm中。我們首先定義構造函數和析構函數,這兩個函數什麼也不做。
- //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();
- };
當我們初始化lesson對象的時候,設定想要使用的顏色來覆蓋原有的任何顏色。在OpenGL中,顏色被指定為紅,綠,藍顏色通道我們熟知的RGB顏色)的強度(intensity)。強度值是一個介於0(無強度)和1(最大強度)之間的浮點數。有時候(比如在JPEG圖像中)強度值是介於0和255之間的。下面添加的顏色模型允許我們描述計算機顯示器可以顯示的所有顏色。
- #include "Lesson01.h"
- //lesson constructor
- Lesson01::Lesson01()
- {
- //initialize values
- }
- //lesson destructor
- Lesson01::~Lesson01()
- {
- //do cleanup
- }
(圖片來源: http://en.wikipedia.org/wiki/File:RGB_farbwuerfel.jpg)
我們將清除顏色設為紅色。這意味著將紅色通道設為最大強度,綠色和藍色通道設為0強度。
看到沒,要傳遞4個參數給glClearColor?最後一個參數指明了alpha值(RGBA顏色),alpha值定義了opacity,這個值在絕大部分面上都設為1。alpha值可用於每一個像素點混合當前的顏色和新的顏色(稱為blending),因為在這裡我們將每個像素的值設為RGBA-tupel,不使用 blending,所以實際上alpha值是無關緊要的。 下面我們告訴OpenGL我們希望清除color buffer。這可以用glClear(GL_COLOR_BUFFER_BIT)命令來實現。因為我們在每一幀的開始都要用這個命令,所以把它放到draw()方法中。
- //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);
- }
祝賀你!你已經完成了你在iOS上的第一個OpenGL程序了。試試別的顏色,確信你已經理解了RGB顏色模型,下一次我們就真的要開始繪畫了。 敬請關注! Carsten
- //drawing a frame
- void Lesson01::draw()
- {
- //clear the color buffer
- glClear(GL_COLOR_BUFFER_BIT);
- //everything should be red now! yay :)
- }