前言
可供學習用的文章和書記
電子書《《Real-Time Rendering 3rd》提煉總結》
游戲引擎架構(第二版)
渲染管線
參考自以下文章。本文會在此次數上在進行一次提煉
【《Real-Time Rendering 3rd》 提煉總結】(二) 第二章 · 圖形渲染管線 The Graphics Rendering Pipeline
Real-Time-Rendering中將渲染管線划分為應用階段,幾何階段以及光柵化階段
應用階段
應用階段是軟件控制的階段,是開發者可以完全控制的階段。在此階段可以實現各種加速算法來為GPU減輕計算負擔,其中一種是層次視錐體裁剪。應用階段末端會通過一次DrawCall將繪制圖元的信息發送給GPU,之后的幾何階段以及光柵化階段將會在GPU上執行
CPU准備繪制所需要的頂點,紋理等信息發送給GPU並通知其進行繪制的過程,稱作一次DrawCall
幾何階段
在RTR中,幾何階段又可以划分為以下幾個步驟
-
模型視點變換:Movel and View Transform。Model將頂點從局部坐標系轉換為世界坐標系;ViewTransform將相機放在原點上,並調整相機朝向,使其看向-Z方向,Y軸指向上方
-
頂點着色:着色階段可不僅僅只會在片元着色器中進行
-
投影:分為正交投影和透視投影(具有近大遠小的效果)兩種,此步驟只會進行投影矩陣變換,不會進行透視除法
-
裁剪:此裁剪為小粒度的裁剪,裁剪的結果將會決定哪些頂點會進入光柵化階段。裁剪階段會生成新的頂點(裁剪是在3D坐標系中進行的)
-
屏幕映射:將裁剪坐標轉變為屏幕坐標,完成了3D到2D的轉化。盡管此時物體都被映射到同一個平面上,但此時物體並沒有前后的關系,具體的遮擋剔除需要等到深度測試時再執行。屏幕映射所執行的操作可以簡單理解為:
- 忽略Z軸坐標
- 通過拉伸將單位正方體變換成長寬為屏幕窗口大小的矩形
光柵化階段
目前經過屏幕映射,GPU擁有多個處在屏幕空間坐標系中的頂點,同時還擁有這些頂點的深度信息(Z值)和各種着色信息,那么光柵化的目的就是完成這些頂點到屏幕上像素的轉化
-
三角形設定:在硬件上執行,不可控。該階段計算了三角形的各種重要數據(三角形邊的方程,深度值等等),供三角形遍歷階段使用
-
三角形遍歷:在硬件上執行,不可控。功能是找出有哪些采樣點在三角形中,並對這些采樣點進行各種屬性的插值(透視投影矯正插值就是在該階段執行的)。由采樣頻率導致的走樣問題可以用抗鋸齒技術解決,如常見的MSAA
-
片元着色器:完全可編程,紋理貼圖,光照模型,陰影處理等都是在此階段進行的
-
逐片元操作/測試合並階段:可配置的階段。各種混合和測試操作都是在這一階段進行的,如裁剪測試,透明測試,模板測試,深度測試和色彩混合等
可編程性
對一個最基礎的Vertex Shader來說,會完成幾何階段中的模型視點變換和投影兩個操作,然后硬件會對變換后的結果進行裁剪和屏幕映射
各個坐標空間
Unity Shader入門精要:P73
在圖形學中,坐標空間分為以下幾種
- 模型空間:即Local Space
- 世界空間:即World Space
- 觀察空間:Unity一個以攝像機為原點,攝像機朝向為-Z,上方為+Y,右方為+X的方向的坐標空間
- 裁剪空間:用於執行這個變換的矩陣叫做投影矩陣。但是為什么要叫做裁剪空間呢,因為在完成投影變換后執行的裁剪操作,就是在這個坐標空間下進行的
- 屏幕空間:經過屏幕空間變換后,我們將能夠得到頂點在屏幕上的位置
世界坐標系轉換為觀察坐標系
假設在Unity中,攝像機的位置是
- Position(0, 10, -10)
- Rotation(30, 0, 0)
那么為了將攝像機變換到原點,需要進行“逆”變換,即先將相機平移至原點,然后進行旋轉
對向量進行一次變換矩陣的矩陣乘法,即可得到位於觀察坐標系下的坐標
觀察坐標系轉換為裁剪坐標系
一個比較直觀而且簡便的理解方式
如果對這種簡便的理解方式不滿意,具體的數學計算流程可以參照
Unity Shader入門精要:P77
裁剪坐標系轉化為屏幕空間坐標系
首先非常重要的一點:真正的投影操作是在完成裁剪操作后執行的
透視除法
所以為了將頂點從裁剪空間轉換到屏幕空間,我們第一步要進行透視除法。其實就是將w歸一,用齊次坐標系的w分量去除x,y,z分量,得到的結果我們稱作歸一化的設備坐標(NDC)。在OpenGL中,這一步會將頂點們變換到一個xyz分量范圍都是[-1, 1]的立方體中
對於透視投影來說,經過變換后的坐標位置長這樣
對於正交投影來說,經過變換后的坐標位置長這樣
平移和拉伸
然后第二步就是進行平移和拉伸操作,與Games101課程中提到的一致。這一步僅僅會對xy分量做變換操作,z值保持不變,它會被用於深度緩存,而w分量則會被用於透視矯正插值中
至此我們就能獲得每個頂點對應到屏幕上的位置了
渲染前CPU和渲染中GPU的裁剪和剔除(待更新)
可能由於翻譯或是其他問題,導致剔除和裁剪這兩個詞常會被混淆,因此我們只需要分清這些操作是CPU執行還是GPU執行,是在哪個階段執行的即可
應用階段CPU會進行一次剔除,叫做視錐體剔除,英文名叫做Culling。這一階段CPU將不與視錐體交叉的物體剔除掉,不會傳遞至幾何階段。注意此時剔除的粒度大,是在Object層面的,會對物體的包圍盒進行檢驗。如下圖中的AD會被剔除。BCE會被傳入到GPU中進行下一步操作

透視校正插值
當經過屏幕空間變換后,3D空間中的物體被映射到2D的屏幕上,之后就可以在光柵化階段前期根據重心坐標進行屬性的插值
這里以深度插值為例,假設對A'和B’而言有一個采樣點位於中點位置,那么該采樣點的值就應該是A‘與B’的平均。但是在3D空間中,它們所對應的點卻明顯不是中點位置,而是深度應該更“近”一些,那么普通的插值結果就是不准確的(假設攝像機采用的是透視投影)
解決方案1:逆變換
由Games101中提到的,重心坐標不能確保透視投影后保持不變(正交投影重心坐標保持不變),因此可以對2D平面上的物體執行一次逆變換,然后在3D平面上求重心坐標,最后進行插值
解決方案2:透視矯正插值
對於深度插值來說,推導如下。本推導僅參照數學過程,忽略攝像機空間的-Z朝向
因此在光柵化階段,只需要取出頂點對應的深度信息(即ZA或ZB),然后取倒數,最后利用重心坐標進行插值就能得到正確的結果
那么能將深度插值到正確的結果后,其他屬性的插值也能在此基礎上推導出來了,以顏色為例
裁剪測試與模板測試
Alpha-Test
Alpha-Test發生在什么時間?Alpha-Test執行什么樣的工作?
Z-Test和Early-Z
GPU的渲染管線:
深度測試發生在fragment shader之后,因此可能會出現在fragment shader中耗時耗力進行的着色,被深度測試拋棄的情況。因此可以采用Early-Z技術,在fragment shader之前就先初步篩選一次
Early-Z(Early-Z-Test)的步驟與Z-Test是相同的,也是會有一個Early-Z-Buffer然后進行深度比較操作,是硬件廠商實現的技術
但是Early-Z也有不小的局限性
-
如果手動修改GPU插值后的深度值,或是開啟了Alpha Test,那么在這種情況下用Early-Z技術是不准確的,必須老老實實用Z-Test,即等到光柵化和着色后才決定是否拋棄像素
-
Early-Z是不穩定的,當渲染順序是從遠往近處渲染時,Early-Z將不會帶來任何優化效果。
眾所周知,繪制的順序並不會影響深度測試的結果,但是對於Early-Z而言,如果CPU給GPU傳遞的頂點信息恰好是從遠往近排列的,那么Early-Z對最遠處的fragment進行測試,結果是通過,直到最近的fragment,測試的結果一直都會是通過,它們都將會執行片元着色器,但是最后進行Z-Test的時候只有最近的fragment會被保留,此時Early-Z將沒有任何優化效果
相反的,如果此時渲染的順序是從近到遠,那么Early-Z的優化效果將會達到最大
那么如何讓渲染順序從近到遠排列呢,在CPU中進行排序然后再發送給GPU顯然是不現實的,這樣不僅會消耗大量的CPU性能,而且也無法進行合批優化
因此Z-Prepass就派上了用場,Z-Prepass要搭配Early-Z使用,以Unity Shader為例。可以在Shader中設置兩個Pass。第一個Pass僅寫入深度(其實就是生成了一張Z-Buffer),不進行像素着色(但是仍然會跑vertex shader);第二個Pass不寫入深度(即不生成Z-Buffer),但是會在fragment shader之前利用第一個Pass產生的Z-Buffer進行深度測試,由於之前生成的Z-Buffer中記錄的已經是最近的深度值,因此只需要進行一個相等的比較,讓通過的fragment執行fragment shader
參考自:
Unity Shader (三)深度測試(depth test)