一. OpenGL的基本概念
OpenGL 的結構可以從邏輯上划分為下面 3 個部分:
- 圖元(Primitives)
- 緩沖區(Buffers)
- 光柵化(Rasterize)
圖元(Primitives)
在 OpenGL 的世界里,我們只能畫點、線、三角形這三種基本圖形,而其它復雜的圖形都可以通過三角形來組成。所以這里的圖元指的就是這三種基礎圖形:
點:點存在於三維空間,坐標用(x,y,z)表示。
線:由兩個三維空間中的點組成。
三角形:由三個三維空間的點組成。
緩沖區(Buffers)
OpenGL 中主要有 3 種 Buffer:
- 幀緩沖區(Frame Buffers) 幀緩沖區:這個是存儲OpenGL 最終渲染輸出結果的地方,它是一個包含多個圖像的集合,例如顏色圖像、深度圖像、模板圖像等。
- 渲染緩沖區(Render Buffers) 渲染緩沖區:渲染緩沖區就是一個圖像,它是 Frame Buffer 的一個子集。
- 緩沖區對象(Buffer Objects) 緩沖區對象就是程序員輸入到 OpenGL 的數據,分為結構類和索引類的。前者被稱為“數組緩沖區對象”或“頂點緩沖區對象”(“Array Buffer Object”或“Vertex Buff er Object”),即用來描述模型的數組,如頂點數組、紋理數組等; 后者被稱為“索引緩沖區對象”(“Index Buffer Object”),是對上述數組的索引。
光柵化(Rasterize)
在介紹光柵化之前,首先來補充 OpenGL 中的兩個非常重要的概念:
Vertex Vertex 就是圖形中頂點,一系列的頂點就圍成了一個圖形。
Fragment Fragment 是三維空間的點、線、三角形這些基本圖元映射到二維平面上的映射區域,通常一個 Fragment 對應於屏幕上的一個像素,但高分辨率的屏幕可能會用多個像素點映射到一個 Fragment,以減少 GPU 的工作。
而光柵化是把點、線、三角形映射到屏幕上的像素點的過程。

着色器程序(Shader)
Shader 用來描述如何繪制(渲染),GLSL 是 OpenGL 的編程語言,全稱 OpenGL Shader Language,它的語法類似於 C 語言。OpenGL 渲染需要兩種 Shader:Vertex Shader 和 Fragment Shader。
- Vertex Shader 頂點着色器:對於3D模型網格的每個頂點執行一次,主要是確定該頂點的最終位置。
- Fragment Shader 片元着色器:對光柵化之后2D圖像中的每個像素處理一次。3D物體的表面最終顯示成什么樣將由它決定,例如為模型的可見表面添加紋理,處理光照、陰影的影響等等。
二. OpenGL ES在屏幕產生圖片的過程
當我們買一個手機的時候,我們會非常關注這個手機的分辨率。分辨率代表着像素的多少,比如我們熟知的 iphone6 的分辨率為 1334×750,而 iphone6 plus 的分辨率是1920×1080。
手機屏幕上的圖片,是由一個一個的像素組成,那么可以計算出來,一個屏幕上的圖片,是由上百萬個像素點組成。而每個像素點都有自己的顏色,每種顏色都是由 RGB 三原色組成。三原色按照不同的比例混合,組成了手機所能顯示出來的顏色。
每個像素的顏色信息都保存在 buffer 中,這塊 buffer 可以分給 RGB 每個通 道各 8bit 進行信息保存,也可以分給 RGB 每個通道不同的空間進行信息保存, 比如由於人眼對綠色最敏感,那么可以分配給 G 通道 6 位,R 和 B 通道各 5 位。這些都是常見的手機配置。假如使用 RGB888 的手機配置,也就是每種顏色的取值從 0 到 255,0 最小,255 最大。那么紅綠藍都為 0 的時候,這個像素點的顏色就是黑色,紅綠藍都為 255 的時候,這個像素點的顏色就是白色。當紅為 255, 綠藍都為 0 的時候,這個像素點的顏色就是紅色。當紅綠為 255,藍為 0 的時候, 這個像素點的顏色就是黃色。當然不是只取 0 或者 255,可以取 0-255 中間的值, 100,200,任意在 0 和 255 中間的值都沒有問題。那么我們可以算一下,按照紅綠藍不同比例進行搭配,每個像素點,可以顯示的顏色有 255255255=16581375 種,這個數字是非常恐怖,所以我們的手機可以顯示出來各種各樣的顏色。 這里在延伸的科普一下,我們看到手機可以顯示那么多種顏色了,但是是不是說我們的手機在顏色上就已經發展到極致了呢?其實是遠遠沒有的,在這個手機配置下,三原色中每一種的取值可以從 0 到 255,而在現實生活中,它們的取 值可以從 0 到 1 億,而我們人類的眼睛所能看到的范圍是,從 0 到 10 萬。所以手機硬件還存在很大的提升空間。而在手機硬件提升之前,我們也可以通過 HDR 等技術盡量的在手機中多顯示一些顏色。所以,講到這里,我們知道了,手機屏幕上顯示的圖片,是由這上百萬個像素點,以及這上百萬個像素點對應的顏色組成的。
用程序員的角度來看,就是手機屏幕對應着一塊 buffer,這塊 buffer 對應上百萬個像素點,每個像素點需要一定的空間來存儲其顏色。如果使用更加形象的例子來比喻,手機屏幕對應的 buffer 就好像一塊巨大的棋盤,棋盤上有上百萬個格子,每個格子都有自己的顏色,那么從遠處整體的看這個棋盤,就是我們看手機的時候顯示的樣子。這就是手機屏幕上圖片的本質。
通過我們對 EGL、GLSL、OpenGL ES 的理解,借助一張圖片,從專業的角度來解釋一下手機屏幕上的圖片是如何生成的。

