1. 程式人生 > >iOS OpenGL ES2.0 開發例項

iOS OpenGL ES2.0 開發例項

本教程原始碼地址下載:https://github.com/wanglixin1999/HelloGL


OpenGL ES 是可以在iphone上實現2D和3D圖形程式設計的低階API。

如果你之前接觸過 cocos2d,sparrow,corona,unity 這些框架,你會發現其實它們都是基於OpenGL上建立的。

多數程式設計師選擇使用這些框架,而不是直接呼叫OpenGL,因為OpenGL實在是太難用了。

而這篇教程,就是為了讓大家更好地入門而寫的。 

在這個系列的文章中,你可以通過一些實用又容易上手的實驗,建立類似hello world的APP。例如顯示一些簡單的立體圖形。

流程大致如下:

·建立一個簡單的OpenGL app

·編譯並執行 vertex & fragment shaders

·通過vertex buffer,在螢幕上渲染一個簡單矩形

·使用投影和 model-view 變形。

·渲染一個可以 depth testing的3D物件。

說明:

我並非OpenGL的專家,這些完全是通過自學得來的。如果大家發現哪些不對的地方,歡迎指出。

OpenGL ES1.0 和 OpenGL ES2.0

第一件你需要搞清楚的事,是OpenGL ES 1.0 和 2.0的區別。

他們有多不一樣?我只能說他們很不一樣。

OpenGL ES1.0:

針對固定管線硬體(fixed pipeline),通過它內建的functions來設定諸如燈光、,vertexes(圖形的頂點數),顏色、camera等等的東西。

OpenGL ES2.0:

針對可程式設計管線硬體(programmable pipeline),基於這個設計可以讓內建函式見鬼去吧,但同時,你得自己動手編寫任何功能。

“TMD”,你可能會這麼想。這樣子我還可能想用2.0麼?

但2.0確實能做一些很cool而1.0不能做的事情,譬如:toon shader(貼材質).

利用opengles2.0,甚至還能建立下面的這種很酷的燈光和陰影效果:

OpenGL ES2.0只能夠在iphone 3GS+、iPod Touch 3G+ 和所有版本的ipad上執行。慶幸現在大多數使用者都在這個範圍。

開始吧

儘管Xcode自帶了OpenGL ES的專案模板,但這個模板自行建立了大量的程式碼,這樣會讓初學者感到迷惘。

因此我們通過自行編寫的方式來進行,通過一步一步編寫,你能更清楚它的工作機制。

啟動Xcode,新建專案。

點選下一步,把這個專案命名為HelloOpenGL,點選下一步,選擇存放目錄,點選“建立”。

接下來,你要在這個OpenGLView.m 檔案下加入很多程式碼。

1)  新增必須的framework (框架)

加入:OpenGLES.frameworks 和 QuartzCore.framework

在專案的Groups&Files 目錄下,選擇target “HelloOpenGL”,展開Link Binary with Libraries部分。這裡是專案用到的框架。

“+”新增,選擇OpenGLES.framework, 重複一次把QuartzCore.framework也新增進來。

2)修改OpenGLView.h

