游戏引擎渲染管线的总结
需要注意的是,本文涉及的內容過多過雜,基本涉及到游戲渲染和圖形管線的方方面面。內容是根據多方面的資料整理而成,比如本人的Unity和Unreal引擎相關的理解和認知,以及引擎相關官方文檔等,以及DirectX和OpenGL相關官方文檔等,以及網絡上各種相關文章和資料等。可能有一些紕漏或者不足之處,或者有些階段的資料來源較為單一,本人主要目的是從概念理解上對應整個游戲引擎的渲染管線,不一定和真實的游戲完全一一對應,比如應用程序階段的知識對應到游戲引擎應該會有一些區別和取舍,幾何階段和光柵化階段主要參考的是OpenGL和DirectX,Vulkan和Metal相關資料參考較少,可能不同的圖形API會有一些出入。由于涉及內容過多,難免理解不到位,有發現比較明顯錯誤的,請指出以盡早修正,避免造成誤解。
一、渲染管線的思維導圖
這是本文內容的思維導圖,通過該圖可以從整體上把握全文的內容,對渲染管線有整理的理解。
二、應用程序階段
2.1 渲染數據加載
這個階段指的是將渲染所需要的相關數據,比如模型、貼圖、材質、Shader等加載到內存中,通常只發生一次,不需要每幀重復加載。比如,Unity游戲需要在運行時,將需要的場景或者人物從AssetBundle中加載出來,然后引擎才能顯示加載的場景或者人物。
2.2 物體級別的裁剪
以下描述的裁剪算法是按照粒度從粗到細的裁剪,相應復雜度和代價也是在遞增。最簡單的是基于距離的裁剪;然后是利用空間數據結構實現的視錐體裁剪;動態的入口裁剪是一種特殊情況,可以算在視口裁剪內也可以用于預計算;然后預計算數據的裁剪;接下來才是動態的遮擋剔除。
2.2.1 基于距離的裁剪
思路是超過一定的視距即不渲染該物體,Unreal引擎支持這個特性,參考Cull Distance Volumes。對于Unity,可以使用CullingGroup實現類似的功能。即使引擎沒有提供類似的支持,在游戲邏輯層面,先可以每幀或者隔幀判斷物體跟攝像機的距離,來動態顯示隱藏物體。
2.2.2 視錐體裁剪
用物體跟攝像機視錐體做相交測試,將完全沒有相交的物體過濾掉。為了加快速度,使用的是物體的包圍盒或者包圍球跟視錐體做相交測試。游戲引擎內一般都會有空間數據結構來組織物體,比如BVH,那么可以直接使用BVH來搜索加速這個計算。具體過程是用視錐體和空間數據結構去做相交測試,如果當前節點沒有相交,那么不需要繼續,如果有相交則繼續遍歷子節點直到葉子節點或者沒有相交,葉子節點中存儲的物體即是需要渲染的物體。
基于空間數據結構的裁剪
四叉樹和八叉樹
四叉樹對應的是二維空間,下面以八叉樹為例來說明。八叉樹是將三維空間平均劃分為八個部分作為八個子節點,重復劃分到一定的粒度為止,比如葉子節點內最多存儲多少個物體,物體存儲在葉子節點內。
優點:概念和實現簡單。
缺點:無限空間不好劃分;物體可能跨越分割面;物體分布不均勻會造成層次過深,搜索效率不高。
適用場景:四叉樹適用于基于高度場的地形管理;八叉樹室適用于室外分布均勻的三維場景(有高度)。
BSP
針對八叉樹這種不均勻劃分,如果將物體均勻劃分成兩部分,那么就是Binary Space Partition Tree,可以避免樹的層次過深。注意,BSP的每個節點存儲的是劃分平面,而不是物體,劃分平面將場景分為前后2個部分,分別對應左右子樹;由于需要BSP樹針對的多邊形,因此可以針對物體的AABB包圍盒做劃分。
優點:物體分布均勻,不會出現樹層次過深;支持任意空間。
缺點:實現復雜,構造時間長,不適合動態場景。
適用場景:緊湊并且分布均勻的室內場景;靜態場景;自帶物體排序,方便實現畫家算法。
KD-Tree
BSP全稱是K-Dimensional Tree。這是一種特殊的BSP,在BSP上進一步將劃分面限制跟坐標軸垂直,但是保持從物體分布的中間劃分,以盡可能得到一個物體分布均勻的樹。KD-Tree不僅僅可以用來做空間劃分,在其它領域經常用來組織K維度的數據來做搜索,比如K維數據查詢、近鄰查詢。
優點:物體分布均勻,不會出現樹層次過深;數據可以組織為數組形式的完全二叉樹,緩存友好等。
缺點:如何確定最優或者較優的劃分面?
適用場景:緊湊并且分布均勻的室內場景;輔助其它數據結構進行鄰域查詢。
BVH
全名是Bounding Volume Hierarchy,中文翻譯層次包圍盒。BSP和KD-Tree的節點代表的都是分割面,但是面有可能穿過物體。層次包圍盒的思想是每個節點代表一個空間,空間計算其包含物體的最小包圍盒,劃分空間后重新計算子空間的包圍盒。與BSP最大區別是節點代表的不再是分割平面而是包含最小包圍盒的子空間。因此,這些子空間可能出現一定的重疊,但是不會出現物體出現在不同的劃分里面。
優點:節點存儲的是物體,方便碰撞檢測等查詢;構建快,動態更新方便。
缺點:如何確定最優的包圍盒?
適用場景:視錐剔除;物體碰撞檢測;射線檢測;光線跟蹤。
空間數據結構的其它應用
除了視錐體裁剪外,空間數據結構還有很多其它應用,比如
1、Ray Casting (射線檢測)
2、碰撞檢測
3、鄰近查詢 (比如查詢玩家周圍敵人)
4、光線追蹤
Portal Culling(入口裁剪)
適用于將場景劃分為格子,格子之間可能存在入口的情形,如下圖所示,
從入口只能看到部分被墻壁遮擋住的物體,因此可以借助這個特性加速視錐體和格子的相交裁剪。Unity中的Occlusion Portal即是這個特性。如果預計算出Protal Culling的結果,那么可以在運行時加快物體裁剪。
2.2.3 預計算遮擋剔除
這是一種空間換時間的算法,會增大內存占用,降低Cpu的裁剪消耗。所以是否需要預計算遮擋數據,還需要具體討論。一般如果內存消耗不大,但是Cpu占用較高的話,可以嘗試開啟預計算遮擋數據。
Precomputed Visibility (UE4)
參考虛幻引擎的Precomputed Visibility。思想是將場景劃分為格子,計算每個格子內可以看到的可見物體集合相關的數據,用于運行時動態查詢。
預計算Occlusion Culling (Unity)
參考Unity的Occlusion culling。類似于UE4的Precomputed Visibility,不過Unity的Occlusion Culling也支持動態物體,但是動態物體只能occludee(被遮擋物體)。Unity的預計算Occlusion Culling應該是入口剔除的一種預計算實現。
2.2.4 動態遮擋查詢
這里講的是在CPU上或者GPU上實現的遮擋查詢。圖形API已經提供了遮擋查詢相關的接口,比如OpenGL的Query Object或者DirectX的Predication Queries。但是不是所有的硬件都能夠支持,因此可以在軟件層面即在CPU上做軟渲染實現遮擋查詢。Hierarchical Z-Buffer Occlusion則是在普通的硬件遮擋查詢上的進一步優化,使用了層次Z-Buffer來進一步加快速度。
軟件遮擋查詢
軟光柵化模仿硬件遮擋查詢,因此不受設備類型限制,只是需要額外消耗CPU。
硬件遮擋查詢
使用圖形接口本身提供的遮擋查詢接口。基本思想是用物體的包圍盒去渲染Z-Buffer,統計通過深度測試的像素數目,如果有通過說明當前物體沒有被完全擋住,保存結果用于下一幀查詢。因此,硬件遮擋查詢會存在兩個問題:額外的渲染消耗和延遲一幀。
Hierarchical Z-Buffer Occlusion
類似硬件遮擋查詢,不過使用Hierarchical Z-Buffer來加快查詢速度。具體實現比較復雜,請參考相關文章。
2.2.5 LOD切換
LOD指的是Level Of Details。如果物體通過了以上的裁剪,那么說明會提交給渲染線程進行處理。LOD切換指的是這些物體的細節層次切換,比如一些不重要的或者看不清楚的物體選擇更簡單的模型。
基于距離的LOD切換
最常見的方式是根據攝像機距離來進行LOD切換,越遠的物體選擇更簡略的LOD,Unity和UE4默認是這種方式。
基于渲染分級切換LOD
但是我們也可以主動切換LOD,比如檢測到當前硬件較差,需要切換到更低的畫質,那么可以根據游戲設置的渲染品質分級來切換低的LOD。
LOD過渡
LOD的一個常見問題是LOD的過渡問題,可能在切換LOD時候會察覺到明顯的過渡。常見的方式是在切換時候混合2個LOD,比如透明度逐漸從1變化到0或者從0變化到1,避免出現明顯的過渡。
2.3 物體級別的渲染排序
為了減少OverDraw或者實現半透明效果,所有通過裁剪的物體會按照一定的次序進行渲染。下面列舉幾個常見的渲染次序。游戲引擎實際的渲染過程還會跟引擎渲染管線的Pass定義順序相關,比如不透明和透明物體在不同的Pass內渲染的,而且是先在一個Pass內渲染透明物體,再在另外一個Pass渲染透明物體。
從前到后渲染(不透明物體)
從前到后渲染可以利用Early Z-Test過濾掉不必要的片元處理。因此,如果先渲染近處的物體,那么后面渲染的遠處物體就不會通過Early Z-Test,就不會進入片段處理階段。不過,不是所有的硬件都需要按照從前到后的物體順序進行渲染,這畢竟需要額外的CPU消耗來排序物體,部分支持HSV(hidden surface removal)特性的GPU,比如PowerVR是不需要做這個排序的。Unity提高了靜態變量SystemInfo.hasHiddenSurfaceRemovalOnGPU來查詢GPU是否支持HSV,
Urp渲染管線會根據這個來判斷是否需要跳過從前到后排序物體。
從后到前渲染(半透明物體)
由于半透明物體的渲染算法要求必須從后到前渲染物體,同時關閉深度測試 ,前面的物體與后面的物體進行顏色混合。那么這個排序過程是無法省掉的,類似從前到后渲染的排序,可以采樣BSP來排序物體。
渲染層級或渲染隊列
Unity同時定義了這2種排序,不過SortingLayer的優先級更高,這個是定義在物體的Renderer組件上。RenderQueue是定義在Shader和材質上,優先級在渲染層級之后。理論上,就是對所有物體進行優先級排序。
最少渲染狀態切換
還有一種方式是盡可能在渲染物體的時候避免渲染狀態切換,這樣能夠盡可能減少CPU消耗。那么可以在CPU計算出來一個最優的渲染順序來盡可能減少渲染狀態切換。
2.4 渲染數據綁定和狀態設置
這一個階段講的是在CPU上設置渲染相關數據和狀態,以及為了減少渲染狀態切換的渲染合批的思想。
視口設置
設置窗口的渲染區域,比如OpenGL的glViewport。通過這個設置,我們可以在一個窗口上渲染多個不同的視口,比如游戲的分屏。
FrameBuffer設置
一般游戲引擎不會直接將物體渲染到默認的渲染緩沖上,單獨的RenderTarget方便進行后處理,在后處理之后再Blit到默認緩沖上。一個FrameBuffer可以包含顏色、深度、模板三個附件,也可以將深度和模板組織成一個32位的RT。
渲染合批
渲染合批指的是為了減少渲染狀態切換的一種優化手段,Unity URP渲染管線的SRP技術可以大幅度優化渲染批次。這是一個在Shader變體層次的合批,與之前的材質層次的合批相比有很大的優化。
頂點輸入綁定
對于OpenGL來說就是創建和綁定VAO(Vertex Array Object)。一個VAO中可以包含VBO(Vertex Buffer Object)、IBO(Index Buffer Object)。然后用glVertexAttribPointer和glEnableVertexAttribArray指定數據到Shader的輸入變量。
頂點屬性通常包括,位置、法線、切線、UV、頂點顏色等。
Shader綁定
渲染數據綁定好之后,需要指定當前使用的Shader,這包括Shader的編譯鏈接和使用等(假設Shader代碼已經加載進來)。
Shader編譯鏈接使用
類似于CPU上運行的程序,Shader也需要編譯鏈接以及開始使用的過程,不過這個過程基本上是固定。
可以參考learnopengl的著色器一節。
Uniform變量綁定
Shader中通常會有很多全局變量,比如MVP、攝像機位置、光的信息等。這些都需要在CPU上傳入Shader中。
Output-Merger Stage相關設置
在渲染管線的最后(片元著色器之后),有一個Output-Merger階段,也叫做Raster Operations。這是一個不可編程階段,但是有很多選擇可以設置。比如剪切測試、模板測試、深度測試、顏色混合因子和函數、sRGB轉換等。這些都需要在應用程序階段進行設置。
2.5 DrawCall調用
終于到了應用程序的最后一步,即DrawCall的調用了。OpenGL對應的接口是glDrawArrays或者glDrawElements。
三、幾何處理階段
這是第二個大的階段,當前階段已經進行GPU中了。該階段的起點和主要過程是頂點著色器。除了著色器之外,其余階段都是硬件自動進行的,除了可選階段之外,其余的都是固定的,應用程序無法根據配置來進行更改。
3.1 頂點著色器
頂點著色器的處理對象是應用程序階段綁定的每個頂點,頂點著色器會獲得頂點屬性以及相應的Uniform變量。頂點著色器的輸出是一個NDC Clip Space的頂點位置。NDC(Normalized device coordinates)是規范化設備坐標系的位置,OpenGL的范圍[-1,1],DirectX的范圍是[0,1]。之所以說是Clip Space,因為該階段得到的頂點數據是一個齊次坐標,還需要進行透視除法,即x、y、z除以w分量才能得到NDC坐標系下的位置。
3.2 曲面細分著色器
曲面細分著色器是一個可選階段,用于將一個簡單模型細分成復雜的模型。其實該階段是2個著色器和一個固定階段的組合。在DirectX中叫做Hull Shader stage、Tessellator stage、Domain Shader stage;在OpenGL的Tessellation中叫做Tessellation Control Shader、Tessellation Primitive eneration、Tessellation Evaluation Shader。具體的介紹和使用方式請參考相關資料。
3.3 幾何著色器
幾何著色器也是一個可選階段。幾何著色器的輸入是圖元的頂點集合(比如三角形圖元有三個頂點,點圖元只有一個頂點),輸出是一個新的圖元,新的圖元也要包含一個頂點集合。簡單來說,幾何著色器的輸入和輸出都是圖元,輸入的圖元是在應用程序階段指定的,輸出的圖元可以在頂點著色器中實現。
3.4 Stream Output (Transform Feedback)
這是一個可選的階段。這個階段在DirectX中叫做Stream Output ,在OpenGL找叫做Transform Feedback。如果該階段開啟,那么頂點數據流會輸出到一個Buffer中,這個Buffer可以給頂點著色器使用也可以返回給CPU,當前渲染管線則不會進行接下來的處理。
3.5 圖元組裝
這一步是將之前得到的頂點數據組合成圖元,比如頂點圖元、線段圖元、三角形圖元。該階段輸出圖元進行接下來的處理。
3.6 透視除法和NDC裁剪
該階段的輸入是組裝好的圖元,輸出的是NDC裁剪之后的圖元。首先對圖元的頂點進行透視除法,這樣得到的頂點數據都會位于NDC內,方便進行NDC裁剪。圖元裁剪后可以會產生新的圖元。
3.7 屏幕空間映射
該階段是將NDC下的圖元頂點坐標映射到屏幕空間。值得注意的是頂點坐標是一個齊次坐標,透視除法后得到的是NDC下的坐標;然后,通過一個縮放和平移變換將x和y映射到屏幕空間。
3.8 面剔除 (Face Culling)
這一個階段指的是三角形的前后面剔除。前或者后的定義是根據正視三角形的時候定義三角形頂點的旋向,可以定義逆時針旋轉或者順時針旋轉為前面。實際上,面剔除跟實際的攝像機位置沒有關系,不管攝像機轉到哪個地方,前后面不會改變,比如渲染立方體的時候,后面都是立方體內部看不到的面,無論攝像機如何旋轉。因為,前后面的定義是固定視角正對三角形時候定義的。
四、光柵化階段
該大的階段的輸入是幾何處理階段輸出的圖元。該階段主要分為四個部分,首先是光柵化圖元得到片元(潛在的像素信息),然后進行Early Fragment Test,通過測試后再進行片元著色器,最終進行輸出合并階段的各種測試以及顏色混合等,再輸出到顏色緩沖區。
4.1 圖元光柵化
該階段是將圖元的頂點信息進行線性插值,然后生成片元數據。每個片元上有頂點信息線性插值而來的片元數據。需要注意的是,這個插值是線性的,如果有一些數據是非線性的,則不能在頂點著色器中計算然后輸出到片元著色器,因為線性插值的結果和在片元著色器中計算的結果是不一致的。
這里需要特別說明的是,關于深度z’的生成。屏幕空間映射后的z’是關于攝像機空間z倒數的一個線性函數。之所以使用1/z而不是z,是為了在近處獲得更好的深度緩沖精度,因為1/z在近處的變化更快,可以優化Z-Fighting這種現象。由于z’不是一個關于z的線性函數,因此z’應該是在光柵化后硬件自動根據1/z計算出來的,而不是先計算z’再光柵化。
4.2 Early Fragment Test
參考OpenGL的Early Fragment Test,可以看到不僅僅通常所說的Early Z-Test還有其它好幾個階段都可以進行EarlyTest,一共是四個測試(Pixel ownership test、Scissor test、
Stencil test、Depth test)和遮擋查詢更新。根據文檔,Pixel ownership test和Scissor test從OpenGL4.2起會總是在EarlyTest階段進行。那么,如果這些測試沒有在EarlyTest階段進行,則會在最終的輸出合并階段進行;如果進行了,那么輸出合并階段也不會重復處理。
Early Z-Test的限制
不要在片元著色器中改變深度,比如glsl的gl_FragDepth;也不要discard片元,通常實現AlphaTest會根據Alphadiscard片元。因為這些操作會導致硬件無法預測最終的深度,從而無法進行提前深度測試。
4.3 片段著色器
片段著色器的輸入是光柵化來的各種頂點屬性,輸出是一個顏色值。該階段是計算光照結果的主要階段。通常片元著色器會有比較復雜的計算,通常的優化手段是將計算轉移到頂點著色器甚至CPU(應用程序階段,用Uniform傳入)上。
4.4 Output-Merger Stage(Raster Operations)
終于進入最后的輸出合并階段,該階段的輸入是一個個的片元。片元需要進行一些列的測試和轉換,最終才會將顏色輸出到緩沖區上。
Pixel ownership test
根據OpenGL的文檔,該階段只對默認緩沖區生效,用于測試像素是否被其它窗口遮擋的情形。對于自定義的FrameBuffer,不存在這個測試。
Alpha Test
需要特別說明的是,Alpha測試當前是已經被廢棄了,從DirectX10和OpenGL3.1開始廢棄,參考Transparency Sorting文檔;當前需要在片元著色器用discard實現。列在這里主要是為了完整性。
Scissor test
參考OpenGL的剪切測試文檔,Scissor Test。通過在應用程序階段設置,可以讓片元只通過視口的一個小矩形區域。根據EarlyTest的文檔,推測該階段目前都在EarlyTest階段進行了。
Multisample operations
如果啟用了MSAA,那么需要進行resolve才能夠輸出到默認顏色緩沖中,進行屏幕顯示。假如在默認緩沖中開了MSAA,那么從MSAA的后備緩沖交換到前向緩沖就需要進行resolve操作,因為前向緩沖是single-sample的。如果是自定義的FrameBuffer開啟了MSAA,那么在Blit到默認緩沖區的時候也需要進行resolve操作。
模板測試
模板測試基本思想是用一個八位的模板緩沖,一個參考值,一個比較函數,一個掩碼,用該參考值和片元對應的模板緩沖值使用比較進行比較(比較之前進行掩碼),通過的則片元可以繼續進行深度測試,否則丟棄。另外還可以定義模板成功和失敗,以及深度測試成功和失敗后模板緩沖如何變化。可以參考OpenGL的Stencil Test文檔。
模板測試的一個常見的應用是描邊或者在像素級別分類。
深度測試
深度測試是根據當前片元的深度值與深度緩沖進行比較,比較函數可以設置,通過比較的片元才會進行接下來的處理,否則丟棄當前片元。
遮擋查詢更新
參考OpenGL的遮擋查詢文檔Query Object。
該階段會更新遮擋查詢的結果,因此遮擋查詢的結果只能用于下一幀渲染。
顏色混合
需要注意的是,容易誤解半透明渲染才會有顏色混合,實際上顏色混合是管線的一個固定的階段,不透明渲染也會有默認的混合方式。
理解顏色混合,首先要明白2個概念,source和dest,source指的是當前的片元,dest指的是要目標緩沖中對應的顏色。
顏色混合主要是需要設置2個函數,一個函數用于設置混合因子,一個函數用來設置混合函數。混合因子有四種,source rgb和dest rgb,source a和dest a,可以一起指定也可以分開指定。具體可以參考OpenGL的Blending文檔。
sRGB轉換
1、我們知道顯示器或者顏色紋理的顏色空間是sRgb,sRGB空間就是Gamma校正的顏色空間,也就是已經Gamma校正過的顏色數據,這樣子在顯示器上才能正常顯示。如果我們使用的線性工作流,也就是在線性空間中制作資源,編寫Shader計算光照結果,那么片元著色器的輸出需要轉換到sRgb空間。這個轉換部分硬件上是自動支持,對于不支持的硬件則需要在Shader里面轉換。
2、如果要硬件自動轉換,首先要創建的必須是srgb顏色空間的FrameBuffer,在OpenGL中可以使用glEnable(GL_FRAMEBUFFER_SRGB)開啟;要保證片元輸出的線性空間的顏色,也就是要采用線性工作流。
3、需要注意的是,避免將sRGB轉換和ToneMaping混合起來,ToneMaping做的是將HDR映射到LDR。這只是一個帶偏向性顏色范圍映射,也就是算法傾向性的增強部分顏色。而sRGB轉換才是將顏色從線性空間轉換到sRGB空間。
Dithering
首先說明一下,顏色格式分為Float、Normalized Integer、Integer三種,默認緩沖區就是Normalized Integer格式的顏色。根據OpenGL的文檔,當將一個Float顏色寫入Normalized Integer緩沖區的時候,可以開啟Dithering。Normalized Integer緩沖區是一個定點數緩沖來存儲浮點值,比如通常我們的顏色是定義在[0,\1]的浮點值,但是顏色緩沖是[0,254]\的Int值,OpenGL會自動進行轉換。
Logic operations
根據OpenGL的文檔,當將顏色寫入Integer(Normalized Or Not)緩沖區的時候,可以開啟Logic operations。這是一些Bool操作。具體可以參考文檔Logical Operation。Logical Operations在sRGB顏色空間是禁止的。
Write mask
該階段可以分別指定Color、Depth、Stencil的寫入掩碼。具體參考文檔Write Mask。
五、RenderPass
5.1 Renderer
以上所有內容在游戲引擎只是一個RenderPass,實際情況下,每幀游戲引擎會按照一定的順序渲染多個Pass。比如,深度Pass(或者深度法線Pass)、陰影Pass、不透明物體Pass、透明物體Pass、后處理Pass等;而且后面的Pass會利用前面的Pass渲染結果來處理,比如深度Pass渲染的深度紋理可以用在后續的Pass實現一些效果。
總而言之,真實的游戲引擎是每幀渲染多個Pass,每個Pass對應上述的內容。
5.2 CameraStack
實際上,在Unity的Urp渲染管線中,更完整的過程是渲染相機堆棧->每個相機堆棧對應一個渲染器->每個渲染器包含多個Pass。不過,Urp里面每個相機堆棧只對應一個FrameBuffer,也就是所有的相機渲染輸出都是這一個FrameBuffer,避免內存和帶寬浪費。如果在場景內創建多個相機堆棧,那么其它的相機堆棧的輸出應該是離屏RT。
六、參考資料
1、Graphics pipeline
2、Rendering Pipeline Overview
3、Per-Sample Processing
4、Output Merger (OM) stage
5、裁剪和空間管理
6、[總結] 漫談HDR和色彩管理(三)SDR和HDR
總結
以上是生活随笔為你收集整理的游戏引擎渲染管线的总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算最后一个单词的字符串长度
- 下一篇: Linus Torvalds谈ECC内存