導語:

對於開發者來說,學習OpenGL或者其他圖形API都不是一件容易的事情。即使是一些對OpenGL有一些經驗的開發者,往往也未必對OpenGL有完整、全面的理解。市面上的OpenGL文章往往零碎不成體系,而教材又十分龐大、晦澀難懂還穿插著各種API的介紹。

因此筆者希望通過多年的圖形開發經驗,結合對OpenGL的理解,對OpenGL整體的知識做一個梳理,剔除掉特別複雜又較少使用的部分。遺留下來常見和易於理解的部分,同時也盡量在介紹的時候兼顧易懂性和嚴謹性。

希望對即將或正在學習OpenGL的開發者,提供一定的幫助。

1、簡介

OpenGL(Open Graphics Library)是一個跨編程語言、跨平臺的編程圖形程序介面,它將計算機的資源抽象稱為一個個OpenGL的對象,對這些資源的操作抽象為一個個的OpenGL指令。

OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 三維圖形 API 的子集,針對手機、PDA和遊戲主機等嵌入式設備而設計,去除了許多不必要和性能較低的API介面。

本文介紹的OpenGL版本是基於OpenGL ES 3.0的。這也是目前覆蓋率最高的OpenGL版本,被廣泛運用在各種終端設備上。

2、OpenGL上下文(Context)

在應用程序調用任何OpenGL的指令之前,需要安排首先創建一個OpenGL的上下文。這個上下文是一個非常龐大的狀態機,保存了OpenGL中的各種狀態,這也是OpenGL指令執行的基礎。

OpenGL的函數不管在哪個語言中,都是類似C語言一樣的面向過程的函數,本質上都是對OpenGL上下文這個龐大的狀態機中的某個狀態或者對象進行操作,當然你得首先把這個對象設置為當前對象。因此,通過對OpenGL指令的封裝,是可以將OpenGL的相關調用封裝成為一個面向對象的圖形API的。

由於OpenGL上下文是一個巨大的狀態機,切換上下文往往會產生較大的開銷,但是不同的繪製模塊,可能需要使用完全獨立的狀態管理。因此,可以在應用程序中分別創建多個不同的上下文,在不同線程中使用不同的上下文,上下文之間共享紋理、緩衝區等資源。這樣的方案,會比反覆切換上下文,或者大量修改渲染狀態,更加合理高效的。

3、幀緩衝區(FrameBuffer)

OpenGL是圖形API,因此可以說所有的運算和結果最終都是需要通過圖像進行輸出的。那麼繪圖必然就需要有一塊畫板,而幀緩衝區就是OpenGL中的畫板。但是特別需要注意的是,幀緩衝區不是常規意義緩衝區(就像鯨魚不是魚一樣),它並不是實際存儲數據的對象,類似畫畫的時候,需要在畫板上放一塊畫布,才能實際在畫布上進行繪畫,這些畫布可以是紋理(Texture)或者是渲染緩衝區(RenderBuffer),而放置這些畫布的位置被稱為幀緩衝區的附著(Attachment)。

3.1、附著(Attachment)

附著可以理解為畫板上的夾子,夾住了哪個畫布,就往對應畫布上輸出數據。

在幀緩衝區中可以附著3種類型的附著,顏色附著(ColorAttachment),深度附著(DepthAttachment),模板附著(StencilAttachment)。這三種附著對應的存儲區域也被稱為顏色緩衝區(ColorBuffer),深度緩衝區(DepthBuffer),模板緩衝區(StencilBuffer)。

顏色附著輸出繪製圖像的顏色數據,也就是平時常見的圖像的RGBA數據。如果使用了多渲染目標(Multiple Render Targets)技術,那麼顏色附著的數量可能會大於一。

深度附著輸出繪製圖像的深度數據,深度數據主要在3D渲染中使用,一般用於判斷物體的遠近來實現遮擋的效果。