如下:引入OpenGL的Header,建立一些後面會用到的例項變數。

  1. #import <UIKit/UIKit.h> 
  2. #import <QuartzCore/QuartzCore.h> 
  3. #include <OpenGLES/ES2/gl.h> 
  4. #include <OpenGLES/ES2/glext.h> 
  5. @interface OpenGLView : UIView { 
  6.     CAEAGLLayer* _eaglLayer; 
  7.     EAGLContext* _context; 
  8.     GLuint _colorRenderBuffer; 
  9. @end 

3)設定layer class 為 CAEAGLLayer

  1. + (Class)layerClass { 
  2.     return [CAEAGLLayer class]; 

想要顯示OpenGL的內容,你需要把它預設的layer設定為一個特殊的layer。(CAEAGLLayer)。這裡通過直接複寫layerClass的方法。

4) 設定layer為不透明(Opaque)

  1. - (void)setupLayer { 
  2.     _eaglLayer = (CAEAGLLayer*) self.layer; 
  3.     _eaglLayer.opaque = YES; 

因為預設的話,CALayer是透明的。而透明的層對效能負荷很大,特別是OpenGL的層。

(如果可能,儘量都把層設定為不透明。另一個比較明顯的例子是自定義tableview cell)

5)建立OpenGL context

  1. - (void)setupContext {    
  2.     EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2; 
  3.     _context = [[EAGLContext alloc] initWithAPI:api]; 
  4.     if (!_context) { 
  5.         NSLog(@"Failed to initialize OpenGLES 2.0 context"); 
  6.         exit(1); 
  7.     } 
  8.     if (![EAGLContext setCurrentContext:_context]) { 
  9.         NSLog(@"Failed to set current OpenGL context"); 
  10.         exit(1); 
  11.     } 

無論你要OpenGL幫你實現什麼,總需要這個 EAGLContext

EAGLContext管理所有通過OpenGL進行draw的資訊。這個與Core Graphics context類似。

當你建立一個context,你要宣告你要用哪個version的API。這裡,我們選擇OpenGL ES 2.0.

(容錯處理,如果建立失敗了,我們的程式會退出)

6)建立render buffer (渲染緩衝區)

  1. - (void)setupRenderBuffer { 
  2.     glGenRenderbuffers(1, &_colorRenderBuffer); 
  3.     glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);         
  4.     [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];     

Render buffer 是OpenGL的一個物件,用於存放渲染過的影象。

有時候你會發現render buffer會作為一個color buffer被引用,因為本質上它就是存放用於顯示的顏色。

建立render buffer的三步:

1.     呼叫glGenRenderbuffers來建立一個新的render buffer object。這裡返回一個唯一的integer來標記render buffer(這裡把這個唯一值賦值到_colorRenderBuffer)。有時候你會發現這個唯一值被用來作為程式內的一個OpenGL 的名稱。(反正它唯一嘛)

2.     呼叫glBindRenderbuffer ,告訴這個OpenGL:我在後面引用GL_RENDERBUFFER的地方,其實是想用_colorRenderBuffer。其實就是告訴OpenGL,我們定義的buffer物件是屬於哪一種OpenGL物件

3.     最後,為render buffer分配空間。renderbufferStorage

7)建立一個 frame buffer (幀緩衝區)

  1. - (void)setupFrameBuffer {     
  2.     GLuint framebuffer; 
  3.     glGenFramebuffers(1, &framebuffer); 
  4.     glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); 
  5.     glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,  
  6.         GL_RENDERBUFFER, _colorRenderBuffer); 
  7.  } 

Frame buffer也是OpenGL的物件,它包含了前面提到的render buffer,以及其它後面會講到的諸如:depth buffer、stencil buffer 和 accumulation buffer。

前兩步建立frame buffer的動作跟建立render buffer的動作很類似。(反正也是用一個glBind什麼的)

而最後一步  glFramebufferRenderbuffer 這個才有點新意。它讓你把前面建立的buffer render依附在frame buffer的GL_COLOR_ATTACHMENT0位置上。

8)清理螢幕

  1. - (void)render { 
  2.     glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0); 
  3.     glClear(GL_COLOR_BUFFER_BIT); 
  4.     [_context presentRenderbuffer:GL_RENDERBUFFER]; 

為了儘快在螢幕上顯示一些什麼,在我們和那些 vertexes、shaders打交道之前,把螢幕清理一下,顯示另一個顏色吧。(RGB 0, 104, 55,綠色吧)

這裡每個RGB色的範圍是0~1,所以每個要除一下255.

下面解析一下每一步動作:

1.      呼叫glClearColor ,設定一個RGB顏色和透明度,接下來會用這個顏色塗滿全屏。

2.      呼叫glClear來進行這個“填色”的動作(大概就是photoshop那個油桶嘛)。還記得前面說過有很多buffer的話,這裡我們要用到GL_COLOR_BUFFER_BIT來宣告要清理哪一個緩衝區。

3.      呼叫OpenGL context的presentRenderbuffer方法,把緩衝區(render buffer和color buffer)的顏色呈現到UIView上。

