[OpenGL ES 02]OpenGL ES渲染管線與着色器


[OpenGL ES 02]OpenGL ES渲染管線與着色器 

羅朝輝 (http://www.cnblogs.com/kesalin/)

本文遵循“署名-非商業用途-保持一致”創作公用協議

前言

在前文《[OpenGL ES 01]iOS上OpenGL ES之初體驗》中我們學習了如何在 iOS 平台上設置OpenGL ES 環境,主要是設置 CAEAGLLayer 屬性,創建 EAGLContext,創建和使用 renderbuffer 和 framebuffer,並知道如何清屏。但實際上並沒有真正描繪點什么。在本文中,我們將學習OpenGL ES 渲染管線,頂點着色器和片元着色器相關知識,然后使用可編程管線在屏幕上描繪一個簡單三角形。

一,渲染管線

在 OpenGL ES 1.0 版本中,支持固定管線,而 OpenGL ES 2.0 版本不再支持固定管線,只支持可編程管線。什么是管線?什么又是固定管線和可編程管線?管線(pipeline)也稱渲染管線,因為 OpenGL ES在渲染處理過程中會順序執行一系列操作,這一系列相關的處理階段就被稱為OpenGL ES 渲染管線。pipeline 來源於福特汽車生產車間的流水線作業,在OpenGL ES 渲染過程中也是一樣,一個操作接着一個操作進行,就如流水線作業一樣,這樣的實現極大地提供了渲染的效率。整個渲染管線如下圖所示:

圖中陰影部分的 Vertex Shader 和 Fragment Shader 是可編程管線。可編程管線就是說這個操作可以動態編程實現而不必固定寫死在代碼中。可動態編程實現這一功能一般都是腳本提供的,在OpenGL ES 中也一樣,編寫這樣腳本的能力是由着色語言(Shader Language)提供的。那可編程管線有什么好處呢?方便我們動態修改渲染過程,而無需重寫編譯代碼,當然也和很多腳本語言一樣,調試起來不太方便。

再回到上圖,這張圖就是 OpenGL ES 的“架構圖”,學習OpenGL ES 就是學習這張圖中的每一個部分,在這里先粗略地介紹一下。

Vertex Array/Buffer objects:頂點數據來源,這時渲染管線的頂點輸入,通常使用 Buffer objects效率更好。在今天的示例中,簡單起見,使用的是 Vertex Array;

Vertex Shader:頂點着色器通過可編程的方式實現對頂點的操作,如進行坐標空間轉換,計算 per-vertex color以及紋理坐標;

Primitive Assembly:圖元裝配,經過着色器處理之后的頂點在圖片裝配階段被裝配為基本圖元。OpenGL ES 支持三種基本圖元:點,線和三角形,它們是可被 OpenGL ES 渲染的。接着對裝配好的圖元進行裁剪clip):保留完全在視錐體中的圖元,丟棄完全不在視錐體中的圖元,對一半在一半不在的圖元進行裁剪;接着再對在視錐體中的圖元進行剔除處理cull):這個過程可編碼來決定是剔除正面,背面還是全部剔除。

Rasterization:光柵化。在光柵化階段,基本圖元被轉換為二維的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,顏色,紋理坐標等信息,這些值是由圖元的頂點信息進行插值計算得到的。這些片元接着被送到片元着色器中處理。這是從頂點數據到可渲染在顯示設備上的像素的質變過程。

Fragment Shader:片元着色器通過可編程的方式實現對片元的操作。在這一階段它接受光柵化處理之后的fragment,color,深度值,模版值作為輸入。

Per-Fragment Operation:在這一階段對片元着色器輸出的每一個片元進行一系列測試與處理,從而決定最終用於渲染的像素。這一系列處理過程如下:

Pixel ownership test:該測試決定像素在 framebuffer 中的位置是不是為當前 OpenGL ES 所有。也就是說測試某個像素是否對用戶可見或者被重疊窗口所阻擋

Scissor Test:剪裁測試,判斷像素是否在由 glScissor 定義的剪裁矩形內,不在該剪裁區域內的像素就會被剪裁掉;

Stencil Test:模版測試,將模版緩存中的值與一個參考值進行比較,從而進行相應的處理;

