1. 程式人生 > >【OpenGL】理解一些基本問題

【OpenGL】理解一些基本問題

寫在前面

啦啦啦,搞了很久的Unity Shaders,越學越覺得基礎知識很重要。學Unity Shader的時候,總會想,shader到底是什麼呢?shader的pipeline是什麼呢?它們是怎麼工作的?有哪些限制?等等問題。但這些問題,Unity是不負責告訴你的。它專注於how,而不是what和why。想要深入理解一些問題,感覺還是要從GL或者DX學起。後面會學習GL龍書第八版~當然Unity我也不會放棄的。

這篇文章旨在回答一些基本問題。We always rant about them...

什麼是OpenGL

這個問題很簡單,它就是應用程式介面,也就是API,用於訪問圖形硬體中的可程式設計特性。OpenGL和DX相比有一個很大的特點就是跨平臺的特性。換句話說,它是不依賴硬體的介面,可以執行在各種不同型別的圖形硬體系統上,甚至完全是一個軟體(而沒有圖形硬體)。

OpenGL是一種客戶端-伺服器(client-server)型別的系統。我們編寫的程式就是一個客戶端,而我們的計算機圖形硬體製造商提供的OpenG的實現就是伺服器。在一些OpenGL的實現裡(例如一些和X Window System相關的應用),客戶端和伺服器可能會在不同的機器上執行,中間用網路連線。在這種情況下,客戶端可以發起OpenGL命令,然後轉換成視窗系統特定的協議,再發送給伺服器,最終在伺服器上執行OpenGL進行影象顯示。

為什麼OpenGL不提供視窗操作

這個問題我經常會問。。。為什麼寫個GL還要用這麼多第三方庫!連個視窗都不能自己畫嗎!這其實不能怪OpenGL,這正是它的優點——跨平臺的特點造成的。因為它可以不依賴硬體和系統,因此就不會包含執行視窗任務的函式,或者處理使用者輸入等。這些函式是由我們使用的應用或系統來提供。

為什麼OpenGL沒有讀取三維模型或者圖片的函式

我以前經常抱怨,發展這麼多年的OpenGL,怎麼連讀取三維模型這麼簡單的畫圖需求都不提供呢!!!好吧,這也是它的跨平臺特性造成的。和上一點一樣,這些操作是和系統儲存格式密切相關的,OpenGL不管的~我們必須從點、線、三角形和patches這樣的幾何圖元集合中自己構建三維物件。

什麼是Shader

這是一類在圖形硬體上執行的特殊函式。我們可以理解成,Shader是一些為圖形處理單元(GPU)編譯的小程式。OpenGL包含了編譯工具來把我們編寫的Shader原始碼編譯成可以在GPU上執行的程式碼。在OpenGL中,我們可以使用四種shader階段。最常見的就是vertex shaders——它們可以處理頂點資料;以及fragment shaders,它們處理光柵化後生成的fragments。

vertex shaders和fragment shaders是每個OpenGL程式必不可少的部分。

Shader有什麼用

請直接看下一節~

OpenGL的渲染流水線(Rendering Pipeline)

渲染流水線,就是一系列有序的處理階段的序列,用於把我們應用中的資料轉化到OpenGL生成一個最終的影象。下圖是OpenGL4.3使用的流水線。(跟早期的相差很大)(左圖來源:OpenGL Wiki

      

上圖中,藍色的方塊表示是可程式設計的shader階段。

OpenGL從我們提供的幾何資料(頂點和幾何圖元)出發,首先使用了一系列shader階段來處理它:vertex shading,tessellation shading(它本身就包含了兩種shaders),接著是geometry shading,然後再傳遞給光柵化程式(rasterizer);光柵化程式將會為每個在裁剪區域(clipping region)內部的圖元生成fragments,然後再為每個fragment執行一個fragment shader。

如你所見,shaders真是無處不在啊!不是所有的階段都是需要的。如上面所說,只有vertex和fragment shaders是我們必須實現的。Tessellation和geometry shaders都是可選的。

下面,我們對每個階段進行更深入地解釋。下面的內容難度係數五顆星(對新手。)!但是,請堅持看下去!它們很重要!

Vertex Specification

準備頂點陣列資料(vertex array data)

即進行頂點規格定義。應用將會建立一個有序的頂點列表,然後傳送給OpenGL。這些頂點定義了圖元的邊界。圖元是基本的繪製形狀,如點、線、三角形。這些頂點列表是如何被組織成一個個圖元的會在後面的階段裡進行處理。

一個頂點的資料就是一系列頂點屬性(vertex attributes)。每一個attribute都是一個數據的集合,用於後面的階段進行處理。雖然這些attributes定義了一個頂點,但沒有要求說一個頂點的attributes集合中必須包含了位置和法線資訊。實際上,attribute資料是完全任意的,它們僅僅是“資料”,在這個準備階段不會有人在意你傳遞的到底是什麼attribute,它們的真正含義會在頂點處理階段進行解讀。

OpenGL要求所有的資料都必須儲存在快取物件中(buffer objects)。快取物件是OpenGL管理的一些記憶體空間。“想要讓我辦事,請先把你的東西放到我的地盤!”把資料放到這些快取裡有很多方法,但最常見的是使用glBufferData()來實現。當然這裡面還有一些其他步驟,我們後面會講到。

向OpenGL傳送資料

在我們定義了頂點相關資訊後,我們可以通過呼叫OpenGL的繪圖操作來要求按一個個幾何圖元繪製到螢幕上。這些操作例如有glDrawArrays()。我們後面會講到。這個繪製的過程意味著我們把頂點資料傳遞給OpenGL伺服器。

Vertex Processing

對於通過繪製命令繪製的每一個頂點,OpenGL將呼叫一個vertex shader來處理關於這個頂點的相關資訊。Vertex shader接受從上個階段傳遞來的attribute作為輸入,然後把每一個輸入頂點轉換成一個輸出頂點(這個關係是1對1的,不會多也不會少)。和輸入的頂點資訊不同,輸出的頂點資料有一些必需的要求——vertex shader必須填充一個位置資訊。

Vertex shaders的複雜性可以變化非常大,有的很簡單,就是僅僅複製資料然後傳遞給下一個流水線階段,我們稱這種為pass-through shader;有的很複雜,會執行很多操作來計算頂點的螢幕位置(通常使用變換矩陣來完成,後面會講到),還可能會進行光照計算來計算頂點的顏色,或者其他技術。

通常,一個應用會包含多個vertex shader,但同一時間只有一個會被啟用(active)。

Primitive Assembly

Primitive assembly就是把vertex shader輸出的頂點資料集合在一起,並把它組合成一個圖元的過程。使用者渲染的圖元的型別決定了這個過程是如何工作的。

這個過程的輸出是一個有序的簡單圖元(點、線或者三角形)序列。

Tessellation Shading

Tessellation,讀 泰斯類什,可以翻譯成曲面細分。在vertex shader處理了每一個頂點的相關資訊後,如果tessellation shader階段被啟用的話,它就會繼續處理這些資料。在後面我們會看到tessellation使用patchs來描述一個物件的形狀,並且允許細化(tessellate)相對簡單的patch集合,來增加幾何圖元的數量,提高模型的平滑度和真實度。Tessellation Shading階段可以使用兩個shaders來控制patch資料,以及中間一個固定函式的tessellator來生成最終的形狀。

更多內容可以參見這篇文章(雖然是DirectX的。。。)

Geometry Shading

這一階段允許在光柵化之前處理單獨的幾何圖元,包括建立新的圖元。這一階段同樣是可選的,但是會很有用!後面會講到。

Geometry shader會處理每一個輸入的圖元,然後返回0個或更多的輸出圖元。它的輸入是primitive assembly的輸出圖元。因此如果我們按triangle strip看待一個圖元,那麼geometry shader看到的就會使一系列三角形。

然而也有一些輸入的圖元型別是專門為geometry shaders定義的。這些相鄰的圖元可以讓GS瞭解關於相鄰頂點的資訊。

GS的輸出可以是0個多更多的簡單圖元。GS可以移除圖元,或者根據一個輸入圖元來輸出更多的圖元來細分(tessellate)它們。GS甚至可以改變圖元的型別,比如把點圖超程式設計三角形,把線圖元變成點。

Transform Feedback

Geometry shader或者primitive assembly的輸出會被寫入一系列的快取物件。這被稱為transform feedback模式。它允許我們通過vertex和geometry shaders來變換資料,然後再等待後續使用。

通過捨棄光柵化的結果,流水線可以在這步就停止了。這允許transform feedback成為渲染的唯一輸出。

裁剪(Clipping)和剔除(Culling)

然後就進入圖元裁剪和適當的剔除階段了。

裁剪以為著,如果有圖元處於視野的邊界上,即一部分在內部一部分在外部,那麼它就會被裁剪成一些小的圖元。而且,vertex shader可以在空間內定義一些裁剪平面,這些裁剪平面又會引起額外的裁剪。

三角面的剔除同樣在這一階段完成。處於視野範圍以外,或者在裁剪平面的邊界內部的圖元,都會被提出。

光柵化(Rasterization)

在裁剪完成後,更新後的圖元就會被髮送給光柵化程式去生成fragments。那麼什麼是fragment呢?一個fragment可以看成是一個“候選畫素”。這類畫素在幀快取中的一塊區域中。一個fragment仍可以被拒絕(reject),並且永遠不會更新它的相關畫素位置。

Wiki上把fragment成為是一個狀態的集合,用於計算一個畫素的最終資料。一個fragment的狀態包含了它在螢幕空間的位置資訊,樣本覆蓋(sample coverage,如果開啟了multisampling的話),以及一些其他由vertex或者geometry shader輸出的資料。這些資料集合是通過該fragment對應的頂點資料進行插值計算而得的。這個插值計算是由輸出這些資料的shader定義的。

處理fragments是後面兩個階段的任務——fragment shading和per-fragment操作。

Fragment Processing

最後一個我們可以程式設計控制顏色的階段就是fragment shading。在這個階段,我們使用一個shader來決定該fragment的最終顏色(其實下個階段,per-fragment操作仍可以進行最後的顏色修改)和它的深度值(depth value)。在fragment shaders裡我們可以進行非常強大的紋理對映的工作。如果一個fragment shader認為某個fragment不應該繪製出來,它還可以終結一個fragment的處理過程。這個過程稱為fragment discard。

一個fragment shader會輸出一個顏色列表、一個深度值和一個stencil值。Fragment shaders不可以為一個fragment設定stencil,但是它們可以控制顏色和深度值。

我們可以想來區分vertex shading(包括tessellation和geometry shading)和fragment shading:vertex shading決定了一個圖元在螢幕上的位置,而fragment shading使用這些資訊來決定該fragment的顏色。

Per-Fragment操作

從fragment處理器輸出的fragment資料會再通過一系列的步驟。

第一個步驟就是各種剔除檢驗(culling tests)。如果開啟了stencil test,如果一個fragment沒有通過檢驗它就會被剔除,而不會寫入到幀快取中;如果開啟了depth test,如果一個fragment沒有通過檢驗它就會被剔除,而不會寫入到幀快取中。只要沒有通過任何一個檢驗,fragments都會被剔除,不會新增到幀快取中。

如果一個fragment成功通過了所有的檢測,它就會直接寫入幀快取中,更新它的畫素顏色(也可能是深度值)。如果blending被開啟了,該fragment的顏色會和當前的畫素顏色進行混合去產生一個新的顏色,再寫入幀快取中。

最後,fragment資料被寫入幀快取中。Masking operation允許使用者避免寫入特定的值。寫顏色、深度和stencil都可以被mask成on或者off;單獨的顏色通道也可以。