iOS OpenGL ES 3 編程 2:繪制三角形、屏幕旋轉與架構設計

1、OpenGL ES的版本區別

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

OpenGL ES程序管線圖

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

2、OpenGL ES 3.0繪制三角形

在開始具體操作前,先認識下OpenGL ES程序的運行流程。ES采用服務器/客戶端編程模型,CPU是客戶端,所調用的函數發送至GPU(服務器端),被GPU轉換成底層圖形硬件支持的繪制命令。

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.

程序運行流程

iOS OpenGL ES 3 編程 1:"Hello world"可知,繪制三角形的操作應該在清除緩沖區操作之后執行。前面描述了具體操作由著色器實現,那么,讓我們來認識下著色器吧。

2.1、著色器(Shader)

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

1、頂點著色器

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多了一項要求:在開頭聲明版本信息,#version 300 es,300表示使用OpenGL ES 3.0。改成310則表示OpenGL ES 3.1,Nexus 6P支持3.1,所有iOS設備目前最高只支持3.0。由于3.0向下兼容2.0,意味著2.0語法編寫的著色器也能正常使用。

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

ES 3.0有三種參數修飾符,in、uniform、out。其中,uniform和ES 2.0一樣,表示不可變的數據,在頂點與片段著色器之間共享數據,每個頂點和片段著色器都可訪問到同一數值,其余對應關系為:

in ==? attribute,表示輸入數據

out == varying,表示輸出數據,供渲染管線后續操作使用

gl_Position為GLSL內建變量,表示頂點坐標,數據類型為vec4。除此之外,還有幾個內建變量,后續文檔再介紹。

2、片段著色器

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

比頂點著色器多一個要求:若使用浮點數,則必須指定浮點數精度。精度越高,對應的顏色過渡更細膩,計算耗時越高,美麗的東西總是要付出更高的代價。由于ES 3.0不再提供gl_FragColor內建變量,當使用完全符合3.0語法的GLSL時,使用gl_FragColor導致編譯錯誤。為表示頂點對應的像素顏色值,在此聲明了一個vec4類型的變量o_color。

有關著色器內容的編碼都完成了,下面介紹如何使用它們。

2.2、編譯及使用著色器

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

2.2.1、編譯著色器

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

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

創建著色器

指定著色器源碼

編譯源碼

檢查編譯錯誤

在合適的時候,刪除已編譯的著色器數據

示例代碼如下:

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;}

有關錯誤輸出,也可直接用字符串數組,省掉分配堆內存的麻煩。

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);

刪除編譯器一般在釋放繪制資源時進行,傳遞前面保存的著色器句柄給void glDeleteShader(GLuint shader);即可,此函數并不立即刪除著色器,而是將指定著色器標志為刪除,當著色器不與任何程序對象(program)關聯(Attach)時才會被清理出內存。

2.2.2、使用著色器

著色器并不能單獨作用于OpenGL,而是通過一個中介組織起來使用,這就是程序對象(program)。OpenGL ES規定一個program必須搭配一對著色器,且只能一對,即有效的progam = vertex shader + fragment shader。

等看到程序執行結果,很多人會有疑問,為何只指定了幾個頂點及其顏色,圖形卻顯現了過渡色彩。

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處理屏幕旋轉

iPhone等設備的屏幕旋轉會讓前述小節所創建的圖形超出屏幕范圍,具體情況是,豎屏啟動App再將屏幕橫過來,或者反過來,如下所示。

豎屏轉橫屏出現偏移

橫屏轉豎屏出現偏移

顯然,這都不是我們希望的結果,需要修復。

3.1、簡單修復

[self.view addSubview:view];添加我們自定義的GLView作為子視圖,在屏幕旋轉時會出現上述偏移問題。一個簡單的處理是,在Storyboard中將View的class設置成我們自定義的GLView或在Controller中令self.view = view;,這兩個語句作用一樣。

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

無論是Storyboard、Xib或initWithFrame:和self.view = view;,視圖在顯示時都會執行layoutSubviews,那么,在這個交集中繪制是個不錯的選擇。將initWithFrame:中的繪制代碼遷移到layoutSubviews并刪除View中其他代碼,再運行App,可發現,在屏幕發生旋轉時,畫面正常。

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

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

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

size為旋轉后屏幕大小,而view.layer.bounds為旋轉前屏幕大小,且layoutIfNeeded并沒令我們自定義的View執行layoutSubviews。

layoutIfNeeded無效

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

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

執行發現,不觸發layoutSubviews。改成[subview setNeedsLayout];,此時觸發layoutSubviews,但是結果還是錯誤的。

遍歷通知子視圖作刷新

4、OpenGL ES 架構設計

OpenGL ES的接口基于C實現,可與Objective-C、Objective-C++、C++等語言無縫混合編程。在圖形編程領域,C++因擁有面向對象特性,非常流行,所以本節以C++語言為例描述渲染引擎架構設計。若不考慮跨平臺,Swift也是個不錯的選擇,語言特性豐富,學習成本低,表達能力強。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容