Depth Test:深度測試,比較下一個片段與幀緩沖區中的片段的深度,從而決定哪一個像素在前面,哪一個像素被遮擋;

Blending:混合,混合是將片段的顏色和幀緩沖區中已有的顏色值進行混合,並將混合所得的新值寫入幀緩沖;

Dithering:抖動,抖動是使用有限的色彩讓你看到比實際圖象更多色彩的顯示方式,以緩解表示顏色的值的精度不夠大而導致的顏色劇變的問題。

Framebuffer:這是流水線的最后一個階段,Framebuffer 中存儲這可以用於渲染到屏幕或紋理中的像素值,也可以從Framebuffer 中讀回像素值,但不能讀取其他值(如深度值,模版值等)。

二,頂點着色器

下面來仔細看看頂點着色器:

頂點着色器接收的輸入:

Attributes:由 vertext array 提供的頂點數據,如空間位置,法向量,紋理坐標以及頂點顏色,它是針對每一個頂點的數據。屬性只在頂點着色器中才有,片元着色器中沒有屬性。屬性可以理解為針對每一個頂點的輸入數據。OpenGL ES 2.0 規定了所有實現應該支持的最大屬性個數不能少於 8 個。

Uniforms:uniforms保存由應用程序傳遞給着色器的只讀常量數據。在頂點着色器中,這些數據通常是變換矩陣,光照參數,顏色等。由 uniform 修飾符修飾的變量屬於全局變量,該全局性對頂點着色器與片元着色器均可見,也就是說,這兩個着色器如果被連接到同一個應用程序中,它們共享同一份 uniform 全局變量集。因此如果在這兩個着色器中都聲明了同名的 uniform 變量,要保證這對同名變量完全相同:同名+同類型,因為它們實際是同一個變量。此外,uniform 變量存儲在常量存儲區,因此限制了 uniform 變量的個數,OpenGL ES 2.0 也規定了所有實現應該支持的最大頂點着色器 uniform 變量個數不能少於 128 個,最大的片元着色器 uniform 變量個數不能少於 16 個。

Samplers:一種特殊的 uniform,用於呈現紋理。sampler 可用於頂點着色器和片元着色器。

Shader program:由 main 申明的一段程序源碼,描述在頂點上執行的操作:如坐標變換,計算光照公式來產生 per-vertex 顏色或計算紋理坐標。

頂點着色器的輸出:

Varying:varying 變量用於存儲頂點着色器的輸出數據,當然也存儲片元着色器的輸入數據,varying 變量最終會在光柵化處理階段被線性插值。頂點着色器如果聲明了 varying 變量,它必須被傳遞到片元着色器中才能進一步傳遞到下一階段,因此頂點着色器中聲明的 varying 變量都應在片元着色器中重新聲明同名同類型的 varying 變量。OpenGL ES 2.0 也規定了所有實現應該支持的最大 varying 變量個數不能少於 8 個。

在頂點着色器階段至少應輸出位置信息-即內建變量:gl_Position,其它兩個可選的變量為:gl_FrontFacing 和 gl_PointSize。

三,片元着色器

接下來仔細看看片元着色器:

片元管理器接受如下輸入: 

Varyings:這個在前面已經講過了,頂點着色器階段輸出的 varying 變量在光柵化階段被線性插值計算之后輸出到片元着色器中作為它的輸入,即上圖中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。OpenGL ES 2.0 也規定了所有實現應該支持的最大 varying 變量個數不能少於 8 個。

Uniforms:前面也已經講過,這里是用於片元着色器的常量,如霧化參數,紋理參數等;OpenGL ES 2.0 也規定了所有實現應該支持的最大的片元着色器 uniform 變量個數不能少於 16 個。

Samples:一種特殊的 uniform,用於呈現紋理。

Shader program:由 main 申明的一段程序源碼,描述在片元上執行的操作。

在頂點着色器階段只有唯一的 varying 輸出變量-即內建變量:gl_FragColor。

 

四,頂點着色與片元着色在編程上的差異

1,精度上的差異

着色語言定了三種級別的精度:lowp, mediump, highp。我們可以在 glsl 腳本文件的開頭定義默認的精度。如下代碼定義在 float 類型默認使用 highp 級別的精度