首先, 通過 EGL 獲取手機屏幕,進而獲取到手機屏幕對應的這個棋盤,同時, 在手機的 GPU 中根據手機的配置信息,生成另外一個的棋盤和一個本子,本子是用於記錄這個棋盤初始顏色等信息。
然后, OpenGL ES 就好像程序員的畫筆,程序員需要知道自己想畫什么東西,比如想畫一個蘋果,那么就需要通過為數不多的基本幾何圖元(如點、直線、三 角形)來創建所需要的模型。比如用幾個三角形和點和線來近似的組成這個蘋果 (圖形學的根本就是點、線和三角形,所有的圖形,都可以由這些基本圖形組成, 比如正方形或者長方形,就可以由兩個三角形組成,圓形可以由無數個三角形組成,只是三角形的數量越多,圓形看上去越圓潤)。
根據這些幾何圖元,建立數學描述,比如每個三角形或者線的頂點坐標位置、每個頂點的顏色。得到這些信息之后,可以先通過 OpenGL ES 將 EGL 生成的棋盤 (buffer)進行顏色初始化,一般會被初始化為黑色。然后將剛才我們獲取到的頂點坐標位置,通過矩陣變化的方式,進行模型變換、觀察變換、投影變換,最后映射到屏幕上,得到屏幕上的坐標。這個步驟可以在 CPU 中完成,也就是在 OpenGL ES 把坐標信息傳給 Shader 之前,在 CPU 中通過矩陣相乘等方式進行更新,或者是直接把坐標信息通過 OpenGL ES 傳給 Shader,同時也把矩陣信息傳給 Shader,通過 Shader 在 GPU 端進行坐標更新,更新的算法通過 GLSL 寫在 Shader 中。這個進行坐標更新的 Shader 被稱為 vertex shader,簡稱 VS,是 OpenGL ES2.0, 也是 GLSL130 版本對應的最重要兩個 shader 之一,作用是完成頂點操作階段中的所有操作。經過矩陣變換后的像素坐標信息,為屏幕坐標系中的坐標信息。在 VS 中,最重要的輸入為頂點坐標、矩陣(還可以傳入頂點的顏色、法線、紋理 坐標等信息),而最重要的運算結果,就是這個將要顯示在屏幕上的坐標信息。 VS 會針對傳入的所有頂點進行運算,比如在 OpenGL ES 中只想繪制一個三角形 和一條線,這兩個圖元不共享頂點,那么在 VS 中,也就傳入了 5 個頂點信息, 根據矩陣變換,這 5 個頂點的坐標轉換成了屏幕上的頂點坐標信息,從圖上顯示, 也就是從左上角的圖一,更新成了中上圖的圖二。
再然后,當圖二生成之后,我們知道了圖元在屏幕上的頂點位置,而頂點的顏色在 VS 中沒有發生變化,所以圖元的頂點顏色我們也是知道的。下面就是根據 OpenGL ES 中設置的狀態,表明哪些點連成線,哪些點組成三角形,進行圖元裝配,也就是我們在右上角的圖三中看到的樣子。這個樣子在 GPU 中不會顯示, 那幾條線也是虛擬的線,是不會顯示在棋盤 buffer 中的,而 GPU 做的是光珊化,這一步是發生在從 VS 出來,進入另外一個Shader (Pixel shader,也稱 fragment shader)之前,在 GPU 中進行的。作用是把線上,或者三角形內部所有的像素點找到,並根據插值或者其他方式計算出其顏色等信息(如果不通過插值,可以使用其他的方法,這些在 OpenGL ES 和 GLSL 中都可以進行設置)。也就生成了下面一行的圖四和圖五。
我們大概可以看到在圖 4 和圖 5 種出現了大量的頂點,大概數一下估計有 40 個點左右,這些點全部都會進入 PS 進行操作,在 PS 中可以對這些點的顏色進行操作,比如可以只顯示這些點的紅色通道,其他的綠藍通道的值設置為 0, 比如之前某個點的 RGB 為 200,100,100。在 PS 中可以將其通過計算,更新為 200,0,0。這樣做的結果就是所顯示的圖片均為紅色,只是深淺不同。這也就好像戴上了一層紅色的濾鏡,其他顏色均為濾掉了。所以用 PS 來做濾鏡是非常方便的。再比如,假如一盞紅色的燈照到了蘋果上,那么顯示出來的顏色就是在蘋果原本的顏色基礎上,紅色值進行一定的增值。
所以,總結一下,經過 VS 和 PS 之后,程序員想要畫的東西,就已經被畫出來了。想要繪制的東西,也就是左下角圖五的樣子。然后再根據 OpenGL ES 的設置,對新繪制出來的東西進行 Depth/Stencil Test,剔除掉被遮擋的部分,將剩余部分與原圖片進行 Blend,生成新的圖片。 最后,通過 EGL,把這個生成的棋盤 buffer 和手機屏幕上對應的棋盤 buffer 進行調換,讓手機屏幕顯示這個新生成的棋盤,舊的那個棋盤再去繪制新的圖片信息。周而復始,不停的把棋盤進行切換,也就像過去看連環畫一樣,動畫就是由一幅幅的圖片組成,當每秒切換的圖片數量超過 30 張的時候,我們的手機也就看到了動態的效果。這就是屏幕上圖片的產生過程。
在這里再進行一下延伸,這個例子中,VS 計算了 5 個頂點的數據,PS 計算 了大概 40 個頂點的數據,而我們剛才說過,手機中存在上百萬個像素點,這上百萬個像素點都可以是頂點,那么這個計算量是非常大的。而這也是為什么要將 shader 運算放在 GPU 中的原因,因為 GPU 擅長進行這種運算。
我們知道 CPU 現在一般都是雙核或者 4 核,多的也就是 8 核或者 16 核,但是 GPU 動輒就是 72 核,多的還有上千核,這么多核的目的就是進行並行運算, 雖然單個的 GPU 核不如 CPU 核,但是單個的 GPU 核足夠進行加減乘除運算,所以大量的 GPU 核用在圖形學像素點運算上,是非常有效的。而 CPU 雖然單個很強大,而且也可以通過多級流水來提高吞吐率,但是終究還是不如 GPU 的多核來得快。但是在通過 GPU 進行多核運算的時候,需要注意的是:如果 shader 中存放判斷語句,就會對 GPU 造成比較大的負荷,不同 GPU 的實現方式不同,多數 GPU 會對判斷語句的兩種情況都進行運算,然后根據判斷結果取其中一個。
我們通過這個例子再次清楚了 OpenGL ES 繪制的整個流程,而這個例子也是最簡單的一個例子,其中有很多 OpenGL ES 的其他操作沒有被涉及到。比如,我們繪制物體的顏色大多是從紋理中采樣出來,那么設計到通過 OpenGL ES 對紋理 進行操作。而 OpenGL ES 的這些功能,我們會在下面一點一點進行學習。
三. OpenGL管線(pipeline)

