一、OpenGL ES介紹
OpenGL(Open Graphics Library)定義了一個(gè)跨編程語言蜕乡、跨平臺(tái)編程的專業(yè)圖形程序接口奸绷。可用于二維或三維圖像的處理和渲染层玲,它是一個(gè)功能強(qiáng)大号醉、調(diào)用方便的底層圖形庫。對(duì)于嵌入式設(shè)備辛块,其提供了OpenGL ES(OpenGL for Embeddled Systems)版本畔派,該版本是針對(duì)手機(jī)、Pad等嵌入式設(shè)備而設(shè)計(jì)的润绵,是OpenGL的一個(gè)子集线椰。到目前為止,OpenGL已經(jīng)經(jīng)歷過很多版本的迭代與更新授药,最新版本是3.0士嚎,而使用最廣泛的還是OpenGL ES 2.0版本呜魄。本文是基于2.0版本進(jìn)行編程并實(shí)現(xiàn)圖像的處理與渲染悔叽,并且只討論2D部分的內(nèi)容。
由于OpenGL是基于跨平臺(tái)設(shè)計(jì)的爵嗅,所以在每個(gè)平臺(tái)上都要有它的具體實(shí)現(xiàn)娇澎,即要提供OpenGL ES的上下文環(huán)境以及窗口的管理。在OpenGL的設(shè)計(jì)中睹晒,OpenGL是不負(fù)責(zé)管理窗口的趟庄,窗口的管理將交由各個(gè)設(shè)備自己來完成,上下文環(huán)境也是一樣的伪很,其在各個(gè)平臺(tái)上都有自己的實(shí)現(xiàn)戚啥。在iOS平臺(tái)上使用EAGL提供本地平臺(tái)對(duì)OpenGL ES的實(shí)現(xiàn)。
這里需要介紹一個(gè)庫——libSDL,它可以為開發(fā)者提供面向libSDL的API編程锉试,libSDL內(nèi)部會(huì)解決多個(gè)平臺(tái)的OpenGL上下文環(huán)境以及窗口的管理問題猫十,開發(fā)者只需要交叉編譯這個(gè)庫到各自的平臺(tái)上就可以做到只寫一份代碼即可運(yùn)行多個(gè)平臺(tái)。其中FFmpeg中的ffplay這一工具就是基于libSDL進(jìn)行開發(fā)的。但是對(duì)于移動(dòng)開發(fā)者來講拖云,這樣就會(huì)失去一些更加靈活的控制贷笛,甚至某些場(chǎng)景下的功能不能實(shí)現(xiàn)。
上面介紹了OpenGL ES是什么宙项,下面再來介紹一下OpenGL ES能做什么乏苦。其實(shí)從名字上就可以看出來,OpenGL主要是做圖形圖像處理的庫尤筐,尤其是在移動(dòng)設(shè)備上進(jìn)行圖形圖像處理汇荐,它的性能優(yōu)勢(shì)更能體現(xiàn)出來。GLSL(OpenGL Shading Language)是OpenGL的著色器語言叔磷,開發(fā)人員利用這種語言編寫程序運(yùn)行在GPU(Graphic Processor Unit,圖形圖像處理單元郁季,可以理解為是一種高并發(fā)的運(yùn)算器)上以進(jìn)行圖像的處理和渲染。GLSL著色器代碼分為兩個(gè)部分矮锈,即Vertex Shader(頂點(diǎn)著色器)與Fragment Shade(片元著色器)兩部分怕敬,分別完成各自在OpenGL渲染管線中的功能。對(duì)于OpenGL ES,業(yè)界有一個(gè)著名的開源庫GPUImage,它的實(shí)現(xiàn)非常優(yōu)雅秕狰,尤其是在iOS平臺(tái)上實(shí)現(xiàn)的非常完備稠腊,不僅有攝像頭采集實(shí)時(shí)渲染、視頻播放器鸣哀、離線保存等功能架忌,更有強(qiáng)大的濾鏡實(shí)現(xiàn)。在GPUImage的濾鏡實(shí)現(xiàn)中我衬,可以找到大部分圖形圖像處理Shader的實(shí)現(xiàn)叹放,包括:亮度、對(duì)比度挠羔、飽和度井仰、色調(diào)曲線、白平衡破加、灰度等調(diào)整顏色的處理俱恶,以及銳化、高斯模糊等圖像像素處理的實(shí)現(xiàn)等范舀,還有素描合是、卡通效果、浮雕效果等視覺效果的實(shí)現(xiàn)锭环,最后還有各種混合模式的實(shí)現(xiàn)等聪全。當(dāng)然除了GPUImage提供的這些圖像處理的Shader之外,開發(fā)者也可以自己實(shí)現(xiàn)一些有意思的Shader,比如美顏濾鏡效果辅辩、瘦臉效果以及粒子效果等难礼。
二吱七、OpenGL ES的實(shí)踐
1.OpenGL 渲染管線
要想學(xué)習(xí)著色器,并理解著色器的工作機(jī)制鹤竭,就要對(duì)OpenGL 固定的渲染管線有深入的了解踊餐。同樣,先來統(tǒng)一一下術(shù)語臀稚。
- 幾何圖元:包括點(diǎn)吝岭、直線、三角形吧寺,均是通過頂點(diǎn)(vertex)來指定的窜管。
- 模型:根據(jù)幾何圖元?jiǎng)?chuàng)建的物體。
- 渲染:計(jì)算機(jī)根據(jù)模型創(chuàng)建圖像的過程稚机。
最終渲染過程結(jié)束之后幕帆,人眼所能看到的圖像就是由屏幕上的所有像素點(diǎn)組成的,在內(nèi)存中赖条,這些像素點(diǎn)可以組織成一個(gè)大的一維數(shù)組失乾,每4個(gè)字節(jié)即表示一個(gè)像素點(diǎn)的RGBA數(shù)據(jù),而在顯卡中纬乍,這些像素點(diǎn)可以組織成幀緩沖區(qū)的形式碱茁,幀緩沖區(qū)保存了圖形硬件為了控制屏幕上所有像素的顏色和強(qiáng)度所需要的全部信息。理解了幀緩沖區(qū)的概念仿贬,接下來就來討論一下OpenGL的渲染管線纽竣,這部分內(nèi)容對(duì)于OpenGL來說是非常重要的。
那么OpenGL的渲染管線具體是做什么的呢茧泪?其實(shí)就是OpenGL引擎渲染圖像的流程蜓氨,也就是說OpenGL引擎是一步一步地將圖片渲染到屏幕上去的過程。渲染管線分為以下幾個(gè)階段队伟。
階段一:指定幾何對(duì)象
所謂幾何對(duì)象穴吹,就是上面說過的幾何圖元,這里將根據(jù)具體執(zhí)行的指令繪制幾何圖元缰泡。比如刀荒,OpenGL提供給開發(fā)者的繪制方法glDrawArrays,這個(gè)方法里的第一個(gè)參數(shù)是mode,就是制定繪制方式代嗤,可選值有一下幾種棘钞。
- GL_POINT:以點(diǎn)的形式進(jìn)行繪制,通常用在繪制粒子效果的場(chǎng)景中干毅。
- GL_LINES:以線的形式進(jìn)行繪制宜猜,通常用在繪制直線的場(chǎng)景中。
- GL_TRIANGLE_STRIP:以三角形的形式進(jìn)行繪制硝逢,所有二維圖像的渲染都會(huì)使用這種方式姨拥。
具體選用哪一種繪制方式?jīng)Q定了OpenGL渲染管線的第一階段應(yīng)如何去繪制幾何圖元绅喉,所以這就是第一階段指定的幾何對(duì)象。
階段二:頂點(diǎn)處理
不論以上的幾何對(duì)象是如何指定的叫乌,所有的幾何數(shù)據(jù)都將會(huì)經(jīng)過這個(gè)階段柴罐。這個(gè)階段所做的操作就是,根據(jù)模型視圖和投影矩陣進(jìn)行變換來改變頂點(diǎn)的位置憨奸,根據(jù)紋理坐標(biāo)與紋理矩陣來改變紋理坐標(biāo)的位置革屠,如果涉及三維的渲染,那么這里還要處理光照計(jì)算與法線變換排宰。這里的輸出是以gl_Position來表示具體的頂點(diǎn)位置的似芝,如果是以點(diǎn)來繪制幾何圖元,那么還應(yīng)該輸出gl_PointSize板甘。
階段三:圖元組裝
在經(jīng)過階段二的頂點(diǎn)處理操作之后党瓮,還是紋理坐標(biāo)都是已經(jīng)確定好了的。在這個(gè)階段盐类,頂點(diǎn)將會(huì)根據(jù)應(yīng)用程序送往圖元的規(guī)則(如GL_POINT寞奸、GL_TRIANGLE_STRIP),將紋理組裝成圖元。
階段四:柵格化操作
由階段三傳遞過來的圖元數(shù)據(jù)在跳,在此將會(huì)分解成更小的單元并對(duì)應(yīng)于幀緩沖區(qū)的各個(gè)像素蝇闭。這些單元稱為片元,一個(gè)片元可能包含窗口顏色硬毕、紋理坐標(biāo)等屬性呻引。片元的屬性是根據(jù)頂點(diǎn)坐標(biāo)利用插值來確定的,這就是柵格化操作吐咳,也就是確認(rèn)好每一個(gè)片元是什么逻悠。
階段五:片元處理
通過紋理坐標(biāo)取得紋理(texture)中相對(duì)應(yīng)的片元像素值(texel),根據(jù)自己的業(yè)務(wù)處理(比如提亮、飽和度調(diào)節(jié)韭脊、對(duì)比度調(diào)節(jié)童谒、高斯模糊)來變換這個(gè)片元的顏色。這里的輸出是gl_FragColor,用于表示修改之后的像素的最終結(jié)果沪羔。
階段六:幀緩沖操作
該階段主要執(zhí)行幀緩沖的寫入操作饥伊,這也是渲染管線的最后一步,負(fù)責(zé)將最終的像素值寫入到幀緩沖區(qū)中蔫饰。
前面也提到過琅豆,OpenGL ES提供了可編程的著色器來代替渲染管線的某個(gè)階段。具體如下所示:
Vertex Shader(頂點(diǎn)著色器)用來替代頂點(diǎn)處理階段篓吁。
Fragment Shader(片元著色器茫因,又稱為像素著色器)用來替換片元處理階段。
glFinish和glFlush
提交給OpenGL的繪圖指令并不會(huì)馬上發(fā)送給圖形硬件執(zhí)行杖剪,而是放到一個(gè)緩沖區(qū)里面冻押,等待緩沖區(qū)滿了之后再將這些指令發(fā)送給圖形硬件執(zhí)行驰贷,所以指令較少或較簡(jiǎn)單時(shí)是無法填充緩沖區(qū)的,這些指令自然不能馬上執(zhí)行以達(dá)到所需要的效果洛巢。因此每次寫完繪圖代碼括袒,需要讓其立即完成效果時(shí),開發(fā)者需要在代碼后面添加glFlush()或glFinish()函數(shù)稿茉。
- glFlush()的作用是將緩沖區(qū)中的指令(無論是否為滿)立即發(fā)送給圖形硬件執(zhí)行箱熬,發(fā)送完立即返回。
- glFinish()的作用也是將緩沖區(qū)中的指令(無論是否為滿)立即發(fā)送給圖形硬件執(zhí)行狈邑,但是要等待圖形硬件執(zhí)行完成之后才返回這些指令城须。
2.GLSL語法與內(nèi)建函數(shù)
GLSL是為了實(shí)現(xiàn)著色器的功能而向開發(fā)人員提供的一種開發(fā)語言。
(1)GLSL的修飾符與基本數(shù)據(jù)類型
具體來說米苹,GLSL的語法與C語言非常類似糕伐,學(xué)習(xí)一門語言,首先要看它的數(shù)據(jù)類型表示蘸嘶,然后再學(xué)習(xí)具體的運(yùn)行流程良瞧。對(duì)于GLSL,其數(shù)據(jù)類型表示具體如下.
修飾符
具體如下:
- const:用于聲明非可寫的編譯時(shí)常量變量训唱。
- attribute:用于經(jīng)常更改的信息褥蚯,只能在頂點(diǎn)著色器中使用。
- uniform:用于不經(jīng)常更改的信息况增,可用于頂點(diǎn)著色器和片元著色器赞庶。
- varying:用于修飾從頂點(diǎn)著色器向片元著色器傳遞的變量
基本數(shù)據(jù)類型
int、float澳骤、bool,這些與C語言都是一致的歧强,需要強(qiáng)調(diào)的一點(diǎn)就是,這里面的float是有一個(gè)修飾符的为肮,即可以指定精度摊册。三種修飾符的范圍(范圍一般視顯卡而定)和應(yīng)用情況具體如下。
- highp:32bit,一般用于頂點(diǎn)坐標(biāo)(vertex Coordinate)颊艳。
- medium:16bit,一般用于紋理坐標(biāo)(texure Coordinate)茅特。
- lowp:8bit,一般用于顏色顯示(color)。
向量類型
向量類型是Shader中非常重要的一個(gè)數(shù)據(jù)類型棋枕,因?yàn)樵谧鰯?shù)據(jù)傳遞的時(shí)候需要經(jīng)常傳遞多個(gè)參數(shù)白修,相較于寫多個(gè)基本數(shù)據(jù)類型,使用向量類型是非常好的選擇戒悠。列舉一個(gè)最經(jīng)典的例子熬荆,要將物體坐標(biāo)和紋理坐標(biāo)傳遞到Vertex Shader中舟山,用的就是向量類型绸狐,每一個(gè)頂點(diǎn)就是一個(gè)四維向量卤恳,在Vertex Shader中利用這兩個(gè)四維向量即可完成自己的紋理坐標(biāo)映射操作。聲明方式如下(GLSL代碼):
attribute vec4 position;
矩陣類型
有一些效果器需要開發(fā)者傳入矩陣類型的數(shù)據(jù)寒矿,比如后面會(huì)接觸到的懷舊效果器突琳,就需要傳入一個(gè)矩陣來改變?cè)嫉南袼財(cái)?shù)據(jù)。聲明方式如下:
uniform lowp mat4 colorMatrix;
上面的代碼表示了一個(gè)4x4的浮點(diǎn)矩陣符相,如果是mat2就是2x2的浮點(diǎn)矩陣拆融,如果是mat3就是3x3的浮點(diǎn)矩陣。若要傳遞一個(gè)矩陣到實(shí)際的Shade中啊终,則可以直接調(diào)用如下函數(shù):
glUniformMarix4fv(mColorMatrixLocation,1,false,mColorMatrix);
紋理類型
一般僅在Fragment Shader中使用這個(gè)類型镜豹,二維紋理的聲明方式如下
uniform sample2D texSampler;
當(dāng)客戶端接收到這個(gè)句柄時(shí),就可以為它綁定一個(gè)紋理蓝牲,代碼如下:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texId);
glUniformli(mGLUniformTexture,0);
上述代碼第一行激活的是哪一個(gè)紋理句柄趟脂,第三行代碼中的第二個(gè)參數(shù)需要傳遞對(duì)應(yīng)的Index,就像代碼中激活的紋理句柄是GL_TEXTURE0,對(duì)應(yīng)的Index就是0,如果激活的紋理句柄是GL_TEXTURE1,那么對(duì)應(yīng)的Index就是1例衍,在不同的平臺(tái)上句柄的個(gè)數(shù)也是不一樣昔期,但是一般都會(huì)在32個(gè)以上。
varying
這個(gè)修飾符變量用于在Vertex Shader和Fragment Shader之間傳遞函數(shù)佛玄。首先在頂點(diǎn)著色器中聲明這個(gè)類型的變量代表紋理的坐標(biāo)點(diǎn)硼一,并且對(duì)這個(gè)變量進(jìn)行賦值,代碼如下:
attribute vec2 texoord;
varying vec2 v_texcoord;
void main(void)
{
// 計(jì)算頂點(diǎn)坐標(biāo)
v_texcoord = texcoord;
}
緊接著在Fragment Shader中也聲明同名的變量梦抢,然后使用texture2D方法取出二維紋理中該紋理坐標(biāo)點(diǎn)上的紋理像素值般贼,代碼如下
varying vec2 v_texcoord;
vec4 texel = texture2D(texSampler,v_texcoord);
取出了該坐標(biāo)點(diǎn)上的像素值之后,就可以進(jìn)行像素變化操作了奥吩,比如說提高對(duì)比度具伍,最終將改變的像素值復(fù)制給gl_FragColor。
(2)GLSL的內(nèi)置函數(shù)與內(nèi)置變量
首先來看內(nèi)置變量圈驼,最常見的是兩個(gè)Shader的輸出變量人芽。
先來看Vertex Shader的內(nèi)置變量:
vec4 gl_position;
上述代碼用來設(shè)置頂點(diǎn)轉(zhuǎn)換到屏幕坐標(biāo)的位置,Vertex Shader一定要去更新這個(gè)數(shù)值绩脆。另外還有一個(gè)內(nèi)置變量萤厅,
float gl_pointSize;
在粒子效果的場(chǎng)景下,需要為粒子設(shè)置大小靴迫,改變?cè)搩?nèi)置變量的值就是為了設(shè)置每一個(gè)粒子矩形的大小惕味。
其次是Fragment Shader的內(nèi)置變量,代碼如下
vec4 gl_FragColor;
上述代碼用于指定當(dāng)前紋理坐標(biāo)所代表的像素點(diǎn)的最終顏色值玉锌。
然后是內(nèi)置函數(shù)名挥,具體的函數(shù)可以去官方文檔中查詢,這里僅介紹幾個(gè)常用的函數(shù)主守。
abs(genType x):絕對(duì)值函數(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,這個(gè)函數(shù)在mix兩個(gè)紋理圖像的時(shí)候非常有用陡舅。
GLSL的控制流與C語言非常類似舒萎,既可以使用for、while蹭沛、以及do-while實(shí)現(xiàn)循環(huán)臂寝,也可以使用if和if-else進(jìn)行條件分支的操作。
3.創(chuàng)建顯卡執(zhí)行程序
如何將Shader傳遞給OpenGL的渲染管線摊灭。
1)創(chuàng)建shader的過程
第一步是調(diào)用glCreateShader方法創(chuàng)建一個(gè)對(duì)象咆贬,作為shade的容器,該函數(shù)會(huì)返回一個(gè)容器的句柄帚呼,函數(shù)的原型如下:
GLuint glCreateShader(GLenum shaderType);
函數(shù)原型中的參數(shù)shaderType有兩種類型掏缎,當(dāng)要?jiǎng)?chuàng)建VertexShader時(shí),開發(fā)者應(yīng)該傳入類型GL_VERTEX_SHADER;當(dāng)要?jiǎng)?chuàng)建FragmentShader時(shí)煤杀,開發(fā)者應(yīng)該傳入GL_FRAGMENT_SHADER類型眷蜈。
下一步就是為創(chuàng)建的這個(gè)shader添加源代碼,源代碼就是根據(jù)GLSL語法和內(nèi)嵌函數(shù)編寫的兩個(gè)著色器程序(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);
待編譯完成之后,還需要驗(yàn)證該Shader是否編譯成功了枯途。那么忌怎,應(yīng)該如何驗(yàn)證呢?使用下面的函數(shù)即可進(jìn)行驗(yàn)證:
void glCetShaderiv(GLuint shader,GLenum pname,GLint *params);
其中第一個(gè)參數(shù)就是需要驗(yàn)證的Shader句柄酪夷;第二個(gè)參數(shù)值是需要驗(yàn)證的Shader狀態(tài)值榴啸,這里一般是驗(yàn)證編譯是否成功,該狀態(tài)值一般是選取GL_COMPILE_STATUS;第三個(gè)參數(shù)是返回值晚岭。當(dāng)返回1時(shí)鸥印,則說明該Shader是編譯成功的;如果為0,則說明該Shader沒有被編譯成功库说,此時(shí)獲取的是改Shader的另外一個(gè)狀態(tài)狂鞋,該狀態(tài)值應(yīng)該選取GL_INFO_LOG_LENGTH,返回值返回的則是錯(cuò)誤原因字符串的長度,我們可以利用這個(gè)長度分配出一個(gè)buffer,然后調(diào)用獲取Shader的InfoLog函數(shù)璃弄,函數(shù)原型如下:
void glGetShaderInfoLog(GLuint object,int maxLen,int *len,char *log);
之后可以把InfoLog打印出來要销,以幫助我們調(diào)試實(shí)際Shader中的錯(cuò)誤构回。
2)如何通過兩個(gè)Shader來創(chuàng)建顯卡可執(zhí)行程序
首先創(chuàng)建一個(gè)對(duì)象夏块,作為程序的容器,此函數(shù)將返回容器的句柄纤掸。函數(shù)原型如下:
GLuint glCreateProgram(void);
第二步是把前文編譯的Shader附加到剛剛創(chuàng)建的程序中脐供,調(diào)用的函數(shù)名稱如下:
void glAttachShader(GLuint program,GLuint shader);
第一個(gè)參數(shù)就是傳入上一步返回的程序容器的句柄,第二個(gè)參數(shù)就是編譯的Shader容器的句柄借跪,當(dāng)然要為每一個(gè)Shader都調(diào)用一次這個(gè)方法才能把兩個(gè)Shader都關(guān)聯(lián)到Program中去政己。
最后一步就是鏈接程序了,鏈接函數(shù)原型如下:
void glLinkProgram(GLuint program);
傳入?yún)?shù)就是程序容器的句柄掏愁,那么這個(gè)程序有沒有鏈接成功呢歇由?OpenGL提供了一個(gè)函數(shù)來檢查該程序的狀態(tài),函數(shù)原型如下:
void glGetProgramiv(GLuint program,GLenum pname,GLint *params);
第一個(gè)參數(shù)就是傳入程序容器的句柄果港,第二個(gè)參數(shù)代表需要檢查該程序的哪一個(gè)狀態(tài)沦泌,這里傳入的是GL_LINK_STATUS,最后一個(gè)參數(shù)就是返回值。返回值為1則代表鏈接成功辛掠,如果返回值為0則代表鏈接失敗谢谦。如果想獲取具體的錯(cuò)誤信息,第二個(gè)參數(shù)要傳遞GL_INFO_LOG_LENGTH,代表獲取該程序的InfoLog的長度萝衩,獲取到長度之后我們分配出一個(gè)char *的內(nèi)存空間以獲取InfoLog,函數(shù)原型如下:
void glGetProgramInfoLog(GLuint object,int maxLen,int *len,char *log);
該函數(shù)返回InfoLog之后可以將其打印出來回挽。
使用構(gòu)建的這個(gè)函數(shù),調(diào)用glUseProgram方法就可以了猩谊。如果想讓其完全運(yùn)行在手機(jī)上千劈,還需要為其提供一個(gè)上下文環(huán)境。
三牌捷、iOS上下文環(huán)境搭建
在iOS平臺(tái)上不允許開發(fā)者使用OpenGL ES直接渲染屏幕队塘,必須使用FrameBuffer與RenderBuffer來進(jìn)行渲染。若要使用EAGL宜鸯,則必須先創(chuàng)建一個(gè)RenderBuffer,然后讓OpenGL ES渲染到該RenderBuffer上去憔古。而該RenderBuffer則需要綁定到一個(gè)CAEAGLLayer上面去,這樣開發(fā)者最后調(diào)用EAGLContext的presentRenderBuffer方法淋袖,就可以將渲染結(jié)果輸出到屏幕上去了鸿市。實(shí)際上,在調(diào)用這個(gè)方法時(shí),EAGL也會(huì)執(zhí)行類似于前面的swapBuffer過程焰情,將OpenGL ES渲染的結(jié)果繪制到物理屏幕上去(View的Layer),具體使用步驟如下陌凳。
首先編寫一個(gè)View,繼承自UIView,然后重寫父類UIView的一個(gè)方法layerClass,并且返回CAEAGLLayer類型:
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
然后在該View的initWithFrame方法中,獲得layer并且強(qiáng)制類型轉(zhuǎn)換為CAEAGLLayer類型的變量内舟,同時(shí)為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)時(shí),應(yīng)用程序必須完全重繪一次耕蝉。將該設(shè)置為 TRUE 對(duì)性能和資源影像較大崔梗,
因此只有當(dāng)renderbuffer需要保持其內(nèi)容不變時(shí),我們才設(shè)置 kEAGLDrawablePropertyRetainedBacking 為 TRUE垒在。
kEAGLDrawablePropertyColorFormat 設(shè)置繪制對(duì)象內(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上蒜魄,之前也提到過,必須為某一個(gè)線程綁定綁定OpenGL ES上下文场躯。所以首先必須開辟一個(gè)線程谈为,開發(fā)者在iOS中開辟一個(gè)新線程有多種方式,可以使用dispatch_queue,也可以使用NSOperationQueue,甚至使用pthread也可以踢关,反正必須在一個(gè)線程中執(zhí)行以下操作伞鲫,首先創(chuàng)建OpenGL ES的上下文:
EAGLContext *_context;
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
然后實(shí)施綁定操作,代碼如下:
[EAGLContext setCurrentContext:_context];
此時(shí)就已經(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ū)分配存儲(chǔ)區(qū)瘪菌,此處將CAEAGLLayer的繪制存儲(chǔ)區(qū)作為繪制緩沖區(qū)的存儲(chǔ)區(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(顏色附著點(diǎn)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è)備的屏幕)連接起來了撒会,繪制完一幀之后(當(dāng)然繪制過程也必須在這個(gè)線程之中),調(diào)用一下代碼:
[_context presentRenderbuffer:GL_RENDERBUFFER];
這樣就可以將繪制的結(jié)果顯示到屏幕上了师妙。
四诵肛、OpenGL ES中的紋理
OpenGL中的紋理可以用來表示圖像、照片默穴、視頻畫面等數(shù)據(jù)怔檩,在視頻渲染中只需要處理二維的紋理,每個(gè)二維紋理都由很多小的紋理元素組成蓄诽,它們都是小塊數(shù)據(jù)薛训,類似于前面章節(jié)所說的像素點(diǎn)。要使用紋理仑氛,最常用的是直接從一個(gè)圖像文件加載數(shù)據(jù)乙埃。
為了訪問到每一個(gè)紋理元素闸英,每個(gè)二維紋理都有自己的坐標(biāo)空間,其范圍是從左下角的(0介袜,0)到右上角的(1甫何,1)。
我們所熟知的不論是計(jì)算機(jī)還是手機(jī)的屏幕坐標(biāo)系遇伞,x軸從左到右都是從0到1辙喂,y軸從上到下是從0到1,與圖片的存儲(chǔ)恰好是一致的鸠珠,假設(shè)圖片的存儲(chǔ)是把所有的像素點(diǎn)都存儲(chǔ)到一個(gè)大數(shù)組中巍耗,圖片存儲(chǔ)的第一個(gè)像素點(diǎn)是左上角的像素點(diǎn)(即第一排第一列的像素點(diǎn)),然后第二個(gè)像素點(diǎn)(第一排第二列)存儲(chǔ)在數(shù)組的第二個(gè)元素中跳芳,那么芍锦,這里的坐標(biāo)和OpenGL中的紋理坐標(biāo)正好做了一個(gè)180度的旋轉(zhuǎn)竹勉。
下面再來看一下如何加載一張圖片作為OpenGL中的紋理飞盆,首先要在顯卡中創(chuàng)建一個(gè)紋理對(duì)象,OpenGL ES提供的方法原型如下:
void glGenTextures(GLsizei n,GLuint *textures)
這個(gè)方法傳遞進(jìn)去的第一個(gè)參數(shù)是需要?jiǎng)?chuàng)建幾個(gè)紋理對(duì)象次乓,并且把創(chuàng)建好的紋理對(duì)象的句柄放到第二個(gè)參數(shù)中去吓歇,所以第二個(gè)參數(shù)是一個(gè)數(shù)組(指針)的形式。如果只需要?jiǎng)?chuàng)建一個(gè)紋理對(duì)象的話票腰,則只需要聲明一個(gè)GLuint類型的texId,然后針對(duì)該紋理ID取地址城看,并將其作為第二個(gè)參數(shù),就可以創(chuàng)建出這個(gè)紋理對(duì)象了杏慰,代碼如下:
glGenTextures(1,&texId);
執(zhí)行完這行代碼之后测柠,就會(huì)在顯卡中創(chuàng)建一個(gè)紋理對(duì)象,并且把該紋理對(duì)象的返回給texId變量缘滥。緊接著開發(fā)者要操作該紋理對(duì)象轰胁,但是在OpenGL ES的操作過程中必須告訴OpenGL ES具體操作的是哪一個(gè)紋理對(duì)象,所以必須調(diào)用OpenGL ES提供的一個(gè)綁定紋理對(duì)象的方法朝扼,調(diào)用代碼如下:
glBindTexture(GL_TEXTURE_2D,texId);
執(zhí)行完上面這行代碼之后赃阀,下面的操作就都是針對(duì)于texId這個(gè)紋理對(duì)象的了,最終對(duì)該紋理的對(duì)象操作完畢之后擎颖,我們可以調(diào)用一次解綁定的代碼:
glBindTexture(GL_TEXTURE_2D,0);
這行代碼執(zhí)行完畢之后榛斯,代表開發(fā)者不會(huì)對(duì)texId紋理做任何操作了,所以上面這行代碼只在最后的時(shí)候才調(diào)用搂捧。
接下來就是最關(guān)鍵的部分驮俗,即如何將本地磁盤中的一個(gè)PNG的圖片上傳到顯卡中的這個(gè)紋理對(duì)象上。在將圖片上傳到這個(gè)紋理上之前允跑,首先應(yīng)該要對(duì)這個(gè)紋理對(duì)象設(shè)置一些參數(shù)王凑,具體參數(shù)有哪些提佣?其實(shí)就是紋理的過濾方式,當(dāng)紋理對(duì)象(可以理解為一張圖片)被渲染到物體表面上的時(shí)候(實(shí)際上OpenGL繪制管線將紋理的元素映射到OpenGL生成的片段上的時(shí)候)荤崇,有可能要被放大或者縮小拌屏,而當(dāng)其放大或者縮小的時(shí)候,具體應(yīng)該如何確定每個(gè)像素是如何被填充的术荤,就由開發(fā)者配置的紋理對(duì)象的紋理過濾器來指明倚喂。
magnification(放大):
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
minification(縮小)
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
一般在視頻的渲染與處理的時(shí)候使用GL_LINEAR這種過濾方式,該過濾方式稱為雙線性過濾瓣戚,可使用雙線性插值平滑像素之間的過渡端圈,OpenGL會(huì)使用四個(gè)臨近的紋理元素,并在它們之間用一個(gè)線性插值算法做插值子库,該過濾方式是最主要的過濾方式舱权,當(dāng)然OpenGL中還提供了另外幾種過濾方式。常見的有GL_NEAREST,稱為最臨近過濾仑嗅,該方式將為每個(gè)片段選擇最近的紋理元素宴倍,但是當(dāng)其放大的時(shí)候會(huì)有很嚴(yán)重的鋸齒效果(因?yàn)橄喈?dāng)于將原始的直接放大,其實(shí)就是降采樣)仓技,而當(dāng)其縮小的時(shí)候鸵贬,因?yàn)闆]有足夠的片段來繪制所有的紋理單元,(這個(gè)是真正的降采樣)脖捻,許多細(xì)節(jié)都會(huì)丟失阔逼;其實(shí)OpenGL還提供了另外一種技術(shù),稱為MIP貼圖地沮,但是這種技術(shù)會(huì)占用更多的內(nèi)存嗜浮,其優(yōu)點(diǎn)是渲染也會(huì)更快。當(dāng)縮小和放大到一定程度之后效果也比雙線性過濾的方式更好摩疑,但是其對(duì)紋理的尺寸及內(nèi)存的占用是有一定限制的危融,不過,在視頻的處理以及渲染的時(shí)候不需要放大或者縮小這么多倍未荒,所以在進(jìn)行視頻的處理以及渲染的場(chǎng)景下专挪,MIP貼圖并不適用。
緊接著來看一下對(duì)于紋理對(duì)象的另外一個(gè)設(shè)置片排,也就是在紋理坐標(biāo)系的s軸和t軸的紋理映射過程中用到的重復(fù)映射或者簡(jiǎn)約映射的規(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軸的坐標(biāo)設(shè)置為GL_CLAMP_TO_EDGE類型率寡,因?yàn)榧y理坐標(biāo)可以超出(0迫卢,1)的范圍,而按照上述設(shè)置規(guī)則冶共,所有大于1的紋理都要設(shè)置為1乾蛤,所有小于0的紋理都要置為0每界。
接下來,就是將PNG素材的內(nèi)容放到該紋理對(duì)象上家卖,OpenGL的大部分紋理一般都只接受RGBA類型的數(shù)據(jù)(否則還得去轉(zhuǎn)化)眨层,所以我們需要對(duì)PNG這種壓縮格式進(jìn)行解碼操作,如果想要采用一種更通用的方式上荡,那么可以引用libpng庫來進(jìn)行解碼操作趴樱,當(dāng)然也可以使用各自平臺(tái)的API進(jìn)行解碼,最終可以得到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所代表的紋理對(duì)象中去了,以后只要使用該紋理對(duì)象逛薇,其實(shí)表示的就是這個(gè)PNG圖片捺疼。
OpenGL中的紋理表示如何為物體增加細(xì)節(jié),現(xiàn)在我們已經(jīng)準(zhǔn)備好了該紋理永罚,那么如何把這張圖片(或者說這個(gè)紋理)繪制到屏幕上呢啤呼?首先來看一下OpenGL中的物體坐標(biāo)系,物體坐標(biāo)系中x軸從左到右是從-1到1變化的尤蛮,y軸從下到上是從-1到1變化的媳友,物體的中心點(diǎn)恰好是(0斯议,0)的位置产捞。
接下來的任務(wù)就是如何將這個(gè)紋理繪制到屏幕上,其實(shí)相關(guān)的基礎(chǔ)知識(shí)已經(jīng)都講解過了哼御,首先是搭建好各自平臺(tái)的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中讀取出兩個(gè)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會(huì)放置到mGLUniformTexture變量里液肌,利用上面兩個(gè)Shader創(chuàng)建好的Program,稱為mGLProgId.緊接著進(jìn)行真正的繪制操作挟炬,下面將詳細(xì)講一下繪制部分。
1)規(guī)定窗口的大朽露摺:
glViewport(0,0,screenWidth,screenHeight);
假定screenWidth表示繪制區(qū)域的寬度谤祖,screenHeight表示繪制區(qū)域的高度。
2)使用顯卡繪制程序
glUserProgram(mGLProgId);
3)設(shè)置物體坐標(biāo):
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è)置紋理坐標(biāo)
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這個(gè)紋理坐標(biāo)老速,因?yàn)槠浼y理對(duì)象是將一個(gè)PNG圖片的RGBA格式的形式上傳到顯卡上(即計(jì)算機(jī)坐標(biāo))粥喜,如果該紋理對(duì)象是OpenGL中的一個(gè)普通紋理對(duì)象,則需要使用texCoords1橘券,這兩個(gè)紋理坐標(biāo)恰恰就是要做一個(gè)上下的翻轉(zhuǎn)额湘,從而將計(jì)算機(jī)坐標(biāo)系和OpenGL坐標(biāo)系進(jìn)行轉(zhuǎn)換卿吐。
5)指定將要繪制的紋理對(duì)象并且傳遞給對(duì)應(yīng)的FragmentShader:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texId);
glUniformli(mGLVertexCoords,0);
6)執(zhí)行繪制操作:
glDrawArrays(GL_TRIANGLE_STRIP,0,4);
至此就可以在繪制區(qū)域(屏幕)繪制出最初的PNG圖片了。
如果該紋理對(duì)象不再使用了锋华,則需要將其刪除掉嗡官,需要執(zhí)行的代碼是:
glDeleteTextures(1,&texId);
當(dāng)然,只有在最終不再使用這個(gè)紋理的時(shí)候才會(huì)調(diào)用上述的這個(gè)方法毯焕,如果不調(diào)用谨湘,會(huì)造成顯存的泄漏。