Unity Shader入門精要學習筆記-渲染流水線
一、什麼是渲染流水線
渲染流水線的工作在與有一個三維場景出發,生成或者說渲染一張二維影象。
即計算機從一些列的頂點資料和紋理等資訊出發,將這些資訊轉換成一張人眼可以看到的影象。
《Real-Time Rendering》一書將渲染流程分為三個階段:應用階段 Application Stage,幾何階段 Geometry Stage,光柵化階段 Rasterizer Stage。
應用階段
這個階段是由我們的應用主導的, 因此通常由CPU負責實現。在這一階段中,開發者有3個主要任務。
①準備好場景資料, 例如攝像機的位置、視錐體、模型、些光源等等。
②做一個粗粒度剔除工作,把那些不可見的物體剔除出去,可以提高渲染效能。
③設定每個模型的渲染狀態。 渲染狀態包括但不限於它使用的材質、紋理和Shader等。
這一階段最重要的輸出是渲染所需的幾何資訊, 即渲染圖元(rendering primitives)。
通俗來講, 渲染圖元可以是點、線、三角面等。這些渲染圖元將會被傳遞給下一個階段——幾何階段。
幾何階段
幾何階段處理所有和我們要繪製的幾何相關的事情。
例如決定需要繪製的圖元是什麼,怎樣繪製它們,在哪裡繪製他們。這一階段通常在GPU上進行。
幾何階段和每個渲染圖元打交道,進行逐頂點、逐多邊形的操作。該階段可進一步被分解為更小的流水線階段。
幾何階段的另一個重要的任務是把頂點座標變換道螢幕空間中,再交給光柵器進行處理。
這一階段會將輸出螢幕空間的二維頂點座標、每個頂點對應的深度值、著色等相關資訊傳遞給下個階段。
光柵化階段
這一階段會使用上一階段傳遞的資料來產生螢幕上的畫素,然後渲染出最終的影象,這一階段在GPU上執行。
光柵化階段的任務主要是決定每個渲染圖元中的那些畫素應該被繪製在螢幕上。
他需要對上階段的到的逐頂點資料進行插值,然後再進行逐畫素處理。
與幾何階段類似,光柵化階段又可以分為更小的流水線階段。
二、CPU和GPU之間的通訊
渲染流水線的起點是應用階段,其可分為以下三個階段:
①把資料載入到視訊記憶體中
②設定渲染狀態
③呼叫Draw Call
把資料載入到視訊記憶體中
渲染所需的資料都要從硬碟載入到系統記憶體,然後網格和紋理等資料又被載入到視訊記憶體。
因為顯示卡對RAM沒有直接的訪問權利,而且視訊記憶體更快。
設定渲染狀態
這些狀態定義了場景中網格是怎麼被渲染的,比如使用那個shader,light,material等。
如果沒有更改渲染狀態,那麼所有網格都使用同一種。
準備好以上的工作後,CPU呼叫一個渲染命令告訴GUP,這個渲染命令就是Draw Call
呼叫 Draw Call
Draw Call就是一個命令。他的發起方是CPU,接收方是GPU。
這個命令僅僅會指向一個需要被渲染的圖元列表,而且不會再包含任何材質資訊,上個階段已經完成了。
給定一個 Draw Call 後GPU根據渲染狀態,如材質紋理著色器等和所有輸入的頂點資料來進行計算,最終輸出成螢幕上顯示的畫素。
這個計算過程就是GPU流水線。
GPU流水線
GPU從CPU得到渲染命令後就進行一系列流水線操作,將圖元渲染到螢幕上。
概述
GPU對開發者開放了很多控制權。
從圖中可以看出GPU渲染流水線接受頂點資料作為輸入。
這些資料是應用階段載入到視訊記憶體中,再由Draw Call指定的。這些資料隨後被傳給shader。
頂點著色器 Vertex Shader
頂點著色器是完全可程式設計的,通常用於實現頂點的空間變換、頂點著色等功能。
曲面細分著色器 Tessellation Shader
曲面細分著色器是一個可選的著色器,用於細分圖元。
幾何著色器 Geometry Shader
幾何著色器是一個可選的著色器,用於執行逐圖元著色操作,或產生更多圖元。
裁剪 Clipping
是將那些不在攝像機視野內的頂點剪裁掉,並剔除某些三角圖元的面片。
這個階段可以配置,我們可以使用自定義的裁剪平面來配置裁剪區域,也可通過指令控制裁剪三角圖元的正面或背面。
螢幕對映 Screen Mapping
螢幕對映是不可配置和程式設計的,負責把每個圖元的座標轉換到螢幕座標系。
頂點著色器
頂點著色器的處理單位
頂點著色器是流水線的第一個階段,其輸入來自CPU。
頂點著色器的處理單位是頂點,每個進入的頂點都會呼叫一次頂點著色器。
其本身不能建立或者銷燬任何頂點,因此無法得到頂點之間的關係。比如無法得知兩頂點是否屬於同一個三角網格。
這種獨立性的好處是GPU可以利用本身特性並行優化每一個頂點,這一階段處理速度會非常快。
頂點著色器計算頂點顏色
頂點著色器的工作還有座標變換和逐頂點光照,以及輸出後續階段所需的資料。
如下圖演示頂點著色器對頂點位置進行座標變換並計算頂點顏色的過程。
頂點著色器對頂點座標進行座標變換
顧名思義就是對頂點的座標進行某種變換,頂點著色器可以在這一步改變頂點的位置。無論怎樣改變頂點的位置,都必須把頂點座標從模型空間轉換到其次裁剪空間。
o.pos = mul(UNITY_MVP, v.position);
類似上面這句程式碼的作用就是把頂點座標轉換到齊次裁剪座標系下,接著通常再由硬體做透視除法後,最終得到歸一化的裝置座標NDC。
需要注意的是上圖給出的座標範圍是OpenGL也是Unity使用的NDC,其z分量在[-1,1]。而DirectX中,NDC的z分量範圍是[0,1]。
頂點著色器的輸出方式
頂點著色器可以有不同的輸出方式,最常見的是經光柵化後交給片元著色器進行處理,現代Shader Model中還能把資料傳送給曲面細分著色器。
裁剪
攝像機的視野範圍不會覆蓋所有的場景物體,而不在視野範圍的物體應該被裁剪處理掉。
圖元和攝像機的視野關係
完全在視野內、部分在視野內、完全在視野外。
完全在視野內的圖元就傳遞給一下一階段,完全在視野外的圖元不會被傳遞,部分在視野內的就需要進行裁剪了。
如何裁剪
視野外部頂點應該使用一個新的頂點來代替,新頂點位於這條線段和視野邊界的交點處。
因為一直在NDC下的頂點位置,頂點位置在一個立方體中,所以裁剪就非常簡單了,將圖元裁剪到單位立方體內即可。
這一步是不可程式設計的,但我們可以自定義一個裁剪操作對這一步進行配置。
螢幕對映
這一步輸入的座標仍然是三維座標下的座標,是齊次裁剪空間下的座標。
螢幕對映的任務是把每個圖元的x和y座標轉換到螢幕座標系下。
螢幕座標系是一個二維座標系,她和我們用於顯示畫面的解析度有很大的關係。
實際上,螢幕對映不會對輸入的z座標做任何處理,螢幕座標和z座標一起構成了一個座標系,叫做視窗座標系。這些值會一起被傳到光柵化階段。
螢幕對映得到的螢幕座標決定了這個頂點對應螢幕上哪個畫素以及距離這個畫素有多遠。
注意,OpenGL和DirectX,在螢幕座標系上有差異,前者將螢幕左下角當作最小的視窗座標值,後者則是定義為左上角。
三角形設定
這一步開始進入了光柵化階段。上階段輸出的資訊是螢幕座標系下的頂點位置以及和他相關的額外資訊,如深度值z座標、法線方向、視角方向等。
光柵化階段第一個流水線階段是三角形設定,該階段會計算光柵化一個三角形網格所需的資訊。
具體來說,上一個階段輸出的都是三角形網格的頂點,也就是三角形網格每條邊的兩個端點。
如果要得到整個三角網格對畫素的覆蓋情況,我們就必須計算每條邊上的畫素座標。
為了能夠計算邊界畫素的座標資訊,就需要得到三角形邊界的表示方式。這樣的計算三角形網格表示資料的過程就叫做三角形設定。
三角形遍歷
該階段會將檢查每個畫素是否被一個三角網格所覆蓋。如果覆蓋的話就會生成一個片元。
而這樣一個找到哪些畫素被三角網格覆蓋的過程就是三角形遍歷,也稱為掃描變換。
三角形遍歷階段會根據上一個階段的計算結果判斷一個三角形網格覆蓋了哪些畫素,並使用三角網格3頂點的頂點資訊對整個覆蓋區域進行插值。
這一步的輸出就是得到一個片元序列。
一個片元並不是真正意義上的畫素,而是包含了很多狀態的集合,這些狀態用於計算每個畫素的最終顏色。
這些狀態包括了他的螢幕座標、深度資訊、以及其他幾何階段輸出的頂點資訊,如法線、紋理座標等。
片元著色器
片元著色器是另一個非常重要的可程式設計著色器階段。
DirectX中被稱為畫素著色器,但是片元更合適,因為此時片元不是一個真正的畫素。
前面的光柵化階段實際不會影響螢幕上每個畫素的顏色值,而是產生一系列的資料資訊,來表述一個三角網格怎樣覆蓋每個畫素。
每個片元就負責儲存這些資料。真正對畫素產生影響的階段是逐片元操作。
片元著色器的輸入是上階段對頂點資訊插值的結果,具體來說就是從頂點著色器中輸出的資料插值得到的。而他輸出的是一個或者多個顏色值。
紋理取樣
這階段可以完成很多重要的渲染技術,比如紋理取樣。
為了在片元著色器中進行紋理取樣,先在頂點著色器階段輸出每個頂點對應的紋理座標,
然後經過光柵化階段對三角形網格的三個頂點對應的紋理座標進行插值後,就可以得到其覆蓋的片元的紋理座標了。
其侷限在於僅可以影響單個片元。即執行片元著色器時,不能將結果直接發給旁邊的鄰居,除了導數資訊。
逐片元操作
這是OpenGL中的說法,在DirectX中,這階段被稱為輸出合併階段,Output-Merger。
該階段是對每一片片元進行操作,主要任務有:
①決定每個片元的可見性,如深度測試、模板測試。
②如果一個片元通過了所有測試,就把這個片元的顏色值和已經儲存在顏色緩衝區的顏色進行合併,混合。
該階段是高度可配置的,我們可以設定每一步的操作細節。該階段首先解決的是,每個片元的可見性問題。
這需要一系列測試,通過了才能和顏色緩衝區進行合併。沒通過任何一個測試,片元都會被丟棄。
測試過程是很複雜的,不同介面實現細節也不同。
模板測試
與之相關的是模板緩衝Stencil Buffer。實際上模板緩衝和顏色換成深度緩衝幾乎是一類東西。
開啟了模板測試,GPU就會使用讀取掩碼讀取模板緩衝區中該片元的模板值,將該值和讀取到的參考值進行比較。
這個比較函式可以是開發者指定的,例如小於時捨棄該片元或者大於時捨棄該片元。
不管一個片元有沒有通過模板測試都可以根據模板測試和下面的深度測試結果來修改模板緩衝區。
這個修改操作也是由開發者指定的。模板測試通常用於限制渲染的區域,高階用法有渲染陰影、輪廓渲染等。
深度測試
通過模板測試後,片元就會進行深度測試。其同樣是高度可配置的。
開啟後,GPU會把該片元深度值和已存在與深度緩衝區的深度值進行比較,這個比較函式也是開發者設定的。
例如我們總想只顯示裡攝像機最近的物體,而其他被遮擋的的物體就不需要顯示在螢幕上。
如果一個片元沒有通過這個測試他就沒有權利更改深度緩衝區中的值。
通過之後開發者還能指定是否用該片元的深度值覆蓋原來的深度值。這是通過開啟/關閉深度寫入做到的。
合併操作
通過了所有測試後,片元就來到了合併。
每個畫素的資訊被儲存在一個名為顏色緩衝的地方,因此執行此次渲染時,顏色緩衝中往往已經有了上次渲染之後的結果。
合併就是要決定到底是使用這次渲染得到的顏色完全覆蓋之前的還是進行其他處理。
對於不透明物體,開發者可以關閉混合操作。這樣片元著色器計算得到的顏色值就會直接覆蓋顏色緩衝區中的畫素值。
對於半透明物體,我們需要使用混合操作來讓這個物體看起來是透明的。
混合操作也是可以高度配置的。開啟了混合,GPU會取出源顏色和目標顏色將兩者混合。
源顏色是片元著色器得到的顏色,目標顏色是已經存在於顏色緩衝區中的顏色值。
之後就會使用一個混合函式進行混合操作。該函式和透明通道息息相關,例如根據透明通道的值進行相加減乘等。
提前測試
雖然邏輯上這些測試是在片元著色器之後進行的,但對於大多數GPU來說,他們會盡可能在執行片元著色器之前進行這些測試。
儘可能早知道哪些片元會被捨棄可以提高效能,比如unity的渲染流水線中其深度測試就在片元著色器之前。
這種將深度測試提前的技術被稱為Early-Z技術。
但將這些測試提前其檢驗結果可能會與片元著色器中一些操作產生衝突。
雙重快取策略
當模型的圖元經過了上面的層層計算和測試後就會顯示到螢幕上。螢幕顯示的就是顏色緩衝區中的顏色值。
為了避免我們看到那些光柵化的圖元,GPU會使用雙重緩衝策略。
即對場景的渲染是在幕後發生的,在後置緩衝中,一旦已經被渲染到後置緩衝中,GPU就會交換後置緩衝取和前置緩衝區中的內容。