9)把前面的動作串起來修改一下OpenGLView.m

  1. // Replace initWithFrame with this 
  2. - (id)initWithFrame:(CGRect)frame 
  3.     self = [super initWithFrame:frame]; 
  4.     if (self) {         
  5.         [self setupLayer];         
  6.         [self setupContext];                 
  7.         [self setupRenderBuffer];         
  8.         [self setupFrameBuffer];                 
  9.         [self render];         
  10.     } 
  11.     return self; 
  12. // Replace dealloc method with this 
  13. - (void)dealloc 
  14.     [_context release]; 
  15.     _context = nil; 
  16.     [super dealloc]; 

10)把App Delegate和OpenGLView 連線起來

在HelloOpenGLAppDelegate.h 中修改一下:

  1. // At top of file 
  2. #import "OpenGLView.h" 
  3. // Inside @interface 
  4. OpenGLView* _glView; 
  5. // After @interface 
  6. @property (nonatomic, retain) IBOutlet OpenGLView *glView; 

接下來修改.m檔案:

  1. // At top of file 
  2. @synthesize glView=_glView; 
  3. // At top of application:didFinishLaunchingWithOptions 
  4. CGRect screenBounds = [[UIScreen mainScreen] bounds];     
  5. self.glView = [[[OpenGLView alloc] initWithFrame:screenBounds] autorelease]; 
  6. [self.window addSubview:_glView]; 
  7. // In dealloc 
  8. [_glView release]; 

一切順利的話,你就能看到一個新的view在螢幕上顯示。

這裡是OpenGL的世界。

新增shaders:頂點著色器和片段著色器

在OpenGL ES2.0 的世界,在場景中渲染任何一種幾何圖形,你都需要建立兩個稱之為“著色器”的小程式。

著色器由一個類似C的語言編寫- GLSL。知道就好了,我們不深究。

這個世界有兩種著色器(Shader):

·Vertex shaders – 在你的場景中,每個頂點都需要呼叫的程式,稱為“頂點著色器”。假如你在渲染一個簡單的場景:一個長方形,每個角只有一個頂點。於是vertex shader 會被呼叫四次。它負責執行:諸如燈光、幾何變換等等的計算。得出最終的頂點位置後,為下面的片段著色器提供必須的資料。

·Fragment shaders – 在你的場景中,大概每個畫素都會呼叫的程式,稱為“片段著色器”。在一個簡單的場景,也是剛剛說到的長方形。這個長方形所覆蓋到的每一個畫素,都會呼叫一次fragment shader。片段著色器的責任是計算燈光,以及更重要的是計算出每個畫素的最終顏色。

下面我們通過簡單的例子來說明。

開啟你的xcode,File\New\New File… 選擇iOS\Other\Empty, 點選下一步。命名為:

SimpleVertex.glsl 點選儲存。