模板附著輸出模板數據,模板數據是渲染中較為高級的用法,一般用於渲染時進行像素級別的剔除和遮擋效果,常見的應用場景比如三維物體的描邊。

4、紋理(Texture)和渲染緩衝區(RenderBuffer)

前面已經說過,幀緩衝區並不是實際存儲數據的地方,實際存儲圖像數據數據的對象就是紋理和渲染緩衝區。

他們三者的關係是這樣的,紋理或渲染緩衝區作為幀緩衝區的附著。

那麼,紋理和渲染緩衝區又有什麼關係和區別呢?

紋理和渲染緩衝區同樣是存儲圖像的對象。一般來說,渲染緩衝區對應操作系統提供的窗口,而紋理代表列離屏的圖像存儲區域。因此,渲染緩衝區都是2D的圖像類型,而紋理一般有立方體紋理,1D、2D、3D紋理等類型,同時紋理還額外支持了mipmap等其他特性。

值得注意的是,一般來說渲染緩衝區和紋理不能同時掛載在同一個幀緩衝區上。

5、頂點數組(VertexArray)和頂點緩衝區(VertexBuffer)

準備好了畫布之後,就要開始畫圖了。畫圖一般是先畫好圖像的骨架,然後再往骨架裡面填充顏色,這對於OpenGL也是一樣的。頂點數據就是要畫的圖像的骨架,和現實中不同的是,OpenGL中的圖像都是由圖元組成。在OpenGL ES中,有3種類型的圖元:點、線、三角形。那這些頂點數據最終是存儲在哪裡的呢?開發者可以選擇設定函數指針,在調用繪製方法的時候,直接由內存傳入頂點數據,也就是說這部分數據之前是存儲在內存當中的,被稱為頂點數組。而性能更高的做法是,提前分配一塊顯存,將頂點數據預先傳入到顯存當中。這部分的顯存,就被稱為頂點緩衝區。

6、索引數組(ElementArray)和索引緩衝區(ElementBuffer)

其實我覺得索引在OpenGL叫Element確實有點不夠貼切,而在DirectX中叫做IndexBuffer更加合適一些。

索引數據的目的主要是為了實現頂點的復用,在繪製圖像時,總是會有一些頂點被多個圖元共享,而反覆對這個頂點進行運算常常是沒有必要的(也有某些特殊場景需要)。因此對通過索引數據,指示OpenGL繪製頂點的順序,不但能防止頂點的重複運算,也能在不修改頂點數據的情況下,一定程度的重新組合圖像。

和頂點數據一樣,索引數據也可以以索引數組的形式存儲在內存當中,調用繪製函數時傳入;或者提前分配一塊顯存,將索引數據存儲在這塊顯存當中,這塊顯存就被稱為索引緩衝區。同樣的,使用緩衝區的方式,性能一般會比直接使用索引數組的方式更加高效。

OpenGL ES提供了2種主要的繪製方法:glDrawArrays和glDrawElements。前者對應的就是沒有索引數據的情況,後者對應的是有索引數據的情況。

7、著色器程序(Shader)

在固定渲染管線時代,這一步並不是必須的。而是由內置的一段包含了光照、坐標變換、裁剪等等諸多功能的固定shader程序來完成。而可自定義shader,可以說是現代圖形API最重要的能力了,沒有之一。可以說,shader提供對圖形運算的精細操作,帶來了各式各樣的處理能力,極度的豐富了圖形API所能實現的效果。

OpenGL和其他主流的圖形API早在好幾年前,就全面的將固定渲染管線架構變為了可編程渲染管線。因此,OpenGL在實際調用繪製函數之前,還需要指定一個由shader編譯成的著色器程序。

常見的著色器主要有頂點著色器(VertexShader),片段著色器(FragmentShader)/像素著色器(PixelShader),幾何著色器(GeometryShader),曲面細分著色器(TessellationShader)。片段著色器和像素著色器只是在OpenGL和DX中的不同叫法而已。可惜的是,直到OpenGL ES 3.0,依然只支持了頂點著色器和片段著色器這兩個最基礎的著色器。

OpenGL在處理shader時,和其他編譯器一樣。通過編譯、鏈接等步驟,生成了著色器程序(glProgram),著色器程序同時包含了頂點著色器和片段著色器的運算邏輯。在OpenGL進行繪製的時候,首先由頂點著色器對傳入的頂點數據進行運算。再通過圖元裝配,將頂點轉換為圖元。然後進行光柵化,將圖元這種矢量圖形,轉換為柵格化數據。最後,將柵格化數據傳入片段著色器中進行運算。片段著色器會對柵格化數據中的每一個像素進行運算,並決定像素的顏色,也可以在這個階段將某些像素丟棄。

其中像素的顏色可以是具體的數值或者是由某種演算法計算而來的。如果圖元有紋理,就必須用紋理來產生圖元的二維渲染圖象上每個像素的顏色。對於圖元在二維屏幕上圖象的每個像素來說,都必須從紋理中獲得一個顏色值。我們把這一過程稱為紋理過濾(texture filtering),紋理過濾根據不同的過濾方式會由一個或多個像素確定最終獲得的顏色。表示這個像素位置的數據被稱為紋理坐標(TextureCoordinate)而尋找這個紋理中對應像素位置的方法被稱為紋理定址方式或者紋理環繞方式(TextureWrap)。

最終,沒有被丟棄的像素,下一步會進入測試階段。通過了深度測試和模板測試,會和幀緩衝區上的顏色附著(FrameBuffer上的ColorAttachment)上的顏色進行混合,決定最終留在畫布上的顏色是什麼。

7.1、頂點著色器(VertexShader)

頂點著色器是OpenGL中用於計算頂點屬性的程序。頂點著色器是逐頂點運算的程序,也就是說每個頂點數據都會執行一次頂點著色器,當然這是並行的,並且頂點著色器運算過程中無法訪問其他頂點的數據。

頂點著色器的數據輸入主要有兩種,統一變數(Uniform)、頂點屬性(VertexAttribute)。統一變數在所有頂點運算中是一樣的,而頂點屬性則是從外部輸入的頂點數據中獲取,一般在每個頂點運算中都是不同的。

一般來說典型的需要計算的頂點屬性主要包括頂點坐標變換、逐頂點光照運算等等。頂點坐標由自身坐標系轉換到歸一化坐標系的運算,就是在這裡發生的。

同時頂點著色器的輸出結果,也會作為片段著色器的輸入。

7.2、片段著色器(FragmentShader)

片段著色器是OpenGL中用於計算片段(像素)顏色的程序。片段做社區是逐像素運算的程序,也就是說每個像素都會執行一次片段著色器,當然也是並行的。

片段著色器的的數據輸入主要有三種種,統一變數(Uniform)、頂點著色器輸入變數(也被稱為可變變數varying)、採樣器(Sampler)。統一變數的值,在同個OpenGL著色器程序中的頂點著色器和片段著色器中是一致的。頂點著色器輸入變數在每個像素運算中則一般是不同的,它的值由組成圖元的頂點的頂點著色器運算輸出的值,根據像素位置進行插值的結果而決定。採樣器則是用於從設定好的紋理中,獲取紋理的像素顏色的。

在片段著色器中允許丟棄像素,而使得像素不參與後續的運算。

8、逐片段操作(Per-Fragment Operation)

8.1、測試(Test)

在著色器程序完成之後,我們得到了像素數據。這些數據必須要通過測試才能最終繪製到畫布,也就是幀緩衝上的顏色附著上。

測試主要可以分為像素所有者測試(PixelOwnershipTest)、裁剪測試(ScissorTest)、模板測試(StencilTest)和深度測試(DepthTest),執行的順序也是按照這個順序進行執行。

最開始進行的測試是像素所有者測試,主要是剔除不屬於當前程序的像素運算。

之後裁剪測試,主要是剔除窗口區域之外的像素。

這兩個測試都是由OpenGL內部實現的,無需開發者幹預,因此不再進行贅述。

深度測試,主要是通過對像素的運算出來的深度,也就是像素離屏幕的距離進行對比,根據OpenGL設定好的深度測試程序,決定是否最終渲染到畫布上。一般默認的程序是將離屏幕較近的像素保留,而將離屏幕較遠的像素丟棄。如果像素最終被渲染到畫布上,根據設定好的OpenGL深度覆寫狀態,可能會更新幀緩衝區上深度附著的值,方便進行下一次的比較。

模板測試和深度測試的執行原理一致,但是執行的順序是在深度測試之前的,放在後面 主要是比深度測試更加難以理解一些,初學者可以暫時跳過這個部分。模板測試同樣也是通過模板測試程序去決定最終的像素是否丟棄,同樣也是根據OpenGL的模板覆寫狀態決定是否更新像素的模板值。模板測試給開發者提供了高性能的裁剪方案,三維物體的描邊技術,就是模板測試典型的用處之一。

8.2、混合(Blending)

在測試階段之後,如果像素依然沒有被剔除,那麼像素的顏色將會和幀緩衝區中顏色附著上的顏色進行混合,混合的演算法可以通過OpenGL的函數進行指定。但是OpenGL提供的混合演算法是有限的,如果需要更加複雜的混合演算法,一般可以通過像素著色器進行實現,當然性能會比原生的混合演算法差一些。

8.3、抖動(Dithering)

在混合階段過後,根據OpenGL的狀態設置,會決定是否有抖動這個階段。

抖動是一種針對對於可用顏色較少的系統,可以以犧牲解析度為代價,通過顏色值的抖動來增加可用顏色數量的技術。抖動操作是和硬體相關的,允許程序員所做的操作就只有打開或關閉抖動操作。實際上,若機器的解析度已經相當高,激活抖動操作根本就沒有任何意義。默認情況下,抖動是激活的。

9、渲染到紋理

有些OpenGL程序並不希望渲染出來的圖像立即顯示在屏幕上,而是需要多次渲染。可能其中一次渲染的結果是下次渲染的輸入。因此,如果幀緩衝區的顏色附著設置為一張紋理,那麼渲染完成之後,可以重新構造新的幀緩衝區,並將上次渲染出來的紋理作為輸入,重新進行前面所述的流程。

10、渲染上屏/交換緩衝區(SwapBuffer)

前面已經提過,渲染緩衝區一般映射的是系統的資源比如窗口。如果將圖像直接渲染到窗口對應的渲染緩衝區,則可以將圖像顯示到屏幕上。

但是,值得注意的是,如果每個窗口只有一個緩衝區,那麼在繪製過程中屏幕進行了刷新,窗口可能顯示出不完整的圖像。

為瞭解決這個問題,常規的OpenGL程序至少都會有兩個緩衝區。顯示在屏幕上的稱為屏幕緩衝區,沒有顯示的稱為離屏緩衝區。在一個緩衝區渲染完成之後,通過將屏幕緩衝區和離屏緩衝區交換,實現圖像在屏幕上的顯示。

由於顯示器的刷新一般是逐行進行的,因此為了防止交換緩衝區的時候屏幕上下區域的圖像分屬於兩個不同的幀,因此交換一般會等待顯示器刷新完成的信號,在顯示器兩次刷新的間隔中進行交換,這個信號就被稱為垂直同步信號,這個技術被稱為垂直同步。

使用了雙緩衝區和垂直同步技術之後,由於總是要等待緩衝區交換之後再進行下一幀的渲染,使得幀率無法完全達到硬體允許的最高水平。為瞭解決這個問題,引入了三緩衝區技術,在等待垂直同步時,來回交替渲染兩個離屏的緩衝區,而垂直同步發生時,屏幕緩衝區和最近渲染完成的離屏緩衝區交換,實現充分利用硬體性能的目的。

推薦閱讀:

相關文章