OpenGL ES2.0 – Iphone開發指引

原文鏈接地址:http://www.raywenderlich.com/3664/opengl-es-2-0-for-iphone-tutorial

 

教程截圖:

  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,新建項目-選擇Window-based Application, 讓我們從零開始。

  點擊下一步,把這個項目命名爲HelloOpenGL,點擊下一步,選擇存放目錄,點擊“創建”。

  CMD+R,build and run。你會看到一個空白的屏幕。

  如你所見的,Window-based 模板創建了一個沒有view、沒有view controller或者其它東西的項目。它只包含了一個必須的UIWindow。

  File/New File,新建文件:選擇iOS\Cocoa Touch\Objective-c Class, 點擊下一步。

  選擇subclass UIView,點擊下一步,命名爲 OpenGLView.m., 點擊保存。

  接下來,你要在這個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,創建一些後面會用到的實例變量。

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
 
@interface OpenGLView : UIView {
    CAEAGLLayer* _eaglLayer;
    EAGLContext* _context;
    GLuint _colorRenderBuffer;
}
 
@end


3)設置layer class 爲 CAEAGLLayer

+ (Class)layerClass {
    return [CAEAGLLayer class];
}

  想要顯示OpenGL的內容,你需要把它缺省的layer設置爲一個特殊的layer。(CAEAGLLayer)。這裏通過直接複寫layerClass的方法。

4) 設置layer爲不透明(Opaque)

 

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

 

  因爲缺省的話,CALayer是透明的。而透明的層對性能負荷很大,特別是OpenGL的層。

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

5)創建OpenGL context

 
- (void)setupContext {   
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    _context = [[EAGLContext alloc] initWithAPI:api];
    if (!_context) {
        NSLog(@"Failed to initialize OpenGLES 2.0 context");
        exit(1);
    }
 
    if (![EAGLContext setCurrentContext:_context]) {
        NSLog(@"Failed to set current OpenGL context");
        exit(1);
    }
}

 

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

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

  當你創建一個context,你要聲明你要用哪個version的API。這裏,我們選擇OpenGL ES 2.0.

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

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

- (void)setupRenderBuffer {
    glGenRenderbuffers(1, &_colorRenderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);        
    [_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 (幀緩衝區)

- (void)setupFrameBuffer {    
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
        GL_RENDERBUFFER, _colorRenderBuffer);
 }

 

  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)清理屏幕

 

- (void)render {
    glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    [_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

 
// Replace initWithFrame with this
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {        
        [self setupLayer];        
        [self setupContext];                
        [self setupRenderBuffer];        
        [self setupFrameBuffer];                
        [self render];        
    }
    return self;
}
 
// Replace dealloc method with this
- (void)dealloc
{
    [_context release];
    _context = nil;
    [super dealloc];
}

 

10)把App Delegate和OpenGLView 連接起來

  在HelloOpenGLAppDelegate.h 中修改一下:

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

  接下來修改.m文件:

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

 

  一切順利的話,你就能看到一個新的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 點擊保存。

  打開這個文件,加入下面的代碼:

 
attribute vec4 Position; // 1
attribute vec4 SourceColor; // 2
 
varying vec4 DestinationColor; // 3
 
void main(void) { // 4
    DestinationColor = SourceColor; // 5
    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 保存。

  打開這個文件,加入以下代碼:

varying lowp vec4 DestinationColor; // 1
 
void main(void) { // 2
    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: 方法上方加入:

 
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
 
    // 1
    NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName 
        ofType:@"glsl"];
    NSError* error;
    NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath 
        encoding:NSUTF8StringEncoding error:&error];
    if (!shaderString) {
        NSLog(@"Error loading shader: %@", error.localizedDescription);
        exit(1);
    }
 
    // 2
    GLuint shaderHandle = glCreateShader(shaderType);    
 
    // 3
constchar* shaderStringUTF8 = [shaderString UTF8String];    
    int shaderStringLength = [shaderString length];
    glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
 
    // 4
    glCompileShader(shaderHandle);
 
    // 5
    GLint compileSuccess;
    glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
    if (compileSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSLog(@"%@", messageString);
        exit(1);
    }
 
    return shaderHandle;
 
}

 

  下面解析:

 

 

 

  1 這是一個UIKit編程的標準用法,就是在NSBundle中查找某個文件。大家應該熟悉了吧。

  2 調用 glCreateShader來創建一個代表shader 的OpenGL對象。這時你必須告訴OpenGL,你想創建 fragment shader還是vertex shader。所以便有了這個參數:shaderType

  3 調用glShaderSource ,讓OpenGL獲取到這個shader的源代碼。(就是我們寫的那個)這裏我們還把NSString轉換成C-string

  4 最後,調用glCompileShader 在運行時編譯shader

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

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

- 把它們倆關聯起來

- 告訴OpenGL來調用這個程序,還需要一些指針什麼的。

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

 
- (void)compileShaders {
 
    // 1
    GLuint vertexShader = [self compileShader:@"SimpleVertex" 
        withType:GL_VERTEX_SHADER];
    GLuint fragmentShader = [self compileShader:@"SimpleFragment" 
        withType:GL_FRAGMENT_SHADER];
 
    // 2
    GLuint programHandle = glCreateProgram();
    glAttachShader(programHandle, vertexShader);
    glAttachShader(programHandle, fragmentShader);
    glLinkProgram(programHandle);
 
    // 3
    GLint linkSuccess;
    glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSLog(@"%@", messageString);
        exit(1);
    }
 
    // 4
    glUseProgram(programHandle);
 
    // 5
    _positionSlot = glGetAttribLocation(programHandle, "Position");
    _colorSlot = glGetAttribLocation(programHandle, "SourceColor");
    glEnableVertexAttribArray(_positionSlot);
    glEnableVertexAttribArray(_colorSlot);
}

 

  下面是解析:

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

  2       調用了glCreateProgram glAttachShader  glLinkProgram 連接 vertex 和 fragment shader成一個完整的program。

  3       調用 glGetProgramiv  lglGetProgramInfoLog 來檢查是否有error,並輸出信息。

  4       調用 glUseProgram  讓OpenGL真正執行你的program

  5       最後,調用 glGetAttribLocation 來獲取指向 vertex shader傳入變量的指針。以後就可以通過這寫指針來使用了。還有調用 glEnableVertexAttribArray來啓用這些數據。(因爲默認是 disabled的。)

  最後還有兩步:

  1 在 initWithFrame方法裏,在調用render之前要加入這個:

[self compileShaders];

  2 在@interface in OpenGLView.h 中添加兩個變量:

GLuint _positionSlot;
GLuint _colorSlot;

  編譯!運行!

 

 

 

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

爲這個簡單的長方形創建 Vertex Data!

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

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

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

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

  打開OpenGLView.m文件,創建一個純粹的C結構以及一些array來跟蹤我們的矩形信息,如下:

typedef struct {
    float Position[3];
    float Color[4];
} Vertex;
 
const Vertex Vertices[] = {
    {{1, -1, 0}, {1, 0, 0, 1}},
    {{1, 1, 0}, {0, 1, 0, 1}},
    {{-1, 1, 0}, {0, 0, 1, 1}},
    {{-1, -1, 0}, {0, 0, 0, 1}}
};
 
const GLubyte Indices[] = {
     0, 1, 2,
     2, 3, 0
};

 

  這段代碼的作用是:

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

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

  3 一個用於表示三角形頂點的數組。

  數據準備好了,我們來開始把數據傳入OpenGL

創建Vertex Buffer 對象

  傳數據到OpenGL的話,最好的方式就是用Vertex Buffer對象。

  基本上,它們就是用於緩存頂點數據的OpenGL對象。通過調用一些function來把數據發送到OpenGL-land。(是指OpenGL的畫面?)

這裏有兩種頂點緩存類型– 一種是用於跟蹤每個頂點信息的(正如我們的Vertices array),另一種是用於跟蹤組成每個三角形的索引信息(我們的Indices array)。

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

[self setupVBOs];

  下面是定義這個setupVBOs:

 
- (void)setupVBOs {
 
    GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
 
    GLuint indexBuffer;
    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
 
}

 

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

  glGenBuffers - 創建一個Vertex Buffer 對象

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

  glBufferData – 把數據傳到OpenGL-land

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

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

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

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

 

  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,做一下修改:

// Add right before the main
uniform mat4 Projection;
 
// Modify gl_Position line as follows
gl_Position = Projection * Position;

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

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

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

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

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

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

  這裏,http://d1xzuxjlafny7l.cloudfront.net/downloads/Cocos3DMathLib.zip

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

  在OpenGLView.h 中加入一個實例變量:

GLuint _projectionUniform;

  然後到OpenGLView.m文件中加上:

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

 

  ·通過調用  glGetUniformLocation 來獲取在vertex shader中的Projection輸入變量

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

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

  ·最後,把之前的vertices數據修改一下,讓z座標爲-7. 

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

嘗試移動和旋轉吧

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

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

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

  再回到 SimpleVertex.glsl

// Add right after the Projection uniform
uniform mat4 Modelview;
 
// Modify the gl_Position line
gl_Position = Projection * Modelview * Position;

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

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

GLuint _modelViewUniform;

  到OpenGLView.m中修改:

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

  ·獲取那個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文件,修改如下:


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


 

 

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

不費功夫地旋轉

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

  再到OpenGLView.h 中,添加成員變量。

float _currentRotation;

  在OpenGLView.m的render中,在populateFromTranslation的調用後面加上:

 

 

 

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

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

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

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

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

 

 

 

 

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

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

  把之前的vertices、indices數組註釋掉吧。

  然後加上新的:

const Vertex Vertices[] = {
    {{1, -1, 0}, {1, 0, 0, 1}},
    {{1, 1, 0}, {1, 0, 0, 1}},
    {{-1, 1, 0}, {0, 1, 0, 1}},
    {{-1, -1, 0}, {0, 1, 0, 1}},
    {{1, -1, -1}, {1, 0, 0, 1}},
    {{1, 1, -1}, {1, 0, 0, 1}},
    {{-1, 1, -1}, {0, 1, 0, 1}},
    {{-1, -1, -1}, {0, 1, 0, 1}}
};
 
const GLubyte Indices[] = {
    // Front
0, 1, 2,
    2, 3, 0,
    // Back
4, 6, 5,
    4, 7, 6,
    // Left
2, 7, 3,
    7, 6, 2,
    // Right
0, 4, 1,
    4, 1, 5,
    // Top
6, 2, 1, 
    1, 6, 5,
    // Bottom
0, 3, 7,
    0, 7, 4    
};

 

  編譯運行,你會看到一個方塊了。

 

 

  但這個方塊有時候讓人覺得假,因爲你可以看到方塊裏面。

  這裏還有一個叫做 depth testing(深度測試)的功能,啓動它,OpenGL就可以跟蹤在z軸上的像素。這樣它只會在那個像素前方沒有東西時,纔會繪畫這個像素。

  到OpenGLView.h中,添加成員變量。

 

GLuint _depthRenderBuffer;

 

  在OpenGLView.m:

 
// Add new method right after setupRenderBuffer
- (void)setupDepthBuffer {
    glGenRenderbuffers(1, &_depthRenderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _depthRenderBuffer);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, self.frame.size.width, self.frame.size.height);    
}
 
// Add to end of setupFrameBuffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthRenderBuffer);
 
// In the render method, replace the call to glClear with the following
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
 
// Add to initWithFrame, right before call to setupRenderBuffer
[self setupDepthBuffer];

 

  ·setupDepthBuffer方法創建了一個depth buffer。這個與前面的render/color buffer類似,不再重複了。值得注意的是,這裏使用了glRenderbufferStorage, 然不是context的renderBufferStorage(這個是在OpenGL的view中特別爲color render buffer而設的)。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  ·接着,我們調用glFramebufferRenderbuffer,來關聯depth buffer和render buffer。還記得,我說過frame buffer中儲存着很多種不同的buffer?這正是一個新的buffer。

  ·在render方法中,我們在每次update時都清除深度buffer,並啓用depth  testing。

  編譯運行,看看這個教程最後的效果。

  一個選擇的立方塊,用到了OpenGL ES2.0。

何去何從?

  這裏有本教程的完整源代碼

  這只是OpenGL的一篇引導教程,希望能讓你輕鬆地入門。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章