precision highp float;

在頂點着色階段,如果沒有用戶自定義的默認精度,那么 int 和 float 都默認為 highp 級別;而在片元着色階段,如果沒有用戶自定義的默認精度,那么就真的沒有默認精度了,我們必須在每個變量前放置精度描述符。此外,OpenGL ES 2.0 標准也沒有強制要求所有實現在片元階段都支持 highp 精度的。我們可以通過查看是否定義 GL_FRAGMENT_PRECISION_HIGH 來判斷具體實現是否在片元着色器階段支持 highp 精度,從而編寫出可移植的代碼。當然,通常我們不需要在片元着色器階段使用 highp 級別的精度,推薦的做法是先使用 mediump 級別的精度,只有在效果不夠好的情況下再考慮 highp 精度。

2,attribute 修飾符只可用於頂點着色。這個前面已經說過了。

3,或由於精度的不同,或因為編譯優化的原因,在頂點着色和片元着色階段同樣的計算可能會得到不同的結果,這會導致一些問題(z-fighting)。因此 glsl 引入了 invariant 修飾符來修飾在兩個着色階段的同一變量,確保同樣的計算會得到相同的值。

 

五,使用頂點着色器與片元着色器

好了,理論知識講得足夠多了,下面我們來看看如何在代碼中添加頂點着色器與片元着色器。我們在前一篇文章《[OpenGL ES 01]iOS上OpenGL ES之初體驗》代碼的基礎上進行編碼。在前面提到可編程管線通過用 shader 語言編寫腳本文件實現的,這些腳本文件相當於 C 源碼,有源碼就需要編譯鏈接,因此需要對應的編譯器與鏈接器,shader 對象與 program 對象就相當於編譯器與鏈接器。shader 對象載入源碼,然后編譯成 object 形式(就像C源碼編譯成 .obj文件)。經過編譯的 shader 就可以裝配到 program 對象中,每個 program對象必須裝配兩個 shader 對象:一個頂點 shader,一個片元 shader,然后 program 對象被連接成“可執行文件”,這樣就可以在 render 中是由該“可執行文件”了。

1,創建,裝載和編譯 shader

首先,我們向工程中添加新的類 GLESUtils,讓它繼承自 NSObject。修改 GLESUtils.h 為:

#import <Foundation/Foundation.h>
#include <OpenGLES/ES2/gl.h>

@interface GLESUtils : NSObject

