1. 程式人生 > 實用技巧 >從零講解 iOS OpenGL ES 的紋理渲染 原來是澤鏡啊

從零講解 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 座標系

的範圍是 -1 ~ 1,是一個三維的座標系,通常用 X、Y、Z 來表示。Z 軸的正方向指向螢幕外。在不考慮 Z 軸的情況下,左下角為 (-1, -1, 0),右上角為 (1, 1, 0)。

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 步:

  1. 生成(Generate):生成快取識別符號 glGenBuffers()
  2. 繫結(Bind):對接下來的操作,繫結一個快取 glBindBuffer()
  3. 快取資料(Buffer Data):從CPU的記憶體複製資料到快取的記憶體 glBufferData() / glBufferSubData()
  4. 啟用(Enable)或者禁止(Disable):設定在接下來的渲染中是否要使用快取的資料 glEnableVertexAttribArray() / glDisableVertexAttribArray()
  5. 設定指標(Set Pointers):告知快取的資料型別,及相應資料的偏移量 glVertexAttribPointer()
  6. 繪圖(Draw):使用快取的資料進行繪製 glDrawArrays() / glDrawElements()
  7. 刪除(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、開始繪製

我們呼叫 GLKViewdisplay 方法,即可以觸發 glkView:drawInRect: 回撥,開始渲染的邏輯。

程式碼如下:

[self.glkView display];

至此,使用 GLKit 實現紋理渲染的過程就介紹完畢了。

是不是覺得意猶未盡,那就趕快進入下一節,瞭解如何直接通過 GLSL 編寫的著色器來渲染紋理。

三、通過 GLSL 渲染

在這一小節,我們會講解在不使用 GLKit 的情況下,怎麼實現紋理渲染。我們會著重介紹與 GLKit 渲染不同的部分。

注:大家實際去檢視 demo 的時候,會發現還是有引入 <GLKit/GLKit.h> 這個標頭檔案。這裡主要是為了使用 GLKVector3GLKVector2 這兩個型別,當然不使用也是完全可以的。目的是為了和 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 修飾符只存在於頂點著色器中,用於儲存每個頂點資訊的輸入,比如這裡定義了 PositionTextureCoords ,用於接收頂點的位置和紋理資訊。

vec4vec2 是資料型別,分別指四維向量和二維向量。

varying 修飾符指頂點著色器的輸出,同時也是片段著色器的輸入,要求頂點著色器和片段著色器中都同時宣告,並完全一致,則在片段著色器中可以獲取到頂點著色器中的資料。

gl_Positiongl_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,而 0GL_TEXTURE0 對應,這裡如果寫 1glActiveTexture 也要傳入 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