iOS OpenGL ES 3 編程 2:繪制三角形、屏幕旋轉(zhuǎn)與架構(gòu)設(shè)計(jì)

1、OpenGL ES的版本區(qū)別

由于OpenGL ES 2.0及以上版本都改為可編程管線,ES 1.0是固定功能管線,它們之間的編程模式區(qū)別較大。可以認(rèn)為對(duì)同一問(wèn)題的處理,ES 2.0、3.0等更底層、可操作空間更大,缺點(diǎn)是,實(shí)現(xiàn)同一功能需要更多代碼,增大了開(kāi)發(fā)難度。而且,這些OpenGL ES版本并不是相互取代關(guān)系,而是有不同的側(cè)重點(diǎn)。ES 1.0消耗資源少,傾向二維圖形處理,三維圖形處理能力較弱。ES 2.0開(kāi)始加強(qiáng)了三維圖形的處理能力,當(dāng)然消耗的資源也隨之增加,提高了對(duì)硬件設(shè)備的要求,同時(shí)編程模式與ES 1.0區(qū)別較大,具體操作基本在著色器(Shader)中完成,不向下兼容導(dǎo)致ES 1.0很多函數(shù)在2.0和3.0中被刪除。ES 2.0、3.0的一般處理流程為頂點(diǎn)數(shù)據(jù)達(dá)到頂點(diǎn)著色器(Vertex Shader),經(jīng)頂點(diǎn)著色器對(duì)坐標(biāo)進(jìn)行變換處理,進(jìn)入圖元裝配(Primitive Assembly)形成指定繪制的圖形,接著進(jìn)入片段著色器(Fragment Shader)進(jìn)行像素的顏色處理,隨后開(kāi)始光柵化(Rasterization)等等操作,如下圖所示。可編程管線具體表現(xiàn)為,向開(kāi)發(fā)者開(kāi)放了兩種著色器(桌面版OpenGL還有Compute Shader等),所有奇妙的功能都由著色器編程實(shí)現(xiàn)。

OpenGL ES程序管線圖

著色器是一種語(yǔ)法類似C語(yǔ)言的小程序片段。不同的圖形接口有不同的著色器語(yǔ)言,對(duì)于OpenGL,是GLSL(OpenGL Shader Language),Metal也有自己的著色器語(yǔ)言。就我經(jīng)歷而言,Metal編程模型更直觀,OpenGL過(guò)于古老,不好理解。

2、OpenGL ES 3.0繪制三角形

在開(kāi)始具體操作前,先認(rèn)識(shí)下OpenGL ES程序的運(yùn)行流程。ES采用服務(wù)器/客戶端編程模型,CPU是客戶端,所調(diào)用的函數(shù)發(fā)送至GPU(服務(wù)器端),被GPU轉(zhuǎn)換成底層圖形硬件支持的繪制命令。

OpenGL is designed to translate function calls into graphics commands that can be sent to underlying graphics hardware.? Because this underlying hardware is dedicated to processing graphics commands, OpenGL drawing is typically very fast.

程序運(yùn)行流程

iOS OpenGL ES 3 編程 1:"Hello world"可知,繪制三角形的操作應(yīng)該在清除緩沖區(qū)操作之后執(zhí)行。前面描述了具體操作由著色器實(shí)現(xiàn),那么,讓我們來(lái)認(rèn)識(shí)下著色器吧。

2.1、著色器(Shader)

著色器在Xcode中并不會(huì)被編譯,而是以源碼字符串形式存在,等App運(yùn)行期間,由ARM(在iOS上)處理器運(yùn)行期間編譯成當(dāng)前圖形硬件兼容的可執(zhí)行文件,過(guò)程類似C語(yǔ)言程序的編譯鏈接過(guò)程。雖然OpenGL ES標(biāo)準(zhǔn)提供了加載已編譯的著色器二進(jìn)制數(shù)據(jù),但是iOS不支持這種做法,有關(guān)此問(wèn)題后續(xù)再展開(kāi)描述。

1、頂點(diǎn)著色器

Vertices are transformed and lit, assembled into primitives, and rasterized to create a 2D image.

#version300eslayout(location =0) in vec4 position;voidmain(){? ? gl_Position = position;}

OpenGL ES 3.0的著色器編寫比2.0多了一項(xiàng)要求:在開(kāi)頭聲明版本信息,#version 300 es,300表示使用OpenGL ES 3.0。改成310則表示OpenGL ES 3.1,Nexus 6P支持3.1,所有iOS設(shè)備目前最高只支持3.0。由于3.0向下兼容2.0,意味著2.0語(yǔ)法編寫的著色器也能正常使用。

in表示輸入?yún)?shù),vec4為類型,表示向量(x, y, z, w),類似的vec3則為向量(x, y, z),以此類推。position則為參數(shù)名,數(shù)據(jù)一般由CPU上傳到GPU,可當(dāng)作是CPU與GPU之間的通信端口。layout(location = 0)是指定屬性索引為0,ES 3.0最多支持16個(gè)屬性,默認(rèn)按自然順序遞增排列,可用location修改它們的順序,這也是后續(xù)CPU上傳數(shù)據(jù)到GPU的依據(jù)。

ES 3.0有三種參數(shù)修飾符,in、uniform、out。其中,uniform和ES 2.0一樣,表示不可變的數(shù)據(jù),在頂點(diǎn)與片段著色器之間共享數(shù)據(jù),每個(gè)頂點(diǎn)和片段著色器都可訪問(wèn)到同一數(shù)值,其余對(duì)應(yīng)關(guān)系為:

in ==? attribute,表示輸入數(shù)據(jù)

out == varying,表示輸出數(shù)據(jù),供渲染管線后續(xù)操作使用

gl_Position為GLSL內(nèi)建變量,表示頂點(diǎn)坐標(biāo),數(shù)據(jù)類型為vec4。除此之外,還有幾個(gè)內(nèi)建變量,后續(xù)文檔再介紹。

2、片段著色器

#version300esprecision highpfloat;out vec4 o_color;voidmain(){? ? o_color = vec4(1.0,1.0,0,1.0);// RGBA}

比頂點(diǎn)著色器多一個(gè)要求:若使用浮點(diǎn)數(shù),則必須指定浮點(diǎn)數(shù)精度。精度越高,對(duì)應(yīng)的顏色過(guò)渡更細(xì)膩,計(jì)算耗時(shí)越高,美麗的東西總是要付出更高的代價(jià)。由于ES 3.0不再提供gl_FragColor內(nèi)建變量,當(dāng)使用完全符合3.0語(yǔ)法的GLSL時(shí),使用gl_FragColor導(dǎo)致編譯錯(cuò)誤。為表示頂點(diǎn)對(duì)應(yīng)的像素顏色值,在此聲明了一個(gè)vec4類型的變量o_color。

有關(guān)著色器內(nèi)容的編碼都完成了,下面介紹如何使用它們。

2.2、編譯及使用著色器

前面提及了著色器是以源碼字符串形式保存,且在App運(yùn)行期間編譯,那么,下面介紹編譯著色器的步驟。

2.2.1、編譯著色器

需要編譯兩種著色器:頂點(diǎn)(GL_VERTEX_SHADER)、片段(GL_FRAGMENT_SHADER)。著色器源碼可能存在編寫錯(cuò)誤導(dǎo)致編譯失敗,故需要做編譯檢查,OpenGL ES不會(huì)主動(dòng)提示編譯結(jié)果,需要主動(dòng)查詢。

著色器的編譯與編譯C代碼流程類似:

創(chuàng)建著色器

指定著色器源碼

編譯源碼

檢查編譯錯(cuò)誤

在合適的時(shí)候,刪除已編譯的著色器數(shù)據(jù)

示例代碼如下:

GLuintcompileShader(char*shaderContent, GLenum shaderType){// 1GLuint shader = glCreateShader(shaderType);// 2glShaderSource(shader,1, &shaderContent,NULL);// 3glCompileShader(shader);// 4GLint compileStatus;? ? glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);if(compileStatus == GL_FALSE) {? ? ? ? GLint infoLength;? ? ? ? glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength);if(infoLength >0) {? ? ? ? ? ? GLchar *infoLog =malloc(sizeof(GLchar) * infoLength);? ? ? ? ? ? glGetShaderInfoLog(shader, infoLength,NULL, infoLog);printf("%s -> \n%s\n", C_STRING(shaderType), infoLog);free(infoLog);? ? ? ? }? ? }returnshader;}

有關(guān)錯(cuò)誤輸出,也可直接用字符串?dāng)?shù)組,省掉分配堆內(nèi)存的麻煩。

GLint shaderCompileLogLength;glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &shaderCompileLogLength);char compileMessage[shaderCompileLogLength];glGetShaderInfoLog(shader, shaderCompileLogLength, NULL, compileMessage);printf("%s-> \n%s\n", C_STRING(shaderType), compileMessage);

刪除編譯器一般在釋放繪制資源時(shí)進(jìn)行,傳遞前面保存的著色器句柄給void glDeleteShader(GLuint shader);即可,此函數(shù)并不立即刪除著色器,而是將指定著色器標(biāo)志為刪除,當(dāng)著色器不與任何程序?qū)ο螅╬rogram)關(guān)聯(lián)(Attach)時(shí)才會(huì)被清理出內(nèi)存。

2.2.2、使用著色器

著色器并不能單獨(dú)作用于OpenGL,而是通過(guò)一個(gè)中介組織起來(lái)使用,這就是程序?qū)ο螅╬rogram)。OpenGL ES規(guī)定一個(gè)program必須搭配一對(duì)著色器,且只能一對(duì),即有效的progam = vertex shader + fragment shader。

等看到程序執(zhí)行結(jié)果,很多人會(huì)有疑問(wèn),為何只指定了幾個(gè)頂點(diǎn)及其顏色,圖形卻顯現(xiàn)了過(guò)渡色彩。

The OpenGL ES specification does not define a windowing layer; instead, the hosting operating system must provide functions to create an OpenGL ES rendering context, which accepts commands, and a framebuffer, where the results of any drawing commands are written to. Working with OpenGL ES on iOS requires using iOS classes to set up and present a drawing surface and using platform-neutral API to render its contents.

3、OpenGL ES處理屏幕旋轉(zhuǎn)

iPhone等設(shè)備的屏幕旋轉(zhuǎn)會(huì)讓前述小節(jié)所創(chuàng)建的圖形超出屏幕范圍,具體情況是,豎屏啟動(dòng)App再將屏幕橫過(guò)來(lái),或者反過(guò)來(lái),如下所示。

豎屏轉(zhuǎn)橫屏出現(xiàn)偏移

橫屏轉(zhuǎn)豎屏出現(xiàn)偏移

顯然,這都不是我們希望的結(jié)果,需要修復(fù)。

3.1、簡(jiǎn)單修復(fù)

[self.view addSubview:view];添加我們自定義的GLView作為子視圖,在屏幕旋轉(zhuǎn)時(shí)會(huì)出現(xiàn)上述偏移問(wèn)題。一個(gè)簡(jiǎn)單的處理是,在Storyboard中將View的class設(shè)置成我們自定義的GLView或在Controller中令self.view = view;,這兩個(gè)語(yǔ)句作用一樣。

Storyboard設(shè)置這種方式要求我們覆蓋initWithCoder:,而我們覆蓋的是initWithFrame:,導(dǎo)致代碼并不執(zhí)行,還得將類似邏輯在initWithCoder:中實(shí)現(xiàn)才有效果,這樣造成了代碼冗余。

無(wú)論是Storyboard、Xib或initWithFrame:和self.view = view;,視圖在顯示時(shí)都會(huì)執(zhí)行l(wèi)ayoutSubviews,那么,在這個(gè)交集中繪制是個(gè)不錯(cuò)的選擇。將initWithFrame:中的繪制代碼遷移到layoutSubviews并刪除View中其他代碼,再運(yùn)行App,可發(fā)現(xiàn),在屏幕發(fā)生旋轉(zhuǎn)時(shí),畫(huà)面正常。

但是,[self.view addSubview:view];的方式調(diào)用問(wèn)題依舊。這需要ViewController通知View重新布局子視圖才能觸發(fā)layoutSubviews。就此問(wèn)題作進(jìn)一步分析。

首先,Controller覆蓋- viewWillTransitionToSize: withTransitionCoordinator:。

-? (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator {NSLog(@"size = %@, layer rect = %@",NSStringFromCGSize(size),NSStringFromCGRect(self.view.layer.bounds));? ? [self.viewlayoutIfNeeded];}

size為旋轉(zhuǎn)后屏幕大小,而view.layer.bounds為旋轉(zhuǎn)前屏幕大小,且layoutIfNeeded并沒(méi)令我們自定義的View執(zhí)行l(wèi)ayoutSubviews。

layoutIfNeeded無(wú)效

同樣,[self.view setNeedsLayout];也不觸發(fā)layoutSubviews。既然,我們的View是Controller的View的子視圖,那么,遍歷子視圖逐一發(fā)送刷新通知會(huì)如何?

for(UIView*subviewinself.view.subviews) {? ? [subview layoutIfNeeded];}

執(zhí)行發(fā)現(xiàn),不觸發(fā)layoutSubviews。改成[subview setNeedsLayout];,此時(shí)觸發(fā)layoutSubviews,但是結(jié)果還是錯(cuò)誤的。

遍歷通知子視圖作刷新

4、OpenGL ES 架構(gòu)設(shè)計(jì)

OpenGL ES的接口基于C實(shí)現(xiàn),可與Objective-C、Objective-C++、C++等語(yǔ)言無(wú)縫混合編程。在圖形編程領(lǐng)域,C++因擁有面向?qū)ο筇匦?,非常流行,所以本?jié)以C++語(yǔ)言為例描述渲染引擎架構(gòu)設(shè)計(jì)。若不考慮跨平臺(tái),Swift也是個(gè)不錯(cuò)的選擇,語(yǔ)言特性豐富,學(xué)習(xí)成本低,表達(dá)能力強(qiáng)。

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

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

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