本文主要介紹,如何使用 OpenGL ES 來渲染一張圖片。內容包括:基礎概念的講解,如何使用 GLKit 來渲染紋理,如何使用 GLSL 編寫的着色器來渲染紋理。html
OpenGL(Open Graphics Library) 是 Khronos Group (一個圖形軟硬件行業協會,該協會主要關注圖形和多媒體方面的開放標準)開發維護的一個規範,它是硬件無關的。它主要爲咱們定義了用來操做圖形和圖片的一系列函數的 API,OpenGL 自己並不是 API。ios
OpenGL ES(OpenGL for Embedded Systems) 是 OpenGL 的子集,針對手機、PDA 和遊戲主機等嵌入式設備而設計。該規範也是由 Khronos Group 開發維護。git
OpenGL ES 去除了四邊形(GL_QUADS)、多邊形(GL_POLYGONS) 等複雜圖元,以及許多非絕對必要的特性,剩下最核心有用的部分。能夠理解成是一個在移動平臺上可以支持 OpenGL 最基本功能的精簡規範。github
目前 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。
OpenGL ES 部分運行在 CPU 上,部分運行在 GPU 上,爲了協調這兩部分的數據交換,定義了緩存(Buffers) 的概念。CPU 和 GPU 都有獨自控制的內存區域,緩存能夠避免數據在這兩塊內存區域之間進行復制,提升效率。緩存實際上就是指一塊連續的 RAM 。小程序
紋理是一個用來保存圖像顏色的元素值的緩存,渲染是指將數據生成圖像的過程。紋理渲染則是將保存在內存中的顏色值等數據,生成圖像的過程。數組
一、OpenGL ES 座標系緩存
OpenGL ES 座標系的範圍是 -1 ~ 1,是一個三維的座標系,一般用 X、Y、Z 來表示。Z 軸的正方向指向屏幕外。在不考慮 Z 軸的狀況下,左下角爲 (-1, -1, 0),右上角爲 (1, 1, 0)。app
二、紋理座標系函數
紋理座標系的範圍是 0 ~ 1,是一個二維座標系,橫軸稱爲 S 軸,縱軸稱爲 T 軸。在座標系中,點的橫座標通常用 U 表示,點的縱座標通常用 V 表示。左下角爲 (0, 0),右上角爲 (1, 1)。
注: UIKit 座標系的 (0, 0) 點在左上角,其縱軸的方向和紋理座標系縱軸的方向恰好相反。
注: (U, V) 可能會超出 0 ~ 1 這個範圍,須要經過
glTextParameteri()
配置相應的方案,來映射到 S 軸和 T 軸。
在實際應用中,咱們須要使用各類各樣的緩存。好比在紋理渲染以前,須要生成一塊保存了圖像數據的紋理緩存。下面介紹一下緩存管理的通常步驟:
使用緩存的過程能夠分爲 7 步:
glGenBuffers()
glBindBuffer()
glBufferData()
/ glBufferSubData()
glEnableVertexAttribArray()
/ glDisableVertexAttribArray()
glVertexAttribPointer()
glDrawArrays()
/ glDrawElements()
glDeleteBuffers()
這 7 步很重要,如今先有個印象,後面咱們在實際例子中會反覆用到。
OpenGL ES 是一個狀態機,相關的配置信息會被保存在一個上下文(Context) 中,這個些值會被一直保存,直到被修改。但咱們能夠配置多個上下文,經過調用 [EAGLContext setCurrentContext:context]
來切換。
圖元(Primitive) 是指 OpenGL ES 中支持渲染的基本圖形。OpenGL ES 只支持三種圖元,分別是頂點、線段、三角形。複雜的圖形得經過渲染多個三角形來實現。
渲染三角形的基本流程按照上圖所示。其中,頂點着色器和片斷着色器是可編程的部分,着色器(Shader) 是一個小程序,它們運行在 GPU 上,在主程序運行的時候進行動態編譯,而不用寫死在代碼裏面。編寫着色器用的語言是 GLSL(OpenGL Shading Language) ,在第三節中咱們會詳細介紹。
下面介紹一下渲染流程的每一步都作了什麼:
一、頂點數據
爲了渲染一個三角形,咱們須要傳入一個包含 3 個三維頂點座標的數組,每一個頂點都有對應的頂點屬性,頂點屬性中能夠包含任何咱們想用的數據。在上圖的例子裏,咱們的每一個頂點包含了一個顏色值。
而且,爲了讓 OpenGL ES 知道咱們是要繪製三角形,而不是點或者線段,咱們在調用繪製指令的時候,都會把圖元信息傳遞給 OpenGL ES 。
二、頂點着色器
頂點着色器會對每一個頂點執行一次運算,它可使用頂點數據來計算該頂點的座標、顏色、光照、紋理座標等。
頂點着色器的一個重要任務是進行座標轉換,例如將模型的原始座標系(通常是指其 3D 建模工具中的座標)轉換到屏幕座標系。
三、圖元裝配
在頂點着色器程序輸出頂點座標以後,各個頂點按照繪製命令中的圖元類型參數,以及頂點索引數組被組裝成一個個圖元。
經過這一步,模型中 3D 的圖元已經被轉化爲屏幕上 2D 的圖元。
四、幾何着色器
在「OpenGL」的版本中,頂點着色器和片斷着色器之間有一個可選的着色器,叫作幾何着色器(Geometry Shader)。
幾何着色器把圖元形式的一系列頂點的集合做爲輸入,它能夠經過產生新頂點構造出新的圖元來生成其餘形狀。
OpenGL ES 目前還不支持幾何着色器,這個部分咱們能夠先不關注。
五、光柵化
在光柵化階段,基本圖元被轉換爲供片斷着色器使用的片斷。片斷表示能夠被渲染到屏幕上的像素,它包含位置、顏色、紋理座標等信息,這些值是由圖元的頂點信息進行插值計算獲得的。
在片斷着色器運行以前會執行裁切,處於視圖之外的全部像素會被裁切掉,用來提高執行效率。
六、片斷着色器
片斷着色器的主要做用是計算每個片斷最終的顏色值(或者丟棄該片斷)。片斷着色器決定了最終屏幕上每個像素點的顏色值。
七、測試與混合
在這一步,OpenGL ES 會根據片斷是否被遮擋、視圖上是否已存在繪製好的片斷等狀況,對片斷進行丟棄或着混合,最終被保留下來的片斷會被寫入幀緩存中,最終呈如今設備屏幕上。
因爲 OpenGL ES 只能渲染三角形,所以多邊形須要由多個三角形來組成。
如圖所示,一個五邊形,咱們能夠把它拆分紅 3 個三角形來渲染。
渲染一個三角形,咱們須要一個保存 3 個頂點的數組。這意味着咱們渲染一個五邊形,須要用 9 個頂點。並且咱們能夠看到,其中 V0 、 V2 、V3 都是重複的頂點,顯得有點冗餘。
那麼有沒有更簡單的方式,可讓咱們複用以前的頂點呢?答案是確定的。
在 OpenGL ES 中,對於三角形有 3 種繪製模式。在給定的頂點數組相同的狀況下,能夠指定咱們想要的鏈接方式。以下圖所示:
一、GL_TRIANGLES
GL_TRIANGLES
就是咱們一開始說的方式,沒有複用頂點,以每三個頂點繪製一個三角形。第一個三角形使用 V0 、 V1 、V2 ,第二個使用 V3 、 V4 、V5 ,以此類推。若是頂點的個數不是 3 的倍數,那麼最後的 1 個或者 2 個頂點會被捨棄。
二、GL_TRIANGLE_STRIP
GL_TRIANGLE_STRIP
在繪製三角形的時候,會複用前兩個頂點。第一個三角形依然使用 V0 、 V1 、V2 ,第二個則會使用 V1 、 V2 、V3,以此類推。第 n 個會使用 V(n-1) 、 V(n) 、V(n+1) 。
三、GL_TRIANGLE_FAN
GL_TRIANGLE_FAN
在繪製三角形的時候,會複用第一個頂點和前一個頂點。第一個三角形依然使用 V0 、 V1 、V2 ,第二個則會使用 V0 、 V2 、V3,以此類推。第 n 個會使用 V0 、 V(n) 、V(n+1) 。這種方式看上去像是在繞着 V0 畫扇形。
恭喜你終於看完了枯燥的概念講解。從這裏開始,咱們開始會進入實際的例子,用代碼來說解渲染的過程。
在 GLKit 中,蘋果爸爸對 OpenGL ES 中的一些操做進行了封裝,所以咱們使用 GLKit 來渲染會省去一些步驟。
那麼好奇的你確定會問,在「紋理渲染」這件事情上,GLKit 幫咱們作了什麼呢?
先不着急,等咱們講完第三節中使用 GLSL 渲染的方式,再來回答這個問題。
如今,讓咱們懷着忐忑又期待的心情,來看看 GLKit 是怎麼渲染紋理的。
定義頂點數據,用一個三維向量來保存 (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.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];
使用 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:
來讀取圖片,在反覆加載相同紋理的時候,會出現上下顛倒的錯誤。
在 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; }
咱們調用 GLKView
的 display
方法,便可以觸發 glkView:drawInRect:
回調,開始渲染的邏輯。
代碼以下:
[self.glkView display];
至此,使用 GLKit 實現紋理渲染的過程就介紹完畢了。
是否是以爲意猶未盡,那就趕快進入下一節,瞭解如何直接經過 GLSL 編寫的着色器來渲染紋理。
在這一小節,咱們會講解在不使用 GLKit 的狀況下,怎麼實現紋理渲染。咱們會着重介紹與 GLKit 渲染不一樣的部分。
注: 你們實際去查看 demo 的時候,會發現仍是有引入<GLKit/GLKit.h>
這個頭文件。這裏主要是爲了使用GLKVector3
、GLKVector2
這兩個類型,固然不使用也是徹底能夠的。目的是爲了和 GLKit 的例子保持數據格式的一致,方便你們把注意力放在二者真正差別的部分。
首先,咱們須要本身編寫着色器,包括頂點着色器和片斷着色器,使用的語言是 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()
方法能夠根據紋理座標,獲取對應的顏色信息。
那麼這兩段代碼的含義就很明確了,頂點着色器將輸入的頂點座標信息直接輸出,並將紋理座標信息傳遞給片斷着色器;片斷着色器根據紋理座標,獲取到每一個片斷的顏色信息,輸出到屏幕。
少了 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; }
對於寫好的着色器,須要咱們在程序運行的時候,動態地去編譯連接。編譯一個着色器的代碼也比較固定,這裏經過後綴名來區分着色器類型,直接看代碼:
- (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));
在渲染紋理的時候,咱們須要指定 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; }
經過以上步驟,咱們已經擁有了紋理,以及頂點的位置信息。如今到了最後一步,咱們要怎麼將緩存與視圖關聯起來?換句話說,假如屏幕上有兩個視圖,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 主要幫咱們作了如下幾個點:
GLKTextureLoader
封裝了一個將 Image 轉化爲 Texture 的方法。GLKBaseEffect
內部實現了着色器的編譯連接過程,咱們在使用過程當中基本能夠忽略「着色器」這個概念。GLKView
在調用 display
方法的時候,會在內部去設置。GLKView
內部會調用 renderbufferStorage:fromDrawable:
將自身的 layer
設置爲渲染緩存的輸出層。所以,在調用 display
方法的時候,內部會調用 presentRenderbuffer:
去將渲染緩存呈現到屏幕上。請到 GitHub 上查看完整代碼。
獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】從零講解 iOS 中 OpenGL ES 的紋理渲染