從零講解 iOS OpenGL ES 的紋理渲染 原來是澤鏡啊
本文主要介紹,如何使用 OpenGL ES 來渲染一張圖片。內容包括:基礎概念的講解,如何使用 GLKit 來渲染紋理,如何使用 GLSL 編寫的著色器來渲染紋理。
前言
OpenGL(Open Graphics Library)是 Khronos Group (一個圖形軟硬體行業協會,該協會主要關注圖形和多媒體方面的開放標準)開發維護的一個規範,它是硬體無關的。它主要為我們定義了用來操作圖形和圖片的一系列函式的 API,OpenGL 本身並非 API。
OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 的子集,針對手機、PDA 和遊戲主機等嵌入式裝置而設計。該規範也是由 Khronos Group 開發維護。
OpenGL ES 去除了四邊形(GL_QUADS)、多邊形(GL_POLYGONS)等複雜圖元,以及許多非絕對必要的特性,剩下最核心有用的部分。可以理解成是一個在移動平臺上能夠支援 OpenGL 最基本功能的精簡規範。
本人5年iOS開發經驗,曾就職於阿里巴巴。 善於把艱澀的iOS知識轉化為通俗易懂的白話文字,同時也歡迎大家加入小編的iOS交流群 413038000 ,群裡會提供相關面試資料,書籍歡迎大家入駐!
目前 iOS 平臺支援的有 OpenGL ES 1.0,2.0,3.0。OpenGL ES 3.0 加入了一些新的特性,但是它除了需要 iOS 7.0 以上之外,還需要 iPhone 5S 之後的裝置才能支援。出於現有裝置的考慮,我們主要使用 OpenGL ES 2.0。
注:下文中的 OpenGL ES 均指代 OpenGL ES 2.0。
一、概念
1、快取是什麼
OpenGL ES 部分執行在 CPU 上,部分執行在 GPU 上,為了協調這兩部分的資料交換,定義了快取(Buffers)的概念。CPU 和 GPU 都有獨自控制的記憶體區域,快取可以避免資料在這兩塊記憶體區域之間進行復制,提高效率。快取實際上就是指一塊連續的 RAM 。
2、紋理渲染的含義
紋理是一個用來儲存影象顏色的元素值的快取,渲染是指將資料生成影象的過程。紋理渲染則是將儲存在記憶體中的顏色值等資料,生成影象的過程。
3、座標系
1、OpenGL ES 座標系
OpenGL ES 座標系
2、紋理座標系
紋理座標系的範圍是 0 ~ 1,是一個二維座標系,橫軸稱為 S 軸,縱軸稱為 T 軸。在座標系中,點的橫座標一般用 U 表示,點的縱座標一般用 V 表示。左下角為 (0, 0),右上角為 (1, 1)。
注: UIKit 座標系的 (0, 0) 點在左上角,其縱軸的方向和紋理座標系縱軸的方向剛好相反。
4、紋理相關的概念
- 紋素(Texel):一個影象初始化為一個紋理快取後,每個畫素會變成一個紋素。紋理的座標是範圍是 0 ~ 1,在這個單位長度內,可能包含任意多個紋素。
- 光柵化(Rasterizing):將幾何形狀資料轉換為片段的渲染步驟。
- 片段(Fragment):視口座標中的顏色畫素。沒有使用紋理時,會使用物件頂點來計算片段的顏色;使用紋理時,會根據紋素來計算。
- 對映(Mapping):對齊頂點和紋素的方式。即將頂點座標 (X, Y, Z) 與 紋理座標 (U, V) 對應起來。
- 取樣(Sampling):在頂點固定後,每個片段根據計算出來的 (U, V) 座標,去找相應紋素的過程。
- 幀快取(Frame Buffer):一個接收渲染結果的緩衝區,為 GPU 指定儲存渲染結果的區域。更通俗點,可以理解成儲存螢幕上最終顯示的一幀畫面的區域。
注:(U, V) 可能會超出 0 ~ 1 這個範圍,需要通過
glTextParameteri()
配置相應的方案,來對映到 S 軸和 T 軸。
5、怎麼使用快取
在實際應用中,我們需要使用各種各樣的快取。比如在紋理渲染之前,需要生成一塊儲存了影象資料的紋理快取。下面介紹一下快取管理的一般步驟:
使用快取的過程可以分為 7 步:
- 生成(Generate):生成快取識別符號
glGenBuffers()
- 繫結(Bind):對接下來的操作,繫結一個快取
glBindBuffer()
- 快取資料(Buffer Data):從CPU的記憶體複製資料到快取的記憶體
glBufferData()
/glBufferSubData()
- 啟用(Enable)或者禁止(Disable):設定在接下來的渲染中是否要使用快取的資料
glEnableVertexAttribArray()
/glDisableVertexAttribArray()
- 設定指標(Set Pointers):告知快取的資料型別,及相應資料的偏移量
glVertexAttribPointer()
- 繪圖(Draw):使用快取的資料進行繪製
glDrawArrays()
/glDrawElements()
- 刪除(Delete):刪除快取,釋放資源
glDeleteBuffers()
這 7 步很重要,現在先有個印象,後面我們在實際例子中會反覆用到。
6、OpenGL ES 的上下文
OpenGL ES 是一個狀態機,相關的配置資訊會被儲存在一個上下文(Context)中,這個些值會被一直儲存,直到被修改。但我們可以配置多個上下文,通過呼叫 [EAGLContext setCurrentContext:context]
來切換。
7、OpenGL ES 中的圖元
圖元(Primitive)是指 OpenGL ES 中支援渲染的基本圖形。OpenGL ES 只支援三種圖元,分別是頂點、線段、三角形。複雜的圖形得通過渲染多個三角形來實現。
8、怎麼渲染三角形
渲染三角形的基本流程按照上圖所示。其中,頂點著色器和片段著色器是可程式設計的部分,著色器(Shader)是一個小程式,它們執行在 GPU 上,在主程式執行的時候進行動態編譯,而不用寫死在程式碼裡面。編寫著色器用的語言是 GLSL(OpenGL Shading Language),在第三節中我們會詳細介紹。
下面介紹一下渲染流程的每一步都做了什麼:
1、頂點資料
為了渲染一個三角形,我們需要傳入一個包含 3 個三維頂點座標的陣列,每個頂點都有對應的頂點屬性,頂點屬性中可以包含任何我們想用的資料。在上圖的例子裡,我們的每個頂點包含了一個顏色值。
並且,為了讓 OpenGL ES 知道我們是要繪製三角形,而不是點或者線段,我們在呼叫繪製指令的時候,都會把圖元資訊傳遞給 OpenGL ES 。
2、頂點著色器
頂點著色器會對每個頂點執行一次運算,它可以使用頂點資料來計算該頂點的座標、顏色、光照、紋理座標等。
頂點著色器的一個重要任務是進行座標轉換,例如將模型的原始座標系(一般是指其 3D 建模工具中的座標)轉換到螢幕座標系。
3、圖元裝配
在頂點著色器程式輸出頂點座標之後,各個頂點按照繪製命令中的圖元型別引數,以及頂點索引陣列被組裝成一個個圖元。
通過這一步,模型中 3D 的圖元已經被轉化為螢幕上 2D 的圖元。
4、幾何著色器
在「OpenGL」的版本中,頂點著色器和片段著色器之間有一個可選的著色器,叫做幾何著色器(Geometry Shader)。
幾何著色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的圖元來生成其他形狀。
OpenGL ES 目前還不支援幾何著色器,這個部分我們可以先不關注。
5、光柵化
在光柵化階段,基本圖元被轉換為供片段著色器使用的片段。片段表示可以被渲染到螢幕上的畫素,它包含位置、顏色、紋理座標等資訊,這些值是由圖元的頂點資訊進行插值計算得到的。
在片段著色器執行之前會執行裁切,處於檢視以外的所有畫素會被裁切掉,用來提升執行效率。
6、片段著色器
片段著色器的主要作用是計算每一個片段最終的顏色值(或者丟棄該片段)。片段著色器決定了最終螢幕上每一個畫素點的顏色值。
7、測試與混合
在這一步,OpenGL ES 會根據片段是否被遮擋、檢視上是否已存在繪製好的片段等情況,對片段進行丟棄或著混合,最終被保留下來的片段會被寫入幀快取中,最終呈現在裝置螢幕上。
9、怎麼渲染多變形
由於 OpenGL ES 只能渲染三角形,因此多邊形需要由多個三角形來組成。
如圖所示,一個五邊形,我們可以把它拆分成 3 個三角形來渲染。
渲染一個三角形,我們需要一個儲存 3 個頂點的陣列。這意味著我們渲染一個五邊形,需要用 9 個頂點。而且我們可以看到,其中 V0 、 V2 、V3 都是重複的頂點,顯得有點冗餘。
那麼有沒有更簡單的方式,可以讓我們複用之前的頂點呢?答案是肯定的。
在 OpenGL ES 中,對於三角形有 3 種繪製模式。在給定的頂點陣列相同的情況下,可以指定我們想要的連線方式。如下圖所示:
1、GL_TRIANGLES
GL_TRIANGLES
就是我們一開始說的方式,沒有複用頂點,以每三個頂點繪製一個三角形。第一個三角形使用 V0 、 V1 、V2 ,第二個使用 V3 、 V4 、V5 ,以此類推。如果頂點的個數不是 3 的倍數,那麼最後的 1 個或者 2 個頂點會被捨棄。
2、GL_TRIANGLE_STRIP
GL_TRIANGLE_STRIP
在繪製三角形的時候,會複用前兩個頂點。第一個三角形依然使用 V0 、 V1 、V2 ,第二個則會使用 V1 、 V2 、V3,以此類推。第 n 個會使用 V(n-1) 、 V(n) 、V(n+1) 。
3、GL_TRIANGLE_FAN
GL_TRIANGLE_FAN
在繪製三角形的時候,會複用第一個頂點和前一個頂點。第一個三角形依然使用 V0 、 V1 、V2 ,第二個則會使用 V0 、 V2 、V3,以此類推。第 n 個會使用 V0 、 V(n) 、V(n+1) 。這種方式看上去像是在繞著 V0 畫扇形。
二、通過 GLKit 渲染
恭喜你終於看完了枯燥的概念講解。從這裡開始,我們開始會進入實際的例子,用程式碼來講解渲染的過程。
在 GLKit 中,蘋果爸爸對 OpenGL ES 中的一些操作進行了封裝,因此我們使用 GLKit 來渲染會省去一些步驟。
那麼好奇的你肯定會問,在「紋理渲染」這件事情上,GLKit 幫我們做了什麼呢?
先不著急,等我們講完第三節中使用 GLSL 渲染的方式,再來回答這個問題。
現在,讓我們懷著忐忑又期待的心情,來看看 GLKit 是怎麼渲染紋理的。
1、獲取頂點資料
定義頂點資料,用一個三維向量來儲存 (X, Y, Z) 座標,用一個二維向量來儲存 (U, V) 座標:
typedef struct {
GLKVector3 positionCoord; // (X, Y, Z)
GLKVector2 textureCoord; // (U, V)
} SenceVertex;
初始化頂點資料:
self.vertices = malloc(sizeof(SenceVertex) * 4); // 4 個頂點
self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}}; // 左上角
self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}}; // 左下角
self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}}; // 右上角
self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}}; // 右下角
退出的時候,記得手動釋放記憶體:
- (void)dealloc {
// other code ...
if (_vertices) {
free(_vertices);
_vertices = nil;
}
}
2、初始化 GLKView 並設定上下文
// 建立上下文,使用 2.0 版本
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
// 初始化 GLKView
CGRect frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width);
self.glkView = [[GLKView alloc] initWithFrame:frame context:context];
self.glkView.backgroundColor = [UIColor clearColor];
self.glkView.delegate = self;
[self.view addSubview:self.glkView];
// 設定 glkView 的上下文為當前上下文
[EAGLContext setCurrentContext:self.glkView.context];
3、載入紋理
使用 GLKTextureLoader
來載入紋理,並用 GLKBaseEffect
儲存紋理的 ID ,為後面渲染做準備。
NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"sample.jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)};
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage]
options:options
error:NULL];
self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.texture2d0.name = textureInfo.name;
self.baseEffect.texture2d0.target = textureInfo.target;
因為紋理座標系和 UIKit 座標系的縱軸方向是相反的,所以將 GLKTextureLoaderOriginBottomLeft
設定為 YES
,用來消除兩個座標系之間的差異。
注:這裡如果用
imageNamed:
來讀取圖片,在反覆載入相同紋理的時候,會出現上下顛倒的錯誤。
4、實現 GLKView 的代理方法
在 glkView:drawInRect:
代理方法中,我們要去實現頂點資料和紋理資料的繪製邏輯。這一步是重點,注意觀察「快取管理的 7 個步驟」的具體用法。
程式碼如下:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
[self.baseEffect prepareToDraw];
// 建立頂點快取
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer); // 步驟一:生成
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); // 步驟二:繫結
GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW); // 步驟三:快取資料
// 設定頂點資料
glEnableVertexAttribArray(GLKVertexAttribPosition); // 步驟四:啟用或禁用
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord)); // 步驟五:設定指標
// 設定紋理資料
glEnableVertexAttribArray(GLKVertexAttribTexCoord0); // 步驟四:啟用或禁用
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord)); // 步驟五:設定指標
// 開始繪製
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 步驟六:繪圖
// 刪除頂點快取
glDeleteBuffers(1, &vertexBuffer); // 步驟七:刪除
vertexBuffer = 0;
}
5、開始繪製
我們呼叫 GLKView
的 display
方法,即可以觸發 glkView:drawInRect:
回撥,開始渲染的邏輯。
程式碼如下:
[self.glkView display];
至此,使用 GLKit 實現紋理渲染的過程就介紹完畢了。
是不是覺得意猶未盡,那就趕快進入下一節,瞭解如何直接通過 GLSL 編寫的著色器來渲染紋理。
三、通過 GLSL 渲染
在這一小節,我們會講解在不使用 GLKit 的情況下,怎麼實現紋理渲染。我們會著重介紹與 GLKit 渲染不同的部分。
注:大家實際去檢視 demo 的時候,會發現還是有引入
<GLKit/GLKit.h>
這個標頭檔案。這裡主要是為了使用GLKVector3
、GLKVector2
這兩個型別,當然不使用也是完全可以的。目的是為了和 GLKit 的例子保持資料格式的一致,方便大家把注意力放在兩者真正差異的部分。
1、著色器編寫
首先,我們需要自己編寫著色器,包括頂點著色器和片段著色器,使用的語言是 GLSL 。這裡對於 GLSL 就不展開講了,只解釋一下我們等下會用到的部分,更詳細的語法內容,可以參見 這裡。
新建一個檔案,一般頂點著色器用字尾 .vsh
,片段著色器用字尾 .fsh
(當然你不喜歡這麼命名也可以,但是為了方便其他人閱讀,最好是還是按照這個規範來),然後就可以寫程式碼了。
頂點著色器的程式碼如下:
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;
void main (void) {
gl_Position = Position;
TextureCoordsVarying = TextureCoords;
}
片段著色器的程式碼如下:
precision mediump float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
void main (void) {
vec4 mask = texture2D(Texture, TextureCoordsVarying);
gl_FragColor = vec4(mask.rgb, 1.0);
}
GLSL 是類 C 語言寫成,如果學習過 C 語言,上手是很快的。下面對這兩個著色器的程式碼做一下簡單的解釋。
attribute
修飾符只存在於頂點著色器中,用於儲存每個頂點資訊的輸入,比如這裡定義了 Position
和 TextureCoords
,用於接收頂點的位置和紋理資訊。
vec4
和 vec2
是資料型別,分別指四維向量和二維向量。
varying
修飾符指頂點著色器的輸出,同時也是片段著色器的輸入,要求頂點著色器和片段著色器中都同時宣告,並完全一致,則在片段著色器中可以獲取到頂點著色器中的資料。
gl_Position
和 gl_FragColor
是內建變數,對這兩個變數賦值,可以理解為向螢幕輸出片段的位置資訊和顏色資訊。
precision
可以為資料型別指定預設精度,precision mediump float
這一句的意思是將 float
型別的預設精度設定為 mediump
。
uniform
用來儲存傳遞進來的只讀值,該值在頂點著色器和片段著色器中都不會被修改。頂點著色器和片段著色器共享了 uniform
變數的名稱空間,uniform
變數在全域性區宣告,同個 uniform
變數在頂點著色器和片段著色器中都能訪問到。
sampler2D
是紋理控制代碼型別,儲存傳遞進來的紋理。
texture2D()
方法可以根據紋理座標,獲取對應的顏色資訊。
那麼這兩段程式碼的含義就很明確了,頂點著色器將輸入的頂點座標資訊直接輸出,並將紋理座標資訊傳遞給片段著色器;片段著色器根據紋理座標,獲取到每個片段的顏色資訊,輸出到螢幕。
2、紋理的載入
少了 GLKTextureLoader
的相助,我們就只能自己去生成紋理了。生成紋理的步驟比較固定,以下封裝成一個方法:
- (GLuint)createTextureWithImage:(UIImage *)image {
// 將 UIImage 轉換為 CGImageRef
CGImageRef cgImageRef = [image CGImage];
GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
CGRect rect = CGRectMake(0, 0, width, height);
// 繪製圖片
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc(width * height * 4);
CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);
CGContextDrawImage(context, rect, cgImageRef);
// 生成紋理
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); // 將圖片資料寫入紋理快取
// 設定如何把紋素對映成畫素
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 解綁
glBindTexture(GL_TEXTURE_2D, 0);
// 釋放記憶體
CGContextRelease(context);
free(imageData);
return textureID;
}
3、著色器的編譯連結
對於寫好的著色器,需要我們在程式執行的時候,動態地去編譯連結。編譯一個著色器的程式碼也比較固定,這裡通過後綴名來區分著色器型別,直接看程式碼:
- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType {
// 查詢 shader 檔案
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"]; // 根據不同的型別確定字尾名
NSError *error;
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSAssert(NO, @"讀取shader失敗");
exit(1);
}
// 建立一個 shader 物件
GLuint shader = glCreateShader(shaderType);
// 獲取 shader 的內容
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = (int)[shaderString length];
glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
// 編譯shader
glCompileShader(shader);
// 查詢 shader 是否編譯成功
GLint compileSuccess;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSAssert(NO, @"shader編譯失敗:%@", messageString);
exit(1);
}
return shader;
}
頂點著色器和片段著色器同樣都需要經過這個編譯的過程,編譯完成後,還需要生成一個著色器程式,將這兩個著色器連結起來,程式碼如下:
- (GLuint)programWithShaderName:(NSString *)shaderName {
// 編譯兩個著色器
GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];
// 掛載 shader 到 program 上
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
// 連結 program
glLinkProgram(program);
// 檢查連結是否成功
GLint linkSuccess;
glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSAssert(NO, @"program連結失敗:%@", messageString);
exit(1);
}
return program;
}
這樣,我們只要將兩個著色器命名統一,按照規範新增字尾名。然後將著色器名稱傳入這個方法,就可以獲得一個編譯連結好的著色器程式。
有了著色器程式後,我們就需要往程式中傳入資料,首先要獲取著色器中定義的變數,具體操作如下:
注:不同型別的變數獲取方式不同。
GLuint positionSlot = glGetAttribLocation(program, "Position");
GLuint textureSlot = glGetUniformLocation(program, "Texture");
GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
傳入生成的紋理 ID:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
glUniform1i(textureSlot, 0);
glUniform1i(textureSlot, 0)
的意思是,將 textureSlot
賦值為 0
,而 0
與 GL_TEXTURE0
對應,這裡如果寫 1
,glActiveTexture
也要傳入 GL_TEXTURE1
才能對應起來。
設定頂點資料:
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));
設定紋理資料:
glEnableVertexAttribArray(textureCoordsSlot);
glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));
4、Viewport 的設定
在渲染紋理的時候,我們需要指定 Viewport 的尺寸,可以理解為渲染的視窗大小。呼叫 glViewport
方法來設定:
glViewport(0, 0, self.drawableWidth, self.drawableHeight);
// 獲取渲染快取寬度
- (GLint)drawableWidth {
GLint backingWidth;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
return backingWidth;
}
// 獲取渲染快取高度
- (GLint)drawableHeight {
GLint backingHeight;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
return backingHeight;
}
5、渲染層的繫結
通過以上步驟,我們已經擁有了紋理,以及頂點的位置資訊。現在到了最後一步,我們要怎麼將快取與檢視關聯起來?換句話說,假如螢幕上有兩個檢視,OpenGL ES 要怎麼知道將影象渲染到哪個檢視上?
所以我們要進行渲染層繫結。通過 renderbufferStorage:fromDrawable:
來實現:
- (void)bindRenderLayer:(CALayer <EAGLDrawable> *)layer {
GLuint renderBuffer; // 渲染快取
GLuint frameBuffer; // 幀快取
// 繫結渲染快取要輸出的 layer
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
// 將渲染快取繫結到幀快取上
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,
renderBuffer);
}
以上程式碼生成了一個幀快取和一個渲染快取,並將渲染快取掛載到幀快取上,然後設定渲染快取的輸出層為 layer
。
最後,將繫結的渲染快取呈現到螢幕上:
[self.context presentRenderbuffer:GL_RENDERBUFFER];
至此,使用 GLSL 渲染紋理的關鍵步驟就結束了。
最終效果:
綜上所述,我們可以回答第二節的問題了,GLKit 主要幫我們做了以下幾個點:
- 著色器的編寫:GLKit 內建了簡單的著色器,不用我們自己去編寫。
- 紋理的載入:
GLKTextureLoader
封裝了一個將 Image 轉化為 Texture 的方法。 - 著色器的編譯連結:
GLKBaseEffect
內部實現了著色器的編譯連結過程,我們在使用過程中基本可以忽略「著色器」這個概念。 - Viewport 的設定:在渲染紋理的時候,需要指定 Viewport 的大小,
GLKView
在呼叫display
方法的時候,會在內部去設定。 - 渲染層的繫結:
GLKView
內部會呼叫renderbufferStorage:fromDrawable:
將自身的layer
設定為渲染快取的輸出層。因此,在呼叫display
方法的時候,內部會呼叫presentRenderbuffer:
去將渲染快取呈現到螢幕上。
原始碼
請到 GitHub 上檢視完整程式碼。
參考
《OpenGL ES 應用開發實踐指南》
你好,三角形 - LearnOpenGL CN
OpenGL ES iOS 入門篇2 - 繪製一個多邊形
iOS - OpenGLES 之圖片紋理
獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】從零講解 iOS 中 OpenGL ES 的紋理渲染
作者:雷曼同學
連結:https://www.jianshu.com/p/b64b387cecfb