開啟這個檔案,加入下面的程式碼:

  1. attribute vec4 Position; // 1 
  2. attribute vec4 SourceColor; // 2 
  3. varying vec4 DestinationColor; // 3 
  4. void main(void) { // 4 
  5.     DestinationColor = SourceColor; // 5 
  6.     gl_Position = Position; // 6 

我們一行一行解析:

1 “attribute”聲明瞭這個shader會接受一個傳入變數,這個變數名為“Position”。在後面的程式碼中,你會用它來傳入頂點的位置資料。這個變數的型別是“vec4”,表示這是一個由4部分組成的向量。

2 與上面同理,這裡是傳入頂點的顏色變數。

3 這個變數沒有“attribute”的關鍵字。表明它是一個傳出變數,它就是會傳入片段著色器的引數。“varying”關鍵字表示,依據頂點的顏色,平滑計算出頂點之間每個畫素的顏色。

文字比較難懂,我們一圖勝千言:

圖中的一個畫素,它位於紅色和綠色的頂點之間,準確地說,這是一個距離上面頂點55/100,距離下面頂點45/100的點。所以通過過渡,能確定這個畫素的顏色。

4 每個shader都從main開始– 跟C一樣嘛。

5 設定目標顏色 = 傳入變數:SourceColor

6 gl_Position 是一個內建的傳出變數。這是一個在 vertex shader中必須設定的變數。這裡我們直接把gl_Position = Position; 沒有做任何邏輯運算。

一個簡單的vertex shader 就是這樣了,接下來我們再建立一個簡單的fragment shader。

新建一個空白檔案:

File\New\New File… 選擇iOS\Other\Empty

命名為:SimpleFragment.glsl 儲存。

開啟這個檔案,加入以下程式碼:

  1. varying lowp vec4 DestinationColor; // 1 
  2. void main(void) { // 2 
  3.     gl_FragColor = DestinationColor; // 3 

下面解析:

1 這是從vertex shader中傳入的變數,這裡和vertex shader定義的一致。而額外加了一個關鍵字:lowp。在fragment shader中,必須給出一個計算的精度。出於效能考慮,總使用最低精度是一個好習慣。這裡就是設定成最低的精度。如果你需要,也可以設定成medp或者highp.

2 也是從main開始嘛

3 正如你在vertex shader中必須設定gl_Position, 在fragment shader中必須設定gl_FragColor.

這裡也是直接從 vertex shader中取值,先不做任何改變。

還可以吧?接下來我們開始運用這些shader來建立我們的app。

編譯 Vertex shader 和 Fragment shader

目前為止,xcode僅僅會把這兩個檔案copy到application bundle中。我們還需要在執行時編譯和執行這些shader。

你可能會感到詫異。為什麼要在app執行時編譯程式碼?

這樣做的好處是,我們的著色器不用依賴於某種圖形晶片。(這樣才可以跨平臺嘛)

下面開始加入動態編譯的程式碼,開啟OpenGLView.m

在initWithFrame: 方法上方加入:

  1. - (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType { 
  2.     // 1 
  3.     NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName  
  4.         ofType:@"glsl"]; 
  5.     NSError* error; 
  6.     NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath  
  7.         encoding:NSUTF8StringEncoding error:&error]; 
  8.     if (!shaderString) { 
  9.         NSLog(@"Error loading shader: %@", error.localizedDescription); 
  10.         exit(1); 
  11.     } 
  12.     // 2 
  13.     GLuint shaderHandle = glCreateShader(shaderType);     
  14.     // 3 
  15. constchar* shaderStringUTF8 = [shaderString UTF8String];     
  16.     int shaderStringLength = [shaderString length]; 
  17.     glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength); 
  18.     // 4 
  19.     glCompileShader(shaderHandle); 
  20.     // 5 
  21.     GLint compileSuccess; 
  22.     glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess); 
  23.     if (compileSuccess == GL_FALSE) { 
  24.         GLchar messages[256]; 
  25.         glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]); 
  26.         NSString *messageString = [NSString stringWithUTF8String:messages]; 
  27.         NSLog(@"%@", messageString); 
  28.         exit(1); 
  29.     } 
  30.     return shaderHandle; 

下面解析:

1 這是一個UIKit程式設計的標準用法,就是在NSBundle中查詢某個檔案。大家應該熟悉了吧。

2 呼叫 glCreateShader來建立一個代表shader 的OpenGL物件。這時你必須告訴OpenGL,你想建立 fragment shader還是vertex shader。所以便有了這個引數:shaderType

3 呼叫glShaderSource ,讓OpenGL獲取到這個shader的原始碼。(就是我們寫的那個)這裡我們還把NSString轉換成C-string

5 大家都是程式設計師,有程式的地方就會有fail。有程式設計師的地方必然會有debug。如果編譯失敗了,我們必須一些資訊來找出問題原因。 glGetShaderiv 和 glGetShaderInfoLog  會把error資訊輸出到螢幕。(然後退出)

我們還需要一些步驟來編譯vertex shader 和frament shader。

- 把它們倆關聯起來

- 告訴OpenGL來呼叫這個程式,還需要一些指標什麼的。

在compileShader: 方法下方,加入這些程式碼

  1. - (void)compileShaders { 
  2.     // 1 
  3.     GLuint vertexShader = [self compileShader:@"SimpleVertex"  
  4.         withType:GL_VERTEX_SHADER]; 
  5.     GLuint fragmentShader = [self compileShader:@"SimpleFragment"  
  6.         withType:GL_FRAGMENT_SHADER]; 
  7.     // 2 
  8.     GLuint programHandle = glCreateProgram(); 
  9.     glAttachShader(programHandle, vertexShader); 
  10.     glAttachShader(programHandle, fragmentShader); 
  11.     glLinkProgram(programHandle); 
  12.     // 3 
  13.     GLint linkSuccess; 
  14.     glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess); 
  15.     if (linkSuccess == GL_FALSE) { 
  16.         GLchar messages[256]; 
  17.         glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]); 
  18.         NSString *messageString = [NSString stringWithUTF8String:messages]; 
  19.         NSLog(@"%@", messageString); 
  20.         exit(1); 
  21.     } 
  22.     // 4 
  23.     glUseProgram(programHandle); 
  24.     // 5 
  25.     _positionSlot = glGetAttribLocation(programHandle, "Position"); 
  26.     _colorSlot = glGetAttribLocation(programHandle, "SourceColor"); 
  27.     glEnableVertexAttribArray(_positionSlot); 
  28.     glEnableVertexAttribArray(_colorSlot); 

下面是解析:

1       用來呼叫你剛剛寫的動態編譯方法,分別編譯了vertex shader 和 fragment shader

4       呼叫 glUseProgram  讓OpenGL真正執行你的program

5       最後,呼叫 glGetAttribLocation 來獲取指向 vertex shader傳入變數的指標。以後就可以通過這寫指標來使用了。還有呼叫 glEnableVertexAttribArray來啟用這些資料。(因為預設是 disabled的。)

最後還有兩步:

1 在 initWithFrame方法裡,在呼叫render之前要加入這個:

  1. [self compileShaders]; 

2 在@interface in OpenGLView.h 中新增兩個變數:

  1. GLuint _positionSlot; 
  2. GLuint _colorSlot; 

編譯!執行!

如果你仍能正常地看到之前那個綠色的螢幕,就證明你前面寫的程式碼都很好地工作了。

為這個簡單的長方形建立 Vertex Data!

在這裡,我們打算在螢幕上渲染一個正方形,如下圖:

在你用OpenGL渲染圖形的時候,時刻要記住一點,你只能直接渲染三角形,而不是其它諸如矩形的圖形。所以,一個正方形需要分開成兩個三角形來渲染。

圖中分別是頂點(0,1,2)和頂點(0,2,3)構成的三角形。

OpenGL ES2.0的一個好處是,你可以按你的風格來管理頂點。

開啟OpenGLView.m檔案,建立一個純粹的C結構以及一些array來跟蹤我們的矩形資訊,如下:

  1. typedef struct { 
  2.     float Position[3]; 
  3.     float Color[4]; 
  4. } Vertex; 
  5. const Vertex Vertices[] = { 
  6.     {{1, -1, 0}, {1, 0, 0, 1}}, 
  7.     {{1, 1, 0}, {0, 1, 0, 1}}, 
  8.     {{-1, 1, 0}, {0, 0, 1, 1}}, 
  9.     {{-1, -1, 0}, {0, 0, 0, 1}} 
  10. }; 
  11. const GLubyte Indices[] = { 
  12.      0, 1, 2, 
  13.      2, 3, 0 
  14. }; 

這段程式碼的作用是:

1 一個用於跟蹤所有頂點資訊的結構Vertex (目前只包含位置和顏色。)

2 定義了以上面這個Vertex結構為型別的array。

3 一個用於表示三角形頂點的陣列。

資料準備好了,我們來開始把資料傳入OpenGL

建立Vertex Buffer 物件

傳資料到OpenGL的話,最好的方式就是用Vertex Buffer物件。

基本上,它們就是用於快取頂點資料的OpenGL物件。通過呼叫一些function來把資料傳送到OpenGL-land。(是指OpenGL的畫面?)

這裡有兩種頂點快取型別– 一種是用於跟蹤每個頂點資訊的(正如我們的Vertices array),另一種是用於跟蹤組成每個三角形的索引資訊(我們的Indices array)。

下面我們在initWithFrame中,加入一些程式碼:

  1. [self setupVBOs]; 

下面是定義這個setupVBOs:

  1. - (void)setupVBOs { 
  2.     GLuint vertexBuffer; 
  3.     glGenBuffers(1, &vertexBuffer); 
  4.     glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); 
  5.     glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW); 
  6.     GLuint indexBuffer; 
  7.     glGenBuffers(1, &indexBuffer); 
  8.     glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); 
  9.     glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW); 

如你所見,其實很簡單的。這其實是一種之前也用過的模式(pattern)。

glGenBuffers - 建立一個Vertex Buffer 物件

  1. glBindBuffer – 告訴OpenGL我們的vertexBuffer 是指GL_ARRAY_BUFFER 

glBufferData – 把資料傳到OpenGL-land

想起哪裡用過這個模式嗎?要不再回去看看frame buffer那一段? 

萬事俱備,我們可以通過新的shader,用新的渲染方法來把頂點資料畫到螢幕上。

用這段程式碼替換掉之前的render:

  1. - (void)render { 
  2.     glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0); 
  3.     glClear(GL_COLOR_BUFFER_BIT); 
  4.     // 1 
  5.     glViewport(0, 0, self.frame.size.width, self.frame.size.height); 
  6.     // 2 
  7.     glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE,  
  8.         sizeof(Vertex), 0); 
  9.     glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE,  
  10.         sizeof(Vertex), (GLvoid*) (sizeof(float) *3)); 
  11.     // 3 
  12.     glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]),  
  13.         GL_UNSIGNED_BYTE, 0); 
  14.     [_context presentRenderbuffer:GL_RENDERBUFFER]; 

1       呼叫glViewport 設定UIView中用於渲染的部分。這個例子中指定了整個螢幕。但如果你希望用更小的部分,你可以更變這些引數。

2       呼叫glVertexAttribPointer來為vertex shader的兩個輸入引數配置兩個合適的值。

第二段這裡,是一個很重要的方法,讓我們來認真地看看它是如何工作的:

·第一個引數,宣告這個屬性的名稱,之前我們稱之為glGetAttribLocation

·第二個引數,定義這個屬性由多少個值組成。譬如說position是由3個float(x,y,z)組成,而顏色是4個float(r,g,b,a)

·第三個,宣告每一個值是什麼型別。(這例子中無論是位置還是顏色,我們都用了GL_FLOAT)

·第四個,嗯……它總是false就好了。

·第五個,指 stride 的大小。這是一個種描述每個 vertex資料大小的方式。所以我們可以簡單地傳入 sizeof(Vertex),讓編譯器計算出來就好。

·最好一個,是這個資料結構的偏移量。表示在這個結構中,從哪裡開始獲取我們的值。Position的值在前面,所以傳0進去就可以了。而顏色是緊接著位置的資料,而position的大小是3個float的大小,所以是從 3 * sizeof(float) 開始的。

回來繼續說程式碼,第三點:

3       呼叫glDrawElements ,它最後會在每個vertex上呼叫我們的vertex shader,以及每個畫素呼叫fragment shader,最終畫出我們的矩形。

它也是一個重要的方法,我們來仔細研究一下:

·第一個引數,宣告用哪種特性來渲染圖形。有GL_LINE_STRIP 和 GL_TRIANGLE_FAN。然而GL_TRIANGLE是最常用的,特別是與VBO 關聯的時候。

·第二個,告訴渲染器有多少個圖形要渲染。我們用到C的程式碼來計算出有多少個。這裡是通過個 array的byte大小除以一個Indice型別的大小得到的。

·第三個,指每個indices中的index型別

·最後一個,在官方文件中說,它是一個指向index的指標。但在這裡,我們用的是VBO,所以通過index的array就可以訪問到了(在GL_ELEMENT_ARRAY_BUFFER傳過了),所以這裡不需要.

編譯執行的話,你就可以看到這個畫面喇。

你可能會疑惑,為什麼這個長方形剛好佔滿整個螢幕。在預設狀態下,OpenGL的“camera”位於(0,0,0)位置,朝z軸的正方向。

當然,後面我們會講到projection(投影)以及如何控制camera。

增加一個投影

為了在2D螢幕上顯示3D畫面,我們需要在圖形上做一些投影變換,所謂投影就是下圖這個意思:

基本上,為了模仿人類的眼球原理。我們設定一個遠平面和一個近平面,在兩個平面之前,離近平面近的影象,會因為被縮小了而顯得變小;而離遠平面近的影象,也會因此而變大。

開啟SimpleVertex.glsl,做一下修改:

  1. // Add right before the main 
  2. uniform mat4 Projection; 
  3. // Modify gl_Position line as follows 
  4. gl_Position = Projection * Position; 

這裡我們增加了一個叫做projection的傳入變數。uniform 關鍵字表示,這會是一個應用於所有頂點的常量,而不是會因為頂點不同而不同的值。

mat4 是 4X4矩陣的意思。然而,Matrix math是一個很大的課題,我們不可能在這裡解析。所以在這裡,你只要認為它是用於放大縮小、旋轉、變形就好了。

Position位置乘以Projection矩陣,我們就得到最終的位置數值。

無錯,這就是一種被稱之“線性代數”的東西。我在大學時期後,早就忘大部分了。

其實數學也只是一種工具,而這種工具已經由前面的才子解決了,我們知道怎麼用就好。

Bill Hollings,cocos3d的作者。他編寫了一個完整的3D特性框架,並整合到cocos2d中。(作者:可能有一天我也會弄一個3D的教程)無論任何,Cocos3d包含了Objective-C的向量和矩陣庫,所以我們可以很好地應用到這個專案中。

有一個zip檔案,(作者:我移除了一些不必要的依賴)下載並copy到你的專案中。記得選上:“Copy items into destination group’s folder (if needed)” 點選Finish。

在OpenGLView.h 中加入一個例項變數:

  1. GLuint _projectionUniform; 

然後到OpenGLView.m檔案中加上:

  1. // Add to top of file 
  2. #import "CC3GLMatrix.h" 
  3. // Add to bottom of compileShaders 
  4. _projectionUniform = glGetUniformLocation(programHandle, "Projection"); 
  5. // Add to render, right before the call to glViewport 
  6. CC3GLMatrix *projection = [CC3GLMatrix matrix]; 
  7. float h =4.0f* self.frame.size.height / self.frame.size.width; 
  8. [projection populateFromFrustumLeft:-2 andRight:2 andBottom:-h/2 andTop:h/2 andNear:4 andFar:10]; 
  9. glUniformMatrix4fv(_projectionUniform, 1, 0, projection.glMatrix); 
  10. // Modify vertices so they are within projection near/far planes 
  11. const Vertex Vertices[] = { 
  12.     {{1, -1, -7}, {1, 0, 0, 1}}, 
  13.     {{1, 1, -7}, {0, 1, 0, 1}}, 
  14.     {{-1, 1, -7}, {0, 0, 1, 1}}, 
  15.     {{-1, -1, -7}, {0, 0, 0, 1}} 
  16. }; 

·通過呼叫  glGetUniformLocation 來獲取在vertex shader中的Projection輸入變數

·然後,使用math library來建立投影矩陣。通過這個讓你指定座標,以及遠近屏位置的方式,來建立矩陣,會讓事情比較簡單。

·你用來把資料傳入到vertex shader的方式,叫做 glUniformMatrix4fv. 這個CC3GLMatrix類有一個很方便的方法 glMatrix,來把矩陣轉換成OpenGL的array格式。

·最後,把之前的vertices資料修改一下,讓z座標為-7. 

編譯後執行,你應該可以看到一個稍稍有點距離的正方形了。

嘗試移動和旋轉吧

如果總是要修改那個vertex array才能改變圖形,這就太煩人了。

而這正是變換矩陣該做的事(又來了,線性代數)

在前面,我們修改了應用到投影矩陣的vertex array來達到移動圖形的目的。何不試一下,做一個變形、放大縮小、旋轉的矩陣來應用?我們稱之為“model-view”變換。

再回到 SimpleVertex.glsl

  1. // Add right after the Projection uniform 
  2. uniform mat4 Modelview; 
  3. // Modify the gl_Position line 
  4. gl_Position = Projection * Modelview * Position; 

就是又加了一個 Uniform的矩陣而已。順便把它應用到gl_Position當中。

然後到 OpenGLView.h中加上一個變數:

  1. GLuint _modelViewUniform; 

到OpenGLView.m中修改:

  1. // Add to end of compileShaders 
  2. _modelViewUniform = glGetUniformLocation(programHandle, "Modelview"); 
  3. // Add to render, right before call to glViewport 
  4. CC3GLMatrix *modelView = [CC3GLMatrix matrix]; 
  5. [modelView populateFromTranslation:CC3VectorMake(sin(CACurrentMediaTime()), 0, -7)]; 
  6. glUniformMatrix4fv(_modelViewUniform, 1, 0, modelView.glMatrix); 
  7. // Revert vertices back to z-value 0 
  8. const Vertex Vertices[] = { 
  9.     {{1, -1, 0}, {1, 0, 0, 1}}, 
  10.     {{1, 1, 0}, {0, 1, 0, 1}}, 
  11.     {{-1, 1, 0}, {0, 0, 1, 1}}, 
  12.     {{-1, -1, 0}, {0, 0, 0, 1}} 
  13. }; 

·獲取那個model view uniform的傳入變數

·使用cocos3d math庫來建立一個新的矩陣,在變換中裝入矩陣。

·變換是在z軸上移動-7,而為什麼sin(當前時間) 呢?

哈哈,如果你還記得高中時候的三角函式。sin()是一個從-1到1的函式。已PI(3.14)為一個週期。這樣做的話,約每3.14秒,這個函式會從-1到1迴圈一次。

·把vertex 結構改回去,把z座標設回0.

編譯執行,就算我們把z設回0,也可以看到這個位於中間的正方形了。

什麼?一動不動的?

當然了,我們只是呼叫了一次render方法。

接下來,我們在每一幀都呼叫一次看看。

渲染和 CADisplayLink

理想狀態下,我們希望OpenGL的渲染頻率跟螢幕的重新整理頻率一致。

幸運的是,Apple為我們提供了一個CADisplayLink的類。這個很好用的,馬上就用吧。

在OpenGLView.m檔案,修改如下:

  1. // Add new method before init 
  2. - (void)setupDisplayLink { 
  3.     CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)]; 
  4.     [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];     
  5. // Modify render method to take a parameter 
  6. - (void)render:(CADisplayLink*)displayLink { 
  7. // Remove call to render in initWithFrame and replace it with the following 
  8. [self setupDisplayLink]; 

這就行了,有CADisplayLink在每一幀都呼叫你的render方法,我們的圖形看起身就好似被sin()週期地變型了。現在這個方塊會前前後後地來回移動。

不費功夫地旋轉

讓圖形旋轉起來,才算得上有型。

再到OpenGLView.h 中,新增成員變數。

  1. float _currentRotation; 

在OpenGLView.m的render中,在populateFromTranslation的呼叫後面加上:

  1. _currentRotation += displayLink.duration *90; 
  2. [modelView rotateBy:CC3VectorMake(_currentRotation, _currentRotation, 0)]; 

·添加了一個叫_currentRotation的float,每秒會增加90度。

·通過修改那個model view矩陣(這裡相當於一個用於變型的矩陣),增加旋轉。

·旋轉在x、y軸上作用,沒有在z軸的。

編譯執行,你會看到一個很有型的翻轉的3D效果。

不費功夫地變成3D方塊?

之前的只能算是2.5D,因為它還只是一個會旋轉的面而已。現在我們把它改造成3D的。

把之前的vertices、indices陣列註釋掉吧。

然後加上新的:

  1. const Vertex Vertices[] = { 
  2.     {{1, -1, 0}, {1, 0, 0, 1}}, 
  3.     {{1, 1, 0}, {1, 0, 0, 1}}, 
  4.     {{-1, 1, 0}, {0, 1, 0, 1}}, 
  5.     {{-1, -1, 0}, {0, 1, 0, 1}}, 
  6.     {{1, -1, -1}, {1, 0, 0, 1}}, 
  7.     {{1, 1, -1}, {1, 0, 0, 1}}, 
  8.     {{-1, 1, -1}, {0, 1, 0, 1}}, 
  9.     {{-1, -1, -1}, {0, 1, 0, 1}} 
  10. }; 
  11. const GLubyte Indices[] = { 
  12.     // Front 
  13. 0, 1, 2, 
  14.     2, 3, 0,