OpenGL ES

一、OpenGL ES介紹

OpenGL(Open Graphics Library)定義了一個跨編程語言、跨平臺編程的專業(yè)圖形程序接口??捎糜诙S或三維圖像的處理和渲染,它是一個功能強大、調(diào)用方便的底層圖形庫。對于嵌入式設(shè)備,其提供了OpenGL ES(OpenGL for Embeddled Systems)版本,該版本是針對手機、Pad等嵌入式設(shè)備而設(shè)計的,是OpenGL的一個子集。到目前為止,OpenGL已經(jīng)經(jīng)歷過很多版本的迭代與更新,最新版本是3.0,而使用最廣泛的還是OpenGL ES 2.0版本。本文是基于2.0版本進行編程并實現(xiàn)圖像的處理與渲染,并且只討論2D部分的內(nèi)容。

由于OpenGL是基于跨平臺設(shè)計的,所以在每個平臺上都要有它的具體實現(xiàn),即要提供OpenGL ES的上下文環(huán)境以及窗口的管理。在OpenGL的設(shè)計中,OpenGL是不負責管理窗口的,窗口的管理將交由各個設(shè)備自己來完成,上下文環(huán)境也是一樣的,其在各個平臺上都有自己的實現(xiàn)。在iOS平臺上使用EAGL提供本地平臺對OpenGL ES的實現(xiàn)。

這里需要介紹一個庫——libSDL,它可以為開發(fā)者提供面向libSDL的API編程,libSDL內(nèi)部會解決多個平臺的OpenGL上下文環(huán)境以及窗口的管理問題,開發(fā)者只需要交叉編譯這個庫到各自的平臺上就可以做到只寫一份代碼即可運行多個平臺。其中FFmpeg中的ffplay這一工具就是基于libSDL進行開發(fā)的。但是對于移動開發(fā)者來講,這樣就會失去一些更加靈活的控制,甚至某些場景下的功能不能實現(xiàn)。

上面介紹了OpenGL ES是什么,下面再來介紹一下OpenGL ES能做什么。其實從名字上就可以看出來,OpenGL主要是做圖形圖像處理的庫,尤其是在移動設(shè)備上進行圖形圖像處理,它的性能優(yōu)勢更能體現(xiàn)出來。GLSL(OpenGL Shading Language)是OpenGL的著色器語言,開發(fā)人員利用這種語言編寫程序運行在GPU(Graphic Processor Unit,圖形圖像處理單元,可以理解為是一種高并發(fā)的運算器)上以進行圖像的處理和渲染。GLSL著色器代碼分為兩個部分,即Vertex Shader(頂點著色器)與Fragment Shade(片元著色器)兩部分,分別完成各自在OpenGL渲染管線中的功能。對于OpenGL ES,業(yè)界有一個著名的開源庫GPUImage,它的實現(xiàn)非常優(yōu)雅,尤其是在iOS平臺上實現(xiàn)的非常完備,不僅有攝像頭采集實時渲染、視頻播放器、離線保存等功能,更有強大的濾鏡實現(xiàn)。在GPUImage的濾鏡實現(xiàn)中,可以找到大部分圖形圖像處理Shader的實現(xiàn),包括:亮度、對比度、飽和度、色調(diào)曲線、白平衡、灰度等調(diào)整顏色的處理,以及銳化、高斯模糊等圖像像素處理的實現(xiàn)等,還有素描、卡通效果、浮雕效果等視覺效果的實現(xiàn),最后還有各種混合模式的實現(xiàn)等。當然除了GPUImage提供的這些圖像處理的Shader之外,開發(fā)者也可以自己實現(xiàn)一些有意思的Shader,比如美顏濾鏡效果、瘦臉效果以及粒子效果等。

二、OpenGL ES的實踐

1.OpenGL 渲染管線

要想學習著色器,并理解著色器的工作機制,就要對OpenGL 固定的渲染管線有深入的了解。同樣,先來統(tǒng)一一下術(shù)語。

  • 幾何圖元:包括點、直線、三角形,均是通過頂點(vertex)來指定的。
  • 模型:根據(jù)幾何圖元創(chuàng)建的物體。
  • 渲染:計算機根據(jù)模型創(chuàng)建圖像的過程。

最終渲染過程結(jié)束之后,人眼所能看到的圖像就是由屏幕上的所有像素點組成的,在內(nèi)存中,這些像素點可以組織成一個大的一維數(shù)組,每4個字節(jié)即表示一個像素點的RGBA數(shù)據(jù),而在顯卡中,這些像素點可以組織成幀緩沖區(qū)的形式,幀緩沖區(qū)保存了圖形硬件為了控制屏幕上所有像素的顏色和強度所需要的全部信息。理解了幀緩沖區(qū)的概念,接下來就來討論一下OpenGL的渲染管線,這部分內(nèi)容對于OpenGL來說是非常重要的。

那么OpenGL的渲染管線具體是做什么的呢?其實就是OpenGL引擎渲染圖像的流程,也就是說OpenGL引擎是一步一步地將圖片渲染到屏幕上去的過程。渲染管線分為以下幾個階段。

階段一:指定幾何對象

所謂幾何對象,就是上面說過的幾何圖元,這里將根據(jù)具體執(zhí)行的指令繪制幾何圖元。比如,OpenGL提供給開發(fā)者的繪制方法glDrawArrays,這個方法里的第一個參數(shù)是mode,就是制定繪制方式,可選值有一下幾種。

  • GL_POINT:以點的形式進行繪制,通常用在繪制粒子效果的場景中。
  • GL_LINES:以線的形式進行繪制,通常用在繪制直線的場景中。
  • GL_TRIANGLE_STRIP:以三角形的形式進行繪制,所有二維圖像的渲染都會使用這種方式。

具體選用哪一種繪制方式?jīng)Q定了OpenGL渲染管線的第一階段應(yīng)如何去繪制幾何圖元,所以這就是第一階段指定的幾何對象。

階段二:頂點處理

不論以上的幾何對象是如何指定的,所有的幾何數(shù)據(jù)都將會經(jīng)過這個階段。這個階段所做的操作就是,根據(jù)模型視圖和投影矩陣進行變換來改變頂點的位置,根據(jù)紋理坐標與紋理矩陣來改變紋理坐標的位置,如果涉及三維的渲染,那么這里還要處理光照計算與法線變換。這里的輸出是以gl_Position來表示具體的頂點位置的,如果是以點來繪制幾何圖元,那么還應(yīng)該輸出gl_PointSize。

階段三:圖元組裝

在經(jīng)過階段二的頂點處理操作之后,還是紋理坐標都是已經(jīng)確定好了的。在這個階段,頂點將會根據(jù)應(yīng)用程序送往圖元的規(guī)則(如GL_POINT、GL_TRIANGLE_STRIP),將紋理組裝成圖元。

階段四:柵格化操作

由階段三傳遞過來的圖元數(shù)據(jù),在此將會分解成更小的單元并對應(yīng)于幀緩沖區(qū)的各個像素。這些單元稱為片元,一個片元可能包含窗口顏色、紋理坐標等屬性。片元的屬性是根據(jù)頂點坐標利用插值來確定的,這就是柵格化操作,也就是確認好每一個片元是什么。

階段五:片元處理

通過紋理坐標取得紋理(texture)中相對應(yīng)的片元像素值(texel),根據(jù)自己的業(yè)務(wù)處理(比如提亮、飽和度調(diào)節(jié)、對比度調(diào)節(jié)、高斯模糊)來變換這個片元的顏色。這里的輸出是gl_FragColor,用于表示修改之后的像素的最終結(jié)果。

階段六:幀緩沖操作

該階段主要執(zhí)行幀緩沖的寫入操作,這也是渲染管線的最后一步,負責將最終的像素值寫入到幀緩沖區(qū)中。

前面也提到過,OpenGL ES提供了可編程的著色器來代替渲染管線的某個階段。具體如下所示:
Vertex Shader(頂點著色器)用來替代頂點處理階段。
Fragment Shader(片元著色器,又稱為像素著色器)用來替換片元處理階段。

glFinish和glFlush

提交給OpenGL的繪圖指令并不會馬上發(fā)送給圖形硬件執(zhí)行,而是放到一個緩沖區(qū)里面,等待緩沖區(qū)滿了之后再將這些指令發(fā)送給圖形硬件執(zhí)行,所以指令較少或較簡單時是無法填充緩沖區(qū)的,這些指令自然不能馬上執(zhí)行以達到所需要的效果。因此每次寫完繪圖代碼,需要讓其立即完成效果時,開發(fā)者需要在代碼后面添加glFlush()或glFinish()函數(shù)。

  • glFlush()的作用是將緩沖區(qū)中的指令(無論是否為滿)立即發(fā)送給圖形硬件執(zhí)行,發(fā)送完立即返回。
  • glFinish()的作用也是將緩沖區(qū)中的指令(無論是否為滿)立即發(fā)送給圖形硬件執(zhí)行,但是要等待圖形硬件執(zhí)行完成之后才返回這些指令。

2.GLSL語法與內(nèi)建函數(shù)

GLSL是為了實現(xiàn)著色器的功能而向開發(fā)人員提供的一種開發(fā)語言。

(1)GLSL的修飾符與基本數(shù)據(jù)類型

具體來說,GLSL的語法與C語言非常類似,學習一門語言,首先要看它的數(shù)據(jù)類型表示,然后再學習具體的運行流程。對于GLSL,其數(shù)據(jù)類型表示具體如下.

修飾符

具體如下:

  • const:用于聲明非可寫的編譯時常量變量。
  • attribute:用于經(jīng)常更改的信息,只能在頂點著色器中使用。
  • uniform:用于不經(jīng)常更改的信息,可用于頂點著色器和片元著色器。
  • varying:用于修飾從頂點著色器向片元著色器傳遞的變量
基本數(shù)據(jù)類型

int、float、bool,這些與C語言都是一致的,需要強調(diào)的一點就是,這里面的float是有一個修飾符的,即可以指定精度。三種修飾符的范圍(范圍一般視顯卡而定)和應(yīng)用情況具體如下。

  • highp:32bit,一般用于頂點坐標(vertex Coordinate)。
  • medium:16bit,一般用于紋理坐標(texure Coordinate)。
  • lowp:8bit,一般用于顏色顯示(color)。
向量類型

向量類型是Shader中非常重要的一個數(shù)據(jù)類型,因為在做數(shù)據(jù)傳遞的時候需要經(jīng)常傳遞多個參數(shù),相較于寫多個基本數(shù)據(jù)類型,使用向量類型是非常好的選擇。列舉一個最經(jīng)典的例子,要將物體坐標和紋理坐標傳遞到Vertex Shader中,用的就是向量類型,每一個頂點就是一個四維向量,在Vertex Shader中利用這兩個四維向量即可完成自己的紋理坐標映射操作。聲明方式如下(GLSL代碼):

attribute vec4 position;
矩陣類型

有一些效果器需要開發(fā)者傳入矩陣類型的數(shù)據(jù),比如后面會接觸到的懷舊效果器,就需要傳入一個矩陣來改變原始的像素數(shù)據(jù)。聲明方式如下:

uniform lowp mat4 colorMatrix;

上面的代碼表示了一個4x4的浮點矩陣,如果是mat2就是2x2的浮點矩陣,如果是mat3就是3x3的浮點矩陣。若要傳遞一個矩陣到實際的Shade中,則可以直接調(diào)用如下函數(shù):

glUniformMarix4fv(mColorMatrixLocation,1,false,mColorMatrix);
紋理類型

一般僅在Fragment Shader中使用這個類型,二維紋理的聲明方式如下

uniform sample2D texSampler;

當客戶端接收到這個句柄時,就可以為它綁定一個紋理,代碼如下:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texId);
glUniformli(mGLUniformTexture,0);

上述代碼第一行激活的是哪一個紋理句柄,第三行代碼中的第二個參數(shù)需要傳遞對應(yīng)的Index,就像代碼中激活的紋理句柄是GL_TEXTURE0,對應(yīng)的Index就是0,如果激活的紋理句柄是GL_TEXTURE1,那么對應(yīng)的Index就是1,在不同的平臺上句柄的個數(shù)也是不一樣,但是一般都會在32個以上。

varying

這個修飾符變量用于在Vertex Shader和Fragment Shader之間傳遞函數(shù)。首先在頂點著色器中聲明這個類型的變量代表紋理的坐標點,并且對這個變量進行賦值,代碼如下:

attribute vec2 texoord;
varying vec2 v_texcoord;
void main(void)
{
      // 計算頂點坐標
      v_texcoord = texcoord;
}

緊接著在Fragment Shader中也聲明同名的變量,然后使用texture2D方法取出二維紋理中該紋理坐標點上的紋理像素值,代碼如下

varying vec2 v_texcoord;
vec4 texel = texture2D(texSampler,v_texcoord);

取出了該坐標點上的像素值之后,就可以進行像素變化操作了,比如說提高對比度,最終將改變的像素值復制給gl_FragColor。

(2)GLSL的內(nèi)置函數(shù)與內(nèi)置變量

首先來看內(nèi)置變量,最常見的是兩個Shader的輸出變量。
先來看Vertex Shader的內(nèi)置變量:

vec4 gl_position;

上述代碼用來設(shè)置頂點轉(zhuǎn)換到屏幕坐標的位置,Vertex Shader一定要去更新這個數(shù)值。另外還有一個內(nèi)置變量,

float gl_pointSize;

在粒子效果的場景下,需要為粒子設(shè)置大小,改變該內(nèi)置變量的值就是為了設(shè)置每一個粒子矩形的大小。
其次是Fragment Shader的內(nèi)置變量,代碼如下

vec4 gl_FragColor;

上述代碼用于指定當前紋理坐標所代表的像素點的最終顏色值。

然后是內(nèi)置函數(shù),具體的函數(shù)可以去官方文檔中查詢,這里僅介紹幾個常用的函數(shù)。

  • abs(genType x):絕對值函數(shù)。

  • floor(genType x):向下取整函數(shù)。

  • ceil(genType x):向上取整函數(shù)。

  • mod(genType x,genType y):取模函數(shù)。

  • min(genType x,genType y):取的最小值函數(shù)。

  • max(genType x,genType y):取得最大值函數(shù)。

  • clamp(genType x,genType y,genType z):取得中間值函數(shù)。

  • step(genType x,genType y):如果x<edge,則返回0.0,否則返回1.0。

  • smoothstep(genType edge0,genType edge1,genType x):如果x<=edge0,則返回0.0;如果x>=edge1,則返回1.0;如果edge0<x<edge1,則執(zhí)行0-1的平衡插值。

  • mix(genType x,genType y,genType a):返回線性混合的x和y,用公式表示為x(1-a)+ya,這個函數(shù)在mix兩個紋理圖像的時候非常有用。

GLSL的控制流與C語言非常類似,既可以使用for、while、以及do-while實現(xiàn)循環(huán),也可以使用if和if-else進行條件分支的操作。

3.創(chuàng)建顯卡執(zhí)行程序

如何將Shader傳遞給OpenGL的渲染管線。

1)創(chuàng)建shader的過程

第一步是調(diào)用glCreateShader方法創(chuàng)建一個對象,作為shade的容器,該函數(shù)會返回一個容器的句柄,函數(shù)的原型如下:

GLuint glCreateShader(GLenum shaderType);

函數(shù)原型中的參數(shù)shaderType有兩種類型,當要創(chuàng)建VertexShader時,開發(fā)者應(yīng)該傳入類型GL_VERTEX_SHADER;當要創(chuàng)建FragmentShader時,開發(fā)者應(yīng)該傳入GL_FRAGMENT_SHADER類型。

下一步就是為創(chuàng)建的這個shader添加源代碼,源代碼就是根據(jù)GLSL語法和內(nèi)嵌函數(shù)編寫的兩個著色器程序(Shader),其為字符串類型。函數(shù)原型如下:

void glShaderSource(GLuint shader,int numOfStrings,const char **strings,int *lenOfStrings)

上述函數(shù)的作用就是把開發(fā)者編寫的著色器程序加載到著色器句柄所關(guān)聯(lián)的內(nèi)存中。

最后一步就是編譯該Shader,編譯Shader的函數(shù)原型如下:

void glCompileShader(GLuint shader);

待編譯完成之后,還需要驗證該Shader是否編譯成功了。那么,應(yīng)該如何驗證呢?使用下面的函數(shù)即可進行驗證:

void glCetShaderiv(GLuint shader,GLenum pname,GLint *params);

其中第一個參數(shù)就是需要驗證的Shader句柄;第二個參數(shù)值是需要驗證的Shader狀態(tài)值,這里一般是驗證編譯是否成功,該狀態(tài)值一般是選取GL_COMPILE_STATUS;第三個參數(shù)是返回值。當返回1時,則說明該Shader是編譯成功的;如果為0,則說明該Shader沒有被編譯成功,此時獲取的是改Shader的另外一個狀態(tài),該狀態(tài)值應(yīng)該選取GL_INFO_LOG_LENGTH,返回值返回的則是錯誤原因字符串的長度,我們可以利用這個長度分配出一個buffer,然后調(diào)用獲取Shader的InfoLog函數(shù),函數(shù)原型如下:

void glGetShaderInfoLog(GLuint object,int maxLen,int *len,char *log);

之后可以把InfoLog打印出來,以幫助我們調(diào)試實際Shader中的錯誤。

2)如何通過兩個Shader來創(chuàng)建顯卡可執(zhí)行程序

首先創(chuàng)建一個對象,作為程序的容器,此函數(shù)將返回容器的句柄。函數(shù)原型如下:

GLuint glCreateProgram(void);

第二步是把前文編譯的Shader附加到剛剛創(chuàng)建的程序中,調(diào)用的函數(shù)名稱如下:

void glAttachShader(GLuint program,GLuint shader);

第一個參數(shù)就是傳入上一步返回的程序容器的句柄,第二個參數(shù)就是編譯的Shader容器的句柄,當然要為每一個Shader都調(diào)用一次這個方法才能把兩個Shader都關(guān)聯(lián)到Program中去。
最后一步就是鏈接程序了,鏈接函數(shù)原型如下:

void glLinkProgram(GLuint program);

傳入?yún)?shù)就是程序容器的句柄,那么這個程序有沒有鏈接成功呢?OpenGL提供了一個函數(shù)來檢查該程序的狀態(tài),函數(shù)原型如下:

void glGetProgramiv(GLuint program,GLenum pname,GLint *params);

第一個參數(shù)就是傳入程序容器的句柄,第二個參數(shù)代表需要檢查該程序的哪一個狀態(tài),這里傳入的是GL_LINK_STATUS,最后一個參數(shù)就是返回值。返回值為1則代表鏈接成功,如果返回值為0則代表鏈接失敗。如果想獲取具體的錯誤信息,第二個參數(shù)要傳遞GL_INFO_LOG_LENGTH,代表獲取該程序的InfoLog的長度,獲取到長度之后我們分配出一個char *的內(nèi)存空間以獲取InfoLog,函數(shù)原型如下:

void glGetProgramInfoLog(GLuint object,int maxLen,int *len,char *log);

該函數(shù)返回InfoLog之后可以將其打印出來。

使用構(gòu)建的這個函數(shù),調(diào)用glUseProgram方法就可以了。如果想讓其完全運行在手機上,還需要為其提供一個上下文環(huán)境。

三、iOS上下文環(huán)境搭建

在iOS平臺上不允許開發(fā)者使用OpenGL ES直接渲染屏幕,必須使用FrameBuffer與RenderBuffer來進行渲染。若要使用EAGL,則必須先創(chuàng)建一個RenderBuffer,然后讓OpenGL ES渲染到該RenderBuffer上去。而該RenderBuffer則需要綁定到一個CAEAGLLayer上面去,這樣開發(fā)者最后調(diào)用EAGLContext的presentRenderBuffer方法,就可以將渲染結(jié)果輸出到屏幕上去了。實際上,在調(diào)用這個方法時,EAGL也會執(zhí)行類似于前面的swapBuffer過程,將OpenGL ES渲染的結(jié)果繪制到物理屏幕上去(View的Layer),具體使用步驟如下。
首先編寫一個View,繼承自UIView,然后重寫父類UIView的一個方法layerClass,并且返回CAEAGLLayer類型:

+ (Class)layerClass
{
      return  [CAEAGLLayer class];
}

然后在該View的initWithFrame方法中,獲得layer并且強制類型轉(zhuǎn)換為CAEAGLLayer類型的變量,同時為layer設(shè)置參數(shù),其中包括色彩模式等屬性:

- (id)initWithFrame:(CGRect)frame{
      if(self = [super initWithFrame:frame]){
              CAEAGLLayer *eaglLayer = (CAEAGLLayer *)[self layer];
 /*
     kEAGLDrawablePropertyRetainedBacking 設(shè)置是否需要保留已經(jīng)繪制到圖層上面的內(nèi)容 用NSNumber來包裝,kEAGLDrawablePropertyRetainedBacking 
     為FALSE,表示不想保持呈現(xiàn)的內(nèi)容,因此在下一次呈現(xiàn)時,應(yīng)用程序必須完全重繪一次。將該設(shè)置為 TRUE 對性能和資源影像較大,
     因此只有當renderbuffer需要保持其內(nèi)容不變時,我們才設(shè)置 kEAGLDrawablePropertyRetainedBacking  為 TRUE。
     kEAGLDrawablePropertyColorFormat 設(shè)置繪制對象內(nèi)部的顏色緩沖區(qū)的格式 32位的RGBA的形式
     包含的格式
     kEAGLColorFormatRGBA8; 32位RGBA的顏色 4x8=32
     kEAGLColorFormatRGB565; 16位的RGB的顏色
     kEAGLColorFormatSRGBA8 SRGB
*/
              NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO],kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGB565,kEAGLDrawablePropertyColorFormat,nil];
              [eaglLayer setOpaque:YES];
              [eaglLayer setDrawableProperties:dict];
       }
       return self;
}

接下來構(gòu)造EAGLContext與RenderBuffer并且綁定到Layer上,之前也提到過,必須為某一個線程綁定綁定OpenGL ES上下文。所以首先必須開辟一個線程,開發(fā)者在iOS中開辟一個新線程有多種方式,可以使用dispatch_queue,也可以使用NSOperationQueue,甚至使用pthread也可以,反正必須在一個線程中執(zhí)行以下操作,首先創(chuàng)建OpenGL ES的上下文:

EAGLContext *_context;
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

然后實施綁定操作,代碼如下:

[EAGLContext setCurrentContext:_context];

此時就已經(jīng)為該線程綁定了剛剛創(chuàng)建好的上下文環(huán)境了,也就是已經(jīng)建立好了EAGL與OpenGL ES的連接,接下來在建立另一端的連接。
創(chuàng)建幀緩沖區(qū):

glGenFramebuffers(1,&_FrameBuffer);

創(chuàng)建繪制緩沖區(qū):

glGenRenderbuffers(1,&renderbuffer)

綁定幀緩沖區(qū)到渲染管線:

glBindFramebuffer(GL_FRAMEBUFFER,_FrameBuffer);

綁定繪制緩沖區(qū)到渲染管線:

glBindRenderbuffer(GL_RENDERBUFFER,_renderbuffer);

為繪制緩沖區(qū)分配存儲區(qū),此處將CAEAGLLayer的繪制存儲區(qū)作為繪制緩沖區(qū)的存儲區(qū):

[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];

獲取繪制緩沖區(qū)的像素高度:

glGetRenderBufferParameteriv(GL_RENDER_BUFFER,GL_RENDER_HEIGHT,&_backingHeight);

獲取繪制緩沖區(qū)的像素高度:

glGetRenderBufferParameteriv(GL_RENDER_BUFFER,GL_RENDER_WIDTH,&_backingWidth);

將繪制緩沖區(qū)綁定到幀緩沖區(qū):

// 把GL_RENDERBUFFER里的colorRenderbuffer附在GL_FRAMEBUFFER的GL_COLOR_ATTACHMENT0(顏色附著點0)上
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_RENDERBUFFER,_renderbuffer);

檢查FrameBuffer的status:

GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE){
       // failed to make complete frame buffer object
}

至此我們就將EAGL與Layer(設(shè)備的屏幕)連接起來了,繪制完一幀之后(當然繪制過程也必須在這個線程之中),調(diào)用一下代碼:

[_context presentRenderbuffer:GL_RENDERBUFFER];

這樣就可以將繪制的結(jié)果顯示到屏幕上了。

四、OpenGL ES中的紋理

OpenGL中的紋理可以用來表示圖像、照片、視頻畫面等數(shù)據(jù),在視頻渲染中只需要處理二維的紋理,每個二維紋理都由很多小的紋理元素組成,它們都是小塊數(shù)據(jù),類似于前面章節(jié)所說的像素點。要使用紋理,最常用的是直接從一個圖像文件加載數(shù)據(jù)。
為了訪問到每一個紋理元素,每個二維紋理都有自己的坐標空間,其范圍是從左下角的(0,0)到右上角的(1,1)。


20181218202105492.png

我們所熟知的不論是計算機還是手機的屏幕坐標系,x軸從左到右都是從0到1,y軸從上到下是從0到1,與圖片的存儲恰好是一致的,假設(shè)圖片的存儲是把所有的像素點都存儲到一個大數(shù)組中,圖片存儲的第一個像素點是左上角的像素點(即第一排第一列的像素點),然后第二個像素點(第一排第二列)存儲在數(shù)組的第二個元素中,那么,這里的坐標和OpenGL中的紋理坐標正好做了一個180度的旋轉(zhuǎn)。

下面再來看一下如何加載一張圖片作為OpenGL中的紋理,首先要在顯卡中創(chuàng)建一個紋理對象,OpenGL ES提供的方法原型如下:

void glGenTextures(GLsizei n,GLuint *textures)

這個方法傳遞進去的第一個參數(shù)是需要創(chuàng)建幾個紋理對象,并且把創(chuàng)建好的紋理對象的句柄放到第二個參數(shù)中去,所以第二個參數(shù)是一個數(shù)組(指針)的形式。如果只需要創(chuàng)建一個紋理對象的話,則只需要聲明一個GLuint類型的texId,然后針對該紋理ID取地址,并將其作為第二個參數(shù),就可以創(chuàng)建出這個紋理對象了,代碼如下:

glGenTextures(1,&texId);

執(zhí)行完這行代碼之后,就會在顯卡中創(chuàng)建一個紋理對象,并且把該紋理對象的返回給texId變量。緊接著開發(fā)者要操作該紋理對象,但是在OpenGL ES的操作過程中必須告訴OpenGL ES具體操作的是哪一個紋理對象,所以必須調(diào)用OpenGL ES提供的一個綁定紋理對象的方法,調(diào)用代碼如下:

glBindTexture(GL_TEXTURE_2D,texId);

執(zhí)行完上面這行代碼之后,下面的操作就都是針對于texId這個紋理對象的了,最終對該紋理的對象操作完畢之后,我們可以調(diào)用一次解綁定的代碼:

glBindTexture(GL_TEXTURE_2D,0);

這行代碼執(zhí)行完畢之后,代表開發(fā)者不會對texId紋理做任何操作了,所以上面這行代碼只在最后的時候才調(diào)用。

接下來就是最關(guān)鍵的部分,即如何將本地磁盤中的一個PNG的圖片上傳到顯卡中的這個紋理對象上。在將圖片上傳到這個紋理上之前,首先應(yīng)該要對這個紋理對象設(shè)置一些參數(shù),具體參數(shù)有哪些?其實就是紋理的過濾方式,當紋理對象(可以理解為一張圖片)被渲染到物體表面上的時候(實際上OpenGL繪制管線將紋理的元素映射到OpenGL生成的片段上的時候),有可能要被放大或者縮小,而當其放大或者縮小的時候,具體應(yīng)該如何確定每個像素是如何被填充的,就由開發(fā)者配置的紋理對象的紋理過濾器來指明。
magnification(放大):

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

minification(縮小)

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

一般在視頻的渲染與處理的時候使用GL_LINEAR這種過濾方式,該過濾方式稱為雙線性過濾,可使用雙線性插值平滑像素之間的過渡,OpenGL會使用四個臨近的紋理元素,并在它們之間用一個線性插值算法做插值,該過濾方式是最主要的過濾方式,當然OpenGL中還提供了另外幾種過濾方式。常見的有GL_NEAREST,稱為最臨近過濾,該方式將為每個片段選擇最近的紋理元素,但是當其放大的時候會有很嚴重的鋸齒效果(因為相當于將原始的直接放大,其實就是降采樣),而當其縮小的時候,因為沒有足夠的片段來繪制所有的紋理單元,(這個是真正的降采樣),許多細節(jié)都會丟失;其實OpenGL還提供了另外一種技術(shù),稱為MIP貼圖,但是這種技術(shù)會占用更多的內(nèi)存,其優(yōu)點是渲染也會更快。當縮小和放大到一定程度之后效果也比雙線性過濾的方式更好,但是其對紋理的尺寸及內(nèi)存的占用是有一定限制的,不過,在視頻的處理以及渲染的時候不需要放大或者縮小這么多倍,所以在進行視頻的處理以及渲染的場景下,MIP貼圖并不適用。

緊接著來看一下對于紋理對象的另外一個設(shè)置,也就是在紋理坐標系的s軸和t軸的紋理映射過程中用到的重復映射或者簡約映射的規(guī)則,代碼如下:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);

上述代碼所表示的含義是,將該紋理的s軸和t軸的坐標設(shè)置為GL_CLAMP_TO_EDGE類型,因為紋理坐標可以超出(0,1)的范圍,而按照上述設(shè)置規(guī)則,所有大于1的紋理都要設(shè)置為1,所有小于0的紋理都要置為0。

接下來,就是將PNG素材的內(nèi)容放到該紋理對象上,OpenGL的大部分紋理一般都只接受RGBA類型的數(shù)據(jù)(否則還得去轉(zhuǎn)化),所以我們需要對PNG這種壓縮格式進行解碼操作,如果想要采用一種更通用的方式,那么可以引用libpng庫來進行解碼操作,當然也可以使用各自平臺的API進行解碼,最終可以得到RGBA數(shù)據(jù)。待得到RGBA數(shù)據(jù)之后,記為uint8_t數(shù)組類型的pixels,然后執(zhí)行如下操作:

glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,pixels);

這樣就可以將該RGBA的數(shù)組表示的像素內(nèi)容上傳到顯卡里面texId所代表的紋理對象中去了,以后只要使用該紋理對象,其實表示的就是這個PNG圖片。

OpenGL中的紋理表示如何為物體增加細節(jié),現(xiàn)在我們已經(jīng)準備好了該紋理,那么如何把這張圖片(或者說這個紋理)繪制到屏幕上呢?首先來看一下OpenGL中的物體坐標系,物體坐標系中x軸從左到右是從-1到1變化的,y軸從下到上是從-1到1變化的,物體的中心點恰好是(0,0)的位置。

接下來的任務(wù)就是如何將這個紋理繪制到屏幕上,其實相關(guān)的基礎(chǔ)知識已經(jīng)都講解過了,首先是搭建好各自平臺的OpenGL ES的環(huán)境(包括上下文與窗口管理),然后創(chuàng)建顯卡可執(zhí)行程序,書寫Vertex Shader,代碼如下:

static char * COMMON_VERTEX_SHADER = 
"attribute vec4 position;              \n" 
"attribute vec2 texcoord;             \n" 
"varying vec2 v_texcoord;            \n" 
"                                                       \n" 
"void main(void)                            \n" 
"{                                                      \n" 
"     gl_Position = position;             \n" 
"     v_texcoord = texcoord;            \n" 
"}                                                       \n" ;

在客戶端代碼中,開發(fā)者要從VertexShader中讀取出兩個attribute,并放置到全局變量的mGLVertexCoords與mGLTextureCoords中,接下來是Fragment Shader的內(nèi)容,代碼如下所示:

static char * COMMON_FRAG_SHADER = 
"precision highp float;                                   \n"
"varying highp vec2 v_texcoord;                   \n"
"uniform sampler2D texSampler;                  \n"
"void main()  {                                                  \n"
"     gl_FragColor = texture2D(texSample,v_texcoord);                                           \n"

從FragmentShader中讀取出來的uniform會放置到mGLUniformTexture變量里,利用上面兩個Shader創(chuàng)建好的Program,稱為mGLProgId.緊接著進行真正的繪制操作,下面將詳細講一下繪制部分。
1)規(guī)定窗口的大?。?/p>

glViewport(0,0,screenWidth,screenHeight);

假定screenWidth表示繪制區(qū)域的寬度,screenHeight表示繪制區(qū)域的高度。
2)使用顯卡繪制程序

glUserProgram(mGLProgId);

3)設(shè)置物體坐標:

GLfloat vertices[] = {-1.0f,-1.0f,1.0f,-1.0f,-1.0f,,1.0f,1.0f,1.0f};
glVertexAttribPointer(mGLVertexCoords,2,GL_FLOAT,0,0,vertices);
glEnableVertexAttribArray(mGLVertexCoords);

4)設(shè)置紋理坐標

GLfloat texCoords1[] = {0.0f,0.0f,1.0f,0.0f,0.0f,,1.0f,1.0f,1.0f};
GLfloat texCoords2[] = {0.0f,1.0f,1.0f,1.0f,0.0f,,0.0f,1.0f,0.0f};
glVertexAttribPointer(mGLVertexCoords,2,GL_FLOAT,0,0,texCoords2);
glEnableVertexAttribArray(mGLVertexCoords);

這里需要注意的是texCoords2這個紋理坐標,因為其紋理對象是將一個PNG圖片的RGBA格式的形式上傳到顯卡上(即計算機坐標),如果該紋理對象是OpenGL中的一個普通紋理對象,則需要使用texCoords1,這兩個紋理坐標恰恰就是要做一個上下的翻轉(zhuǎn),從而將計算機坐標系和OpenGL坐標系進行轉(zhuǎn)換。
5)指定將要繪制的紋理對象并且傳遞給對應(yīng)的FragmentShader:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texId);
glUniformli(mGLVertexCoords,0);

6)執(zhí)行繪制操作:

glDrawArrays(GL_TRIANGLE_STRIP,0,4);

至此就可以在繪制區(qū)域(屏幕)繪制出最初的PNG圖片了。
如果該紋理對象不再使用了,則需要將其刪除掉,需要執(zhí)行的代碼是:

glDeleteTextures(1,&texId);

當然,只有在最終不再使用這個紋理的時候才會調(diào)用上述的這個方法,如果不調(diào)用,會造成顯存的泄漏。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容