從零講解 iOS 中 OpenGL ES 的紋理渲染

本文主要介紹,如何使用 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。

1、概念

一、緩存是什麼

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) 點在左上角,其縱軸的方向和紋理座標系縱軸的方向恰好相反。

四、紋理相關的概念

  • 紋素(Texel): 一個圖像初始化爲一個紋理緩存後,每一個像素會變成一個紋素。紋理的座標是範圍是 0 ~ 1,在這個單位長度內,可能包含任意多個紋素。
  • 光柵化(Rasterizing): 將幾何形狀數據轉換爲片斷的渲染步驟。
  • 片斷(Fragment): 視口座標中的顏色像素。沒有使用紋理時,會使用對象頂點來計算片斷的顏色;使用紋理時,會根據紋素來計算。
  • 映射(Mapping): 對齊頂點和紋素的方式。即將頂點座標 (X, Y, Z) 與 紋理座標 (U, V) 對應起來。
  • 取樣(Sampling): 在頂點固定後,每一個片斷根據計算出來的 (U, V) 座標,去找相應紋素的過程。
  • 幀緩存(Frame Buffer): 一個接收渲染結果的緩衝區,爲 GPU 指定存儲渲染結果的區域。更通俗點,能夠理解成存儲屏幕上最終顯示的一幀畫面的區域。
注: (U, V) 可能會超出 0 ~ 1 這個範圍,須要經過 glTextParameteri() 配置相應的方案,來映射到 S 軸和 T 軸。

五、怎麼使用緩存

在實際應用中,咱們須要使用各類各樣的緩存。好比在紋理渲染以前,須要生成一塊保存了圖像數據的紋理緩存。下面介紹一下緩存管理的通常步驟:

使用緩存的過程能夠分爲 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 步很重要,如今先有個印象,後面咱們在實際例子中會反覆用到。

六、OpenGL ES 的上下文

OpenGL ES 是一個狀態機,相關的配置信息會被保存在一個上下文(Context) 中,這個些值會被一直保存,直到被修改。但咱們能夠配置多個上下文,經過調用 [EAGLContext setCurrentContext:context] 來切換。

七、OpenGL ES 中的圖元

圖元(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 畫扇形。

2、經過 GLKit 渲染

恭喜你終於看完了枯燥的概念講解。從這裏開始,咱們開始會進入實際的例子,用代碼來說解渲染的過程。

在 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;
    }
}

二、初始化 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];

三、加載紋理

使用 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 的代理方法

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;
}

五、開始繪製

咱們調用 GLKViewdisplay 方法,便可以觸發 glkView:drawInRect: 回調,開始渲染的邏輯。

代碼以下:

[self.glkView display];

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

是否是以爲意猶未盡,那就趕快進入下一節,瞭解如何直接經過 GLSL 編寫的着色器來渲染紋理。

3、經過 GLSL 渲染

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

注: 你們實際去查看 demo 的時候,會發現仍是有引入 <GLKit/GLKit.h> 這個頭文件。這裏主要是爲了使用 GLKVector3GLKVector2 這兩個類型,固然不使用也是徹底能夠的。目的是爲了和 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 修飾符只存在於頂點着色器中,用於儲存每一個頂點信息的輸入,好比這裏定義了 PositionTextureCoords ,用於接收頂點的位置和紋理信息。

vec4vec2 是數據類型,分別指四維向量和二維向量。

varying 修飾符指頂點着色器的輸出,同時也是片斷着色器的輸入,要求頂點着色器和片斷着色器中都同時聲明,並徹底一致,則在片斷着色器中能夠獲取到頂點着色器中的數據。

gl_Positiongl_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,而 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));

四、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;
}

五、渲染層的綁定

經過以上步驟,咱們已經擁有了紋理,以及頂點的位置信息。如今到了最後一步,咱們要怎麼將緩存與視圖關聯起來?換句話說,假如屏幕上有兩個視圖,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 上查看完整代碼。

參考

獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】從零講解 iOS 中 OpenGL ES 的紋理渲染

相關文章
相關標籤/搜索