【Siggraph 2015】GPU-Driven Rendering Pipelines

本文是育碧的兩個工程師在Siggraph2015上的陳述,是《刺客信條Unity》(以下簡稱ACU,Montreal工作室)開發(fā)過程中所使用的GPU驅(qū)動的渲染管線以及RedLynx工作室的GPU驅(qū)動渲染管線實施方案的介紹。

整個陳述分成如下幾個部分,第一個部分是GPU驅(qū)動渲染管線的背景與動機;第二個部分是GPU/CPU渲染管線都會用到的mesh cluster rendering方法的簡介;第三個部分會對ACU的GPU驅(qū)動渲染管線做一個詳細的介紹;第四個部分則是對Occlusion Depth數(shù)據(jù)的生成算法的介紹,最后給出所有成果的實施效果。

GPU驅(qū)動渲染管線是什么意思?總的來說,就是將此前由CPU完成的物件渲染前剔除處理以及渲染輸出的target viewport的指定工作移交給GPU來完成,物件渲染的整個過程不需要CPU對資源數(shù)據(jù)進行干涉以避免對GPU流程的阻塞。

之所以要這樣做,是因為隨著算法復雜度的增加以及場景復雜度的提升,相對于串行處理運算器CPU而言,并行處理運算器GPU的消耗會更低,計算效率更高,且物體可見性使用時延基本可以忽略。

育碧RedLynx工作室(以下簡稱R工作室)產(chǎn)出的游戲,對于UGC(user generated content)依賴較高:

  1. 包括背景在內(nèi),場景大多是由小塊數(shù)據(jù)拼接而成;
  2. 場景渲染范圍通常也比較廣(深度大);
  3. 場景數(shù)據(jù)需要從服務器下載得到,而由于場景是由小塊組成的,因此離線光照烘焙基本用不了。如果再考慮到陰影的渲染管線的話,整個渲染管線的負擔進一步加重;
  4. 物理模擬以及邏輯腳本系統(tǒng)會占用較多的CPU時間。

背景介紹:

R工作室很早就在Xbox 360上試驗GPU驅(qū)動渲染管線的可行性了,最早是嘗試通過可編程頂點fetch以及memexport方案來實現(xiàn),不過由于硬件限制,當時的性能表現(xiàn)并不能達到要求。

之后Persson在Siggraph 2012上給出的Merge-Instancing技術方案(詳情參考此前的這篇文章)進入了R工作室的視線,這個技術使用了Xbox 360的可編程頂點fetch技術在運行時通過vs對mesh數(shù)據(jù)進行合并處理。這個技術實現(xiàn)過程中不需要通過在內(nèi)存中進行頂點或者索引數(shù)據(jù)的的拷貝來實現(xiàn)不同mesh的一次性繪制,而是通過在VS中模擬index buffer的工作流程來強制對每個三角面片執(zhí)行三遍VS邏輯的方式實現(xiàn)的。在這個過程中不需要用到Post Vertex Cache(即post-transform cache ,指的是那些使用帶索引的渲染API在執(zhí)行的時候,會將一小批近期用到的頂點數(shù)據(jù)存儲到cache中,從而提升后續(xù)渲染時訪問數(shù)據(jù)的速度),因此性能上有一個非常大的提升。

ACU是第一代為新一代硬件而設計的《刺客信條》游戲,在這個游戲中,美術同學添加了大量的幾何物件以實現(xiàn)對真實巴黎的模擬。

同時,ACU也是第一次嘗試實現(xiàn)模型內(nèi)部空間的無縫銜接(seamless interior spaces這個是啥,目的何在?推測是指內(nèi)部面片結構無縫銜接,以達到高真實度的表現(xiàn)效果,通常會需要使用較多的面片來對細節(jié)進行填充)的游戲,這種做法使得需要處理的幾何數(shù)據(jù)進一步上升。

此外,還有眾多的角色模型,進一步加劇了渲染管線的壓力。

為了能夠創(chuàng)建一個如此巨大的游戲場景,巴黎場景的第一輪構建是通過半自動的方式實現(xiàn)的,整個過程使用了數(shù)百個可以復用的模型來創(chuàng)建大量的房屋模塊(house blocks)。如果按照傳統(tǒng)的一個模型占用一個DP的渲染方式,將會導致DP數(shù)超過五萬,而即使使用實例化渲染技術,最終的DP也會高于一萬五。

即使在主機上,CPU也是非常寶貴的資源,為了避免CPU稱為渲染管線的瓶頸,這里給出的做法是為使用更為激進的合批策略,同時采用更為高效的剔除手段。而Mesh cluster rendering正好符合這個標準。

Mesh Cluster Rendering可以在加大剔除粒度的前提下同時得到更為激進的合批策略(??沒看出這兩者有什么矛盾)。由于在GPU中無法通過可編程方式獲得頂點的索引數(shù)據(jù),因此想要通過單個帶實例的DP實現(xiàn)多個不同物件的渲染,就需要強制多個物體使用相同的拓撲結果(為什么使用相同的拓撲結構,就能實現(xiàn)單DP,多Mesh渲染?當每個instance的頂點數(shù)固定時,通過instance_id就能夠拿到當前instance在VB中的起始地址,從而可以一次性取出所有頂點進行VS處理?)。ACU選擇的是“Vertex Strip”拓撲結構:將所有的mesh數(shù)據(jù)分割成64個vertex strips組成的clusters(由于每個mesh的頂點數(shù)不同,組成這個mesh的clusters數(shù)目也有所區(qū)別,相同的是cluster的尺寸是恒定的),也就是62個三角形組成一個cluster。

除了這種拓撲結構之外,其他的比如32四邊形(quad)等固定索引buffer的拓撲結構也是可用的。不管選擇哪種拓撲結構,都是需要在其中添加退化三角形實現(xiàn)多個mesh part之間的連接,以及在每個mesh末尾添加退化三角形來補足最后一個cluster。

這個過程是在mesh編輯完成后進行的,可以看成mesh編輯后處理:從mesh數(shù)據(jù)構建出triangle strips,之后使用一個貪心算法構建一個局部clusters。由于在渲染的時候需要獲取一個cluster的不同頂點數(shù)據(jù),因此這里不能直接使用硬件自帶的vertex fetch函數(shù),而是需要通過vertex id & instance id手動讀取全量數(shù)據(jù)。按照這種方式,我們可以通過一個DrawInstancedIndirect(在D3D11中,這個函數(shù)可以看成是DrawInstanced函數(shù)的重載版本,其作用是將某個instance繪制多遍,其中指定了起始vertex在VB中的Offset跟instance的Offset,如果這個接口真的能夠完成對多個不同mesh的一次性繪制,那么即使每個instance使用不同尺寸的VB應該也是可以的吧?DrawInstancedIndirect有兩個參數(shù),一個是參數(shù)列表指針,第二個是參數(shù)偏移,通過調(diào)整參數(shù)偏移,可以實現(xiàn)對不同instance的繪制,因此在設置好VB之后,多次調(diào)用這個接口就能夠?qū)崿F(xiàn)不同mesh的instance繪制,不過這樣就是多個DP了,跟描述貌似不太符合?這個接口應該是將多個不同mesh的數(shù)據(jù)統(tǒng)一到一個VB中,并且將這些帶有instance的數(shù)據(jù)塞入到instance buffer中,之后調(diào)用一個DrawInstancedIndirect接口,在VS中完成對不同mesh cluster數(shù)據(jù)的讀取與訪問,實現(xiàn)多個mesh的一次性繪制) DP完成任意數(shù)目的mesh的繪制,DP參數(shù)以及cluster stream數(shù)據(jù)會通過GPU計算得到,計算過程會對每個cluster啟用一次culling計算。


對于stripped渲染拓撲結構,每個strip cut(相當于告訴硬件當前strip已經(jīng)結束,下面進入下一個strip)都會導致4個額外的冗余頂點與4個冗余面片,對內(nèi)存占用,VS渲染消耗以及最大多邊形吞吐量有影響。

ACU中將mesh數(shù)據(jù)分割成固定64個頂點的實例數(shù)據(jù)(每個cluster可以看成一個instance),這個過程通過貪心clustering算法完成,這種渲染架構對于頂點數(shù)據(jù)與instance數(shù)據(jù)獲取的時間復雜度為O(1)。

算法Bonus:DX11的DrawInstancedIndirect接口會在每個instance結束的時候自動添加一個strip cut(這個是什么?可以堪稱是一個用于結束繪制的overhead),這個過程是免費的。

在PC可編程管線中,無法通過vertex fetch獲得頂點數(shù)據(jù)。ACU是通過SRV(Shader Resource View)來對頂點數(shù)據(jù)進行讀取的:頂點數(shù)據(jù)按照SoA(Structure of Array,由多個數(shù)組作為成員組成的結構體,對應到VB上,就是將頂點的每個屬性都單獨抽取出來組成一個個的屬性數(shù)組;與之相對應的是AoS,Array of Structure,由相同結構體組成的結構體數(shù)組,對應到VB上,就是將每個頂點的多個屬性組成一個結構體,之后用這種結構體數(shù)組表示VB)方式排布。這種做法可以降低GPU延遲(為啥?對于不同頂點的同一個屬性的讀取,其速度更快)在GCN(這個是啥?Graphics Core Next,是AMD為其GPU所開發(fā)的微結構microarchitecture的代號,也指與之對應的指令集)上比硬件Vertex Buffer讀取數(shù)據(jù)表現(xiàn)更好。

這些特點不但可以用于實現(xiàn)更為激進的合批方案,而且還可以用于對GPU Frustum/Occlusion Bulling效果進行精細調(diào)整。

這里不會對GPU Occlusion Culling的實現(xiàn)細節(jié)進行深入介紹,不過會給出一些參考文獻供大家查閱。

另外,culling方案調(diào)整程度越精細,不但可以剔除更多的頂點數(shù)據(jù),而且還能進一步降低overdraw(如右圖所示),此外對于美術同學的工作也有所幫助:他們不再需要對于物件尺寸做精心規(guī)劃以得到更為有效的裁剪結果。

測試顯示,手動vertex fetch功能比自動vertex fetch功能還要快一點,此外,這種方法還可以用于對cluster depth進行排序,從而起到類似depth pre-pass一樣的降低overdraw的作用。

Stripped triangle instance渲染方法+mesh clustering 渲染方法在所有平臺上的表現(xiàn)都還不錯。不過ACU在上線之前還是選擇切換到另一種渲染方法,原因是為了降低由于退化三角形導致的內(nèi)存消耗以及修復由于無法調(diào)整cluster的渲染順序(non-deterministic cluster order)而導致的深度競爭問題(這些問題通常是由于building模塊對齊關系處理得不夠好導致)。

使用比如手動index buffer數(shù)據(jù)獲取或者cluster depth排序方法可以解決這些問題,不過ACU選擇的是一個類似的mesh cluster渲染方法,即使用多個DrawIndexedInstancedIndirect接口來完成繪制(這個接口跟之前的DrawInstancedIndirect接口大同小異,區(qū)別在于不再使用strip拓撲結構,加上了索引數(shù)據(jù)來降低頂點數(shù)據(jù)的空間占用)。在這種方法中,會借助傳統(tǒng)的頂點緩存優(yōu)化策略來對mesh數(shù)據(jù)進行優(yōu)化,之后將mesh分割成統(tǒng)一的64個triangle的cluster(如果使用索引方法,就沒有辦法通過instance_id + vertex_id來得到頂點在vertex buffer中的位置,反之亦然,此時還有必要保證每個cluster的triangle數(shù)目恒定在64上有什么意義呢?難道僅僅是為了指定一個恒定的cluster尺寸嗎?方便實現(xiàn)cluster的拆分與深度排序)。

這是整個渲染管線的概覽,在CPU側(cè),依然需要進行粗糙的frustum culling,之后將所有未被剔除的物件按照材質(zhì)進行合批處理。在GPU側(cè),對每個instance進行frustum/occlusion culling處理。在對cluster按照frustum/occlusion depth進行cull之前還需要進行一個cluster expansion處理(干了啥,目的何在?)。部分backfacing面片在index buffer compaction過程中會被剔除掉。完成所有可見性測試之后的輸出數(shù)據(jù)被用作multi-drawcall處理階段的輸入。此外,可形變物體不需要經(jīng)過上述管線的cluster相關步驟,直接跳過即可。

正如之前所說,在CPU上,需要進行簡單的quad tree culling。之后對每個動態(tài)實例物件進行數(shù)據(jù)更新,如transform等數(shù)據(jù)的更新,更新過程在GPU Ring Buffer(Ring buffer是GPU Command的索引buffer,存儲了每個GPU Command在Command buffer中的位置與長度)進行,并為所有無法通過GPU Instancing繪制的物體構建一個hash值,之后基于此hash值對DP進行合并處理,并為GPU渲染構建所需要的instance stream。


四叉樹中物體的粒度會隨著數(shù)據(jù)而變化:

1.房屋

2.大型物件

3.部分特別的物體

4.部分細小的動態(tài)物件(比如角色)

DP會在合并之前按照距離進行排序。雖然通過通過boundingbox來進行排序得到的并不是完美的深度順序結果,但是相對于合并后排序,其效果已經(jīng)好很多了。

由于傳統(tǒng)技術的限制,ACU的渲染管線距離完美的CPU目標還有很長的距離。在CPU上依然需要對物件(即使是靜態(tài)的)進行處理,且只能對使用相同材質(zhì)的物件進行實例繪制。

Instance stream包含了各個instance在GPU-buffer中的offset列表,從而使得GPU獲取如transform,instance bounds等數(shù)據(jù)。

GPU會使用這些數(shù)據(jù)進行instance層面的frustum/Occlusion culling。對于所有通過culling test的instance,會生成一個cluster chunks列表。

這里ACU使用中間過程的cluster chunk expansion(這個是啥意思?將mesh數(shù)據(jù)拆分成多個cluster嗎?)而非直接的cluster expansion,這是因為每個mesh的clusters數(shù)目是可變的(1~1000)。直接的cluster export在同一個wavefront的不同GPU線程中可能會非常的不平衡(會有什么結果呢?GPU算力浪費)。而每個cluster chunk卻最多對應于64個clusters。

之后的cluster culling過程會使用instance transform&bounds數(shù)據(jù)來對cluster進行frustum/occlusion culling處理。對于每個cluster,ACU還會獲取一個View Dependent Triangle Mask用于進行烘焙前的backface culling。通過culling的clusters會輸出一個index compaction job,用于構建繪制所需要的index buffer。index compaction job輸出數(shù)據(jù)包含了triangle mask以及索引讀寫offsets。這些offsets可以通過相關的instance面片數(shù)目計算得到。

對所有通過剔除測試的cluster進行index compaction處理,并將結果寫入到一個動態(tài)index buffer中。

這個index buffer是在CPU上分配的,因此需要為每個instance mesh按照全量的未被剔除前的索引數(shù)據(jù)分配所需要的空間。由于index buffer尺寸比較?。?mb,為什么比較小,8mb怎么得到的?),因此一個render pass的數(shù)據(jù)可能無法全部塞入到buffer中,而需要分成多個render passes,因此索引buffer compaction與multi-draw rendering操作會交叉進行。

此時index compaction會將被cluster culling & backface culling的triangle移除掉。在每個index compaction計算任務中,每個wavefront會處理一個cluster,各個wavefront之間相互獨立,每個線程處理一個triangle,且線程之間也是相互獨立的?;赾luster culling輸出input/output offsets以及triangle mask數(shù)據(jù),每個線程會獨立計算輸出數(shù)據(jù)在動態(tài)index buffer的位置(write position)并拷貝3個triangle 索引(每個triangle包含三個頂點,對應三個index)。這一步需要占據(jù)5%~10%的渲染時間。

之后對每個批次調(diào)用MultiDrawIndexInstancedIndirect 接口來進行group rendering,group rendering的DP則是在cluster culling階段通過原子操作生成的。

前面說過,在cluster culling時,會需要取到一個view dependent的面片mask用于計算每個cluster內(nèi)部的可見面片數(shù)目。這里是通過將cluster內(nèi)部面片的可見性數(shù)據(jù)烘焙到一張cubemap中,cubemap的每個像素數(shù)值對應于一個相機位置的可見性結果。比如相機處于圖中黃色像素所對應的frustum區(qū)域內(nèi),那么綠色面片的可見性就可以根據(jù)相機與cluster的相對位置直接從預計算cubemap中讀取出來,用這種做法可以一次性拿到cluster中所有64個面片的可見性結果,這個結果是一個bit mask,之后根據(jù)面片索引就能得到對應面片的可見性結果。

換個視角來解釋這個方法,這個box是cluster的boundingbox,box中帶有箭頭的線段表示的是cluster中的面片與朝向。右邊包裹住相機的綠色區(qū)域表示的是黃色像素對應的frustum。每個面片是否可見可以簡單的轉(zhuǎn)換為面片的正向半空間(half space)跟當前像素對應的frustum是否存在交集。如果相機本身位于box內(nèi)部,那么這個時候會直接將所有面片的可見性設置為可見。

ACU出于對內(nèi)存的考慮,只使用了六個像素來表示cubemap,即每個face只用一個像素表示,這種做法會導致culling精度有所下降,雖然通過增加像素frustum深度范圍可以對提升一點精度,但是提升非常有限,且還可能會導致一些不正確的剔除結果(比如本來是不可見的面片隨著frustum深度范圍的增加變成了可見(如上賬圖片中下面的面片一樣))??偟膩碚f,這種做法可以剔除10%~30%的面片。

要想實現(xiàn)高效的GPU遮擋剔除,就需要一個較好的遮擋深度數(shù)據(jù)(occlusion depth)。這是ACU中的一個經(jīng)典場景,下面將以此為例給出ACU是如何生成不同類型的遮擋深度的。

ACU給出的第一種遮擋深度生成算法,是使用前n個最佳遮擋物(面積夠大,距離夠近)完成一個深度渲染pass(depth pre-pass)。depth渲染結果不但可以用作GPU culling,還可以作為early-z用于實際的normal渲染pass。這種做法可以降低由于合批或者排序問題導致的overdraw。這里生成的depth會被下采樣到512x256分辨率,并與上一幀normal渲染pass輸出的depth buffer經(jīng)過reprojection映射到本幀位置的結果相結合來給出最終的depth輸出。

最佳遮擋物的選擇是基于bounding volume以及美術同學預先設置的標記來進行的。在這些符合條件的遮擋物中,距離相機最近的300個遮擋物會被選擇用于進行depth繪制,(也可以考慮面片數(shù)少的,以及覆蓋屏幕范圍大的)。

ACU這邊嘗試了多種選擇策略(比如考慮屏幕空間投影面積等),最終考慮到時間消耗,選擇了最為簡單的距離判定方法。

在這個pass中,不會進行遮擋剔除,雖然確實可以使用上一幀depth buffer reprojection后的結果來進行遮擋篩選。

這里給出一種填充此前Occlusion depth pre-pass中輸出的depth貼圖中的孔洞的簡易方法??锥吹漠a(chǎn)生可能是由于物件不符合作為遮擋物的條件,也可以是選擇的遮擋物效果比較差,也可能是由于alpha test物體導致。

當相機靜止不動時,使用上一幀的depth buffer結果可以很好的填充這些孔洞。但當相機向著屏幕邊緣移動時或者由于視差原因(相機旋轉(zhuǎn)導致),孔洞的填充就比較難辦了。

如果是大型的動態(tài)物體,上一幀的深度數(shù)據(jù)也會被污染,為了避開這種錯誤的depth數(shù)據(jù),ACU這邊會在處理reprojection時拒絕掉那些距離相機過近的物體(近才會導致在depth buffer中占據(jù)足夠多的像素,錯誤就會很明顯?)。

此外,ACU還為每級Shadow Map生成了一個64x64的低分辨率遮擋深度貼圖。

第一步,通過reprojection操作,將上一幀的depth buffer數(shù)據(jù)投射到光源空間,來得到由陰影接收者組成的occlusion depth數(shù)據(jù),后面會給出更詳細的介紹。

這個occlusion depth結果接下來會跟上一幀生成的shadow map經(jīng)過reprojection后的數(shù)據(jù)結合起來,同樣的,這個過程會由于大型的可移動物體而導致錯誤的occlusion數(shù)據(jù)。

由于fog的計算需要用到下采樣的指數(shù)shadow map,因此上一幀的下采樣陰影貼圖是可以直接拿到的,不需要額外的計算。

通過上一幀的depth buffer reprojection得到的shadow map occlusion,目的是為了剔除那些不會有陰影接收者的陰影投影物體的繪制。在這個示例場景中,院子里的所有處于角色前面(這里指的是靠近光源方向)的陰影投射物體(剔除左側(cè)的大建筑物)都不會對陰影貼圖的輸出有任何貢獻,因為整個院子都被前面的建筑物所遮擋住了。

這里給出一個示意圖。

黃色箭頭表示的是太陽光方向。紅色的線段給出的是一級shadow map的覆蓋區(qū)域。

紅色方塊則是找不到接收陰影物體的陰影投射物體(因為這些物件對應的陰影接收物體被地表或者其他物件所遮擋。)

亮黃色區(qū)域標出的是foreground物件所創(chuàng)建的地平線以下部分,處于這個區(qū)域中的物體都是不可見的(即都是不需要用于繪制陰影的),因此這些物體在shadow map渲染中都可以剔除掉。

黃色區(qū)域上方的紅線在光源空間中的depth數(shù)據(jù)就是我們這里拿來進行occlusion culling的數(shù)據(jù)了。

如果對于相機空間depth buffer中的每個像素,都在這個像素對應的深度處繪制一個cube,之后從近平面向著這個立方體所在的深度延伸,就得到了這些cubes。

可以看到,圖中的綠色線條就是這些cube在光源空間中的最大深度對應的位置,而這些位置與前面給出的黃色區(qū)域上方的紅色線條是一致的。很顯然,如果真的為每個像素繪制一個cube,那么消耗會非常高,ACU這邊的做法是為每16x16個像素組成的tile繪制一個cube,之后使用每個tile中的最大深度用作遮擋剔除的depth,這樣做的結果就是綠線所表示的結果相對于此前紅線表示的結果會更為保守(從位置上來看,綠線會比紅色更為往下)。

另外,在將這些cube繪制到光源空間中時,需要注意修正cube的尺寸以保證繪制的結果不小于一個shadow map像素,且需要考慮shadow map的最大filter尺寸。

將相機空間的depth數(shù)據(jù)通過reprojection投影到光源空間。

ACU這里使用的方法跟[Silvennoinen2012]中的方法是非常相似的,不同的是原文中使用的depth mask,而ACU使用的是depth buffer,這是因為ACU中計算光源空間frontface的是box tile中的遠距面而非近距面,如果使用mask的話,得到的精度會低很多。

由于體積霧使用的是exponential shadow map,因此必須要考慮遠距離陰影投射物體的濾除作用(為什么ESM需要考慮這個,標準SM不用嗎?)。ACU在生成exponential shadow map的時候,會通過相機空間的深度經(jīng)過reprojected 后的數(shù)據(jù)來填充由于一些次要的陰影投射物體被culling掉(不能進行全量shadow map繪制,成本太高)導致shadow map上的孔洞。之所以要這樣做,是因為使用地平線來代替光源空間的遠平面來對陰影投射物體進行剔除的效果要好得多,且光源空間中某個區(qū)域的陰影投射物體距離光源越遠,這個效果就越好。

這里給出ACU實施管線的結果,基本達成目標;GPU可能還需要優(yōu)化下異步處理邏輯,增強GPU處理的并行性。

后面會考慮通過bindless texture進一步降低DP(為什么可以降低?),這樣做除了可以降低CPU使用率,同時還有助于降低由于合批mesh在距離上的順序并不嚴格所導致的GPU overdraw,當DP數(shù)較少的時候,渲染的順序就會再次接近于按照物體box中心點到相機的距離來排序時的表現(xiàn)(推測這里的渲染指的是cluster的渲染),也就是說,當DP數(shù)較低的時候,可以按照cluster到相機的距離來排序了(DP數(shù)多的時候不可以嗎?)

由于DX12&Vulkan極大的降低了DP的消耗,因此GPU渲染管線(如這里ACU介紹的實現(xiàn)算法)的優(yōu)勢將被抑制。而這種GPU渲染管線在DX12&Vulkan下是否能夠生效還取決于所用的數(shù)據(jù)結構與算法。后面部分將會給出一些可能的前進方向。


GPU驅(qū)動渲染管線在DX12上的優(yōu)勢:

1.在DX12中,尤其是PC上,CPU想要獲取到GPU的depth buffer等資源,依然具有較高的延遲-à因此不能根據(jù)可見像素的數(shù)據(jù)來對shadow進行剔除

2.CPU在數(shù)目超過100k的sub-object層面的剔除處理效率依然很低

3.GPU驅(qū)動的剔除算法更為高效。

- 參考: Modified Intel DirectX 12 asteroids demo with ExecuteIndirect. Runs faster on Intel GPU and has much lower CPU usage.

在后面加強異步計算邏輯的控制之后,ACU這邊希望能夠?qū)⒁恍〨PU驅(qū)動的管線中的非渲染的部分移動到異步計算邏輯中以移除由于眾多的細小異步計算任務所導致的pipeline bubbles。

下面要介紹的是RedLynx工作室(下面簡稱R工作室)的GPU驅(qū)動渲染管線,核心技術跟前面介紹過的Montreal的差不多,因此接下來將著重介紹兩者之間的區(qū)別。

首先要介紹的是Virtual Texture(以下簡稱VT)。這種技術方案能夠跟GPU驅(qū)動的渲染管線完美結合起來,從而可以極大的降低DP數(shù)目。

Deferred Texturing不是一個新的想法,不過當將之與VT以及GPU驅(qū)動的渲染管線相結合時,將煥發(fā)出新的生機。后面將會介紹著三種技術方案結合的實施細節(jié)以及R工作室的G-buffer layout,以及后面稱之為MSAA Trick的優(yōu)化方案。

R工作室的遮擋剔除方案跟AC此前的方案有所不同,這種新的方案稱之為Unity剔除系統(tǒng)。需要注意的是,R工作室的核心工作大多跟UGC(user generated content) 有關,因此會需要著重考慮多種不同技術方案的適配與篩選。此外,R工作的陰影貼圖管線也跟前面不一樣,其主要原理跟VT系統(tǒng)類似。

R工作室使用VT方案已經(jīng)有5年了,到目前為止已經(jīng)有兩款成功的產(chǎn)品應用了這項技術:,Trials Evolution and Trials Fusion

其中的核心思想是指將可見的貼圖數(shù)據(jù)維持在內(nèi)存中,主要通過將貼圖拆分成不同的小塊(tile)來實現(xiàn),這些小塊稱之為page。

Page ID會提供Page所需要的精確信息以及每個像素所需要的mip級別,ID數(shù)據(jù)會被加載到一個常數(shù)尺寸的Cache中(按照LRU算法進行更新置換)

R工作室的實現(xiàn)是基于256k^2虛擬地址空間實現(xiàn)的,所有的貼圖數(shù)據(jù)都會被適配到這個atlas中,這個atlas會被分割成128x128分辨率的page。

在運行時,有一張8k的貼圖atlas(7680×4320?)用于存儲當前需要駐守在內(nèi)存中的page數(shù)據(jù)。整個貼圖cache一共包含了5個數(shù)組元素(array slice)用于存儲所有需要的材質(zhì)屬性:基色,高光,粗糙度,法線等,貼圖cache采用的是DXT壓縮。

R工作室跟Montreal工作室的GPU驅(qū)動渲染管線的最大區(qū)別就在于VT的使用。

VT跟GPU驅(qū)動渲染管線能夠?qū)崿F(xiàn)完美契合,可以通過單張貼圖binding來實現(xiàn)所有的可見貼圖數(shù)據(jù)的訪問。

對于GPU驅(qū)動的渲染管線而言,這項特性非常重要,因為可以只用一個DP就允許GPU從任意張數(shù)的貼圖中讀取數(shù)據(jù)。也就是說,通過這項技術,物件的渲染就不需要進行合批了。

前面說過,GPU驅(qū)動渲染管線會一次性取得所有的mesh數(shù)據(jù),而通過VT技術,則可以一次性取得所有的貼圖數(shù)據(jù),也就是說,只用一個DP就可以完成全場景物件的繪制。

通過shader分支,可以實現(xiàn)不同的頂點動畫效果,在現(xiàn)代GPU上,shader分支的執(zhí)行效率很高,通過測試,使用shader分支實現(xiàn)三種不同的復雜動畫類型(包括蒙皮動畫)只有不超過2%的額外消耗。

使用一個DP繪制所有物體的做法有很多優(yōu)點:比如說可以將整個場景的物件cluster按照深度來排序,之后以cluster為基本單元按照從前往后的順序進行繪制。這種做法可以提供類似于early-z(depth prepass)的overdraw優(yōu)化效果,且不需要一個額外的渲染pass

相對于同類解決方案比如說bindless textures,VT還有一些其他的優(yōu)點:

1.可以將復雜材質(zhì)混合的結果以及貼花渲染結果存儲到VT atlas中

2.Atlas Cache可以將高帶寬消耗的操作均攤到多幀完成

(這些優(yōu)點對于場景編輯的同學來說是非常有幫助的,可以使用較低的消耗來得到非常豐富的表現(xiàn)效果。)

3.使用VT技術,可以不管實際上有多少張貼圖,最終繪制所需要的貼圖在內(nèi)存中的尺寸是恒定不變的:美術同學就不需要關注貼圖內(nèi)存預算,將精力專注在效果上。

Deferred Texturing并不是一項新技術,最早可以追溯到2007年的一篇論壇文章. 不過在此之前,貌似并沒有哪款游戲有使用過這項技術,其原因可能是無法實現(xiàn)高效而魯棒的貼圖像素數(shù)據(jù)的存儲與讀取。

Nathan Reed上一年的博客對此問題給出了一個解決方案,不過假設只考慮一些重要的特性比如說各向異性采樣的話,這個方案會將G-Buffer的存儲數(shù)據(jù)量增加到144bits,成本還是有點高。

而這里的GPU渲染管線的一項重要特性就是,所有可能可見的貼圖數(shù)據(jù)都被加載到一張8k的貼圖atlas中了。這張atlas對應的UV坐標足以實現(xiàn)對任意可見的像素的讀寫。這張atlas的dimension尺寸相對于巨大的256k virtual texture而言,可以算是非常小的,從而可以只使用16+16bit的UV坐標就能實現(xiàn)對任意像素的訪問,在這個情況下,texture filtering可以達到8x8的subpixel精度,而這個數(shù)值實際上已經(jīng)可以得到非常不錯的顯示質(zhì)量了。

場景的渲染,只有UV以及depth數(shù)據(jù)是不夠的,還需要考慮各向異性采樣的梯度數(shù)據(jù)以及光照計算所需要的tangent數(shù)據(jù)

梯度數(shù)據(jù)會占用不小的空間,因此這里的做法不存儲這項數(shù)據(jù)。幸運的是,由于G-Buffer中包含了UV坐標數(shù)據(jù),因此可以在屏幕空間中完成對梯度的計算。對于連續(xù)的表面而言,這種計算方法得到的效果是沒什么問題的,不過對于那些在深度上不連續(xù)的情況表現(xiàn)就不太好了。為了解決這個問題,這里給出的方案是,比較從正負xy軸上的相鄰像素數(shù)據(jù),并取其中UV坐標差異小的一個作為輸出,在這個計算原則上,相同表面的相鄰像素可以得到較高的優(yōu)先級。

如果選擇的相鄰像素的UV距離超過了設定的N像素閾值(其中N指的是各向異性采樣的等級),就認為此次搜索失效。在這種情況下,會將算法回退到雙邊線性濾波。實際上這種情況在幾何物體比較纖細的時候發(fā)生的頻率還挺高的,不過從實際表現(xiàn)上來看,并沒有構成很大問題,可能是因為雙邊線性濾波導致subpixel異常在濾波的過程中被模糊掉了。

光照計算所需的tangent數(shù)據(jù)會以32位歸一化的四元數(shù)(quaternion)的形式存儲,其中2位的alpha通道用于存儲主軸索引(major axis index)。VT技術可以免費讓我們得到每個page的屬性數(shù)據(jù),比如mip等級,材質(zhì)ID以及colorize color,這些數(shù)據(jù)不需要存儲到G-Buffer中。而Page索引可以通過UV除上128來得到。

總結:

每個像素64bits,在現(xiàn)代GPU上比如GCN可以得到Full fill rate,不需要使用MRT(multiple RT)

Deferred texturing技術跟VT技術可以很好的結合起來,UV buffer數(shù)據(jù)可以當成page ID來使用。如果部分像素的梯度向量長度低于0.5,就表示此時需要加載更高更清晰的貼圖page了。

這里是實施方案的一些截圖,可以看到梯度重建質(zhì)量跟ground truth非常接近了。

這里還為deferred texturing+VT結合的方案(命名為virtual deferred texturing,簡稱VDT)做了一些優(yōu)化工作,這些優(yōu)化統(tǒng)稱為MSAA trick。

這個trick是基于以下觀察結果得到的:不管是UV坐標,還是tangent數(shù)據(jù),都是可以通過頂點數(shù)據(jù)插值得到,且這種插值是無損的。因此可以將G-buffer數(shù)據(jù)用一個低分辨率存儲下來,之后在使用的時候?qū)θ笔У臄?shù)據(jù)進行插值求取即可。

使用MSAA繪制一個2x2的低分辨率結果,之后使用GCN可編程采樣pattern來構建一個有序的網(wǎng)格,這個網(wǎng)格正好與高分辨率的結果貼圖的像素中心相匹配。

在進行光照計算的時候,會通過multisample加載指令加載G-Buffer中任意的MSAA樣本數(shù)據(jù)

在lighting計算compute shader開始之前,會構建一張1080p的G-buffer貼圖,并使用快速的LDS(這個是啥?Local Data Store)來存儲這張臨時貼圖。

MSAA會通過硬件完成PS計算,被多個三角面片覆蓋的像素,其結果會被存儲多次,這種做法是有必要的,因為可以保證三角形edge兩邊的sample frequency數(shù)據(jù)是有效的,從而可以按照屏幕分辨率(native resolution)實現(xiàn)對edge的重建。

由于MSAA已經(jīng)可以兼顧triangle edge效果了,這里只需要完成三角形內(nèi)部的插值計算就可以實現(xiàn)1080p的UV坐標以及tangent數(shù)據(jù)的重建。不過由于插值并不是perspective correct,因此相對于直接按照屏幕分辨率渲染的結果而言,可能會有一些輕微的差異。

Benchmark使用的是128bits/pixel的G-buffer格式,并將四個2xMSAA像素編碼成一個8xMSAA像素,其最終質(zhì)量與2xMSAA比較接近。

PS waves的數(shù)量削減達到一半,而渲染時間消耗削減達到30%,此外DRAM訪問量也有所削減,這是因為這里給出的算法可以將訪問最多的MSAA subsample plane數(shù)據(jù)遷移到ESRAM中(不太明白是啥意思,硬件改動嗎?)。

接下來介紹下R工作室的遮擋剔除實現(xiàn)技術,其方案跟ACU的有所不同。

最大的區(qū)別在于,R工作室的遮擋剔除方案不需要一個額外的遮擋物體pass(occlusion geometry pass),這是因為R工作室這邊需要逐像素精確的遮擋結果,因此粗糙的遮擋剔除結果并沒有什么意義。

R工作室這邊的項目通常沒有比較大的結構性物體,場景中的大物體通常都是玩家用小物體搭建出來的,比如說一堵墻可能是由若干個細小物體搭建而成,中間可能會包括若干孔洞,如果使用一些低模物體來構建遮擋幾何體的話,最終的表現(xiàn)可能比較差,且玩家也無法理解為什么會這樣。

因此R工作室的遮擋剔除數(shù)據(jù)是基于G-Buffer的深度數(shù)據(jù)計算得到的,從深度buffer生成一個深度金字塔(pyramid),按照GCN HTILE min/max深度buffer方式生成,這種做法可以使得金字塔的生成算法比普通的全分辨率遞歸下采樣算法快12倍。

而遮擋剔除測試則是使用一個gather4采樣指令一次性獲得四個采樣結果的方法進行的。

整個culling跟rendering可以分成兩個階段:

第一個階段使用上一幀創(chuàng)建的深度金字塔數(shù)據(jù)實現(xiàn)視椎剔除與遮擋剔除,這個階段可以看成是一個occlusion hint(什么意思?可以看成是一輪預篩選)。對于通過這個檢測的物體,會以cluster作為基本粒度進行視椎,back-face以及遮擋測試,并將cluster ids輸出到渲染所需要的buffer中。

在第二個階段,會先使用最新繪制完成的partial frame(是部分物體深度結果嗎)對深度金字塔進行更新。對于所有通過其他測試但是沒通過遮擋測試的物體以及cluster,會再進行一次遮擋測試,將那些之前檢測錯誤的物體添加到渲染列表中(為啥不一步到位,直接在這里做遮擋檢測呢?因為這里遮擋檢測的遮擋物來源于當前幀已經(jīng)繪制的物體,因此需要先繪制一遍,而繪制的數(shù)據(jù)來源于第一階段的檢測結果)。

如果不使用GPU驅(qū)動的渲染管線的話,這種剔除方法是很難做到的,因為需要在之前的步驟中以低時延拿到GPU生成的數(shù)據(jù),如果中間夾雜了CPU的數(shù)據(jù)訪問就會嚴重阻塞GPU的工作流。

為了測試剔除效果,這里做了一個壓力測試,在整個場景中渲染了二十五萬個移動物體,對于現(xiàn)存的粗糙遮擋剔除系統(tǒng)而言,這些分散的大量物體絕對是個噩夢。

這個Bencmark使用的是DX渲染路徑,每個cluster使用64個頂點組成。對于這么高面片場景而言,使用MultiDrawIndirect 接口可能沒什么收益。

在光照shader中,物體的貼圖用VT來實現(xiàn),整個場景的繪制只需要兩個DP。

從結果上可以看到,GPU驅(qū)動的剔除以及setup過程消耗很低,加起來不超過0.5ms。需要注意的是,這些時間并不是損耗了的GPU時間,以cluster為粒度的剔除算法在G-buffer步驟中省下來的時間足以彌補GPU驅(qū)動的管線的所有消耗。

在R工作室此前的游戲比如Trials Fusion中,這里列舉的這些工作會占據(jù)大概50%的CPU時間,而現(xiàn)在只需要單核GPU 0.2ms的時間就能完成,因此用這種方法可以騰出CPU給游戲邏輯,物理仿真,破壞等,以創(chuàng)建更豐富生動的游戲表現(xiàn)。

對于陰影渲染,R工作室這邊選取的方法跟此前Montreal給出的方法有所不同。

2001年的時候,F(xiàn)ernando給出了一種稱之為Adaptive Shadow Maps的實現(xiàn)技術這種技術將陰影貼圖分割成細小的tiles,之后根據(jù)距離的不同為每個tile選定不同的分辨率

這種技術的問題在于,需要在渲染的過程中將深度數(shù)據(jù)從GPU傳送到CPU,在CPU中,會對深度數(shù)據(jù)進行分析,并更新culling structures,之后據(jù)此進行shadow rendering,這個過程會極大的阻塞工作流。

而如果使用GPU驅(qū)動的剔除算法,就可以借助類似于VT的virtual shadow mapping方法解決這個問題,因為在這個過程中將不再需要CPU的參與,且能夠移除shadow map tile page邊緣上的頂點處理消耗。DX12允許在VS中訪問RT array index,從而可以不使用geometry shader,也可以一次性渲染所有的shadow pages。

Virtual Shadow Mapping方案輸出的貼圖結果具有最小數(shù)量的under- & oversampling,Shadow map的分辨率將在任意區(qū)域都能夠與屏幕分辨率實現(xiàn)最佳匹配。

最終的性能表現(xiàn)令人非常滿意,尤其是在復雜場景中。在之前給出的25萬面片的超復雜場景中,測試的結果顯示相對于SDSM(標準shadow map方法)而言,這種方法具有3.5倍的優(yōu)勢(怎么定義的)。

至于后面更進一步的優(yōu)化,比如XOR hashing頁面的數(shù)據(jù)重用等,還能夠進一步提升此方案的運行速度。

DX12提供了很多新的特性,而其他的API在PC上也有一些重要的特性,除此之外,相對于下一代主機,還有一些特性是DX以及其他PC API沒有提供的,不過在DX 12_3中將會增加這些特性。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容