本文首發(fā)于個人博客:Lam's Blog - 【OpenGL ES】入門及繪制一個三角形神僵,文章由MarkDown語法編寫袭厂,可能不同平臺渲染效果不一桂对,如果有存在排版錯誤圖片無法顯示等問題阶捆,煩請移至個人博客拓春,如果個人博客無法訪問可以留言告訴我释簿,轉(zhuǎn)載請聲明個人博客出處,謝謝硼莽。
簡介
OpenGL
OpenGL(全寫Open Graphics Library)是指定義了一個跨編程語言庶溶、跨平臺的編程接口規(guī)格的專業(yè)的圖形程序接口。它用于三維圖像(二維亦可)懂鸵,是一個功能強大偏螺,調(diào)用方便的底層圖形庫。此處L代表的是Library而不是Language匆光。
OpenGL在不同的平臺上有不同的實現(xiàn)套像,但是它定義好了專業(yè)的程序接口,不同的平臺都是遵照該接口來進行實現(xiàn)的终息,思想完全相同夺巩,方法名也是一致的,所以使用時也基本一致周崭,只需要根據(jù)不同的語言環(huán)境稍有不同而已柳譬。
OpenGL ES
OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三維圖形 API 的子集,針對手機续镇、PDA和游戲主機等嵌入式設(shè)備而設(shè)計美澳。
OpenGL ES相對于OpenGL來說,減少了許多不是必須的方法和數(shù)據(jù)類型,去掉了不必須的功能人柿,對代價大的功能做了限制柴墩,比OpenGL更為輕量。在OpenGL ES的世界里凫岖,沒有四邊形江咳、多邊形,無論多復(fù)雜的圖形都是由點哥放、線和三角形組成的歼指,也去除了glBegin/glEnd等方法。
EGL
EGL 是 OpenGL ES 渲染 API 和本地窗口系統(tǒng)(native platform window system)之間的一個中間接口層甥雕,它主要由系統(tǒng)制造商實現(xiàn)踩身。
EGL提供如下機制:
- 與設(shè)備的原生窗口系統(tǒng)通信
- 查詢繪圖表面的可用類型和配置
- 創(chuàng)建繪圖表面
- 在OpenGL ES 和其他圖形渲染API之間同步渲染
- 管理紋理貼圖等渲染資源
- 為了讓OpenGL ES能夠繪制在當(dāng)前設(shè)備上,我們需要EGL作為OpenGL ES與設(shè)備的橋梁社露。
在Android上層是用GLSurfaceView
可以方便快捷地結(jié)合Renderer
進行渲染挟阻,GLSurfaceView的源碼實現(xiàn)其實就是創(chuàng)建與使用EGL(Display、Context等等)的過程峭弟。而當(dāng)需要與相機或MediaCodec進行結(jié)合渲染時附鸽,就需要直接與EGL打交道。GLSurfaceView只是幫我們做了一些事情而已瞒瘸。
ES與EGL的關(guān)系
我們來思考一下畫家繪畫的過程:首先要有一名懂得各種繪畫技藝的畫家坷备,然后他需要一張畫布,一些筆情臭,一些顏料省撑,一些輔助工具(尺、模板俯在、橡皮竟秫、調(diào)色板等等)鲸湃,然后他在畫布上繪制第一幅畫癣丧,完成之后展示給人們看;在人們觀賞第一幅畫的時候更舞,他可以在第二張畫布上繪制第二幅畫劈猿,繪制完成后收回第一幅畫,將第二幅畫展現(xiàn)給人們看潮孽;接著使用工具擦除第一幅畫揪荣,在同一張畫布上繪制第三幅畫;周而復(fù)始往史,人們便看到了一幅接一幅的畫(這里就涉及到離屏渲染的概念仗颈,會在后面FBO相關(guān)章節(jié)中講述)。
對比 OpenGL ES/EGL,各要素的對應(yīng)關(guān)系大體如下:
- 畫家:編程人員
- 筆挨决、顏料请祖、輔助工具:OpenGL ES API
- 畫布:EGL 創(chuàng)建的 Surface
所以計算機繪畫的本質(zhì)就是選擇圖像顯示的像素格式,申請一塊內(nèi)存(畫布)脖祈,填充像素(顏色)
肆捕,繪制完成之后,通知計算機顯示到屏幕上(按比例發(fā)射RGB光)盖高,最終就看到了所繪制的畫面慎陵。之所以要先選擇像素格式,是因為無論是所申請內(nèi)存的大小喻奥,還是硬件驅(qū)動解析這塊內(nèi)存的方式席纽,都是由像素格式?jīng)Q定的。
為什么要使用OpenGL ES
通常來說撞蚕,計算機系統(tǒng)中 CPU润梯、GPU 是協(xié)同工作的。CPU 計算好顯示內(nèi)容提交到 GPU甥厦,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū)纺铭,隨后視頻控制器會按照 VSync
信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示矫渔。所以彤蔽,盡可能讓 CPU 和 GPU 各司其職發(fā)揮作用是提高渲染效率的關(guān)鍵。
正如我們之前提到過庙洼,OpenGL 正是給我們提供了訪問 GPU 的能力顿痪,不僅如此,它還引入了緩存(Buffer)
這個概念油够,大大提高了處理效率蚁袭。
<center>{% qnimg 20170111148411700373589.jpg %}</center>
圖中的剪頭,代表著數(shù)據(jù)交換石咬,也是主要的性能瓶頸揩悄。
從一個內(nèi)存區(qū)域復(fù)制到另一個內(nèi)存區(qū)域的速度是相對較慢的,并且在內(nèi)存復(fù)制的過程中鬼悠,CPU 和 GPU 都不能處理這區(qū)域內(nèi)存删性,避免引起錯誤。此外焕窝,CPU / GPU 執(zhí)行計算的速度是很快的蹬挺,而內(nèi)存的訪問是相對較慢的,這也導(dǎo)致處理器的性能處于次優(yōu)狀態(tài)它掂,這種狀態(tài)叫做 數(shù)據(jù)饑餓
巴帮,簡單來說就是空有一身本事卻無用武之地。
針對此,OpenGL 為了提升渲染的性能榕茧,為兩個內(nèi)存區(qū)域間的數(shù)據(jù)交換定義了緩存垃沦。緩存是指 GPU 能夠控制和管理的連續(xù) RAM。程序從 CPU 的內(nèi)存復(fù)制數(shù)據(jù)到 OpenGL ES 的緩存用押。通過獨占
緩存肢簿,GPU 能夠盡可能以有效的方式讀寫內(nèi)存。 這也意味著 GPU 使用緩存中的數(shù)據(jù)工作的同時只恨,運行在 CPU 中的程序可以繼續(xù)執(zhí)行译仗。
總結(jié)
OpenGL的使用需要涉及著色器語言
,但是其自身并不是一種語言官觅,嚴(yán)格來說它本身只是一個協(xié)議規(guī)范
纵菌,定義了一套可以供上層應(yīng)用程序進行調(diào)用的 API,它抽象了 GPU 的功能休涤,使應(yīng)用開發(fā)者不必關(guān)心底層的 GPU 類型和具體實現(xiàn)咱圆。而EGL就是OpenGL與本地窗口系統(tǒng)之間的一個抽象的中間接口層。
即使在我們?nèi)粘ndroid開發(fā)中沒有直接與OpenGL進行接觸功氨,但是在Android的繪制實現(xiàn)中其實就是通過EGL來直接進行的序苏,例如查看SurfaceFlinger的native層代碼就可以發(fā)現(xiàn)其內(nèi)部就有許多相關(guān)操作。
OpenGL ES 目前最新版本為3.0捷凄,3.0支持了新版的著色語言忱详,紋理MSAA抗鋸齒等強大功能,但是目前大多數(shù)在用的仍然是2.0版本跺涤,也出于主流設(shè)備的考慮匈睁,本系列教程將基于2.0進行,同時暫不涉及對1.x固定管線渲染方式(簡單講就是2.0通過頂點著色器
取代了OpenGL ES 1.x中的變換和光照階段
桶错,片元著色器
取代了紋理顏色和環(huán)境求和
航唆、霧
、Alpha測試
等階段院刁。這使得原來由OpenGL ES 1.x固定的階段需要用戶自己開發(fā)著色器處理糯钙,雖然在一定的程度上增加了代碼復(fù)雜度,但是靈活性卻大大增加退腥,同時也能夠處理OpenGL ES 1.x中難以完成的處理任務(wù)任岸。)的介紹以及開發(fā)環(huán)境的搭建。但是這里仍然附上經(jīng)典的固定渲染圖與可編程渲染管線圖:
<center>{% qnimg gl_pipeline.jpg title="渲染管線" %}</center>
OpenGL ES可以做什么
- 圖片處理狡刘。比如圖片色調(diào)轉(zhuǎn)換享潜、美顏等。
- 攝像頭預(yù)覽效果處理颓帝。比如美顏相機米碰、惡搞相機等。
- 視頻處理购城。攝像頭預(yù)覽效果處理可以吕座,這個自然也不在話下了。
- 3D游戲瘪板。比如神廟逃亡吴趴、都市賽車等。
- 侮攀。锣枝。。
OpenGL ES 2.0 基本概念
圖元
在OpenGL中兰英,任何復(fù)雜的三維模型都是由基本的幾何圖元:點撇叁、線段和多邊形組成的,有了這些圖元畦贸,就可以建立比較復(fù)雜的模型陨闹。所有的圖元都是由一系列有順序的頂點集合來描述的。OpenGL ES中的圖元只有點薄坏、線趋厉、三角形,精簡了多邊形等其他圖元胶坠,各種復(fù)雜的幾何形狀都是由三角形構(gòu)成的君账。包括正方形、圓形沈善、正方體乡数、球體等。但是其他更為復(fù)雜的物體矮瘟,我們不可能都自己去用三角形構(gòu)建瞳脓,這個時候就需要通過加載利用其他軟件(比如3DMax)構(gòu)建的3D模型。之后在模型加載章節(jié)再詳細講述澈侠。
<center>{% qnimg 20170112148420555397978.jpg %}</center>
紋理
紋理是一個用來保存圖像的色值
的 OpenGL ES 緩存
劫侧。
現(xiàn)實生活中,紋理最通常的作用是裝飾我們的物體模型哨啃,它就像是貼紙一樣貼在物體表面烧栋,使得物體表面擁有圖案。
但實際上在 OpenGL 中拳球,紋理的作用不僅限于此审姓,它可以用來存儲大量的數(shù)據(jù)。一個典型的例子就是利用紋理存儲畫筆筆刷的mask
信息祝峻。
紋理坐標(biāo)在 x 和 y 軸上魔吐,范圍為 0 到 1 之間(我們使用的是 2D 紋理圖像)扎筒。使用紋理坐標(biāo)獲取紋理顏色叫做采樣。紋理坐標(biāo)起始于(0, 0)酬姆,也就是紋理圖片的左下角
嗜桌,終始于(1, 1),即紋理圖片的右上角
辞色。下面的圖片展示了我們是如何把紋理坐標(biāo)映射到三角形上骨宠。
<center>{% qnimg 20170116148453793035367.jpg %}</center>
正因為紋理坐標(biāo)的與眾不同,所以O(shè)penGL紋理渲染中有一種常見的“BUG”相满,就是紋理顛倒
(垂直鏡像翻轉(zhuǎn))层亿,有興趣的同學(xué)可以自己想想為什么會出現(xiàn)這種現(xiàn)象,解決方案很簡單立美,通過垂直鏡像翻轉(zhuǎn)我們的紋理或者頂點坐標(biāo)就可以解決匿又,后續(xù)在FBO中有涉及到再講述。
著色器
著色器(Shader)是在GPU上運行的小程序建蹄。從名稱可以看出琳省,可通過處理它們來處理頂點。此程序使用OpenGL ES SL語言來編寫躲撰。它是一個描述頂點或像素特性的簡單程序针贬。
頂點著色器
頂點著色器對每個頂點
執(zhí)行一次運算,它可以使用頂點數(shù)據(jù)來計算該頂點的坐標(biāo)拢蛋,顏色桦他,光照,紋理坐標(biāo)等谆棱,在渲染管線中每個頂點都是獨立地被執(zhí)行快压。
在頂點著色器中最重要的任務(wù)是執(zhí)行頂點坐標(biāo)變換
,應(yīng)用程序中設(shè)置的圖元頂點坐標(biāo)通常是針對本地坐標(biāo)系的垃瞧。本地坐標(biāo)系簡化了程序中的坐標(biāo)計算蔫劣,但是 GL 并不識別本地坐標(biāo)系,所以在頂點著色器中要對本地坐標(biāo)執(zhí)行模型視圖變換个从,將本地坐標(biāo)轉(zhuǎn)化為裁剪坐標(biāo)系的坐標(biāo)值脉幢。
頂點著色器的另一個功能是向后面的片段著色器
提供一組易變變量(varying)
。易變變量會在圖元裝配
階段(簡單說嗦锐,圖元裝配之后嫌松,所有 3D 的圖元將被轉(zhuǎn)化為屏幕上 2D 的圖元。)之后被執(zhí)行插值計算奕污,如果是單重采樣萎羔,其插值點為片段的中心,如果多重采樣碳默,其插值點可能為多個采樣片段中的任意一個位置贾陷。易變變量可以用來保存插值計算片段的顏色缘眶,紋理坐標(biāo)等信息
。
頂點著色器的輸入輸出模型如下:
<center>{% qnimg 20161012113348032.jpg %}</center>
片元著色器
可編程的片段著色器是實現(xiàn)一些高級特效如紋理貼圖髓废,光照磅崭,環(huán)境光,陰影等功能的基礎(chǔ)瓦哎。片段著色器的主要作用是計算每一個片段最終的顏色值(或者丟棄該片段)
。
在片段著色器之前的階段柔逼,渲染管線都只是在和頂點蒋譬,圖元打交道。在 3D 圖形程序開發(fā)中愉适,貼圖是最重要的部分犯助,程序可以通過 GL 命令上傳紋理數(shù)據(jù)至 GL 內(nèi)存中,這些紋理可以被片段著色器使用维咸。片段著色器可以根據(jù)頂點著色器輸出的頂點紋理坐標(biāo)對紋理進行采樣剂买,以計算該片段的顏色值。
另外癌蓖,片段著色器也是執(zhí)行光照等高級特效的地方
瞬哼,比如可以傳給片段著色器一個光源位置和光源顏色,可以根據(jù)一定的公式計算出一個新的顏色值租副,這樣就可以實現(xiàn)光照特效坐慰。
片元著色器的輸入輸出模型如下:
<center>{% qnimg 20161012113417566.jpg %}</center>
著色器語言
著色器語言(Shading Language)是一種高級的圖形編程語言,僅適合于GPU編程
用僧,其源自應(yīng)用廣泛的C語言结胀。對于頂點著色器和片元著色器的開發(fā)都需要用到著色器語言進行開發(fā)。它是面向過程
的而非面向?qū)ο笤鹧jP(guān)于著色器語言也會放在之后專門的章節(jié)中講述糟港。
坐標(biāo)系
首先說明一點,網(wǎng)上很多文章說OpenGL的坐標(biāo)系是右手坐標(biāo)系是不完全正確的院仿,他們所指的是除歸一化設(shè)備坐標(biāo)系(NDC)之外的坐標(biāo)系秸抚,OpenGL一共有模型坐標(biāo)系、世界坐標(biāo)系歹垫、裁剪坐標(biāo)系耸别、照相機坐標(biāo)系、規(guī)范化設(shè)備坐標(biāo)系县钥、屏幕坐標(biāo)系
等多個坐標(biāo)系秀姐,會在之后坐標(biāo)系與坐標(biāo)變換的章節(jié)中講述。
這里只需要了解OpenGL中歸一化設(shè)備坐標(biāo)系在不做任何設(shè)置的情況下是左手坐標(biāo)系若贮,其他坐標(biāo)系都是右手坐標(biāo)系
省有,而這些坐標(biāo)系中我們現(xiàn)在所需要知道的就是歸一化設(shè)備坐標(biāo)系(NDC)痒留,該坐標(biāo)系經(jīng)過視口變換后就轉(zhuǎn)為屏幕坐標(biāo)系,也就是我們手機屏幕上的坐標(biāo)系蠢沿,而我們設(shè)置的頂點坐標(biāo)其實是模型坐標(biāo)系下的坐標(biāo)伸头,但如果不經(jīng)過任何模型變換、投影變換等操作的話舷蟀,那么就可以將其視為所設(shè)置的是NDC坐標(biāo)系下的坐標(biāo)恤磷。
標(biāo)準(zhǔn)化設(shè)備坐標(biāo)是一個 x、y 和 z 值在 -1.0 到 1.0 的一小段空間野宜。任何落在范圍外的坐標(biāo)都會被丟棄/裁剪扫步,不會顯示在你的屏幕上。
投影
OpenGL ES 的世界是3D的匈子,但是手機屏幕能夠給我展示的終究是一個平面河胎,只不過是在繪制的過程中利用色彩和線條讓畫面呈現(xiàn)出3D的效果。OpenGL ES將這種從3D到2D的轉(zhuǎn)換過程利用投影的方式使計算相對使用者來說變得簡單可設(shè)置虎敦。
OpenGL ES中有兩種投影方式:正交投影和透視投影游岳。正交投影,物體不會隨距離觀測點的位置而大小發(fā)生變化其徙。而透視投影胚迫,距離觀測點越遠,物體越小唾那,距離觀測點越近晌区,物體越大。
光柵化
<center>{% qnimg 20170112706113F85123B-6006-4633-9D8C-C4C4DB4BA3AC.png %}</center>
在光柵化階段通贞,基本圖元被轉(zhuǎn)換為供片段著色器使用的片段(Fragment)
朗若,Fragment 可以簡單理解為能被渲染到屏幕上的像素,它包含位置昌罩,顏色哭懈,紋理坐標(biāo)等信息
,這些值是由圖元的頂點信息進行插值計算
得到的茎用。這些片元接著被送到片元著色器中處理遣总。這是從頂點數(shù)據(jù)到可渲染在顯示設(shè)備上的像素的質(zhì)變過程。
在片段著色器運行之前會執(zhí)行裁切(Clipping)轨功。裁切會丟棄超出你的視圖以外的所有像素旭斥,用來提升執(zhí)行效率。
片元在成為像素之前古涧,還會做多種測試(比如深度測試垂券、透明度測試、模板測試等羡滑,這些測試目前接觸到的一般在3D圖像中更常使用菇爪,比如深度測試進行物體的遮擋效果的渲染算芯,模板測試可以用于描邊等,2D中應(yīng)用較少)以決定其最終是否會被顯示為像素凳宙。所以熙揍,嚴(yán)格來說,“片元”和“像素”并不是一一對應(yīng)的氏涩。
狀態(tài)機
OpenGL 是一個狀態(tài)機届囚,它維持自己的狀態(tài),并根據(jù)用戶調(diào)用的函數(shù)來改變自己的狀態(tài)是尖。根據(jù)狀態(tài)的不同意系,調(diào)用同樣的函數(shù)也可能產(chǎn)生不同的效果。
在 OpenGL 的世界里析砸,大多數(shù)元素都可以用狀態(tài)來描述,比如:
- 顏色爆袍、紋理坐標(biāo)首繁、光源的各種參數(shù)…
- 是否啟用了光照、是否啟用了紋理陨囊、是否啟用了混合弦疮、是否啟用了深度測試
- 。蜘醋。胁塞。
OpenGL 會保持狀態(tài),除非我們調(diào)用 OpenGL 函數(shù)來改變它
压语。比如你用glEnableXXX
開啟了一個狀態(tài)啸罢,在以后的渲染中將一直保留并應(yīng)用這個狀態(tài),除非你調(diào)用glDisableXXX
及同類函數(shù)來改變該狀態(tài)或程序退出胎食。
又或者當(dāng)前顏色是一個狀態(tài)變量扰才,可以把當(dāng)前顏色設(shè)置為白色、紅色或其他任何顏色厕怜,在此之后繪制的所有物體都將使用這種顏色衩匣,直到把當(dāng)前顏色設(shè)置為其他顏色。
介紹狀態(tài)機是因為OpenGL 當(dāng)中很多 API粥航,其實僅僅是向 OpenGL 這個狀態(tài)機傳數(shù)據(jù)或者讀數(shù)據(jù)琅捏。而這個操作在之后的OpenGL操作中非常普遍。
上下文
上面提到的各種狀態(tài)值递雀,將保存在對應(yīng)的上下文(Context)
中柄延。
通過放置這些狀態(tài)到上下文中,上下文可以跟蹤用于渲染的幀緩存缀程、用于幾何數(shù)據(jù)拦焚、顏色等的緩存蜡坊。還會決定是否使用如紋理、燈光等功能以及會為渲染定義當(dāng)前的坐標(biāo)系統(tǒng)等赎败。并且在多任務(wù)的情況下秕衙,就能很容易的共享硬件設(shè)備,而互不影響各自的狀態(tài)僵刮。
因此渲染的時候据忘,要指定對應(yīng)的當(dāng)前上下文,也就是在按要求創(chuàng)建一系列諸如EGLSurface搞糕、EGLDisplay等對象勇吊,調(diào)用glMakeCurrent之后,當(dāng)前線程便擁有了OpenGL的繪圖能力
窍仰,而在此之后才能使用OpenGL繪圖等操作汉规,否則會出錯。在GLSurfaceView中我們可以看到一個GLThread驹吮,也就是所謂的GL線程
针史,其實這個線程和我們的普通線程沒有區(qū)別,但是其內(nèi)部封裝了OpenGL繪制所需要的整個完整過程碟狞,并且按照這個流程正確地執(zhí)行啄枕,這就是我們所說的具有了OpenGL的繪圖能力。
渲染管線
在 OpenGL 中族沃,任何事物都在 3D 空間中频祝,而屏幕和窗口卻是 2D 像素數(shù)組,這導(dǎo)致 OpenGL 的大部分工作都是關(guān)于把 3D 坐標(biāo)轉(zhuǎn)變?yōu)檫m應(yīng)你屏幕的 2D 像素
脆淹。3D 坐標(biāo)轉(zhuǎn)為 2D 坐標(biāo)的處理過程是由 OpenGL 的圖形渲染管線(Graphics Pipeline常空,實際上指的是一堆原始圖形數(shù)據(jù)途經(jīng)一個輸送管道,期間經(jīng)過各種變化處理最終出現(xiàn)在屏幕的過程)管理的盖溺。圖形渲染管線可以被劃分為兩個主要部分:第一部分把你的3D 坐標(biāo)轉(zhuǎn)換為 2D 坐標(biāo)窟绷,第二部分是把 2D 坐標(biāo)轉(zhuǎn)變?yōu)閷嶋H的有顏色的像素。
2D 坐標(biāo)和像素也是不同的咐柜,2D 坐標(biāo)精確表示一個點在 2D 空間中的位置兼蜈,而 2D 像素是這個點的近似值,2D 像素受到你的屏幕/窗口分辨率的限制拙友。
圖形渲染管線可以被劃分為幾個階段为狸,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數(shù))遗契,并且很容易并行執(zhí)行辐棒。它的工作過程和車間流水線一致,各個模塊各司其職但是又相互依賴。
下圖就是渲染管線:
<center>{% qnimg 20170112148420103614414.jpg %}</center>
OpenGL ES 采用服務(wù)器/客戶端編程模型漾根,客戶端運行在 CPU 上泰涂,服務(wù)端運行在 GPU 上,調(diào)用 OpenGL ES 函數(shù)的時辐怕,由客戶端發(fā)送至服務(wù)器端逼蒙,并被服務(wù)端轉(zhuǎn)換成底層圖形硬件支持的繪制命令。
<center>{% qnimg 20170111148411873373682.jpg %}</center>
其他
其它更多的諸如3D模型加載寄疏、陰影是牢、粒子、混合與霧陕截、標(biāo)志板驳棱、天空盒和與天空穹等內(nèi)容等后面具體應(yīng)用時再詳細介紹。
渲染過程
OpenGL ES 2.0的渲染過程可以簡單敘述為:
讀取頂點數(shù)據(jù)——執(zhí)行頂點著色器——組裝圖元——光柵化圖元——執(zhí)行片元著色器——寫入幀緩沖區(qū)——顯示到屏幕上农曲。
OpenGL作為本地庫直接運行在硬件上社搅,沒有虛擬機,也沒有垃圾回收或者內(nèi)存壓縮乳规。在Java層定義圖像的數(shù)據(jù)需要能被OpenGL存取形葬,因此,需要把內(nèi)存從Java堆復(fù)制到本地堆
驯妄。
頂點著色器是針對每個頂點都會執(zhí)行的程序荷并,是確定每個頂點的位置合砂。同理青扔,片元著色器是針對每個片元都會執(zhí)行的程序,確定每個片元的顏色翩伪。
著色器需要進行編譯
微猖,然后鏈接
到OpenGL程序(Program)
中。一個OpenGL的程序就是把一個頂點著色器和一個片段著色器鏈接在一起變成單個對象缘屹。
繪制一個三角形
正如我們學(xué)習(xí)Java凛剥、C++等編程語言時大多數(shù)教程都會先告訴你怎么寫出一句Hello World
,OpenGL的教程大多數(shù)第一課也是教你如何繪制一個簡單三角形轻姿。接下來我們就按照上述所說的渲染過程犁珠,講解一下如何通過OpenGL ES的API在Android手機上顯示出一個三角形。
在Demo中我們創(chuàng)建一個TriangleActivity
作為我們的界面互亮,使用Android自帶的GLSurfaceView
作為渲染的載體(現(xiàn)在自己創(chuàng)建EGLSurface還為時過早)犁享,同時我們創(chuàng)建一個TriangleRenderer
作為GLSurfaceView的Renderer,在其里面實現(xiàn)實際的渲染操作豹休。為了便于理解炊昆,著色器、頂點數(shù)組等將全部放于該Renderer內(nèi),后續(xù)的例子再進行封裝凤巨。
第一個Renderer
首先我們現(xiàn)在創(chuàng)建并實現(xiàn)整個渲染過程中最核心的部分TriangleRenderer
视乐,并讓其實現(xiàn)Renderer接口。
Renderer接口中有三個需要實現(xiàn)的方法敢茁,分別是onSurfaceCreated
佑淀,onSurfaceChanged
以及onDrawFrame
,前兩個方法如果有接觸過SurfaceView及SurfaceHolder的話就比較熟悉卷要,分別是Surface創(chuàng)建時的回調(diào)以及SUrface如寬高變化時的回調(diào)渣聚,onSurfaceCreated
主要用于初始化
等,onSurfaceChanged
主要用于做模型視圖轉(zhuǎn)換
等操作僧叉,而onDrawFrame
就是當(dāng)OpenGL渲染每一幀的回調(diào)方法奕枝,我們的實際繪制操作就在這里進行。
這三個方法我們先放著瓶堕,先來按照渲染流程隘道,我們創(chuàng)建繪制一個三角形所需要的頂點數(shù)據(jù)
。
頂點數(shù)據(jù)是一個包含了所繪制圖像放置在OpenGL坐標(biāo)系中后郎笆,其各個頂點的三維坐標(biāo)
的數(shù)組(其實頂點數(shù)據(jù)還可以放置顏色等谭梗,通過偏移來獲取不同類型的數(shù)據(jù))。那么剛剛在坐標(biāo)系中說了宛蚓,OpenGL里有多個坐標(biāo)系激捏,但是和我們目前關(guān)系最大的是NDC,NDC坐標(biāo)系:
<center>{% qnimg CFB95B14-3302-49B6-A16E-96AAB5C0DCC5.png %}</center>
即NDC坐標(biāo)系的原點(0,0)默認位置在屏幕中心凄吏,x,y,z軸范圍為[-1,1]远舅,而Android屏幕坐標(biāo)系原點在左上角,x,y軸范圍為[0,各軸分辨率]痕钢。
現(xiàn)在我們要繪制一個三角形图柏,頂點在y軸正向最大值位置,左下角在x軸負向最大值位置任连,右下角在x軸正向最大值位置蚤吹,那么對應(yīng)的頂點數(shù)組為:
<pre><code>
//設(shè)置三角形頂點數(shù)組
private static final float TRIANGLE_COORDS[] = {
//默認按逆時針方向繪制??
0.0f, 1.0f, 0.0f, // 頂點
-1.0f, -0.0f, 0.0f, // 左下角
1.0f, -0.0f, 0.0f // 右下角
};
</code></pre>
接下來我們開始編寫頂點著色器:
<pre><code>
private static final String VERTEX_SHADER =
"http://根據(jù)所設(shè)置的頂點數(shù)據(jù)而插值后的頂點坐標(biāo)\n" +
"attribute vec4 vPosition;" +
"void main() {" +
" //設(shè)置最終坐標(biāo)\n"
" gl_Position = vPosition;" +
"}";
</code></pre>
對于上述著色器只需要知道vPosition
就是我們所設(shè)置的頂點數(shù)據(jù)進行自動插值后的當(dāng)前片元的頂點坐標(biāo),而gl_Position
是OpenGL的內(nèi)置變量随抠,代表著當(dāng)前這個片元最終所處的坐標(biāo)裁着。而vec
是代表向量,坐標(biāo)使用vec4
而不是vec3
的原因是因為齊次坐標(biāo)
的關(guān)系拱她,但是這個在這里不是重點二驰。
組裝圖
,光柵化圖元
OpenGL會自動進行椭懊,這里我們不管诸蚕,接下來我們開始編寫片元著色器步势,來為這個三角形加上顏色:
<pre><code>
private static final String FRAGMENT_SHADER =
"http://設(shè)置float類型默認精度,頂點著色器默認highp背犯,片元著色器需要用戶聲明\n" +
"precision mediump float;" +
"http://顏色值坏瘩,vec4代表四維向量,此處由用戶傳入漠魏,數(shù)據(jù)格式為{r,g,b,a}\n" +
"uniform vec4 vColor;" +
"void main() {" +
"http://該片元最終顏色值\n" +
"gl_FragColor = vColor;" +
"}";
</code></pre>
在上述著色器代碼中倔矾,首先我們聲明了片元著色器中默認float類型變量的精度(中等),在頂點著色器中默認精度為highp柱锹,而片元著色器中必須自己設(shè)置哪自。
之后我們聲明了一個uniform
類型的四維向量,用以存儲用戶所設(shè)置的三角形顏色(當(dāng)然這里顏色的值也是用戶所設(shè)置的顏色數(shù)據(jù)進行逐片元插值后的值)禁熏,gl_FragColor
也是OpenGL的內(nèi)置變量壤巷,表示片元最終的顏色值,這里我們直接將插值后的vColor
作為最終顏色瞧毙。
從片元著色器中可以看到胧华,有一個數(shù)據(jù)還需要用戶自己設(shè)定,那就是三角形的顏色值宙彪,格式是{r,g,b,a}矩动,設(shè)置如下:
<pre><code>
// 設(shè)置三角形顏色和透明度(r,g,b,a)
private static final float COLOR[] = {1.0f, 0.0f, 0f, 1.0f};//紅色不透明
</code></pre>
最后寫入幀緩沖區(qū)
,顯示到屏幕上
也是由OpenGL自動完成释漆,那么至此我們已經(jīng)完成了頂點著色器和片元著色器的實現(xiàn)悲没,也提供了這兩個著色器所需要的頂點數(shù)據(jù)和顏色數(shù)據(jù),那么接下來就是怎么將這些數(shù)據(jù)與著色器內(nèi)的變量相綁定男图,并且告知OpenGL什么時候開始渲染以及怎么渲染示姿。
讓我們回到Renderer那三個未實現(xiàn)的接口上,首先我們在onSurfaceCreated
調(diào)用時享言,也就是Surface正式創(chuàng)建后峻凫,做一些初始化操作:
<pre><code>
private int mProgramId;
private int mColorId;
private int mPositionId;
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
//編譯著色器并鏈接頂點與片元著色器生成OpenGL程序句柄
mProgramId = OpenGlUtils.loadProgram(VERTEX_SHADER, FRAGMENT_SHADER);
//通過OpenGL程序句柄查找獲取頂點著色器中的位置句柄
mPositionId = GLES20.glGetAttribLocation(mProgramId, "vPosition");
//通過OpenGL程序句柄查找獲取片元著色器中的顏色句柄
mColorId = GLES20.glGetUniformLocation(mProgramId, "vColor");
}
</code></pre>
上述代碼中我們借助了一個封裝了編譯著色器并鏈接生成當(dāng)前OpenGL程序句柄的工具類渗鬼,我們先來簡單介紹一下OpenGL中編譯著色器并鏈接至最終的Program上的流程:
<pre><code>
public static int loadShader(final String strSource, final int iType) {
int[] compiled = new int[1];
//創(chuàng)建指定類型的著色器
int iShader = GLES20.glCreateShader(iType);
//將源碼添加到iShader并編譯它
GLES20.glShaderSource(iShader, strSource);
GLES20.glCompileShader(iShader);
//獲取編譯后著色器句柄存在在compiled數(shù)組容器中
GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
//容錯判斷
if (compiled[0] == 0) {
Log.d("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
return 0;
}
return iShader;
}
public static int loadProgram(final String strVSource, final String strFSource) {
int iVShader;
int iFShader;
int iProgId;
int[] link = new int[1];
//獲取編譯后的頂點著色器句柄
iVShader = loadShader(strVSource, GLES20.GL_VERTEX_SHADER);
if (iVShader == 0) {
Log.d("Load Program", "Vertex Shader Failed");
return 0;
}
//獲取編譯后的片元著色器句柄
iFShader = loadShader(strFSource, GLES20.GL_FRAGMENT_SHADER);
if (iFShader == 0) {
Log.d("Load Program", "Fragment Shader Failed");
return 0;
}
//創(chuàng)建一個Program
iProgId = GLES20.glCreateProgram();
//添加頂點著色器與片元著色器到Program中
GLES20.glAttachShader(iProgId, iVShader);
GLES20.glAttachShader(iProgId, iFShader);
//鏈接生成可執(zhí)行的Program
GLES20.glLinkProgram(iProgId);
//獲取Program句柄览露,并存在在link數(shù)組容器中
GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
//容錯
if (link[0] <= 0) {
Log.d("Load Program", "Linking Failed");
return 0;
}
//刪除已鏈接后的著色器
GLES20.glDeleteShader(iVShader);
GLES20.glDeleteShader(iFShader);
return iProgId;
}
</code></pre>
上述代碼中關(guān)鍵代碼點都有注釋,這里可以發(fā)現(xiàn)OpenGL的很多接口調(diào)用方式與C非常相似譬胎,比如獲取句柄的方式是將句柄存入一個數(shù)組容器中秉撇,與C的指針有點相像餐塘。
到這里我們已經(jīng)獲取到了一個Program
,還有后續(xù)需要綁定我們數(shù)據(jù)的vColor
,vPosition
的地址。接下來我們需要設(shè)置視口來告訴OpenGL我們想要顯示在屏幕的哪個區(qū)域內(nèi):
<pre><code>
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
</code></pre>
當(dāng)我們設(shè)置GLSurfaceView為全屏的時候洪囤,那么上述的width
就是屏幕寬度,height
就是屏幕高度搀继,上述設(shè)置的意思就是我們當(dāng)前渲染的視口區(qū)域從屏幕左上角原點(0,0)開始吟策,寬高為全屏。
至此就萬事俱備了,接下來我們便要在OpenGL開始渲染的回調(diào)接口onDrawFrame()
中進行我們最后的渲染操作了:
<pre><code>
//設(shè)置每個頂點的坐標(biāo)數(shù)
private static final int COORDS_PER_VERTEX = 3;
//下一個頂點與上一個頂點之間的不長韵卤,以字節(jié)為單位骗污,每個float類型變量為4字節(jié)
private final int VERTEX_STRID = COORDS_PER_VERTEX * 4;
//頂點個數(shù)
private final int VERTEX_COUNT = TRIANGLE_COORDS.length / COORDS_PER_VERTEX;
@Override
public void onDrawFrame(GL10 gl10) {
//這里網(wǎng)上很多博客說是設(shè)置背景色,其實更嚴(yán)格來說是通過所設(shè)置的顏色來清空顏色緩沖區(qū)沈条,改變背景色只是其作用之一
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);//白色不透明
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//告知OpenGL所要使用的Program
GLES20.glUseProgram(mProgramId);
//啟用指向三角形頂點數(shù)據(jù)的句柄
GLES20.glEnableVertexAttribArray(mPositionId);
//綁定三角形的坐標(biāo)數(shù)據(jù)
GLES20.glVertexAttribPointer(mPositionId, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
VERTEX_STRID, mVertexBuffer);
//綁定顏色數(shù)據(jù)
GLES20.glUniform4fv(mColorId, 1, TRIANGLE_COORDS, 0);
//繪制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, VERTEX_COUNT);
//禁用指向三角形的頂點數(shù)據(jù)
GLES20.glDisableVertexAttribArray(mPositionId);
}
</code></pre>
首先這里我們注意到除了注釋之外需忿,我們的代碼少了一個變量,就是mVertexBuffer
蜡歹,這個變量是一個FloatBuffer類型的變量屋厘,用于開辟處一塊內(nèi)存緩沖區(qū)來存儲供OpenGL使用的頂點數(shù)據(jù),在我們這個Demo中頂點數(shù)據(jù)不會發(fā)生變化月而,所以我們直接在onSurfaceCreated()
接口的最后加上如下代碼進行初始化即可:
<pre><code>
//初始化頂點字節(jié)緩沖區(qū)汗洒,用于存放三角的頂點數(shù)據(jù)
ByteBuffer bb = ByteBuffer.allocateDirect(
//(每個浮點數(shù)占用4個字節(jié)
TRIANGLE_COORDS.length * 4);
//設(shè)置使用設(shè)備硬件的原生字節(jié)序
bb.order(ByteOrder.nativeOrder());
//從ByteBuffer中創(chuàng)建一個浮點緩沖區(qū)
mVertexBuffer = bb.asFloatBuffer();
//把坐標(biāo)都添加到FloatBuffer中
mVertexBuffer.put(TRIANGLE_COORDS);
//設(shè)置buffer從第一個位置開始讀
//因為在每次調(diào)用put加入數(shù)據(jù)后position都會加1,因此要將position重置為0
mVertexBuffer.position(0);
</code></pre>
還有記得在接口外面聲明變量:
<pre><code>
private FloatBuffer vertexBuffer;
</code></pre>
為什么使用java的nio包下的Buffer作為內(nèi)存緩沖區(qū)的形式一方面是出于性能等方面的考慮父款,另一方面 OpenGL 是一個非常底層的繪制接口仲翎,它所使用的緩沖區(qū)存儲結(jié)構(gòu)是和我們的 Java 程序中不相同的(Java 是大端字節(jié)序(BigEdian),而 OpenGL 所需要的數(shù)據(jù)是小端字節(jié)序(LittleEdian))铛漓。所以溯香,我們在將 Java 的緩沖區(qū)轉(zhuǎn)化為 OpenGL 可用的緩沖區(qū)時需要作這樣的一些工作。
而顏色的綁定我們看到就簡單得多浓恶,只需要調(diào)用接口就可以實現(xiàn)玫坛,因為兩者的在這個Demo中變量類型不同(attribute
只能在頂點著色器中使用,通常用于表示頂點坐標(biāo)包晰、紋理坐標(biāo)等湿镀,而uniform
常用于表示常量形式的顏色、矩陣伐憾、材質(zhì)等勉痴,兩者設(shè)置接口也不同,具體會在后續(xù)著色器章節(jié)中講述)树肃。我們也可以通過將顏色與頂點數(shù)據(jù)放置一起蒸矛,然后一起轉(zhuǎn)為FloatBuffer來傳遞給OpenGL,并且設(shè)置每個頂點的顏色不同胸嘴,通過glVertexPointer
與glColorPointer
兩個接口配合使用來繪制出如下的三角形雏掠,這也就是之前一直講的插值的含義,OpenGL會自動對頂點間坐標(biāo)以及顏色進行插值計算:
<center>{% qnimg 1363184395_2222.png %}</center>
至此我們已經(jīng)完成TriangleRender
的實現(xiàn)劣像,最后只需要加上一個回收資源的方法:
<pre><code>
public void destroy() {
GLES20.glDeleteProgram(mProgramId);
}
</code></pre>
GLSurfaceView
前面我們完成了TriangleRender
的實現(xiàn)乡话,那么接下來我們將其與GLSurfaceView
綁定起來以便于看到我們渲染的結(jié)果。
為了簡單起見耳奕,我們直接TriangleActivity
的布局文件中加入GLSurfaceView
绑青,在onCreate()
中加入如下代碼:
<pre><code>
GLSurfaceView glSurfaceView = findViewById(R.id.gl_surface);
// 創(chuàng)建OpenGL ES 2.0的上下文
glSurfaceView.setEGLContextClientVersion(2);
//設(shè)置Renderer用于繪圖
glSurfaceView.setRenderer(new TriangleRender());
//只有在繪制數(shù)據(jù)改變時才繪制view诬像,可以防止GLSurfaceView幀重繪
//該種模式下當(dāng)需要重繪時需要我們手動調(diào)用glSurfaceView.requestRender();
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
</code></pre>
最后我們跑一下項目,就可以在手機上看到如下的三角形了:
<center>{% qnimg device-2018-03-18-15033.png %}</center>
總結(jié)
這章我們介紹了OpenGL(ES)以及EGL的相關(guān)內(nèi)容和一些基本概念闸婴,同時通過繪制一個簡單的三角形來了解了OpenGL的常見繪制流程以及部分接口颅停,感興趣的可以自己嘗試一下如何繪制一個漸變的三角形或者一個正方形等較簡單的幾何圖形。使用OpenGL進行繪制的確比直接使用Android自帶的繪圖API繁瑣一些掠拳,出現(xiàn)了問題也比較難以排查癞揉,因為更接近于底層所以理解上很多地方不太一樣,但是對于圖形渲染或者處理溺欧,OpenGL無論是性能還是可以實現(xiàn)的效果都是勝出一籌的喊熟。
下一章將深入講解一下紋理的相關(guān)內(nèi)容,同時繪制一個簡單的紋理姐刁。該系列教程Demo見OpenGLESLesson
參考
OpenGL ES 基礎(chǔ)概念
OpenGL ES 開篇
卡通渲染(上)
Android 系統(tǒng)圖形棧(一): OpenGL ES 和 EGL 介紹
Android OpenGLES2.0(一)——了解OpenGLES2.0