EGL 是用於與手機設備打交道,比如獲取繪制 buffer,將繪制 buffer 展現到手機屏幕中。那么拋開 EGL 不說,OpenGL ES 與 GLSL 的主要功能,就是往這塊 buffer 上繪制圖片。
所以,我們可以把OpenGL ES和GLSL的流程單獨拿出來進行歸納總結,而這幅流程圖就是著名的 OpenGL ES2.0 pipeline。
首先,最左邊的 API 指的就是 OpenGL ES 的 API,OpenGL ES 其實是一個圖形學庫,由 109 個 API 組成,只要明白了這 109 個 API 的意義和用途,就掌握了OpenGL ES 2.0。
然后,我們通過 API 先設定了頂點的信息,頂點的坐標、索引、顏色等信息,將這些信息傳入 VS。
在 VS 中進行運算,得到最終的頂點坐標。再把算出來的頂點坐標進行圖元裝配,構建成虛擬的線和三角形。再進行光珊化(在光珊化的時候,把頂點連接起來形成直線,或者填充多邊形的時候,需要考慮直線和多邊形的直線寬度、點的大小、漸變算法以及是否使用支持抗鋸齒處理的覆蓋算法。最終的每個像素點,都具有各自的顏色和深度值)。
將光珊化的結果傳入 PS,進行最終的顏色計算。
然后,這所謂最終的結果在被實際存儲到繪制 buffer 之前,還需要進行一系列的操作。這些操作可能會修改甚至丟棄這些像素點。
這些操作主要為 Alpha Test、Depth/Stencil Test、Blend、Dither。
Alpha Test 采用一種很霸道極端的機制,只要一個像素的 alpha 不滿足條件, 那么它就會被 fragment shader 舍棄,被舍棄的 fragments 不會對后面的各種 Tests 產生影響;否則,就會按正常方式繼續下面的檢驗。Alpha Test 產生的效果也很極端,要么完全透明,即看不到,要么完全不透明。
Depth/stencil test 比較容易理解。由於我們繪制的是 3D 圖形,那么坐標為 XYZ,而 Z 一般就是深度值,OpenGL ES 可以對深度測試進行設定,比如設定深度值大的被拋棄,那么假如繪制 buffer 上某個像素點的深度值為 0,而 PS 輸出的 像素點的深度值為 1,那么 PS 輸出的像素點就被拋棄了。而 stencil 測試更加簡單,其又被稱為蒙版測試,比如可以通過 OpenGL ES 設定不同 stencil 值的配拋棄, 那么假如繪制 buffer 上某個像素點的 stencil 值為 0,而 PS 輸出的像素點的 stencil 值為 1,那么 PS 輸出的像素點就被拋棄了。
既然說到了 Depth/stencil,那么就在這里說一下繪制 buffer 到底有多大,存 儲了多少信息。按照我們剛才的說法,手機可以支持一百萬個像素,那么生成的 繪制 buffer 就需要存儲這一百萬個像素所包含的信息,而每個像素包含的信息, 與手機配置有關,假如手機支持 Depth/stencil。那么通過 EGL 獲取的繪制 buffer 中,每個像素點就包含了 RGBA 的顏色值,depth 值和 stencil 值,其中 RGBA 每個分量一般占據 8 位,也就是 8bit,也就是 1byte,而 depth 大多數占 24 位,stencil 占 8 位。所以每個像素占 64bit,也就是 8byte。那么 iphone6 plus 的繪制 buffer 的尺寸為 1920×1080×8=16588800byte=16200KB=15.8MB。
下面還有 blend,通過 OpenGL ES 可以設置 blend 混合模式。由於繪制 buffer 中原本每個像素點已經有顏色了,那么 PS 輸出的顏色與繪制 buffer 中的顏色如何混合,生成新的顏色存儲在繪制 buffer 中,就是通過 blend 來進行設定。
最后的 dither,dither 是一種圖像處理技術,是故意造成的噪音,用以隨機化量化誤差,阻止大幅度拉升圖像時,導致的像 banding(色帶)這樣的問題。也 是通過OpenGL ES 可以開啟或者關閉。
經過了這一系列的運算和測試,也就得到了最終的像素點信息,將其存儲到繪制 buffer 上之后,OpenGL ES 的 pipeline 也就結束了。
整個pipeline中,縱向按照流水線作業,橫線按照獨立作業,多級並行、提高渲染性能。
參考鏈接:
