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,建立一些後面會用到的例項變數。
- #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
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
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的向量和矩陣庫,所以我們可以很好地應用到這個專案中。
有一個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,