最近學習CG,總是有點不懂的地方,回頭想想,覺得應該是渲染流水線方面不是特別透徹的原因,所以,學習了《CG教程_可編程實時圖形權威指南》以及《GPU編程與CG語言之陽春白雪下里巴人》中關於渲染流水線方面的知識,再參入一部分網上博客的內容。有所收獲,所以來與大家分享。
本文的主線:渲染流水線的設計模式》渲染流水線的分類及其意義》渲染流水線的具體流程。
渲染流水線的設計
為了解決D3D或者OpenGL對不同硬件廠商的支持,解決移植性的問題,可以通過將加速卡功能抽象出來,統一定義接口的形式來實現。於是,人們采用了典型的分層模式(參閱:設計模式),將一套應用程序分為3個層次:應用程序層 -> 硬件抽象層 -> 硬件層。如下圖

圖 1
應用層就是游戲和應用軟件開發人員的開發主體,他們調用統一的加速卡API來進行上層開發,而不用考慮移植性問題。
硬件抽象層則抽象出硬件的加速功能,進行有利於應用層開發的封裝,並向應用層開放API。
硬件層將硬件驅動提供給抽象層,以實現抽象層加速功能的有效性。
這個結構有兩個好處:1. 有效的將游戲和應用程序 與硬件加速卡隔離開,這就很好的提升了程序的移植能力。2. 開發人員的知識復用率得到提高,從而降低了這類軟件的開發。
渲染流水線的分類及其意義
首先,我們需要了解一個概念:Shader,中文名,着色器。着色器其實就是一段在GPU運行的程序。我們平時的程序,是在CPU運行。由於GPU的硬件設計結構與CPU有着很大的不同,所以GPU需要一些新的編程語言。目前,微軟提供了 HLSL(High Level Shading Language),通過Direct3D圖形軟件庫來寫Shader。OpenGL提供了GLSL(OpenGL Shading Language)來寫Shader程序。NIVIDIA希望顯卡的程序開發獨立於DX和GL的圖形軟件庫,與微軟共同研發了CG語言(C for graphics)。因為它是在HLSL的基礎上進行開發的,所以他的語法跟HLSL非常相似。並且,CG編寫的Shader可以編譯到D3D和GL能適應的環境。
渲染流水線分為兩種,其中一種為可編程渲染流水線。另外一種為固定渲染流水線。(也稱可編程管線或固定管線,管線就是流水線的意思)。渲染流水線可否編程,取決於程序猿能否在定點着色器以及片段着色器上進行編碼。而現在的渲染流水線,基本都是可編程的,當然,它們也支持固定渲染流水線的功能。學習這個,我想大家心中一定有個疑問?可編程渲染流水線存在的意義是什么?
這個要從現實與虛擬的差距說起,我們知道現實的世界五彩繽紛,而我們對於現實世界的模擬,實際上就是對現實世界里面各種存在的事物進行一一的模擬。而默認的着色器,對於模擬五彩繽紛的世界就捉襟見肘了。而要讓編寫默認着色器的程序員把所有的情況一一列舉,不僅會使得SDK非常龐大,而且也不太可能辦到。所以,有必要讓渲染流水線可編程,以滿足用戶無窮的胃口。打個比方,畫家在畫一幅畫的時候,要用的色彩或許有上萬種,我們無法為他提供所有可能用到的色彩,只能讓畫家可以通過自己的喜好混合不同的顏色。你可以把着色器理解為這里面的各種顏色,自己寫的着色器就是自己調的顏色。
渲染流水線的具體流程
前面說了,渲染流水線包括了應用程序層 -> 硬件抽象層 -> 硬件層。我們首先講解一下應用程序層以及這一層能夠做的事。
1. 應用程序層
應用程序層主要與內存,CPU打交道,諸如碰撞檢測,場景圖監理,視錐裁剪等經典算法在此階段執行。在階段的末端,幾何體的數據(頂點坐標,法向量,紋理坐標,紋理)等通過數據總線傳送到圖形硬件(時間瓶頸)。
這里面講到了兩個東西:經典算法與數據總線。為什么特意提一下呢?這需要我們先了解一下后續的一些知識:GPU會對我們一些不會進行繪制的物體進行剔除,比如物體的背面或者超出視域體范圍的物體。假如我們把游戲里面所有的物體全部拋給GPU,那么GPU的負擔就會特別重。所以這部分的優化做不好,那么即使有很勁爆的 GPU,也難以渲染出一個絢麗的游戲世界。而數據總線,每次能夠傳輸的數據量是有限的,並且數據總線是整台計算機共用的,在我們的游戲里面,數據總線把游戲里面的數據從內存傳送到了GPU。假如我們對它好不客氣,那么不僅我們的游戲運行的時候會經常出現卡頓的現象,我們整台電腦運行的速度也會下降。
2. 硬件抽象層
在這一層,我們目前使用的是DirectX與OpenGL。對於這一部分,主要是一些API等的調用。這方面不是我們今天的重點,所以直接到重點部分。
3. 硬件層
硬件層在渲染流水線中最為復雜,也最為重要。前面已經提到,可編程渲染流水線與固定渲染流水線的區別在於是否對着色器進行編程。
首先我們先了解固定渲染流水線它主要分為以下幾個階段:頂點變換->圖元轉配與光柵化->片段紋理映射和着色->光柵化操作。如下圖

圖 2
下面,我們再看一下可編程渲染流水線硬件層的流程圖。
圖 3
對比上面的兩個圖我們發現,在可編程渲染流水線中,固定渲染流水線中的頂點變換與片段紋理映射和着色被分離出來,作為可編程頂點處理器與可編程片段處理器。而如前面所述,假如我們使用DirectX或者OpenGL自帶着色程序,那么兩條流水線其實是一樣的。所以,下面我們將對可編程渲染流水線進行講解。
下面,我們主要從可編程頂點處理器,圖元裝配,光柵化和插值,可編程片段處理器,光柵化操作來講解硬件層的渲染流水線。
1) 可編程頂點處理器(下面的流水線都是指硬件層部分)
頂點變換:在固定渲染流水線或者可編程渲染流水線中這都是第一個處理階段。這個階段對頂點進行了一系列的數學變換。包括了世界變換,取景變換,投影變換,視口變換。另外,貼圖紋理坐標的產生,照亮頂點以及決定頂點的顏色,都在這個階段進行。
在這里,世界變換,取景變換,視口變換在這里我不多加贅述,但是投影變換還是很想多說幾句,投影和裁剪到底哪一個先進行。下面我們先介紹幾個概念。
投影:把一個物體從n維變換到n-1維的過程稱為投影。所以我們三維的世界轉換到2D的屏幕上的過程也叫做投影。
視域體裁剪:在以攝像機為中心,由視線方向,視角和遠近平面共同構成的一個梯形體,在梯形體內的物體可見,梯形體外的物體不可見。裁剪這部分不可見物體的過程稱為視域體裁剪。
在下圖中,梯形體為三維空間中的一部分,超出梯形體部分的物體將會被剔除。而在視域體中的物體,最終會形成一幅圖像,展示在近平面上。這里默認裁剪平面與投影片面重合。

圖 4
投影矩陣:將3D世界里頂點變換到投影平面上的矩陣。即一個三維的頂點與這個矩陣相乘,能夠將空間中的頂點變換到投影平面上(最終的頂點依然具有4個維度,第四個維度只是用來區分3維的向量是點還是普通的數學向量)。具體,大家可以參考下面的鏈接http://blog.csdn.net/popy007/article/details/1797121#comments
回歸我們的問題,到底是投影先還是裁剪先呢?
實際上,當我們把空間中的點變換到投影平面的時候,假如一個點不在視域體的范圍內,那么變換后的點的坐標不會在(-1,-1,-1)到(1,1,1)的范圍內(OpenGL以這個為標准),說明這個點是被裁剪的頂點。而假如變換后的坐標在這個范圍內,其(x,y)坐標就是對應的視口坐標系的坐標了。所以,實際上我們做投影變換的時候其實也同時為裁剪做了准備,也就是裁剪跟投影其實是同步的,只是裁剪的周期會比較長一點。
這里其實有個問題剛好被我跳過了,就是為什么不直接在3D的空間里面進行裁剪,要轉換到投影屏幕上才進行裁剪。這里涉及到實現的難度的問題。視域體是一個3D的梯形,我們要在3D的空間里對頂點進行剔除,是一件非常難以實現的事,所以才將頂點變換到投影平面上。
下面讓我們看一下可編程頂點着色器的工作流程。

圖 5
2) 圖元裝配
圖元:實際上就是點,線,面。
圖元裝配階段的工作:根據伴隨頂點序列的集合圖元分類信息把頂點裝配幾何圖元。產生一系列的三角形,線段和點。(之前的流水線只是對頂點進行處理)。
挑選:光柵器根據多邊形的朝前或朝后來丟棄一些多邊形。在此階段進行。(CG教程P13)
多邊形經過挑選后 ,進入光柵化插值。
3) 光柵化插值
首先解釋一下,為什么要用光柵化插值而不用光柵化。我學習渲染流水線主要是通過上面的那個流程圖來學習的,而他的作者將光柵化分為了光柵化插值與光柵化操作,所以我也繼續沿用了他的方法。下面先介紹幾個概念。
光柵化:一個決定哪些像素被幾何圖元覆蓋的過程。光柵化的結果是像素位置的集合和片段位置的集合。(CG教程_P13)
片段:是更新一個特定像素潛在需要的一個狀態。
術語片段是因為光柵化會把每個幾何圖元,例如三角形,所覆蓋的像素分解成像素大小的片段。一個片段有一個與之相關聯的像素位置,深度值和經過插值的參數,例如顏色,第二(反射)顏色和一個或多個紋理坐標集。這些各種各樣的經過插值的參數是來自變換過的頂點,這些頂點組成了某個用來生成片段的幾何圖元。如果一個片段通過了各種各樣的光柵化測試,這個片段將被用於跟新幀緩存中的像素。(可以把片段看成是潛在的像素)。(來自CG教程_可編程實時圖形權威指南P14)
在這個階段,光柵器還可以根據多邊形的朝前或朝后來丟棄一些多邊形(挑選culling)。
實際裁減是一個較大的概念,為了減少需要繪制的頂點個數,而識別指定區域內或區域外的圖形部分的算法都稱之為裁減。裁減算法主要包括:視域剔除(View Frustum Culling)、背面剔除(Back-Face Culling)、遮擋剔除(Occlusing Culling)和視口裁減等。在上面提到的裁剪是指背面剔除。遮擋剔除在下一個光柵化操作階段進行。(下里巴人P27)
4) 可編程片段處理器
可編程片段處理器需要許多和可編程頂點處理器一樣的數學操作,但是它們還支持紋理操作,紋理操作使得處理器可以通過一組紋理坐標存取紋理圖像,然后返回一個紋理圖像過濾的采樣。(注意了,紋理在這個地方進行映射。假如沒有編寫着色器程序,也是在這里映射,只不過使用的是默認的着色器)
那什么是紋理過濾。大家可以參考這個鏈接,淺墨講解了DX的四大紋理過濾http://blog.csdn.net/poem_qianmo/article/details/8567848。淺墨的博客在我以前學習DX的時候給了我很大的幫助,大家可以多關注他的博客(認為是賣廣告的請忽略)。
前面有講到,可編程與固定渲染流水線的區別在於是否對着色器進行編程。第一個可能進行編程的地方在可編程頂點處理器,第二個可能進行編程的在可編程片段處理器這里。
可編程片段處理器,因為紋理操作等都在這里進行,所以假如我們希望我們的游戲非常絢麗,特效非常豐富,這部分需要我們投下很大的精力。不過因為我們今天是解釋渲染流水線,所以在這個地方我們不做太多的解釋。下面,我們了解一下其內部流程。

圖 6
5) 光柵化操作
在前面,光柵化與可編程片段處理器會提供給我們片段以及一些相關的數據,比如片段的ALPHA,其深度等(片段的概念見前面,忘了可以認為是沒有存在屏幕上的像素)。在這一步我們將會對其進行各種測試,而假如它通過了所有的測試,片段將會顯示在屏幕上。

圖 7
抖動顯示:一種能夠使用較少的顏色種類模擬較多顏色的顯示模式。
在這個階段,光柵器根據多邊形的朝前或朝后來丟棄一些多邊形(挑選culling)。光柵操作階段將根據許多測試來檢查每個片段,這些測試包括剪切,alpha,模板和 深度等測試。這些測試設計了片段最后的顏色或深度,像素的位置和一些像素值(例如像素的深度值和顏色值)。如果任何一項測試失敗了,片段就會在這個階段被丟棄,而更新像素的顏色值,雖然一個模板寫入的操作也許會發生。通過了深度測試就可以用片段的深度值代替像素的深度值了。(CG,P14)
到這里為止,整個渲染流水線已經結束,最后附上CG教程里的一張圖來結束今天的教程。
寫博客的時候或許會出現一些問題,歡迎大家批評改正!