// Create a shader object, load the shader source string, and compile the shader.
//
+(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString;

+(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath;

@end

修改 GLESUtils.m 為:

#import "GLESUtils.h"

@implementation GLESUtils

+(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath
{
    NSError* error;
    NSString* shaderString = [NSString stringWithContentsOfFile:shaderFilepath 
                                                       encoding:NSUTF8StringEncoding
                                                          error:&error];
    if (!shaderString) {
        NSLog(@"Error: loading shader file: %@ %@", shaderFilepath, error.localizedDescription);
        return 0;
    }
    
    return [self loadShader:type withString:shaderString];
}

+(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString
{   
    // Create the shader object
    GLuint shader = glCreateShader(type);
    if (shader == 0) {
        NSLog(@"Error: failed to create shader.");
        return 0;
    }
    
    // Load the shader source
    const char * shaderStringUTF8 = [shaderString UTF8String];
    glShaderSource(shader, 1, &shaderStringUTF8, NULL);
    
    // Compile the shader
    glCompileShader(shader);
    
    // Check the compile status
    GLint compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    
    if (!compiled) {
        GLint infoLen = 0;
        glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
        
        if (infoLen > 1) {
            char * infoLog = malloc(sizeof(char) * infoLen);
            glGetShaderInfoLog (shader, infoLen, NULL, infoLog);
            NSLog(@"Error compiling shader:\n%s\n", infoLog );            
            
            free(infoLog);
        }
        
        glDeleteShader(shader);
        return 0;
    }

    return shader;
}

@end

輔助類 GLESUtils 中有兩個類方法用來跟進 shader 腳本字符串或 shader 腳本文件創建 shader,然后裝載它,編譯它。下面詳細介紹每個步驟。

1),創建/刪除 shader

函數 glCreateShader 用來創建 shader,參數 GLenum type 表示我們要處理的 shader 類型,它可以是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分別表示頂點 shader 或 片元 shader。它返回一個句柄指向創建好的 shader 對象。

函數 glDeleteShader 用來銷毀 shader,參數為 glCreateShader 返回的 shader 對象句柄。

2),裝載 shader

函數 glShaderSource 用來給指定 shader 提供 shader 源碼。第一個參數是 shader 對象的句柄;第二個參數表示 shader 源碼字符串的個數;第三個參數是 shader 源碼字符串數組;第四個參數一個 int 數組,表示每個源碼字符串應該取用的長度,如果該參數為 NULL,表示假定源碼字符串是 \0 結尾的,讀取該字符串的內容指定 \0 為止作為源碼,如果該參數不是 NULL,則讀取每個源碼字符串中前 length(與每個字符串對應的 length)長度個字符作為源碼。

3),編譯 shader

函數 glCompileShader 用來編譯指定的 shader 對象,這將編譯存儲在 shader 對象中的源碼。我們可以通過函數 glGetShaderiv 來查詢 shader 對象的信息,如本例中查詢編譯情況,此外還可以查詢 GL_DELETE_STATUS,GL_INFO_LOG_STATUS,GL_SHADER_SOURCE_LENGTH 和 GL_SHADER_TYPE。在這里我們查詢編譯情況,如果返回 0,表示編譯出錯了,錯誤信息會寫入 info 日志中,我們可以查詢該 info 日志,從而獲得錯誤信息。

2,編寫着色腳本

GLESUtils 提供的接口讓我們可以使用兩種方式:腳本字符串或腳本文件來提供 shader 源碼,通常使用腳本文件方式有更大的靈活性。(Cocos2D 源碼中倒是提供了不少腳本字符串應對一些常見的情況,有興趣的同學可以查看下)。在這里,我們使用腳本文件方式。

1),添加頂點着色腳本

右擊 Supporting Files 目錄,New File->Other->Empty,輸入名稱:VertexShader.glsl,去除 target Tutorial02 中的勾選。后綴glsl 表示 GL Shader Language。

編輯其內容如下:

attribute vec4 vPosition; 
 
void main(void)
{
    gl_Position = vPosition;
}

然后選擇 Tutorial02,在 Build Phases -> Copy Bundle Sources 中添加 VertexShader.glsl。

頂點着色腳本的源碼很簡單,如果你仔細閱讀了前面的介紹,就一目了然。 attribute 屬性 vPosition 表示從應用程序輸入的類型為 vec4 的位置信息,輸出內建 vary 變量 vPosition。留意:這里使用了默認的精度。

2),添加片元着色腳本

用於添加頂點着色腳本同樣的方式添加名為 FragmentShader.glsl 的文件,編輯其內容如下:

precision mediump float;

void main()
{
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

不用忘記在 Build Phases -> Copy Bundle Sources 中添加 FragmentShader.glsl。

片元着色腳本源碼也很簡單,前面說過片元着色要么自己定義默認精度,要么在每個變量前添加精度描述符,在這里自定義 float 的精度為 mediump。然后為內建輸出變量 gl_FragColor 指定為紅色。

3,創建 program,裝配 shader,鏈接 program,使用 program

1),創建 program

在 OpenGLView.h 的 OpenGLView 類聲明中添加兩個成員:

    GLuint _programHandle;
    GLuint _positionSlot;

然后依然在 OpenGLView.m 中的匿名 category 中添加成員方法:

- (void)setupProgram;

在 - (void)render 方法前,添加其實現:

- (void)setupProgram
{
    // Load shaders
    //
    NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader"
                                                                  ofType:@"glsl"];
    NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader"
                                                                    ofType:@"glsl"];
    GLuint vertexShader = [GLESUtils loadShader:GL_VERTEX_SHADER
                                   withFilepath:vertexShaderPath]; 
    GLuint fragmentShader = [GLESUtils loadShader:GL_FRAGMENT_SHADER
                                     withFilepath:fragmentShaderPath];

    // Create program, attach shaders.
    _programHandle = glCreateProgram();
    if (!_programHandle) {
        NSLog(@"Failed to create program.");
        return;
    }
    
    glAttachShader(_programHandle, vertexShader);
    glAttachShader(_programHandle, fragmentShader);
    
    // Link program
    //
    glLinkProgram(_programHandle);
    
    // Check the link status
    GLint linked;
    glGetProgramiv(_programHandle, GL_LINK_STATUS, &linked );
    if (!linked) 
    {
        GLint infoLen = 0;
        glGetProgramiv (_programHandle, GL_INFO_LOG_LENGTH, &infoLen );
        
        if (infoLen > 1)
        {
            char * infoLog = malloc(sizeof(char) * infoLen);
            glGetProgramInfoLog (_programHandle, infoLen, NULL, infoLog );
            NSLog(@"Error linking program:\n%s\n", infoLog );            
            
            free (infoLog );
        }
        
        glDeleteProgram(_programHandle);
        _programHandle = 0;
        return;
    }
    
    glUseProgram(_programHandle);
    
    // Get attribute slot from program
    //
    _positionSlot = glGetAttribLocation(_programHandle, "vPosition");
}

有了前面的介紹,上面的代碼很容易理解。首先我們是由 GLESUtils 提供的輔助方法從前面創建的腳本中創建,裝載和編譯頂點 shader 和片元 shader;然后我們創建 program,將頂點 shader 和片元 shader 裝配到 program 對象中,再使用 glLinkProgram 將裝配的 shader 鏈接起來,這樣兩個 shader 就可以合作干活了。注意:鏈接過程會對 shader 進行可鏈接性檢查,也就是前面說到同名變量必須同名同型以及變量個數不能超出范圍等檢查。我們如何檢查 shader 編譯情況一樣,對 program 的鏈接情況進行檢查。如果一切正確,那我們就可以調用 glUseProgram 激活 program 對象從而在 render 中使用它。通過調用 glGetAttribLocation 我們獲取到 shader 中定義的變量 vPosition 在 program 的槽位,通過該槽位我們就可以對 vPosition 進行操作。

4,使用示例

在 - (void)layoutSubviews 中調用 render 方法之前,插入對 setupProgram 的調用:

    [self setupProgram];

    [self render];

然后改寫 render 方法:

- (void)render
{
    glClearColor(0, 1.0, 0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);

    // Setup viewport
    //
    glViewport(0, 0, self.frame.size.width, self.frame.size.height);
    
    GLfloat vertices[] = {
        0.0f,  0.5f, 0.0f, 
        -0.5f, -0.5f, 0.0f,
        0.5f,  -0.5f, 0.0f };
    
    // Load the vertex data
    //
    glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices );
    glEnableVertexAttribArray(_positionSlot);
    
    // Draw triangle
    //
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    [_context presentRenderbuffer:GL_RENDERBUFFER];
}

在新增的代碼中,第一句 glViewport 表示渲染 surface 將在屏幕上的哪個區域呈現出來,然后我們創建一個三角形頂點數組,通過 glVertexAttribPointer 將三角形頂點數據裝載到 OpenGL ES 中並與 vPositon 關聯起來,最后通過  glDrawArrays 將三角形圖元渲染出來。

5,編譯運行

編譯運行,將看到一個紅色的三角形顯示在屏幕中央。知道為什么是紅色的么?那是因為 program 也鏈接了片元着色器,在片元着色腳本文件中,我們指定 gl_FragColor 的值為紅色 vec4(1.0, 0.0, 0.0, 1.0)。

 

六,總結

在前文《[OpenGL ES 01]iOS上OpenGL ES之初體驗》和本文中,我們詳細了解了如何在 iPhone 中使用 OpenGL ES 的整個過程,包括設置 CAEAGLLayer 屬性,創建 EAGLContext,創建和使用 renderbuffer 和 framebuffer,了解OpenGL ES 渲染管線,創建和使用 shader,創建和實現 program,使用頂點數組進行描繪。流程已經走通,接下來讓我們進入 OpenGL ES 各個具體的技術領域。

本文源碼可以在這里獲得:https://github.com/kesalin/OpenGLES/tree/master/Tutorial02

 

七,Refference

OpenGL ES 2.0 Programming Guide

OpenGL ES Programming Guide for iOS 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM