渲染管線
蘋果提供了兩種OpenGL ES的可視化模型,一種是客戶端—服務(wù)端的架構(gòu)模型,另一種就是管線的模型。
客戶端—服務(wù)端架構(gòu):應(yīng)用程序狀態(tài)一旦更改,紋理、頂點數(shù)據(jù)以及渲染命令都將傳遞給OpenGL ES客戶端。客戶端將這些數(shù)據(jù)轉(zhuǎn)換成圖形硬件可以理解的格式,發(fā)送給GPU。這個過程增加了app圖形性能的開銷。為了獲得好的性能需要管理好這種開銷。一個好的設(shè)計需要減少OpenGL ES調(diào)用的頻率,使用適合硬件的數(shù)據(jù)格式來減少轉(zhuǎn)換成本,并且管理好自身與OpenGL ES的數(shù)據(jù)流。
管線架構(gòu):另外就是本文將要說的管線架構(gòu),由于管線中每個獨立階段都依賴于上一階段的產(chǎn)出,所以任何階段的工作量太大或者運行太慢,其他階段都會被迫閑置以等待前一階段的完成。好的設(shè)計會根據(jù)硬件功能平衡每個階段的執(zhí)行工作。
在 OpenGL ES 1.0 版本中,支持固定管線(fixed-function pipeline),有一系列固定的函數(shù)用來在屏幕上渲染對象,而不是創(chuàng)建一個單獨的程序來指導(dǎo) GPU 的行為。這樣有很大的局限性,你不能做出任何特殊的效果。如果想知道著色器在工程中可以造成怎樣的不同,看看這篇 Brad Larson 寫的他用著色器替代固定函數(shù)重構(gòu) Molecules 應(yīng)用的博客
而 OpenGL ES 2.0 版本不再支持固定管線,只支持可編程管線。
什么是管線?什么又是固定管線和可編程管線?
管線(pipeline):也稱渲染管線,因為 OpenGL ES在渲染處理過程中會順序執(zhí)行一系列操作,這一系列相關(guān)的處理階段就被稱為OpenGL ES 渲染管線。OpenGL ES 渲染過程就如流水線作業(yè)一樣,這樣的實現(xiàn)極大地提高了渲染的效率。如圖就是 OpenGL ES 的管線圖,學(xué)習OpenGL ES 就是學(xué)習這張圖中的每一個部分。
圖中陰影部分的 Vertex Shader 和 Fragment Shader 是可編程管線。可編程管線就是說這個操作可以動態(tài)編程而不必寫死在代碼中。可動態(tài)編程實現(xiàn)這一功能一般都是腳本提供的,在OpenGL ES 中也一樣,編寫這樣腳本的能力是由著色語言GLSL提供的。那可編程管線有什么好處呢?方便我們動態(tài)修改渲染過程,而無需重寫編譯代碼,當然也和很多腳本語言一樣,調(diào)試起來不太方便。
渲染管線中的各個模塊
Vertex Shader
由圖可見,頂點著色器分輸入輸出兩部分。頂點著色器定義了在 2D 或者 3D 場景中幾何圖形是如何處理的,實現(xiàn)了頂點操作的通用可編程方法。一個頂點指的是 2D 或者 3D 空間中的一個點。在圖像處理中,有 4 個頂點:每一個頂點代表圖像的一個角。頂點著色器設(shè)置頂點的位置,并且把位置和紋理坐標這樣的參數(shù)發(fā)送到片段著色器。
輸入:
- 著色器程序(Shader Program,圖中沒有畫出):由 main 申明的一段程序源碼或可執(zhí)行文件,描述在頂點上執(zhí)行的操作:如坐標變換、計算光照公式產(chǎn)生每個頂點顏色、計算紋理坐標。
- 屬性(Attribute):由 vertext array 提供的頂點數(shù)據(jù),如空間位置,法向量,紋理坐標以及頂點顏色,屬性可以理解為針對每一個頂點的輸入數(shù)據(jù)。屬性只在頂點著色器中才有,片元著色器中沒有屬性。OpenGL ES 2.0 規(guī)定了所有實現(xiàn)應(yīng)該支持的最大屬性個數(shù)不能少于 8 個。
- 常量(Uniforms): Uniforms保存由應(yīng)用程序傳遞給著色器的只讀常量數(shù)據(jù)。在頂點著色器中,這些數(shù)據(jù)通常是變換矩陣,光照參數(shù),顏色等。由 uniform 修飾符修飾的變量屬于全局變量,該全局性對頂點著色器與片元著色器均可見,也就是說,這兩個著色器如果被連接到同一個應(yīng)用程序中,它們共享同一份 uniform 全局變量集。因此如果在這兩個著色器中都聲明了同名的 uniform 變量,要保證這對同名變量完全相同:同名+同類型,因為它們實際是同一個變量。此外,uniform 變量存儲在常量存儲區(qū),因此限制了 uniform 變量的個數(shù),OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大頂點著色器 uniform 變量個數(shù)不能少于 128 個,最大的片元著色器 uniform 變量個數(shù)不能少于 16 個。
- 采樣器(Samplers): 一種特殊的 uniform,用于呈現(xiàn)紋理。sampler 可用于頂點著色器和片元著色器。
輸出: - 可變變量(Varying):varying 變量用于存儲頂點著色器的輸出數(shù)據(jù),也存儲片元著色器的輸入數(shù)據(jù)。varying 變量會在光柵化處理階段被線性插值。頂點著色器如果聲明了 varying 變量,它必須被傳遞到片元著色器中才能進一步傳遞到下一階段,因此頂點著色器中聲明的 varying 變量都應(yīng)在片元著色器中重新聲明為同名同類型的 varying 變量。OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大 varying 變量個數(shù)不能少于 8 個。
- 在頂點著色器階段至少應(yīng)輸出位置信息-即內(nèi)建變量:gl_Position,其它兩個可選的變量為:gl_FrontFacing 和 gl_PointSize。
Primitive Assembly圖元裝配(還有啥好的翻譯?)
圖元(Primitive):OpenGL ES 支持三種基本圖元:點,線和三角形,它們是可被 OpenGL ES 渲染的。經(jīng)過著色器處理之后的頂點在這一階段被裝配為基本圖元。對于每個圖元,必須確定圖元是否位于視錐體(屏幕上可見的3D空間區(qū)域)內(nèi),保留完全在視錐體中的圖元,丟棄完全不在視錐體中的圖元,對一半在一半不在的圖元進行裁剪。裁剪之后,頂點位置就被轉(zhuǎn)換成了屏幕坐標。也可以再對在視錐體中的圖元進行剔除(cull):這個過程可編碼來決定是剔除正面,背面還是全部剔除。裁剪和剔除之后,圖元便準備傳遞給管線的下一個階段——光柵化階段。
Rasterization
在光柵化階段,基本圖元被轉(zhuǎn)換為一組二維的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,顏色,紋理坐標等信息,這些值是由圖元的頂點信息進行插值計算得到的。這些片元接著被送到片元著色器中處理。這是從頂點數(shù)據(jù)到可渲染在顯示設(shè)備上的像素的質(zhì)變過程。
Fragment Shader
GPU 使用片元著色器在對象或者圖片的每一個像素上進行計算,處理由光柵化階段生成的每個片元,最終計算出每個像素的最終顏色。圖片,歸根結(jié)底,實際上僅僅是數(shù)據(jù)的集合。圖片的文檔包含每一個像素的各個顏色分量和像素透明度的值。因為對每一個像素,算式是相同的,GPU 可以流水線作業(yè)這個過程,從而更加有效的進行處理。使用正確優(yōu)化過的著色器,在 GPU 上進行處理,將獲得百倍于在 CPU 上用同樣的過程進行圖像處理的效率。
- 可變變量(Varyings):這個在前面已經(jīng)講過了,頂點著色器階段輸出的 varying 變量在光柵化階段被線性插值計算之后輸出到片元著色器中作為它的輸入,即上圖中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大 varying 變量個數(shù)不能少于 8 個。
- 常量(Uniforms):前面也已經(jīng)講過,這里是用于片元著色器的常量,如霧化參數(shù),紋理參數(shù)等;OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大的片元著色器 uniform 變量個數(shù)不能少于 16 個。
- 采樣器(Samples):一種特殊的 uniform,用于呈現(xiàn)紋理。
- 著色器程序(Shader program):由 main 申明的一段程序源碼,描述在片元上執(zhí)行的操作。
- 在頂點著色器階段只有唯一的 varying 輸出變量-即內(nèi)建變量:gl_FragColor。
頂點著色器與片元著色器的編程區(qū)別
-
精度上的差異
著色語言定了三種級別的精度:lowp, mediump, highp。我們可以在 glsl 腳本文件的開頭定義默認的精度。precision highp float;
- 在頂點著色階段,如果沒有自定義默認精度,那么 int 和 float 都默認為 highp 級別;
- 在片元著色階段,如果沒有自定義默認精度,就真的沒有默認精度了.我們必須在每個變量前放置精度描述符。此外,OpenGL ES 2.0 標準也沒有強制要求所有實現(xiàn)在片元階段都支持 highp 精度。我們可以通過查看是否定義 GL_FRAGMENT_PRECISION_HIGH 來判斷具體實現(xiàn)是否在片元著色器階段支持 highp 精度,從而編寫出可移植的代碼。通常不需要在片元著色器階段使用 highp 級別的精度,推薦先使用 mediump 級別的精度,只有在效果不夠好的情況下再考慮 highp 精度。
attribute 修飾符只可用于頂點著色。
使用頂點著色器和片元著色器
可編程管線通過用 GLSL 語言編寫腳本文件實現(xiàn)的,這些腳本文件相當于 C 源碼,有源碼就需要編譯鏈接,因此需要對應(yīng)的編譯器與鏈接器,shader 對象與 program 對象就相當于編譯器與鏈接器。shader 對象載入源碼,然后編譯成 object 形式(就像C源碼編譯成 .obj文件)。經(jīng)過編譯的 shader 就可以裝配到 program 對象中,每個 program對象必須裝配兩個 shader 對象:一個頂點 shader,一個片元 shader,然后 program 對象被連接成“可執(zhí)行文件”,這樣就可以在 render 中運行該“可執(zhí)行文件”了。
-
創(chuàng)建,裝載和編譯 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 { //創(chuàng)建shader GLuint shader = glCreateShader(type); if (shader == 0) { NSLog(@"Error: failed to create shader."); return 0; } //裝載shader const char * shaderStringUTF8 = [shaderString UTF8String]; glShaderSource(shader, 1, &shaderStringUTF8, NULL); //編譯shader glCompileShader(shader); //查詢變異狀態(tài) 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 腳本文件創(chuàng)建 shader,然后裝載它,編譯它。下面詳細介紹每個步驟。
創(chuàng)建/刪除 shader
函數(shù) glCreateShader 用來創(chuàng)建 shader,參數(shù) GLenum type 表示我們要處理的 shader 類型,它可以是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分別表示頂點 shader 或 片元 shader。它返回一個句柄指向創(chuàng)建好的 shader 對象。
函數(shù) glDeleteShader 用來銷毀 shader,參數(shù)為 glCreateShader 返回的 shader 對象句柄。裝載 shader
函數(shù) glShaderSource 用來給指定 shader 提供 shader 源碼。第一個參數(shù)是 shader 對象的句柄;第二個參數(shù)表示 shader 源碼字符串的個數(shù);第三個參數(shù)是 shader 源碼字符串數(shù)組;第四個參數(shù)一個 int 數(shù)組,表示每個源碼字符串應(yīng)該取用的長度,如果該參數(shù)為 NULL,表示假定源碼字符串是 \0 結(jié)尾的,讀取該字符串的內(nèi)容指定 \0 為止作為源碼,如果該參數(shù)不是 NULL,則讀取每個源碼字符串中前 length(與每個字符串對應(yīng)的 length)長度個字符作為源碼。編譯 shader
函數(shù) glCompileShader 用來編譯指定的 shader 對象,這將編譯存儲在 shader 對象中的源碼。查詢shader對象信息
函數(shù) glGetShaderiv 來查詢 shader 對象的信息,如本例中查詢編譯情況。此外還可以查詢 GL_DELETE_STATUS,GL_INFO_LOG_STATUS,GL_SHADER_SOURCE_LENGTH 和 GL_SHADER_TYPE。在這里我們查詢編譯情況,如果返回 0,表示編譯出錯了,錯誤信息會寫入 info 日志中,我們可以查詢該 info 日志,從而獲得錯誤信息。
-
編寫著色腳本
添加VertexShader.glsl和FragmentShader.glsl文件- VertexShader.glsl
attribute vec4 vPosition; void main(void) { gl_Position = vPosition; }
attribute 屬性 vPosition 表示從應(yīng)用程序輸入的類型為 vec4 的位置信息,輸出內(nèi)建 vary 變量 vPosition。注意:這里使用了默認的精度。
- FragmentShader.glsl
precision mediump float; void main() { gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);//RGBA,此處為黃色 }
片元著色腳本源碼也很簡單,前面說過片元著色要么自己定義默認精度,要么在每個變量前添加精度描述符,在這里自定義 float 的精度為 mediump。然后為內(nèi)建輸出變量 gl_FragColor 指定為黃色。
-
創(chuàng)建 program,裝配 shader,鏈接 program,使用 program
- OpenGLView.h中添加兩個成員
GLuint _programHandle; GLuint _positionSlot;
- 在 OpenGLView.m 中的匿名 category 中添加成員方法:
- (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 提供的輔助方法從前面創(chuàng)建的腳本中創(chuàng)建,裝載和編譯頂點 shader 和片元 shader;然后我們創(chuàng)建 program,將頂點 shader 和片元 shader 裝配到 program 對象中,再使用 glLinkProgram 將裝配的 shader 鏈接起來,這樣兩個 shader 就可以合作干活了。注意:鏈接過程會對 shader 進行可鏈接性檢查,也就是前面說到同名變量必須同名同型以及變量個數(shù)不能超出范圍等檢查。我們?nèi)绾螜z查 shader 編譯情況一樣,對 program 的鏈接情況進行檢查。如果一切正確,那我們就可以調(diào)用 glUseProgram 激活 program 對象從而在 render 中使用它。通過調(diào)用 glGetAttribLocation 我們獲取到 shader 中定義的變量 vPosition 在 program 的槽位,通過該槽位我們就可以對 vPosition 進行操作。
-
使用示例
在 - (void)layoutSubviews 中調(diào)用 render 方法之前,插入對 setupProgram 的調(diào)用[self setupProgram]; [self render];
render方法:
- (void)render {
glClearColor(0.5, 1.0, 0.5, 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 將在屏幕上的哪個區(qū)域呈現(xiàn)出來,然后我們創(chuàng)建一個三角形頂點數(shù)組,通過 glVertexAttribPointer 將三角形頂點數(shù)據(jù)裝載到 OpenGL ES 中并與 vPositon 關(guān)聯(lián)起來,最后通過 glDrawArrays 將三角形圖元渲染出來。
代碼存放在繪制一個三角形中New Group文件夾
OpenGL ES渲染管線與著色器
OpenGL ES 3.0編程指南
Apple文檔——OpenGL ES Design Guidelines
Notes on OpenGL ES Graphics Pipeline
GPU 加速下的圖像處理