<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 剖析虛幻渲染體系(14)- 延展篇:現代渲染引擎演變史Part 4(結果期)

     

     

    14.5 結果期(2016~2022)

    時間晃蕩一下來到了2016年之后,這時期的游戲引擎朝著影視級、更加物理、更高效的方向發展。且看后面分析。

    14.5.1 圖形API

    14.5.1.1 DirectX

    DirectX在2016年之后主要發力于DirectX 12及相關子本版的更新,包含ID3D12Device、Shading Model 6.x、光線追蹤、VRS、根簽名、資源、同步、調試、PSO、命令列表、HLSL等方面的新增或改進。具體可參見:DirectX - WikipediaWhat's new in Direct3D 12

    14.5.1.2 OpenGL

    OpenGL在2016年之后發布了4.6版本,新增或改進了SPIR-V、Multi-draw indirect、頂點著色器獲取數據、統計和轉換反饋溢出查詢、各向異性過濾、夾緊多邊形偏移、創建不報錯的OpenGL上下文、原子計數器的更多操作、避免不必要的發散著色器調用等特性。

    而OpenGL ES在此期間幾乎紋絲不動。

    14.5.1.3 Vulkan

    在2016年及之后,Vulkan發布的版本、時間和特性如下表:

    版本 時間 特性
    Vulkan 1.3 2022-01 查詢給定物理設備可用的擴展、Vulkan Profiles工具解決方案、改進多線程應用程序的驗證層性能、新的擴展。
    Vulkan 1.2 2020-01 時間軸信號量、形式化內存模型、線程同步和內存操作語義、描述符索引、多個著色器重用描述符布局、HLSL著色器的深入支持。
    Vulkan 1.1 2018-03 subgroup、渲染和顯示無法訪問或復制的資源、多視圖渲染、多GPU、16位內存訪問、HLSL內存布局、YCbCr顏色格式、SPIR-V 1.3。

    14.5.1.4 Metal

    Metal作為Apple的自家圖形API,2017年6月5日,蘋果在WWDC上發布了第二個版本的Metal,由macOS High Sierra、iOS 11和tvOS 11支持。Metal 2不是Metal的單獨API,由相同的硬件支持。Metal 2能夠在Xcode中實現更高效的評測和調試,加速機器學習,降低CPU工作負載,支持macOS上的虛擬現實,尤其是Apple A11 GPU的特性。

    在2020年WWDC上,蘋果宣布將Mac遷移到Apple Silicon。使用Apple Silicon的Mac將采用Apple GPU,其功能集結合了之前在macOS和iOS上可用的功能,并將能夠利用針對Apple GPU基于分塊的延遲渲染(TBDR)架構定制的功能。

    Metal旨在提供對GPU的低開銷訪問,命令預先編碼,然后提交給GPU進行異步執行。應用程序控制何時等待執行完成,從而允許應用程序開發人員在GPU上執行命令時通過編碼其它命令來增加吞吐量,或者通過顯式等待GPU執行完成來節省電源。此外,命令編碼與CPU無關,因此應用程序可以獨立地對每個CPU線程的命令進行編碼。渲染狀態是預計算的,允許GPU驅動程序在執行命令之前提前知道如何配置和優化渲染管線。

    Metal為應用程序開發人員提供了創建資源(緩沖區、紋理)的靈活性。可以在CPU、GPU或兩者上分配資源,并提供更新和同步已分配資源的功能。Metal還可以在命令編碼器的生命周期內強制執行資源的狀態。在macOS上,Metal可以讓應用程序開發人員自行決定要執行哪個GPU,可以選擇CPU的低功耗集成GPU、分離式GPU(在某些MacBook和Mac上)或通過Thunderbolt連接的外部GPU。應用程序開發人員還可以優先選擇在哪個GPU上執行GPU命令,并提供某個命令在哪個GPU上執行效率最高的建議。

     

    14.5.2 硬件架構

    在2016年之后,Nvidia先后發布了Pascal、Volta、Turing等架構的GPU,其中Turing架構配備了名為RT Core的專用光線追蹤處理器,能夠以高達每秒10 Giga Rays的速度對光線和聲音在3D環境中的傳播進行加速計算。Turing架構將實時光線追蹤運算加速至上一代Pascal 架構的25倍,并可以以高出CPU30多倍的速度進行電影效果的最終幀渲染。

    Turing架構的SM結構圖。包含了用于光線追蹤的RT Core和用于AI的Tensor Core。

    Multi Adapter Integrated + Discrete GPUs由Intel呈現,闡述了2020年適用于集成和分離GPU的多適配器技術,包含集成+分離的機會、D3D12多適配器背景、實用非對稱多GPU等。

    集成圖形的機會:許多游戲PC既有集成的GPU,也有離散的GPU,集成電路通常是空閑的,集成圖形需要大量計算!而且Intel有一個擴展,可以幫助提高性能。然而,D3D12多適配器有許多缺陷,對于一類算法,有一種方法可以利用集成的GPU,通過適度的工程努力獲得更高的性能。

    D3D12多適配器支持,D3D支持多GPU的兩種方式:

    • 鏈接顯示適配器(LDA, Linked Display Adapter)。顯示為帶有多個節點的一個適配器(D3D設備),跨節點/在節點之間透明地復制或使用資源,通常是對稱的,即同一個的GPU。
    • 顯式多適配器。跨適配器共享資源有很多限制,可能是不對稱的,這正是Intel正在做的。

    多適配器方法:

    • 共享渲染:分割幀、交替幀、棋盤格,非對稱GPU只有很低的投資回報率。
    • 后處理:CMAA、SSAO、相機效果…需要跨PCI總線兩次。
    • 遮擋剔除、物理、人工智能。生產者-消費者,從渲染運行異步時效果更好。

    集成圖形存儲器是系統存儲器,iGPU可以使用跨適配器共享資源,幾乎不受損害:

    D3D12跨適配器的資源:資源由綁定到適配器的D3D設備分配,如何在適配器之間傳輸數據?共享的、跨適配器的資源,必須放置在跨適配器共享堆中。堆創建的步驟:調整數據大小,在任意設備上創建共享堆,創建句柄。

    跨適配器資源創建:打開第二臺設備上的句柄,對兩個適配器都在跨適配器堆中創建放置的資源,使用相同的對齊方式和大小。

    粒子案例中的異步計算:乒乓緩沖區保存源狀態和目標狀態,讀取初始狀態,計算下一個狀態,呈現結果,通過在計算下一個狀態時呈現前一個狀態來并行化狀態(異步計算),緩沖區計數與交換鏈長度匹配。

    多適配器的添加復制階段:想法是每個適配器都是乒乓緩沖區,形成并行管線:2個狀態n的讀取者,計算狀態n+1,渲染狀態n-1。

    多適配器的Pong:每幀交換緩沖區。

    資源分配:渲染緩沖區使用局部適配器堆(Default、Committed),適配器離散存儲器,適配器首選布局。計算緩沖區使用跨適配器堆、放置的交叉適配器,CPU存儲器、線性布局。

    復制隊列:關鍵想法是分離適配器上的復制隊列,從系統內存復制到分離內存,集成內存是系統內存,所以這是一種邏輯安排,顯式復制階段松散的時序。

    利用這種耦合+分離的多GPU架構,可以更加高效地處理資源創建、跨適配器同步、命令隊列等操作。獲得的結果在GpuView顯示并行運行的階段,兩個例子,全屏應用,當3D占主導地位時,就有時間進行計算和復制,在某些情況下,復制可能占主導地位(幀時間>渲染+計算):

    在以下情況下,此技術最適用:渲染GPU已飽和,純生產者-消費者(數據只通過總線一次),任務可以完全卸載(無需協作),渲染不等待(管線有呼吸空間),最好是允許計算>1幀,許多異步計算任務都符合這種模式。請注意PCIe帶寬:Gen3 x16是16GB/s,400萬個粒子,每個粒子float4:64MB,16GB/64MB=256Hz最大幀速率,一些GPU/Config是x8:半帶寬,盡可能降低數據傳輸大小!按使用情況拆分數據緩沖區具有性能優勢。這個方案不僅適用于粒子,還可以是物理、網格變形、人工智能、陰影,許多異步計算任務都符合這種模式,檢查英特爾集成圖形!


    14.5.3 引擎演變

    14.5.3.1 綜合演變

    2016年,Towards Cinematic Quality, Anti-aliasing in Quantum Break談及了影視級的質量和抗鋸齒技術,比如間接照明、參與介質、幾何體鋸齒和鏡面鋸齒。

    Light pre-pass就像完全延遲一樣,想法是將照明與幾何體分離,但與完全延遲不同的是,幾何圖形繪制兩次。第1個Pass寫入照明所需的一切數據,第2個Pass讀取光源,并添加其它材質。

    Light pre-pass的優勢是用于照明的幾何緩沖區占用少,易于在第2個幾何通道上添加材質變化,通常在第2個幾何通道中與MSAA結合。第2個幾何通道決定使用什么樣的照明采樣,常用的方法是將當前幾何圖形與以前的幾何圖形進行比較,繪制幾何緩沖區并選擇最接*的匹配。當有匹配時,效果非常好,當沒有好的樣本可供選擇時,就會出現問題。在頭發和樹葉著色器中,可以使用在alpha測試輸出中使用棋盤格模式,但alpha測試輸出上的棋盤格圖案難以控制,要么在半分辨率照明下運行,要么開始丟失樣本。多層的alpha測試讓問題變得更加困難。

    保存相關樣本是需要解決的關鍵問題,希望可以使用4xMSAA幾何緩沖區,但直接照明開銷太大。文中給出的MSAA解決方案是使用幾何分簇(Geometry Clustering),將幾何緩沖區渲染到4xMSAA目標,將樣本從4xMSAA減少到非MSAA(從4個樣本減少到1個樣本),在減少的樣本上運行昂貴的著色,接著重建為4x MSAA,更多的幾何樣本(成本相對較小),更少的光照樣本(計算繁重)。具體過程如下:

    • 構造亞像素偏移。

      • 計算16個樣本的哈希值。32位值,其中16位法線值、8位深度、8位材質ID。

      • 選擇初始的4個樣本。首先在角落里選擇四個樣本,通過選擇角落,最大限度地增加獨特樣本的數量。

      • 重新分配樣本。對于每個選定的樣本,測試是否存在重復,如果存在,用未選擇的值替換重復項,確保避免不必要的重組,與屏幕保持一致是有道理的。

      • 保存亞像素偏移。每2x2像素區域有32位,每個MSAA樣本有2位,提供最終2x2輸出上的采樣位置,像素著色器輸出一半大小的360p圖像。

    • 下采樣。

      • 讀取亞像素偏移。每個2x2的tile共享單個32位亞像素偏移,在普通像素著色器中運行全屏通道。輸出是最終的幾何緩沖區,將用于AO、SSR、GI和照明。
      • 先寫入或*均值。輸出第一個匹配項,*均值給出的結果稍好一些。

    幾何分簇和非MSAA的效果對比:

    在處理第2個幾何通道時,亞像素偏移提供每個MSAA采樣使用哪個光照樣本,讀取基于2位偏移的光照樣本。無需計算當前樣本的幾何特性,以便與幾何緩沖區進行比較。在像素著色器中使用樣本覆蓋率(HLSL中的SV_Coverage),使用覆蓋樣本的*均值可以提高質量,但成本要高得多,文中使用firstbitlow和采樣一次。

    幾何分簇、非MSAA和TAA的效果對比:

    文中還涉及了時間抗鋸齒和上采樣。具有亞像素相機偏移的4幀,使用旋轉的網格偏移。每幀將前三幀變換為當前投影,以盡可能減小幀間的增量,在單個計算著色器通道中共享夾緊的數據。對于TAA的夾緊,在當前幀計算相鄰像素的最小/最大值,擴大了速度小的范圍,以抑制亞像素偏移相機引起的閃爍。夾緊xyY并只擴展亮度,以便均衡鋸齒和鬼影。對于TAA的上采樣,組合4幀的結果,之前的幀已被重投影和夾緊,由于幀之間的采樣是交錯的,因此是上采樣的良好位置,線性采樣的效果出人意料地好。

    另外,還大量使用了周期性噪點,因為TAA是4個連續幀的*均值。噪點過大可能會導致鄰域夾緊出現問題,使用累積緩沖器降低噪點,在序列噪點中效果良好。

    總之,具有4x MSAA的幾何緩沖區,為720p圖像提供了370萬個分布良好的幾何體樣本。在使用較少樣本計算照明時,需要進行高質量的下采樣,以保留相關數據,下采樣產生的亞像素偏移可用于其它效果。MSAA仍然與幾何體鋸齒相關,擁有更多更好的分布樣本非常有意義。存儲和重投影N個歷史幀,為TAA和高端提供了一個良好的框架,輸入過多變化的TAA會破壞系統,每幀都需要足夠穩定。TAA將鋸齒轉換為鬼影,更緊的夾緊邊界可能會導致閃爍。

    D3D12 & Vulkan: Lessons Learned在DX12和Vulkan發布的一年后呈現的演講,闡述了基于新生代圖形API的引擎架構演變以及帶來的影響。D3D11驅動程序經過了很好的優化,利用掌握的知識可以超越D3D11驅動程序,發明D3D12并不是為了在上面編寫遺留API驅動程序。若將游戲引擎比作火箭,則DirectX 12和Vulkan可視作助推器:

    在沒有DX12和Vulkan的圖形API之前的游戲引擎可視作是0~0.5階段,沒有加速器:

    若融入DX12和Vulkan之后,有了一定的加速器,此時升級到了1.0階段:

    但此時的渲染器和圖形API之間耦合過多,不夠高級別或低級別,只能看作是有限加速的2.0:

    將圖形API下移,抽離出更底層的圖形抽象層之后,則可以升級到3.0,獲得了更大的助力器:

    當時的游戲引擎正在過渡以支持Vulkana和D3D12,仍然需要D3D11支持,大多數引擎處于第1階段和第2階段之間。要充分利用所有API,需要進行大量思考,多隊列支持需要額外的工作,需要兼容到D3D11,建議以D3D12/Vulkana為目標,并在D3D11上運行。

    面向未來的設計,本文將指出常見的設計問題,把引擎準備好,將知識轉化為更好的表現。

    屏障控制:屏障是D3D12/Vulkan中的一個新概念,悲哀的事實是每個人理解錯了。兩個失敗案例:太多或太寬導致性能差,缺失的屏障導致損壞(Corruption)。D3D11驅動在引擎下完成了屏障的這項工作——而且做得很好。到底什么是屏障?

    • 將目標渲染轉換為紋理。可能需要解壓(和緩存刷新),供應商和GPU次代之間會發生什么變化?可能是無操作,可能是等待空閑,可能是完全緩存刷新。
    • 將UAV轉換成資源。如果做得不好,則需要產生刷新或等待空閑,如果操作正確,這些轉換可以是免費的。

    缺失的屏障:格式問題——GPU/特定于驅動程序的損壞,同步問題——依賴時間的損壞。

    子資源(Subresource)需要單獨跟蹤,如下采樣、陰影圖集。如果轉換所有的子資源,應該使用D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,而不是逐個轉換。對于放置資源和初始狀態,使用前必須清除作為放置資源等創建的渲染目標,直接進入清除狀態,不要從一些隨機狀態和轉換開始。下圖存在“基本狀態”或冗余轉換,即轉換到目標狀態然后恢復回基本狀態(沒有實際使用,只是轉換回來了):

    有意思的屏障:

    • ResourceBarrier(0, nullptr),即沒什么變化,表明狀態追蹤做了錯誤的事情
    • 前一狀態等于下一狀態。發生的事情比你想象的要多——說不就行了。
    • 永遠記住——驅動假設你做的是最佳的事情,而不是通過驅動自身的任何啟發式!

    不必追蹤所有資源狀態,99%的資源是不可變的——只讀,找到過渡點——通道的結束時間,在此處批處理屏障,只轉換必需的屏障。

    屏障調試技巧:

    • 有一個寫/讀位。
    • 記錄所有轉換,Grep和spreadsheet是你的朋友,檢查轉換的數量和類型等。
    • 轉換的數量應按可寫資源的數量排序,再說一遍,grep和log是你的朋友,如果屏障數量超過9000,就有些可疑了!
    • 存在每件事都有一個屏障(barrier-everything)的模式,與前面描述的“最壞情況”模式相同,但僅供調試使用
    • 確保資源每幀至少處于已知狀態一次,例如在幀結束/開始處,將所有事物轉換為已知狀態,從而解決TAA或陰影圖集破壞等問題。

    更好的是,給驅動一定時間處理轉換,用D3D12中的“Split barrier”,Vulkan的vkCmdSetEvent+vkCmdWaitEvents。

    屏障總結:確保轉換所有需要的資源(但不是更多),進入能進入的最具體的狀態,可以把不同的狀態聯合起來。

    運行控制,如何為GPU喂食(feed,即發送任務),首先提交命令列表,每幀資源更新和跟蹤時間。不要通過手動分配內核來限制并行性
    ,使用任務/作業系統,自動使用所有內核,需要特別注意高效的工作提交和資源同步。

    上圖發生了什么?瘋狂的線程池,CPU任務在最后提交工作,任務邊界成為CPU/GPU同步點,任務完成后控制命令列表。

    上圖:每個柵欄基本上都是在GPU上等待IDL(或多或少)。更好的做法:保護每幀資源,無論如何,都不太可能在命令列表的“中間幀”開啟工作,,用一個柵欄保護多個資源。確保作業系統能做到這一點,盡可能多地批量提交,提前提交,使GPU始終處于繁忙狀態。理想的提交如下:

    使用多線程設計,恰當地了解工作量和調度:

    渲染通道:為幀建立一個高層次的圖,通過Vulkan的渲染通道和子渲染通道告訴渲染器,允許驅動選擇最佳的調度。允許你很好地表達“don’t care”。

    調試提示:可以選擇在一次提交中提交所有命令列表,有助于解決時序問題,如果不可能,則需要幀內GPU/CPU的同步。可以選擇等待任何命令列表,有助于上傳/資源同步,有些資源被破壞了?更新前刷新GPU。

    提交總結:以每幀粒度追蹤資源,理解幀結構,線程化對于獲得良好的CPU利用率至關重要。

    Practical DirectX 12 - Programming Model and Hardware Capabilities主要闡述了DX12的最佳實踐、硬件特性等內容。

    DX12旨在實現最大的GPU和CPU性能,能夠投入工程時間!引擎注意事項,需要IHV特定路徑,如果做不到這一點,請使用DX11,應用程序替換部分驅動程序和運行時,不能指望同樣的代碼在計算機上運行良好,所有的控制臺、PC都是一樣的,考慮特定于架構的路徑,注意英偉達和AMD的細節。

    工作提交方式有多線程、命令列表、命令束(bundle)、命令隊列等。

    多線程處理在DX11驅動程序:渲染線程(生產者)、驅動程序線程(消費者),在DX12驅動程序:不會啟動工作線程,通過CommandList接口直接構建命令緩沖區。確保引擎在所有的核心上都有擴展,任務圖體系結構效果最好,一個提交命令列表的渲染線程,多個并行構建命令列表的工作線程。

    命令列表可以在提交其它命令列表時構建命令列表,提交或呈現時不要閑著,允許重復使用命令列表,但應用程序負責停止并發使用。不要把工作分成太多的命令列表,目標是每幀15-30個命令列表、5-10個ExecuteCommandLists調用。每個ExecuteCommandLists都有固定的CPU開銷,每個ExecuteCommandLists調用會觸發一個刷新,所以,把命令列表批處理起來,嘗試在每個ExecuteCommandList中放置至少200μs的GPU工作,最好是500μs,提交足夠的工作以隱藏操作系統調度延遲,對ExecuteCommandLists的小調用完成得比OS調度器提交新調用的速度更快。例如下圖:

    高亮顯示的ECL執行時間約20μs,操作系統需要約60μs來調度即將到來的工作,等于有40μs的空閑時間

    命令束(bundle)是在幀中盡早提交工作的好方法,GPU上的束本身沒有更快的速度,需要明智地使用它們!從調用命令列表繼承狀態–充分利用優勢,但協調繼承狀態可能會有CPU或GPU成本。可以獲得很好的CPU提升的是NVIDIA:重復同樣的5+draw/dispatche用一個bundle,AMD:只有在CPU方面遇到困難時才使用bundle。

    硬件狀態包含管線狀態對象(PSO)和根簽名表(RST)。PSO對未使用的字段使用合理且一致的默認值,不允許驅動程序執行PSO編譯,使用工作線程生成PSO,編譯可能需要幾百毫秒。在同一線程上編譯類似的PSO,如具有不同混合狀態的相同VS/PS,如果狀態不影響著色器,將重用著色器編譯,同時編譯相同著色器的工作線程將等待第一次編譯的結果。RST需要盡量保持小,按頻率排列。

    內存管理方面,需要謹慎和合理使用命令分配器、資源、駐留等操作。另外,還需要謹慎處理柵欄、屏障內同步操作。

    GDC2017: D3D12 & Vulkan: Lessons learned是GDC2017上分享的DX12和Vulkan的學習課程。文中提到了引擎使用任務圖的變革圖例:

    在屏障方面,手動操作不再有效,需要更高層次的抽象圖,從第一天開始在Vulkan提供原生支持。著色器方面,著色器排列越來越少,Doom只有幾百個,更多的游戲正在改變創作流程,以便更早地刪減著色器變體,更多的高級工作(圍繞編譯器)正在進行。

    引擎向更高級別的渲染發展,API的改進使其更易于使用,游戲受益!開放游戲/可伸縮性,可伸縮性還沒有解決,游戲支持所有設置的新舊API,移動*臺越來越重要,新的API似乎只是前進的道路。圖形API的一種新方法需要ISV、IHV和標準機構之間的緊密合作,API隨著游戲引擎的發展而發展,自發布以來,大量的變化讓開發人員更輕松!

    DirectX 12 Case Studies由NVidia呈現,講述了DirectX12的相關技術和應用案例,文中涉及DX12的技術包含異步隊列、內存管理、管線狀態對象、著色器模型5.1資源綁定、多線程及其它。

    對于異步隊列,計算隊列可以很好的跨供應商加速,*均5%的提升,異步工作負載主要與分辨率無關(針對1080p進行了調整),隨著分辨率的增加,收益遞減。

    引擎使用3個復制隊列,多個復制隊列簡化了引擎線程同步:

    SM 5.1和/all_resources_bound著色器編譯器標志將性能提高約1.0到1.5%,無需更改著色器代碼,為紋理訪問啟用不太保守的代碼生成。

    Ubisoft的Anvil Next引擎:重新設計以充分利用DX12,最大限度地減少和批處理資源屏障,充分利用并行CMD列表錄制,使用預編譯渲染狀態以最小化運行時工作,最小化內存占用,利用幾個GPU隊列。

    Anvil Next引擎分組屏障圖例。

    Hitman渲染器處理完全動態的場景,例外情況是在水*負載期間產生反射和環境探針。使用分塊延遲照明,前向光源使用單獨的通道,用于剔除照明的門/室(入口/單元)系統。陰影用4個VSM級聯,第4個是靜態的,4~8個額外的陰影貼圖。

    對于CPU性能,代碼可以像引擎代碼一樣進行分析和修復,設置了太多多余的描述符,確保只設置實際使用的描述符。次優的合批:最終批量處理資源轉換和命令列表提交,最終通過多線程與最快的驅動程序相匹配。

    當時的DX12內存管理也存在問題,DX12視頻內存消耗過高(與DX11相比),DX11驅動程序非常擅長在視頻內存之間移動內存。最終實現了一個分頁(逐出)資源的系統,使用非常簡單的LRU模型,很多工作都是出乎意料的,仍然不理想,因為有MakeResident的卡頓,在DX12上的性能更差,尤其是在低顯存的GPU卡上。已實現的渲染目標內存重用系統(放置的資源),引入靜態資源的子分配,為所有內容創建提交的資源將占用大量內存,PC上的內存節省不如控制臺上的高,資源層(Resource tier)防止所有內存被用于各種資源。

    DX12資源分配和圍欄:需要一個超級快速的分配器來實現幀資源的無鎖分配,在一個幀中有大量動態資源分配,如描述符、上傳堆等。柵欄開銷也很大,嘗試將它們用于資源的細粒度重用,最終使用一個信號柵欄來同步所有的資源重用。

    DX12狀態管理:使用像素著色器對象存儲PSO和其它狀態,絕大多數像素著色器只有少量排列,可通過哈希訪問的排列,已從狀態管理中刪除采樣器狀態對象,決定使用16個固定采樣器狀態。

    為每個狀態創建唯一的狀態哈希,將所有狀態塊放入具有唯一ID的池中,狀態塊包括光柵化器狀態、著色器等,使用塊ID作為位來構造狀態哈希。

    DX12資源轉換:實現了簡化的資源轉換系統,假定讀取/SRV為默認狀態,僅支持轉換到RTV、UAV、DSV和轉回,僅UAV屏障需要額外的轉換。主渲染線程提交所有轉換,主渲染線程還可以記錄命令列表,并執行所有多線程同步,大大簡化了代碼。

    3A游戲”的DX12內存管理:顯式內存管理是實現出色且一致性能的關鍵,LRU資源管理戰略有很長的路要走,在上次使用資源后,將其保存在內存中一段時間,只有當它被驅逐時才帶進回收堆里。支持資源綁定tier 2,不使用時,表中的CB描述符必須解除綁定(設置為0),Nvidia驅動程序現在支持未清除的描述符,將CBV移動到根簽名中以跳過解除綁定,當用作根CBV時,CBV只是一個GPU地址,無需調用CreateConstantBufferView()。確保對所有RST條目使用最佳著色器可見性標志,盡可能避免Avoid SHADER_VISIBILITY_ALL標記,在CPU內存中緩存RST狀態以跳過冗余綁定,從而提高CPU性能。結果證明,應最小化RST的變化,使用整個幀的兩種布局。對于屏障,最初的DX12路徑有冗余屏障,屏障隱藏在抽象層中(自動觸發),大部分時間都有效,對于特殊情況,引擎將切換到顯式屏障管理,延遲屏障用于跳過進一步的冗余和合批屏障,將屏障添加到待處理列表中,等到最后一刻刷新列表,過濾掉冗余。

    調試GPU:根據API(CPU)的錯誤代碼檢測到崩潰,在最后N幀命令中發生了崩潰…CPU調用堆棧很可能是個麻煩。


    NVIDIA AFTERMATH可以提高GPU崩潰位置的準確性,使用命令流內聯用戶定義的標記,一旦到達每個標記,GPU就會發出信號,最后到達的標記表示GPU崩潰位置。

    NVIDIA AFTERMATH幫助診斷GPU崩潰的新工具(標頭+DLL),非常靈活/簡單的API,當前與兼容DX11和DX12 UWP和/或Windows。它的局限性是需要NVIDIA GeForce驅動程序版本378.xx及以上,與D3D調試層不兼容。

    Advanced Graphics Tech: Moving to DirectX 12: Lessons Learned也詳細地闡述了育碧的Anvil Next引擎移植到DirectX12的具體過程、優化、技術、策略、經驗教訓等內容,諸如生產者消費者系統、調度圖系統、屏障轉換和優化、資源管理、資源依賴、內存重疊、資源同步、著色器、PSO等等內容。

    總之,利用高級渲染過程知識優化API使用,高級生成系統節省了大量渲染工程師調試時間,粒度更小、基于blob的渲染接口,最大化CPU性能增益,避免重復工作,架構工作將使其它*臺/API受益。利用DX12可獲得約5%的GPU提升,15%~30%的CPU提升。

    實現與DX11性能的對等是一項艱苦的工作,不要將性能視為最終目標,將努力視為通往:解鎖異步計算、mGPU、SM6等功能,比以往任何時候都更接*與控制臺的功能對等,改善引擎架構的機會,之后移植到其它等效的API要容易得多。

    更多關于DX12和Vulkan的知識可參閱:剖析虛幻渲染體系(13)- RHI補充篇:現代圖形API之奧義與指南


    Developing The Northlight Engine: Lessons Learned說明了Northlight引擎開發的DX12移植的過程和經驗教訓。Northlight引擎的渲染管線:

    • G緩沖、速度、陰影通道(多線程)。
    • 全屏陰影。
    • 全屏照明。
    • 主要、透明通道(多線程)。
    • 后處理。

    DX12特性清單包含描述符表、動態資源、管線狀態對象、命令列表/分配器、資源轉換、暫存資源、小型資源、mipmap生成、空資源、計數/追加緩沖區、查詢等。下面對它們進行闡述。

    • 描述符表:是一個表包含任何著色器階段可能使用的所有資源的描述符,每個draw調用都需要一個表,一旦在GPU上完成draw,就可以重復使用。
    • 動態資源:DX12中沒有這種東西,需要自己管理版本控制/重命名/輪換,寫一次(CPU),讀一次(GPU)使用上傳堆環緩沖區。
    • PSO:創建是有問題的部分,理想情況下,在出口管線中輸出,在游戲開始時加載,在第一次接觸它們時就創建了它們,CS PSO可以在CS加載下生成,約500個單獨的圖形PSO需要約200毫秒才能生成。包含根簽名(資源布局)、著色器代碼、頂點著色器輸入布局(不是頂點/索引緩沖區)、基本類型、混合、光柵狀態、MSAA模式、渲染目標和深度模板格式等。
    • 命令列表/分配器:DX11中的即時/延遲上下文,分配器擁有內存。
    • 資源轉換:驅動不再跟蹤使用情況,使用前必須手動轉換至正確狀態:著色器資源、渲染/深度目標、復制源/目的地、UAV、呈現等。
    • 暫存資源/更新子資源:沒有動態資源,更頻繁地使用暫存資源,按需從環形緩沖區或持久緩沖區,無更新子資源,不能依賴模擬的d3dx12.h的版本,無過渡紋理,通過過渡緩沖區進行模擬。
    • 小型資源:CreateCommittedResource以64kB的頁面進行分配,不會為少量資源而運行,理想情況下,在碎片整理堆中對所有資源進行子分配,或特殊情況下的小型資源。
    • GenerateMips:DX12中沒有這種東西,為其編寫一個計算著色器,發現手動實現的性能優于DX11,但需要處理許多不同的情況(2D/3D/數組/顏色空間)。
    • 空資源:不能再綁定nullptr了,需要1D/2D/3D紋理、緩沖區、UAV紋理/緩沖區、CBV、采樣器的空資源,可能需要將空綁定提升到抽象中更高的位置才能了解類型。
    • 計數/追加緩沖區:DX12中沒有這種東西,有一個單獨的計數緩沖區,可以原子增加。
    • 查詢:另一個需要注意的容易忘記的方面,管理/輪詢查詢堆,合并解析(即合批在一起,避免多個調用),在完整的堆中回讀。

    從DX11移植至DX12,你現在是驅動了(成為真正的老司機),注意內存使用和性能,將優化重點放在瓶頸上,在單獨的CPU和GPU時間線中思考。Northlight在移植過程中,對不同的對象操作如下:

    • 資源屏障。在主線程中自動執行資源轉換:綁定RT時、在描述符表中設置資源、復制,其它異步渲染線程不允許轉換,在執行命令列表之前,手動確保資源(主要是RT)處于正確的狀態。不必要地濫用它們可能會扼殺GPU性能,但取決于硬件,僅在必要時使用UAV屏障,它們會迫使GPU在調度之間閑置(DX11樣式)。

    • 繪制。遍歷繪制,捕捉DX11樣式集的調用,跟蹤之前的值,如果PSO發生了變化,請將其標記為臟,如果已臟,在繪圖時進行哈希,從映射表中獲取PSO。僅在更改時設置索引/頂點緩沖區、RT/DS和描述符堆,集合在CPU上很便宜,但會導致硬件上下文滾動。PSO是只讀、綁定和遺忘的,每次draw調用時旋轉到空閑(GPU)描述符表中,在GPU上執行命令列表后重用描述符表。

    • 線程。沒有區分即使/延遲上下文,在任何線程上記錄命令列表,從一個線程提交。

      • 在池中保存描述符表管理器(處理表的旋轉)、描述符表管理器GPU柵欄(讓你知道何時可以重用表)、 命令列表(在GPU完成執行后可以重用)、命令分配器(可用于多個命令列表)

      • 初始化線程時獲取描述符管理器、描述符管理器柵欄、命令分配器、命令列表。
      • 結束線程時釋放CPU可重復使用的描述符管理器、命令分配器。
      • 執行命令列表之后釋放GPU可重用的描述符管理器柵欄、命令列表。

    總之,GPU性能:做正確的事情,匹配DX11,在所有架構上都很重要,搞砸GPU內存管理可能代價高昂。CPU性能:輕松超越DX11,但是,真的受到API開銷的限制嗎?實例化、LOD、良好的剔除使得驅動程序不會被繪制調用淹沒。


    Rendering 'Rainbow Six | Siege'是游戲《彩虹六號|圍城》基于當前新一代渲染引擎的首次迭代,文獻將重點介紹利用當前一代硬件才可能實現的計算的架構優化,以及新的棋盤渲染技術,該技術可以在不造成質量損失的情況下將渲染速度提高50%。

    Siege的GPU幀的層次視圖:幾何體渲染*均花費5毫秒,大量使用剔除,緩存陰影,*均5毫秒用于照明(包括SSR),棋盤渲染相助,SSAO和SSR射線追蹤以異步方式完成,后處理/其它全屏處理*均花費4ms。

    CPU關鍵路徑的層次視圖:關鍵路徑*均10毫秒,所有通道和任務都能夠分叉和連接,以最小化關鍵路徑,緩存陰影,不透明通道的最大線性長度為4ms,基于材質的繪制調用系統!

    不透明物體的渲染管線如下:

    陰影渲染:所有陰影都是基于緩存的,使用緩存的Hi Z進行剔除。太陽陰影以全分辨率完成,分離通道以釋放VGPR壓力,使用緩存陰影貼圖的Hi Z表示法來減少每像素的工作量。局部光源以四分之一的分辨率進行解析,解析的結果存儲在紋理數組中,光照積累時VGPR使用率較低,雙邊上采樣。對于太陽、月亮的陰影,包含加載時生成的所有靜態對象的陰影貼圖。

    通過混合級聯和靜態貼圖來縮放陰影成本的能力,靜態Hi Z陰影貼圖始終用于動態對象剔除。在Xbox One上:第一級級聯是完全動態的(6K分辨率不夠),第二級和第三級級聯僅渲染動態對象,并與靜態陰影貼圖混合,第四級由靜態陰影貼圖替代。

    對于局部光源投影,最多處理8個可見陰影局部光源,流程如下:

    在光照方面,在frustum上使用分簇結構:基于32x32像素的分塊,Z指數分布,分層剔除光源體積以填充結構,被視為光源的局部立方體圖,陰影、立方體圖和遮擋板(gobo)位于紋理陣列中,延遲使用預先解析的陰影紋理數組,前向使用陰影深度緩沖區數組。

    統一緩沖區(UNIFIED BUFFER):彩虹六號中的許多資源都位于某種統一緩沖區中,如統一頂點緩沖區、統一索引緩沖區、統一常數緩沖區、...。結構化緩沖區構建在自動生成代碼的原始緩沖區之上:使用C++數據描述符處理GPU統一數據,傳遞給指定訪問模式的元數據。統一常數緩沖區示例代碼:

    統一緩沖區的好處:完全控制數據布局,可以很容易地嘗試不同的數據類型訪問(AOS、SOA、u32數組的結構、…),自定義打包和對新數據類型的支持,高級API支持廣播值,代碼自動生成允許我們輕松地遷移到新的訪問模式。

    基于材質的繪制調用:幾何和常數是統一的,然后繪制調用由以下內容定義:著色器、非統一資源(紋理等)、渲染狀態(采樣器狀態、光柵狀態),共享上述內容的元素被批處理在一起,不使用資源和狀態子集的通道將進一步批處理在一起。

    收集繪制調用:初始化時,每個子網格實例映射到3個批次:普通、陰影和可見性,批處理類型用于屏蔽非必要的數據,每個批次將對應一個MultiDrawIndexedIndirect命令。

    每個子網格實例都有一個全局唯一的索引:用于獲取所有數據的索引,需要多個間接尋址。

    對于每個通道收集子網格實例索引到動態緩沖區:每個通道只映射到一個批次類型,多線程作業中的緩沖區填充(1.5ms線性)。添加了執行剔除的額外數據:MultiDrawIndexedIndirect條目、新索引緩沖區偏移量、附加剔除標志。

    使用了性能剔除,定義了多種類型的剔除:級別1——子網格實例剔除,級別2:子網格塊(chunck)剔除,級別3:子網格三角形剔除。




    結果:

    非核批的DC數量(共總) 合批的DC數量(VIS + GBUFFER + DECALS) 合批的DC數量(陰影) 裁剪效率
    10537 412 64 73%

    接下來聊棋盤渲染。

    棋盤渲染的基本思想解決了鋸齒問題,在一系列圖像上進行實驗,首先測試質量,對于大多數圖像,使用棋盤模式時PSNR更好:視覺效果也更令人愉悅,從一開始,使用MSAA 2X的想法就開始流行起來。

    上排是線性鄰域插值,下排是棋盤鄰域插值。

    棋盤渲染實現:

    • 渲染到1/4大小(1/2寬 x 1/2高)分辨率,使用MSAA 2X,最終得到全分辨率圖像的一半樣本。
    • D3D MSAA 2x標準模式:2個顏色和Z樣本。
    • 采樣修飾符或SV_SampleIndex輸入以強制渲染所有樣本。
    • 每個樣本都落在全屏渲染目標的精確像素中心。

    棋盤渲染額外的好處:粒子效果可以很容易地按像素而不是按樣本進行評估,可以在ESRAM中獲得更多的東西!無需在著色器中修正漸變。

    通過在每一幀中再次偏移投影矩陣可以改變模式,并不總是可以在PC上更改樣本的位置。

    填補空白處:為了重建下圖未知像素P和Q的顏色,采樣當前幀直接鄰域線性Z、當前幀直接相鄰顏色、歷史顏色與Z。

    歷史顏色/Z:選擇一個鄰居作為運動速度:離相機最*的一個,以保持輪廓,使用運動速度,對之前解析的顏色進行采樣。這樣可以使用過濾,但會引入累積誤差!用B、E、F為Q夾緊重投影的顏色,使用之前從運動計算的深度,計算一個信賴的值,用于向未夾緊的值混合。解析顏色:已有歷史顏色、直接鄰域的插值顏色,使用兩個附加的權重計算最終的顏色:A、B、E、F與Q之間的最小差值和速度的大小。

    完整的流程圖如下,解決相當復雜的問題,內容有很多調整!消耗1.4ms,減少了8到10ms。

    TAA可以和棋盤渲染集成,可以在同一個resolve著色器上運行,在子樣本級別上完成,MSAA 4X風格在棋盤格圖案頂部抖動,重投影顏色權重使用類似的邏輯,額外的信息(Unteething)可用于刪除不良的棋盤模式。

    分辨率會在圖像中引入明顯的鋸齒圖案,應用過濾來消除它們,該過濾可在5個水*或垂直相鄰的像素上工作,將閾值d和二進制像素分別設置為0或1,如果它們在[0, d]或[1-d, 1]范圍內,我們檢測到01010或10101模式。

    Optimizing the Graphics Pipeline With Compute由DICE的Frostbite引擎團隊成員呈現,使用計算著色器優化圖形管線,更具體地說,如何通過不渲染那么多三角形來快速渲染三角形。文中涉及的各種概念如下表:

    概念 全稱 翻譯
    VGT Vertex Grouper \ Tessellator 頂點分組器、曲面細分器
    PA Primitive Assembly 圖元裝配
    CP Command Processor 命令處理器
    IA Input Assembly 輸入裝配
    SE Shader Engine 著色器引擎
    CU Compute Unit 計算單元
    LDS Local Data Share 局部數據共享
    HTILE Hi-Z Depth Compression 層級深度壓縮
    GCN Graphics Core Next AMD的圖形核心稱號
    SGPR Scalar General-Purpose Register 標量通用寄存器
    VGPR Vector General-Purpose Register 向量通用寄存器
    ALU Arithmetic Logic Unit 算術邏輯單元
    SPI Shader Processor Interpolator 著色處理插值器

    當時的Frostbite引擎面臨繪制調用過多(1000+)、圖元填充率過高等問題。解決的機會有:

    • 在CPU上粗糙剔除,在GPU上精細剔除。

    • CPU和GPU之間的延遲會阻止優化。

    • GPU提交!

      • 深度感知剔除。縮小陰影邊界\樣本分布陰影貼圖,剔除沒有貢獻的陰影投射者,從顏色通道中剔除隱藏物體。
      • VR的late-latch剔除。CPU提交保守的視錐體,GPU細化。
      • 三角形與分簇剔除。
    • 直接映射到圖形管線。卸載外殼著色器工作,卸載整個細分管線!程序頂點動畫(風、布料等),在多個過程和幀之間重用結果。

    • 間接映射到圖形管線。邊界體積生成,預處理蒙皮,頂點動畫(Blend Shape),從GPU生成GPU工作,場景和能見度測定。

    • 將繪制當做數據!預構建,緩存和重用,在GPU上生成。

    裁剪概覽圖:

    左上:如圖所示的場景,包含網格集合、特定視圖、攝像機、燈等。右上:場景中的可配置網格子集,共享著色器和三角形帶(頂點/索引)的批次內的網格,與DirectX 12 PSO的比例接*1:1(管線狀態對象)。左下:代表一個索引的繪制調用(三角列表),有自己的頂點緩沖區、索引緩沖區、圖元數量等。右下:波前處理的最佳三角形數,AMD GCN每個波前有64個線程,每個剔除線程處理1個三角形,工作項處理256個三角形。

    剔除概覽如下:

    大致的過程和技術點包含將網格ID映射到多繪制ID、使用非交錯頂點緩沖區、分簇剔除、繪制壓縮、三角形剔除、朝向剔除、小圖元剔除、視錐體剔除、深度剔除(深度分塊、深度金字塔、HTILE、軟光柵Z等)。

    最終的剔除性能和效果如下:


    The Rendering of INSIDE: Low Complexity, High Fidelity分享了當年爆火的一款解密探索類游戲Inside的渲染技術,包含用于實現大氣外觀的各種效果,例如局部陰影體積測量和強大的水渲染系統。通過對每個像素進行微調,將照明設計為完全獨立的漫反射、鏡面反射和反彈光實體,同時關注藝術家可接*的工具,意味著利用分析基于圖元的環境光遮擋和屏幕空間反射。此外,還詳細闡述如何通過適當使用抖動來消除分散注意力的瑕疵,避免藝術品的細微細節淹沒在色帶中。

    Inside是一款暗黑解謎類游戲,可運行于安卓、ios等移動*臺,是當年令筆者沉迷的一款游戲。筆者曾對它給予了很高的評價,有圖為證:

    該文涉及的內容比較多,包含霧與體積度量、HDR泛光、色帶和抖動、投影貼花(定制照明、解析環境遮擋、屏幕空間反射)、水渲染、效果分解(吸引眼球)等。其中Inside使用了Light Prepass渲染,一幀概覽如下:

    霧原來是Inside藝術風格的核心,游戲中的許多初始場景實際上只是霧+剪影。以下是霧的組合效果:

    從上到下:沒有霧、線性霧、線性霧+發光。

    作為大氣散射的發光是非常寬的光暈,半個屏幕,向下采樣,然后是多個模糊,因為只需要大范圍的模糊。HDR發光是第二個發光通道,只對明亮發光的物體,遮罩對象的窄輝光僅發射材質(寫入alpha通道)遮罩值將RGB重新映射為非線性強度,中間HDR值用\(x/(x+1)\)編碼為[0:1]的定點數。

    采樣模式使用了發光過濾器[JIMENEZI4],下采樣時13個樣本模糊,上采樣時9個樣本模糊。后處理設置步驟和流程如下:

    接下來聊體積光照。

    體積照明用raymarch的相機光線,陰影貼圖投影空間中背景深度的步長,每一步計算光照貢獻:采樣陰影圖、cookie、衰減等。

    使用了逐像素3個抖動的樣本,藍色噪聲提供了良好的采樣,而在采樣不足的區域,回到噪聲中尋找失真可以獲得更少的樣本。

    半分辨率用于低頻效果,是上采樣時的深度感知模糊。步驟如下:

    • 清理前深度緩沖為零。
    • 通道1,半分辨率。正面深度。
    • 通道2,半分辨率。射線步進體積霧,輸出光源強度(8位)+最大深度(24位)。
    • 通道3,全分辨率。對半透明對象排序,深度感知解析、上采樣和模糊。

    通道3的具體過程如下:

    另外,使用方差尺寸模糊、4樣本、深度感知樣本等影響TAA、下采樣,讓TAA隨著時間的推移進行集成。以下是幾種不同抖動方法的對比:


    在基礎顏色通道,存在非常明顯的顏色條帶(色階)的瑕疵:

    為了解決上述問題,分別對光照進行抖動、最終通道引入完全均勻的噪點、半透明未引入噪點但使用特殊的混合模式,解決方案在每個通道后都會抖動,手動將所有中間渲染目標轉換為srgb(pow2,沒什么特別的),在較低的分辨率下抖動(用于模糊)會導致大于1像素的噪聲,但幸好結果卻不可見。

    抖動和噪點不僅僅可用于顏色,還可用于法線!!

    在定制光照方面,新增了反彈光照、AO貼花、陰影貼花等效果。反彈光被用于全局照明,幾乎只是一個普通的蘭伯特點光源,除了沒有使用香草點積,而是使用滑塊使其褪色,它被稱為lambert扭曲(lambert wrap)或半lambert(half lambert),提供方向性更小、更*滑的結果。

    de14.png)

    不同強度的反彈光。

    因為它們不是靜態的,所以經常用它們來打開窗戶和移動手電筒。Inside沒有使用常規方法(制作一系列點來覆蓋走廊或一個數組來填充房間),而是使用完整的變換矩陣,以獲得非均勻的形狀,更適合使用拉伸的藥片和壓扁按鈕的盒子,而且更低開銷,因為過繪制和重疊更少。

    文中還涉及了AO貼花和陰影貼圖。



    從上到下:點AO、球體AO、盒子AO效果及實現方法。

    從上到下:無陰影貼花、有陰影貼花、陰影貼花可視化。

    對于SSR,采用了特殊的屏幕空間追蹤方式:

    從左到右:屏幕空間向上發射射線、讓射線移動到包圍盒最*的水*出口、繼續向上追蹤。

    // SSR方向計算(常規版)
    // GPU的片元著色器
    sDirProj = mul(projection, vec4(vDir + vPos, 1.0));
    SDir = normalize(sDirProj.xyz / sDirProj.w - sPos);
    
    // SSR方向計算(改進版)
    // CPU計算投影空間的方向,并設置成uniform變量
    _DirProject = vec(viewportSize, nearClip / (nearClip - fraClip));
    // GPU的片元著色器
    sDir = vec3(vDir.xy - vRay.xy * vDir.z, vDir.z * rcpDepth) * _DirProject;
    

    為了避免階梯式瑕疵,使用了隨機采樣(藍色噪點 + TRAA)。

    水體(水面和水下)使用了分層渲染,包含體積霧、半透明、折射、反射等效果:

    水面的分層組合后的效果。

    水下也和水面一樣,具有相同的分層渲染,還增加位移邊緣、外部或正面面、內部或背面面等層:

    Inside的特效方也非常講究,使用了很多技術和優化技巧。

    在世界空間中將粒子紋理投射到粒子上,每個粒子上都有一個隨機偏移,并向一個方向滾動。圖中示例是向下的,因為是從盒子里出來的方向。

    鏡頭眩光的采樣方式。

    各種水面效果的模擬。

    Temporal Reprojection Anti-Aliasing in INSIDE也涉及了Inside,但專注點在TAA的應用和優化。

    首先是一些基本的直覺,表面片元的局部區域可以在多個幀中保持可見,若觀察者和被攝體之間的關系每一幀都發生變化,那個么光柵化也會發生變化,如果在時間上后退一步,那么可以使用這種變化來優化當前幀。

    要將當前幀片段與前一幀的片元關聯起來,可以在空間上進行,并進行重投影,依賴深度緩沖區信息,僅限于最接*的表面片元,但不總是可行,有時候數據根本就不存在。片元在任何時候都可能被遮擋或不被遮擋,因此很難準確后退,如果觀看者和被攝者之間的關系從未改變,那么退一步就不會獲得額外的信息…

    第一步:抖動視錐。已經確定如果攝像機是靜態的,然后就失去了信息,因此,渲染前的每一幀:從樣本分布中獲取texel偏移量,使用“偏移”計算投影偏移,使用“投影偏移”剪切*截頭體。

    第二步:對于每一個片元執行以下步驟:

    靜態場景的重投影:

    • 從當前片段p_uv開始。
    • 使用當前幀的深度和*截體參數重建世界空間p,lerp角射線,按線性深度縮放。
    • 將p重新投射到前一幀中:q_cs = mul(VP_prev’, p)q_uv = 0.5 * ( q_cs.xy / q_cs.w ) + 0.5
    • 采樣歷史:c_hist = sample( buf_history, q_uv

    動態場景的重投影:

    • 對于動態場景,需要一個速度緩沖區
      • 在處理TAA之前需要專門的pass。
      • 使用靜態重投影初始化攝像機運動:v = p_uv - q_uv
      • 在頂部渲染動態對象:v = compute_ssvel(p, q, VP, VP_prev’)
    • 重投影步驟變為讀取和減法:v = sample(buf_velocity, p_uv)q_uv = p_uv - v

    在TAA過程還需要處理重投影與邊緣運動、約束歷史樣本、鄰域夾緊等細節,最終融合,用約束的歷史加權。

    Inside的TAA 2.0將運動模糊添加到混合中:

    帶運動模糊回退的最終混合步驟:

    • 像以前一樣更新歷史緩沖區:rt _history = c _feedback。
    • 對于輸出目標,與運動模糊輸入混合。
      • c _motion = sample_motion ( buf _color , unjitter( p _uv ), v)
      • rt _output = lerp( c _motion , c _feedback , k_trust)
      • k_trust = invlerp( 15, 2, |v| )
    • 強制過渡到運動模糊(無歷史記錄!)尋找快速移動的片元。

    關于選擇一個好的樣本分布,大量的嘗試和錯誤,采取了實用的方法,頭部靠*屏幕,放大高對比度區域,希望在收斂的質量和速度之間找到良好的*衡,啟發式:側滾游戲。

    測試的一些序列:

    實現總結:用halton(2, 3)的前16個樣本抖動視錐,生成速度緩沖,攝像頭運動+動態(手動標記),基于最*的(深度)片元的速度重投影,使用中心剪輯到RGB最小最大圓形3x3區域的鄰域夾緊,運動模糊回退,當||v||大于2時生效,15時完全生效,但不適用于歷史。

    Temporal Antialiasing In Uncharted 4也分享了神秘海域4的時間抗鋸齒。本文的焦點在于提供更詳細的實現細節。

    對于靜態圖片,設置輸入和輸出,運行全屏幕著色器,在歷史和當前渲染紋理之間插值:

    float3 currColor = currBuffer.Load(pos);
    float3 historyColor = historyBuffer.Load(pos);
    return lerp(historyColor, currColor, 0.05f);
    

    對于運動圖像,不能在同一位置采樣歷史緩沖區,找出上一幀中當前幀像素的位置(如果有),需要處理屏幕外的像素和被遮擋的像素。需要先在GBuffer通道中生成全屏運動向量緩沖區(fp rg16):

    // GBuffer VS
    posProj = posObj * matWvp;
    posLastProj = posLastObj * matLastWvp;
    
    // GBuffer PS
    posNdc -= g_projOffset;
    posLastNdc -= g_projOffsetLast;
    float2 motionVector = (posLastNdc - posNdc) * float2(0.5f, -0.5f);
    

    對于程序化的動畫對象(植物、毛發、水頂點運動等),需要對當前幀和最后幀執行兩次,需要確定性,輸入最后一幀的時間,產生完全相同的頂點位置,在紋理上滾動uv,即在表面uv空間(deltaU,deltaV)中滾動會導致屏幕空間(deltaX,deltaY)發生多少變化:

    deltaU = ddx(U) * deltaX + ddy(U) * deltaY;
    deltaV = ddx(V) * deltaX + ddy(V) * deltaY;
    

    已解析的deltaX和deltaY以屏幕像素為單位,將它們轉換為運動向量單位。對于反射,無法使用鏡像的運動矢量,因為鏡像像素在上一幀中通常反射不相同的東西。這個問題不難,但涉及很多步驟,僅適應于*面反射。一切都應該有運動矢量,但還有一些不受支持的:復雜紋理動畫,如粒子煙、水流、云運動等;透明對象,如藝術家控制運動矢量不透明度。為什么不在TAA之后繪制呢?繪圖順序并不總是允許,任何跳過TAA的東西都會抖動,刪除抖動還不夠,因為使用了抖動深度緩沖進行測試。在TAA之后擺脫了問題:雨滴,彈痕,火花…除此之外,沒有其它東西擺脫,一旦使用了TAA,就得一路走下去。將運動矢量緩沖區作為輸入添加到TAA,使用運動矢量采樣歷史緩沖區:

    float2 uvLast = uv + motionVectorBuffer.Sample(point, uv);
    float3 historyColor = historyBuffer.Sample(linear, uvLast);
    

    使用線性采樣器的重要事項:點采樣可能會落在不相關的像素上,線性采樣不會完全丟失,但會導致模糊(稍后討論)。

    歷史和當前顏色可能不匹配,由于遮擋/屏幕外而不存在、燈光變化、在邊緣的不同側面(投影抖動)等。需要夾緊歷史顏色,使用Dust 514的夾緊方法,以當前幀像素為中心采樣3x3鄰域,計算每個rgb通道的最小/最大鄰域:

    float3 neighborMin, neighborMax;
    // calculate neighborMin, neighborMax by
    // iterating through 9 pixels in neighborhood
    historyColor = clamp(historyColor, neighborMin, neighborMax);
    

    對于不存在和燈光變化的情況,夾緊掉太不相同的歷史顏色,3x3鄰域確保夾緊不會影響邊緣AA,對于邊緣像素,邊緣另一側的歷史不會被夾緊。是時候再次混合歷史和當前顏色,這一次支持運動圖像:

    return lerp(historyColor, currColor, blendFactor /*0.05f*/);
    

    需要動態的blendFactor來*衡模糊和抖動。基于UE4方法:局部對比度較低時增加,當像素運動達到亞像素時減少,當歷史接*夾緊時減少,結果還是有點模糊。為了解決模糊,需要一個全屏幕通道使用以下的鄰域權重進行處理:

    \[\begin{vmatrix} 0 & -1 & 0\\ -1 & 4 & -1\\ 0 & -1 & 0 \end{vmatrix} \]

    return saturate(center + 4*center - up - down - left - right);
    

    TAA仍然存在一個問題:鬼影,夾緊應該可以防止它,但在某些領域似乎不起作用。鬼影發生的原因是包含高頻和高強度的顏色變化,如多棱茂密的草、黑暗中被強光照亮的凹凸不*的表面。鄰域最小/最大值變得巨大,使夾緊失效:

    historyColor = clamp(historyColor, neighborMin, neighborMax);
    
    // 將上面的原有代碼改成以下代碼
    uint currStencil = stencilBuffer.Sample(point, uv);
    uint lastStencil = lastStencilBuffer.Sample(point, uvLast);
    blendFactor = (lastStencil & 0x18) == (currStencil & 0x18) ? blendFactor : 1.f;
    

    鬼影消失了,但新顯示的像素看起來格外銳利和鋸齒:

    當blendFactor為1時返回高斯模糊顏色:

    blendFactor = (lastStencil & 0x18) == (currStencil & 0x18) ? blendFactor : 1.0f;
    
    float3 blurredCurrColor;
    // Gaussian blur currColor with 3x3 neighborhood 
    if (blendFactor == 1.0f)
        return blurredCurrColor;
    

    其中高斯權重如下:

    \[\begin{vmatrix} \cfrac{1}{16} & \cfrac{1}{8} & \cfrac{1}{16}\\ \cfrac{1}{8} & \cfrac{1}{4} & \cfrac{1}{8}\\ \cfrac{1}{16} & \cfrac{1}{8} & \cfrac{1}{16} \end{vmatrix} \]

    在模板修復后,仍然有1像素厚的鬼影:

    顏色歷史是線性采樣的,而模板歷史是點采樣的,邊緣不匹配,解決方案是使模板緩沖區中的對象輪廓擴大1個像素。使用當前幀深度和模板緩沖作為輸入創建全屏著色器,每個像素(p)將其深度與4個鄰域像素(左上、右上、左下、右下)進行比較,忽視其它鄰域像素。輸出最接*深度的像素模板,將擴大的模板緩沖區添加到TAA輸入,上一幀的模板應來自擴大版本,使用擴大版本進行模板測試,邊緣檢測使用未擴大版本。修復了模板標記對象邊緣周圍的輕微抖動,避免給草之類的東西標記,手動標記的一個原因,運動模糊也會隱藏鬼影。

    對于屏幕外的像素,像素歷史記錄可能在上一幀離開屏幕,在著色器中檢測并將blendFactor設置為1,與新發現的情況一致:沒有歷史,使用高斯模糊的當前顏色。

    在1080p的PS4 GPU上,主著色器低于0.8ms,許多其它相關成本:運動矢量計算、銳化著色器(0.15毫秒)、擴大模板著色器(0.4ms)。TAA的其它收益:每像素采集多個樣本的圖形功能,將計算擴展到多個幀,使用TAA進行合并,將顯示一個樣本,但在多個樣本中使用。

    該文還談及了將TAA用于RSM(Reflective Shadow Map,反射陰影圖)的思想。將場景從手電筒透視渲染到256x256緩沖區,每個像素都被視為一個VPL(虛擬點光源),運行全屏著色器,其中每個像素由所有(65536)VPL照亮,但算法復雜度是O(n*m),成本太高了!神秘海域4將VPL緩沖區下采樣至16x16,1/4寬x 1/4高的全屏著色器,但由此產生的結果過于模糊和模糊,VPL(256)不足導致失真。需要一個不那么暴力的解決方案,渲染分辨率無法承擔,每像素隨機采樣16個VPL,相鄰像素采樣不同的16個VPL,每個64x64屏幕空間tile可以覆蓋所有64k的VPL。使用PBR手冊中描述的低差異序列,確保64x64的tile中沒有兩個像素使用相同的VPL,在64k中采樣16次具有巨大的差異,導致嚴重的噪點。因此可引入時間采樣,每個像素每幀采樣不同的16個VPL,實際上有超過16個VPL,TAA將幀收斂到穩定圖像,最佳部分–無需更改TAA著色器。

    TAA還可用于屏幕空間反射、陰影模糊、屏幕空間環境遮擋、*相機透明度、LOD轉換、一些頭發透明。未解決的問題:不受支持的運動矢量,一些顆粒/水在沒有TAA的情況下看起來更好;下采樣和模糊不友好,無法讓SSR在半分辨率內工作;TAA在較高的幀速率下工作得更好,收斂速度更快,瑕疵不那么明顯。

    Mixed Resolution Rendering in 'Skylanders: SuperChargers'講述了游戲Skylanders: SuperChargers所使用的混合分辨率渲染的技術。首次嘗試使用了單通道的混合分辨率,場景深度被下采樣,然后根據低分辨率緩沖區進行光柵化,使用雙邊上采樣對低分辨率渲染進行上采樣,然后最終合成到場景緩沖區。

    雙邊上采樣的代碼:

    float4 vBilinearWeights = GetBilinearweights(vTexcoord); 
    
    float4 vSampleDepths = GetLowResolutionDepths(vrexcoord); 
    float vPixelDepth = GetHighResolutionDepth(vTexcoord); 
    float4 vDepthWeights = GetDepthsimilarity( vPixelDepth, vSampleDepths); 
    
    return vDepthWeights * vBilinearWeights;
    

    深度下采樣時把最小值和最大值結合起來,因為它們處理的像素幾乎是互斥的。事實證明,簡單的事情有時也能奏效,只需在棋盤模式中交替使用最小值和最大值,這將為整個4x4像素塊提供良好的深度表示。

    Texture2D SourceDepthTexture;
    SamplerState PointSampler;
    
    float main(float2 vTexcoord : TEXCOORD0, float2 vWindowPos : SV_Position) : SV_Target
    {
        // Gather the 4 depth taps from the high resolution texture that cover this texel SourceDepthTexture. 
        float4 fDepthTaps = GatherRed(PointSampler, vTexcoord, 0);
        
        // Identify the min and max depth out of the 4 taps  
        // NOTE:  It doesn't matter if your depth is negative or positive here  
        float fMaxDepth = max4( fDepthTaps.x, fDepthTaps.y, fDepthTaps.z, fDepthTaps.w);
        float fMinDepth = min4( fDepthTaps.x, fDepthTaps.y, fDepthTaps.z, fDepthTaps.w);
        
        // Classify the low resolution texel as either a max texel or min texel based on the window pos  
        return checkerboard( vWindowPos) > 0.5f ? fMaxDepth : fMinDepth;
    }
    

    由此產生的深度緩沖將高頻深度不連續轉化為棋盤格圖案,正如從下圖的拱門下的草葉中看到的那樣。因此,需要一種評估它們的方法。

    幾種不同的深度下采樣方法的像素誤差值如下,可知min/max 方法誤差最小:

    原始參考和min/max的渲染對比:

    雙邊上采樣的模式如下:

    然而第二次嘗試有時結果看起來比決心更糟糕,樣本是否有問題?是否使用點采樣更佳?事實證明,在某些情況下,當將結果與一個簡單的線性濾波進行比較時,如果沒有深度權重,結果會出現一些奇怪的偽影,就可以判斷出發生了什么。那么為什么會發生下圖這種情況(雙邊過濾明顯的鋸齒)呢?這與雙邊上采樣以及如何計算深度相似性有關。

    深度相似度一般由下面的代碼計算:

    float4 GetDepthSimilarity( float fCenterDepth, float4 vSampleDepths)
    {
        // fThreshold控制著深度相似度的強度
        loat fScale = 1.0f / fThreshold; 
        float4 vDepthDifferences = abs(vSampleDepths - fCenterDepth); 
        return min(1.0f / (fScale * vDepthDifferences + fEpsilon) , 1.0f);
    }
    

    通常,通過確定高分辨率和低分辨率像素的深度差來計算深度相似性。該數字與閾值(fThreshold)一起用于確定深度是否相似。所以,如果選擇一個小的閾值,最終會檢測到假邊緣(下圖左)。類似地,如果閾值太大,則會丟失靠*的邊(下圖右)。

    這是因為對雙邊加權使用了固定的深度偏差值。問題在于,隨著表面逐漸退到屏幕中,單個像素步所代表的單位數增加了。因此,應該根據深度設置閾值。

    文中改成使用*面被推回進場景的模型作為確定閾值的基礎。給出單個像素的角度差和*面的斜率作為輸入,以確保在前面保持線性混合。從而變成了一個簡單的三角問題,可以得到結果,此外,還縮放該值以補償這樣一個事實——下采樣時深度值可能來自2個像素之外。

    改進后的閾值的效果和采樣結果如下兩圖:


    但是,下面是使用目標坡度閾值前后另一個問題的示例,圖左所示得到的是水*過濾,而不是垂直過濾。

    因此,第三次嘗試有些影響看起來仍然很糟糕,有很多鋸齒,透明模型更糟糕。為了減少邊緣鋸齒,采用了雙通道方法方法:

    對深度和顏色的邊緣的檢測采樣了以下方法:

    部分上采樣步驟:

    • 第1個Pass:

      • 裁減邊緣。
      • 在通道中設置模板位。
    • 第2個Pass:

      • 配置高分辨率模板(hi-stencil)。
      • 繪制全屏矩形以重新加載高分辨率模板。

    第四次嘗試得到的效果達到了發布水*,在360上運行的性能如下:

    由于在游戲中使用了抖動衰減,在下圖可以看到最小/最大緩沖區清楚地保持了正在淡出的樹的抖動特性。

    優勢是有助于游戲的內容,屏幕外的渲染目標非常有用,性能可擴展。劣勢是預乘的渲染目標,有限的混合模式,依賴高分辨率模板,最壞情況下的成本更高,高開銷。

    How we rethought driver abstraction談及了如何更好地抽象和封裝圖形API。文中的抽象最初是基于DX9的輕量層,更新后與DX10匹配,抽象方法/調用,不是渲染過程本身。舊的渲染管線如下:

    工作項(work item)是描述單個渲染工作所需的最少數據,如輸入、輸出、程序等,盡可能通用,不一定是GPU工作,完全獨立。

    資源可以是任何東西,是客戶端代碼的黑盒,可以在不同*臺甚至不同運行有不同的實現,可能有實例,生命周期長,不一定存在于整個生命周期的內存中,資源的典型代表是紋理、著色器、模型等。實例是一種資源,是臨時的,在客戶端代碼是黑盒,由與資源相同的代碼處理,例如在當前幀中渲染的模型實例。句柄有設置(colour_handle, colour::red)、獲取(colour_handle)等接口,在內部句柄是實例內存的偏移量,客戶端訪問資源和實例的唯一方法,如果給定實例或資源沒有請求的參數,則為NOP,調試中有類型檢測。

    新的管線如下,增加了資源管理器,隔離開高級和低級渲染層,從而達到更好的解耦,并獲得優化的機會:

    總之,盡可能多地為底層代碼提供上下文,不要在高層做出任何假設,保留所有選項,在不影響性能的情況下,不要害怕概括和抽象事物,API上的輕量級封裝不再是最佳選擇。

    From Pixels to Reality – Thoughts on Next Game Engine談及了2016年的游戲引擎的現狀以及未來的趨勢。2016年,主流游戲引擎的特點是多線程、多*臺、現代圖形API、延遲渲染、PBR、全局照明、動態環境及60FPS@1080P,當時不夠好的點有鋸齒、陰影、透明度、動畫、加載時間、性能、內容制作等,新興的技術包含基于社區的游戲、用戶創建內容、移動端、VR/AR、電子競技、廣播等。電影和游戲的區別見下表:

    電影 游戲
    Reyes/Ray Tracing Direct 3D / OpenGL
    物體空間著色 屏幕空間著色
    大量可見性樣本 少量可見性樣本
    照片級質量 吞吐量

    在當時,REYES/Ray Tracing用于游戲的時機還不夠成熟,因為基于GPU的REYES面臨小三角形、場景復雜度、不夠快等問題,而實時光追面臨分辨率、多顯示器、不夠快等問題。更好的方法是借鑒和結合的思想。對象空間中的陰影=固有穩定,將可見性采樣與著色分離=多速率,可見性緩沖區=減少內存和帶寬。可能的渲染管線如下:

    多速率可進一步地擴展到可見性樣本、著色樣本、照明樣品、物理、人工智能、輸入、光源傳輸更新等。在鋸齒方面,鏡面鋸齒可用Lean Mapping[OlanoBaker 2010],陰影鋸齒可用Frustum Traced Raster Shadows[WymanHoeltzleinLefohn15],半透明可用OIT。加載時間可用比zlib好2倍的壓縮技術、程序化合成的SubstanceWang Tiles [Wang61] [Stam97] [Liyi04]。

    云可支持巨大的世界、成千上萬的玩家、微型客戶端、內容制作、盒子里的工作室。亞馬遜的云渲染GameLift架構圖如下:

    2018年的游戲引擎幾乎沒有鋸齒、正確的空間、正確的頻率、正確的位置、感知引導的“重要性”、程序化內容、云連接等。研究熱點有程序化合成、壓縮、三維掃描、感知科學、多速率渲染、動畫、分布式的物理/人工智能/渲染等。

    Building a Low-Fragmentation Memory System for 64-bit Games分享了游戲內的低碎片化的內存管理系統。舊的內存系統從PS3中移植而來,具有固定大小的內存池,模擬VRAM。存在的問題是浪費了很多內存,每個內存池都能應付最壞的情況,小型分配的開銷,碎片化,無法支持紋理流。內存碎片是在小的非連續塊中碎片化的堆,即使內存足夠,分配也可能失敗,由混合分配生命周期引起。

    設計目標是低碎片、高利用率、簡單配置、支持PlayStation?4操作系統和PC、支持高效的紋理流、全面的調試支持。

    虛擬內存是在進程使用虛擬地址,是映射到物理地址的虛擬地址,CPU查找物理地址,需要操作系統和硬件支持。

    虛擬內存可以減少內存碎片,碎片就是地址碎片,使用虛擬地址,虛擬地址空間大于物理地址空間,連續的虛擬內存在物理內存中不連續。內存頁(Memory Page)在頁面中映射,x64支持4kB和2MB頁面,PlayStation4操作系統使用16kB(4x4kB)和2MB,GPU有更多的尺寸。2MB頁面最快,16kB頁面占用的內存更少,文中使用64kB(4x16kB頁面),64kB是PlayStation 4 GPU的最小最佳尺寸,特殊情況也使用16kB。

    洋蔥總線(Onion Bus)和大蒜總線(Garlic Bus)都可以倍CPU和GPU訪問,但它們的帶寬不同,洋蔥=快速CPU訪問,大蒜=快速GPU訪問。

    文中的內存系統分割整個虛擬地址空間,按需映射物理內存,分配器模塊管理自己的空間,每個模塊都是專門的,分配器對象是系統的接口。

    class Allocator
    {
    public:
        virtual void* Allocate(size_t size, size_t align) = 0;
        virtual void Deallocate(void* pMemory) = 0;
        virtual size_t GetSize(void* pMemory) { return 0; }
        
        const char* GetName(void) const;
    };
    
    // 示例
    void* GeneralAllocator::Allocate(size_t size, size_t align)
    {
        if (SmallAllocator::Belongs(size, align))
            return SmallAllocator::Allocate(size, align);
        else if (m_mediumAllocator.Belongs(size, align))
            return m_mediumAllocator.Allocate(size, align);
        else if (LargeAllocator::Belongs(size, align))
            return LargeAllocator::Allocate(size, m_mappingFlags);
        else if (GiantAllocator::Belongs(size, align))
            return GiantAllocator::Allocate(size, m_mappingFlags);
        
        return nullptr;
    }
    

    虛擬地址空間:

    小型分配模塊:大多數分配小于等于64字節,約25萬個分配,約25M。打包以防止碎片,大小相同的16kB頁面,沒有頭信息。

    小型分配模塊的利弊,+輕量實現,+非常低的浪費,+利用靈活的內存,+快速,-難以檢測的內存瓶頸。

    大型分配模塊保留巨大的虛擬地址空間(160GB),每個表被分成大小相等的插槽,按需映射和取消映射64kB頁面,保證連續內存。

    紋理流預留大的分配槽,四舍五入到最*的2的N次方,加載最小mip和64kB的最大值,按需映射和取消映射頁面,無需復制或整理碎片。大型分配模塊的利弊,+沒有頭信息,+簡單的實現(大約200行代碼),+沒有碎片,-大小四舍五入到頁面大小,-映射和取消映射內核調用相對較慢。

    中型分配模塊應對中等尺寸的分配,無頭信息,除了大型和小型之外其它尺寸都在這里分配,非連續虛擬頁,增長和收縮,傳統的帶頭信息的雙鏈表,不適合大蒜(GPU)的內存(與數據一起存儲的頭),Pow2自由列表。

    無頭分配模塊(Headerless Allocation Module)用于GPU分配,中小型分配,哈希表查找。

    分配器類型包含GeneralAllocator、VramAllocator、MappedAllocator、GpuScratchAllocator、FrameAllocator等。GPU暫存分配器(GpuScratchAllocator)是渲染器用于每幀分配,雙緩沖,不需要釋放分配,受原子保護。GpuScratchAllocator的優缺點:+沒有頭或賬單,+沒有碎片,+快速,-固定尺寸,-最糟糕的情況是對齊浪費空間。幀分配器(FrameAllocator),幀被推入和彈出,不需要釋放內存,每個線程都是唯一的,適用于臨時工作緩沖區。

    #include <ls_common/memory/ScratchMem.h>
    
    struct Elem
    {
        …
    };
        
    void ProcessElements(size_t numElements)
    {
        ls::ScratchMem frame;
        Elem* pElements = (Elem*)MM_ALLOC_P(&frame, sizeof(Elem) * numElements);
    }
    

    FrameAllocator的優缺點:+沒有頭或賬單,+沒有碎片,+沒有同步,+快速,-小心指針傳遞!

    線程安全:最低級別的互斥體,分配器實例不受保護,幀分配器沒有鎖,又好又簡單。

    性能不是重點,但仍然很重要,映射/取消映射速度慢,沒有明顯的區別,游戲期間不要太多分配,文件加載是一個瓶頸。內存的清理值有以下幾種(memset的字節值,保持可讀可記):

    0xFA: Flexible memory allocated
    0xFF: Flexible memory free
    0xDA: Direct memory allocated
    0xDF: Direction memoryfree
    0xA1: Memory allocated
    0xDE: Memory deallocated
    

    統計信息,追蹤一切可能的事情,實時圖表可用,通過自動測試記錄。內存頭保護,中型分配頭中的空閑字節,檢測內存瓶頸,但往往為時已晚。內存塊哨兵,繞過正常分配器,每個分配都在自己的頁面中,前后未映射的頁面,寫得太多/太少時崩潰。

    總結:現代控制臺具有豐富的虛擬內存支持,虛擬內存提供了許多選項,圍繞分配模式設計內存系統,分析很重要,小型分配是一個良好的開端,模塊化分配器使定制變得容易!調試功能非常重要!

    The devil is in the details: idTech 666講述了idTech引擎的渲染管線的渲染技術和性能優化。當時的idTech的渲染管線流程和性能數據如下:

    其分簇光照系統衍生自“Clustered Deferred and Forward Shading” [Olson12]和“Practical Clustered Shading” [Person13],可工作于透明表面,不需要額外的通道或工作,獨立于深度緩沖區,深度不連續處無誤報。分簇光照步驟如下:

    • 體素化/光柵化處理。在CPU上完成,每個深度片1個作業。

    • 對數深度分布。擴大**面和遠*面:\(\text{ZSlice}=\text{Near}_z\times \Big(\cfrac{\text{Far}_z}{\text{Near}_z}\Big)^\frac{\text{slice}}{\text{numSlices}}\)

    • 對每個項目進行體素化,項目可以是燈光、環境探針或貼花,項目形狀是OBB或*截體(投影者),由屏幕空間\(\min_{xy}\)\(\max_{xy}\)和深度邊界的光柵化來界定。

    • 在剪輯空間中進行細化。剪輯空間中的單元格是AABB,N個*面和單元格AABB,OBB是6個*面,*截體是5個*面,所有體積的代碼都相同,使用SIMD。

      //Pseudo-code - 1 job per depth slice ( if any item )
      for (y=MinY; y<MaxY; ++y)     
      {
          for (x =MinX; x<MaxX; ++x) 
          {
              intersects = N planes vs cell AABB
              if (intersects) 
              {
                  Register item
              }
          }    
      }
      

    細節化世界的技術:虛擬紋理更新;反照率、鏡面反射、*滑度、法線、HDR光照貼圖,硬件sRGB支持,將Toksvig烘焙成*滑度,用于鏡面抗鋸齒;直接將UAV輸出回讀至最終分辨率;異步計算轉碼,成本幾乎無關緊要;設計缺陷依然存在,例如反應性紋理流=紋理彈出;嵌入幾何柵格化的貼花;實時替換到Mega紋理的壓印(Stamping),更快的工作流程/更少的磁盤存儲;法線貼圖混合、所有通道的線性正確混合、mipmap/各向異性、透明度、排序、0個繪制調用、使用BC7的8k x 8k貼花圖集;還有盒子投影、索引到貼花圖集,由藝術家手工放置,包括混合設置,“混合層”的推廣;每個視錐視圖限制為4k,通常1k或更少可見;LOD化,藝術設置最大視距,玩家質量設置也會影響觀看距離;研究動態不可變形幾何體,將對象變換應用于貼花。

    在光照方面,使用單一/統一照明代碼路徑,對于不透明通道、延遲、透明和解耦粒子照明,沒有著色器排列,現在靜態/連貫分支非常好——盡情使用它!對所有靜態幾何體使用相同的著色器,減少上下文切換。光照組件包含:漫反射間接照明——用于靜態幾何體的光照貼圖,用于動態的輻照度體積,鏡面間接照明——反射(環境探針、SSR、鏡面遮擋),用于動態的燈光和陰影。

    // Pseudocode
    ComputeLighting( inputs, outputs ) 
    {
        Read & Pack base textures
    
        for each decal in cell 
        {
            early out fragment check
            Read textures
            Blend results
        }
        
        for each light in cell 
        {
            early out fragment check
            Compute BRDF / Apply Shadows
            Accumulate lighting
        }
    }
    

    陰影被緩存/打包到圖集中,PC用32位的8k x 8k的圖集(高規格),控制臺用16位的8k x 4k,基于距離的可變分辨率,時間切片也基于距離,靜態幾何優化網格。如果光源不移動,緩存靜態幾何體陰影貼圖,如果frustum內部沒有更新則跳過,如果沒有更新,用緩存結果組合動態幾何體,仍然可以使用動畫(例如閃爍),藝術設置/質量設置會影響以上所有內容。索引到陰影截錐投影矩陣,所有燈光類型的PCF查找代碼相同,減少VGPR壓力,包括*行光級聯,級聯之間使用抖動,單級級聯查找,嘗試VSM及其導數,有一些瑕疵,從概念上講,對前向渲染有很好的發展潛力,例如將過濾頻率與光柵化分離。

    光照時,注意VGPR壓力,打包壽命長的數據,例如float4表示HDR顏色 <--> RGBE編碼的uint,最小化寄存器生命周期,最小化嵌套循環/最壞情況路徑,最小化分支,控制臺上56個VGRS(PS4),由于編譯器效率低下,在PC上更高(@AMD編譯器團隊,漂亮的plz修復程序-拋出性能)。未來使用半精度支持將有所幫助,Nvidia:使用UBOs/常量緩沖區(所需的分區緩沖區=更多/丑陋的代碼),AMD:優先SSBO/UAV。

    粗糙玻璃*似:最高mip為半分辨率,總共4個mip,高斯核(*似GGX瓣),基于表面*滑度的混合mips,折射傳輸限制為每幀2次,以提高性能,通過貼花實現表面參數化/變化。

    對于后處理,通過以下方法優化了數據讀取:非分歧運算的GCN標量單位,非常適合加速數據獲取,節省VGRP,一致性分支,指令更少(SMEM:64字節,VMEM:16字節)。分簇著色用例,每個像素從其所屬單元獲取燈光/貼花,本質上是不同的,但值得分析。

    大多數wavefront只能進入一個cell,附*的cell共享大部分內容,線程主要獲取相同的數據。每線程單元數據獲取不是最優的,不利用這種數據融合。合并單元格內容上可能的標量迭代,不要讓所有線程獨立地獲取完全相同的數據。

    利用訪問模式:對于數據,每個單元格的項目(燈光/貼花)ID排序數組,同樣的結構適用于燈光和貼花處理,每個線程可能訪問不同的節點,每個線程在這些數組上獨立迭代。標量加載用序列化迭代,計算所有線程中的最小項目ID值,ds_swizzle_b32/小型位置不一致,與所選索引匹配的線程的進程項,統一索引->標量指令,匹配的線程移動到下一個索引。

    特殊路徑:如果只接觸一個單元格,則為快速路徑,避免計算最小的物品ID,在GCN1和2上不便宜,一些額外的(次要的)標量獲取和操作,序列化假定線程之間存在局部性,如果接觸過多的單元格,速度會明顯減慢,禁用粒子照明圖集生成。

    動態分辨率縮放:基于GPU負載的自適應分辨率,PS4上的比例大多為100%,Xbox上的比例更大,在同一目標中渲染,調整視口大小
    侵入式:需要額外的著色器代碼,OpenGL上的唯一選項。未來:重疊多個渲染目標,可能在控制臺和Vulkan上,TAA可以收集不同分辨率的樣本,異步計算中的上采樣。

    異步后處理:陰影和深度過程幾乎不使用計算單位,固定的圖形管線繁重。不透明通道也不是100%忙,可以與后處理重疊,在特效隊列上的預乘Alpha的緩沖區中渲染GUI,計算隊列上處理后處理/AA/upsample/compose UI,與第N+1幀的陰影/深度/不透明重疊,從計算隊列中顯示(如果可用),潛在更低的延遲。

    此外,文中還涉及了GCN架構上的特定優化。未來的工作是解構頻率的消耗以獲得收益,改進紋理質量、全局照明、整體細節、工作流程等。

    [Top five technologies to watch: 2017 to 2021](https://www.bizcommunity.com/Article/196/706/154305.html#:~:text= Top five technologies to watch%3A 2017 to,commonplace%2C while VR will remains niche. More)闡述了2017到2021的技術趨勢和新興的技術。五項技術將幫助我們獲得洞察力:

    最重要的是人工智能與認知技術。以下五大技術支持快速互聯:

    包含移動邊緣計算、cloudlets、fog計算、工業互聯網、SDN和OpenNFV。Edge computing將全面幫助人們,但需要更高層次的技術。

    The Latest Graphics Technology in Remedy's Northlight Engine討論了Remedy的Northlight引擎中渲染技術的引擎內實現和一些最新進展的結果,包含DirectX光線追蹤、區域陰影、環境遮擋、反射、間接漫反射等。

    實時光線追蹤中的基于可見性的算法圖例如下:

    在AO上,SSAO和光追AO的對比如下:

    而光追AO在不同的rpp上,效果也有所不同:

    反射上,SSR和光追反射也有明顯的區別:

    對于間接漫反射,與AO類似,許多非相干光線,GI存儲在稀疏網格體積中。基于靜態幾何圖形和靜態燈光集計算的輻照度,動態幾何體可以接收光,但不影響計算的輻照度,動態幾何沒有貢獻,三線性采樣創建階梯樣式,薄幾何體會導致光照泄漏,通過采樣余弦分布上的輻射來收集照明,考慮丟失的幾何圖形。

    我們還可以在幾何體交點上采樣照明,最終的光追各分量和組合效果如下:


    總之,通過DXR輕松訪問最先進的GPU光線跟蹤,性能正在達到目標,易于不適合光柵化的原型化算法,可與現有低頻結構相結合。

    Advanced Graphics Techniques Tutorial: GPU-Based Clay Simulation and Ray-Tracing Tech in 'Claybook'闡述了Second Order公司的第一款游戲《Claybook》包含了大量創新技術,包括基于GPU的粘土和流體模擬器、完全可變形的世界和角色、有向距離場建模和光線跟蹤視覺效果。并討論這項技術的產生、優化和技巧才能在當前一代游戲機上用游戲的光線跟蹤視覺效果和模擬達到60 fps,以及他們的非標準渲染器和模擬技術是如何集成在虛幻引擎4之上的。

    有向距離場(SDF):SDF(P)=到P處最*表面的有符號距離,有解析距離函數和體積紋理兩種形式。解析距離函數在場景demo中流行,巨大的著色器,很多數學知識,沒有數據。體積紋理存儲距離函數,三線性濾波器,Claybook將體積紋理與mip貼圖結合使用。Claybook的世界SDF的分辨率=1024x1024x512,格式=8位有符號,大小=586 MB(5 mip級別),[-4,+4]體素的距離,256個值/8個體素,1/32體素精度,每mip級別最大步進(世界空間)翻倍。

    在GPU上生成世界SDF的步驟:

    • 生成SDF筆刷網格。64x64x32的dispatch,4x4x4的線程組。

      • 在分塊中心T處采樣刷子體積。如果SDF>光柵分塊邊界+4個體素,則剔除。如果接受,原子添加+存儲到GSM。
      • 通過GSM中的筆刷循環。細胞中心C處的樣本[i],如果接受,存儲到網格(線性),局部+全局原子壓縮。

    • 生成調度坐標。64x64x32的調度,4x4x4的線程組。

      • 讀取刷子網格單元。
      • 如果不是空的:原子加法(L+G)得到寫索引,將單元格坐標寫入緩沖區。

    • 生成Mip掩碼。4x調度(mips),4x4x4的線程組。

      • 分組:加載1個更寬的體素網格L-1鄰域,下采樣1=0掩碼并存儲到GSM。
      • 將掩碼放大1個體素(3x3x3)。
      • 掩碼=0則寫入網格單元坐標。
    • 在8x8x8的tile中生成0級(稀疏)。間接調度,8x8的線程組。

      • 分組:讀取網格單元坐標(SV_GroupId)。
      • 從網格讀取筆刷并存儲到GSM。
      • 通過GSM中的筆刷循環,采樣[i],執行exp*滑最小/最大操作。
      • 將體素寫入WorldSDF的級別0。
    • 生成mips(稀疏)。4倍間接調度(mips),8x8的線程組。

      • 分組:加載更寬的L-1鄰域的4個體素。2x2x2下采樣(*均值)并在GSM中存儲為\(12^3\),+-4體素帶變成+-2體素階(band)。
      • 分組:在GSM中運行3步eikonal方程(下圖),擴展階:2個體素變成4個體素。
      • 存儲8x8x8鄰域的中心。

    ?

    球體追蹤算法:

    • D = SDF(P)。
    • P += ray * D。
    • if (D < epsilon) break。

    多層體紋理跟蹤:

    Loop
        D = volume.SampleLevel(origin + ray*t, mip)
        t += worldDistance(D, mip)
        
        IF D == 1.0 -> mip += 2
        IF D <= 0.25 -> mip -= 2; D -= halfVoxel
        IF D < pixelConeWidth * t -> BREAK
    // 如果曲面位于像素內邊界圓錐體內,則中斷,獲得完美的LOD!
    

    最后一步:球體追蹤需要無限步才能收斂,假設我們碰到一個*面,三線性過濾=分段線性曲面,幾何級數,使用最后兩個樣本,\(Step = D/(1-(D-D_{-1}))\)

    錐體追蹤解析解:

    粗糙錐體追蹤Prepass:

    光線追蹤結果:錐形追蹤跳過大面積的空白空間,大幅縮短步長,體積采樣更多緩存局部性。Mip映射改善緩存的局部性,Log8數據縮放:100%、12.5%、1.6%、0.2%、...測量(1080p渲染),訪問8MB數據(512MB),99.85%的緩存命中率。存在的問題有:過步進(Overstepping)、加載*衡等,它們有各自的緩解方案。

    對于AO,在表面法線方向構造圓錐體,加上隨機變化+時間累積,AO射線使用低SDF mip,更好的GPU緩存位置和更少的帶寬,軟遠程AO。Claybook也使用UE4 SSAO,小規模(*)的環境遮擋。

    上:SSAO;下:SSAO + RTAO。

    軟陰影球體跟蹤:柔和的半影擴大陰影,沿光線步進SDF*似最大圓錐體覆蓋率,Demoscene圓錐體覆蓋*似:

    c = min(c, light_size * SDF(P) / time);
    

    Claybook對軟陰影的改進:三角測量最*距離,Demoscene=單個樣本(最小),三角測量cur和prev樣本,更少條帶。抖動陰影光線,UE4時間累積,隱藏剩余的帶狀瑕疵,較寬的內半影。

    改進前后對比:

    光追的各項時間消耗:

    SDF到網格的轉換:雙通道*似,多個三角形指向同一個粒子,首先需要生成粒子。輸出用于PBD模擬器的線性粒子數組(表面)和三角形渲染的索引緩沖區。使用單個間接繪制調用繪制的所有網格。轉成粒子使用64x64x64的dispatch、4x4x4的線程組,過程如下:

    • 分組:將\(6^3\)個SDF鄰域加載到GSM。
    • 讀取\(2^3\) GSM的鄰域,如果在邊緣內/外找到:
      • 將P移動到表面(梯度下降)。
      • 分配粒子id(L+G原子)。
      • 將P寫入數組[id]。
      • 將粒子id寫入\(64^3\)網格。

    轉成三角形使用64x64x64的dispatch、4x4x4的線程組,過程如下:

    • 分組:將\(6^3\)個SDF鄰域加載到GSM。
    • 讀取\(2^3\) GSM的鄰域,如果找到XYZ邊緣:
      • 每個XYZ邊分配2倍三角形(L+G原子)。
      • \(64^3\)的id網格讀取3倍的粒子id。
      • 將三角形寫入索引緩沖區(3倍的粒子id)。

    異步計算:

    • 將幀拆分為3個異步段。
      • 重疊UE4的GBuffer和陰影級聯。
      • 重疊UE4的速度渲染和深度解壓縮。
      • 重疊UE4的照明和后處理。
    • 工作立即提交。
      • 計算隊列等待柵欄啟動(x3)。
      • 主隊列等待柵欄繼續(x3)。

    異步計算可以讓fps提升19%+。

    集成到UE4渲染器:

    • GBuffer組合。
      • 全屏PS組合光線追蹤數據。
      • 采樣材質貼圖(自定義gather4過濾)。
      • 寫入UE4的GBuffer+深度緩沖區(SV_Depth)。
    • 陰影遮蔽(shadow mask)組合。
      • 全屏PS到球體追蹤陰影。
      • 寫入UE4陰影遮罩緩沖區(使用alpha混合)。

    UE4 RHI定制:

    • 在不進行隱式同步的情況下設置渲染目標。
      • 可以對重疊深度/顏色進行解壓縮。
      • 可以將繪制重疊到多個RT(下圖)。
    • 清除RT/buffer而不進行隱式同步。
    • 缺少異步計算功能。
      • 緩沖區/紋理復制并清除。
    • 計算著色器索引緩沖區寫入。

    UE4 RHI定制(額外):GPU->CPU緩沖區回讀,UE4僅支持2d紋理回讀而不停頓,其它readback API會讓整個GPU陷入停頓,緩沖區可以有原始視圖和類型化視圖,寬原始寫入=高效填充窄類型緩沖區。

    UE4優化:允許間接分派/提取的重疊,允許清除和復制操作重疊,允許不同RT的繪制重疊,減少GPU緩存刷新和停頓(下圖),優化的暫存緩沖區,快速清晰的改進。優化屏障和柵欄,優化紋理數組子資源屏障,更好的3d紋理GPU分塊模式,改進的部分2d/3d紋理更新,5倍更快的直方圖+眼睛適應著色器,4倍更快的離線CPU SDF生成器(烘焙)。

    實現說明:物理數據存儲在一個大的原始緩沖區中,寬加載4/Store4指令(16字節),位壓縮:粒子位置:16位范數、粒子速度:fp16、粒子標志(活動、碰撞等)的位字段,基準工具:https://github.com/sebbbi/perftest。Groupshared內存是一個巨大的性能利器,SDF生成、網格生成、物理,重復加載相同數據時使用。標量加載是AMD在性能上的一大勝利,用例:常量索引原始緩沖區加載,用例:基于SV_GroupID的原始緩沖區加載,存儲到SGPR的負載獲得更好的占用率。

    Entity-Component Systems & Data Oriented Design In Unity分享了Unity的ECS和面向數據設計的技術,包含面向對象設計、面向數據的設計、實體組件系統、實踐案例等。OO的典型實現:類層次結構,虛擬函數,封裝經常被違反,因為東西需要知道,“一次只做一件事”的方法,遲來的決定。簡單OO組件系統:

    // Component base class. Knows about the parent game object, and has some virtual methods.
    class Component
    {
    public:
        Component() : m_GameObject(nullptr) {}
        virtual ~Component() {}
        virtual void Start() {}
        virtual void Update(double time, float deltaTime) {}
        const GameObject& GetGameObject() const { return *m_GameObject; }
        GameObject& GetGameObject() { return *m_GameObject; }
        void SetGameObject(GameObject& go) { m_GameObject = &go; }
    private:
        GameObject* m_GameObject;
    };
    
    class GameObject
    {
    public:
        GameObject(const std::string&& name) : m_Name(name) { }
        ~GameObject() 
        { 
            for (auto c : m_Components) 
                delete c; 
        }
        // get a component of type T, or null if it does not exist on this game object
        template<typename T> T* GetComponent()
        {
            for (auto i : m_Components) 
            { 
                T* c = dynamic_cast<T*>(i); 
                if (c != nullptr) 
                    return c; 
            }
            return nullptr;
        }
        // add a new component to this game object
        void AddComponent(Component* c)
        {
            c->SetGameObject(*this); 
            m_Components.emplace_back(c);
        }
        void Start() 
        { 
            for (auto c : m_Components) 
                c->Start(); 
        }
        void Update(double time, float deltaTime) 
        { 
            for (auto c : m_Components) 
                c->Update(time, deltaTime); 
        }
        
    private:
        std::string m_Name;
        ComponentVector m_Components;
    };
    
    // Utilities
    // Finds all components of given type in the whole scene
    template<typename T>
    static ComponentVector FindAllComponentsOfType()
    {
        ComponentVector res;
        for (auto go : s_Objects)
        {
            T* c = go->GetComponent<T>();
            if (c != nullptr) res.emplace_back(c);
        }
        return res;
    }
    // Find one component of given type in the scene (returns first found one)
    template<typename T>
    static T* FindOfType()
    {
        for (auto go : s_Objects)
        {
            T* c = go->GetComponent<T>();
            if (c != nullptr) return c;
        }
        return nullptr;
    }
    
    // various components
    
    // 2D position: just x,y coordinates
    struct PositionComponent : public Component
    {
        float x, y;
    };
    // Sprite: color, sprite index (in the sprite atlas), and scale for rendering it
    struct SpriteComponent : public Component
    {
        float colorR, colorG, colorB;
        int spriteIndex;
        float scale;
    };
    
    // Move around with constant velocity. When reached world bounds, reflect back from them.
    struct MoveComponent : public Component
    {
        float velx, vely;
        WorldBoundsComponent* bounds;
        MoveComponent(float minSpeed, float maxSpeed)
        {
            /* … */
        }
        virtual void Start() override
        {
            bounds = FindOfType<WorldBoundsComponent>();
        }
        virtual void Update(double time, float deltaTime) override
        {
            /* … */
        }
    };
    
    // components logic
    virtual void Update(double time, float deltaTime) override
    {
        // get Position component on our game object
        PositionComponent* pos = GetGameObject().GetComponent<PositionComponent>();
        // update position based on movement velocity & delta time
        pos->x += velx * deltaTime;
        pos->y += vely * deltaTime;
        // check against world bounds; put back onto bounds and mirror
        // the velocity component to "bounce" back
        if (pos->x < bounds->xMin) { velx = -velx; pos->x = bounds->xMin; }
        if (pos->x > bounds->xMax) { velx = -velx; pos->x = bounds->xMax; }
        if (pos->y < bounds->yMin) { vely = -vely; pos->y = bounds->yMin; }
        if (pos->y > bounds->yMax) { vely = -vely; pos->y = bounds->yMax; }
    }
    
    // game update loop
    void GameUpdate(sprite_data_t* data, double time, float deltaTime)
    {
        // go through all objects
        for (auto go : s_Objects)
        {
            // Update all their components
            go->Update(time, deltaTime);
            // For objects that have a Position & Sprite on them: write out
            // their data into destination buffer that will be rendered later on.
            PositionComponent* pos = go->GetComponent<PositionComponent>();
            SpriteComponent* sprite = go->GetComponent<SpriteComponent>();
            if (pos != nullptr && sprite != nullptr)
            {
                /* … emit data for sprite rendering … */
            }
        }
    }
    

    OO設計的問題:將代碼放在哪里?游戲中的許多系統不屬于“一個對象”,例如碰撞、損壞、AI:在2+個物體上工作。游戲中的“精靈避免泡泡”:把回避邏輯放在回避某事的事情上?把回避邏輯放在應該避免的事情上?其它地方?許多語言都是“單一分派”,有一些對象和方法可以使用它們,但我們需要的是“多次派遣”,回避系統對兩組物體起作用。

    OO設計的問題:很難知道什么是什么。有沒有開過Unity項目并試圖弄清楚它是如何工作的? “游戲邏輯”分散在數百萬個組件中,沒有概述。

    OO設計的問題:“凌亂的基類”問題:

    EntityType entityType() const override;
    
    void init(World* world, EntityId entityId, EntityMode mode) override;
    void uninit() override;
    
    Vec2F position() const override;
    Vec2F velocity() const override;
    
    Vec2F mouthPosition() const override;
    Vec2F mouthOffset() const;
    Vec2F feetOffset() const;
    Vec2F headArmorOffset() const;
    Vec2F chestArmorOffset() const;
    Vec2F legsArmorOffset() const;
    Vec2F backArmorOffset() const;
    
    // relative to current position
    RectF metaBoundBox() const override;
    
    // relative to current position
    RectF collisionArea() const override;
    
    void hitOther(EntityId targetEntityId, DamageRequest const& damageRequest) override;
    void damagedOther(DamageNotification const& damage) override;
    
    List<DamageSource> damageSources() const override;
    
    bool shouldDestroy() const override;
    void destroy(RenderCallback* renderCallback) override;
    
    Maybe<EntityAnchorState> loungingIn() const override;
    bool lounge(EntityId loungeableEntityId, size_t anchorIndex);
    void stopLounging();
    
    float health() const override;
    float maxHealth() const override;
    DamageBarType damageBar() const override;
    float healthPercentage() const;
    
    float energy() const override;
    float maxEnergy() const;
    float energyPercentage() const;
    float energyRegenBlockPercent() const;
    
    bool energyLocked() const override;
    bool fullEnergy() const override;
    bool consumeEnergy(float energy) override;
    
    float foodPercentage() const;
    
    float breath() const;
    float maxBreath() const;
    
    void playEmote(HumanoidEmote emote) override;
    
    bool canUseTool() const;
    
    void beginPrimaryFire();
    void beginAltFire();
    
    void endPrimaryFire();
    void endAltFire();
    
    void beginTrigger();
    void endTrigger();
    
    ItemPtr primaryHandItem() const;
    ItemPtr altHandItem() const;
    
    // … etc.
    

    OO設計的問題:性能。100萬個精靈,20個泡泡:330ms游戲更新,470ms啟動時間,分散在內存中的數據,虛擬函數調用。

    OO設計的問題:內存使用。100萬個精靈,20個泡泡:310MB內存使用率,每個組件都有指向游戲對象的指針,但很少有人需要它,每個組件都有一個指向虛擬函數表的指針,每個游戲對象/組件都是單獨分配的。典型內存視圖:

    OO設計的問題:可優化。如何多線程化?如何跑在GPU?在很多OO設計中非常難,沒有清晰的誰讀取什么數據和寫入什么數據。

    OO設計的問題:可測試性。將如何為此編寫測試?OO設計通常需要大量的設置/模擬/偽造來進行測試,創建對象層次結構、管理器、適配器、單例…

    CPU性能趨勢:

    CPU-內存性能差距:

    計算機中的延遲數據:

    • 從CPU一級緩存讀取:0.5ns。
    • 分支預測失敗:5ns。
    • 從CPU二級緩存讀取:7ns。
    • 從RAM讀取:100ns。
    • 從SSD讀取:150000ns。
    • 從RAM讀取1MB:250000ns。
    • 發送網絡數據包CA->NL->CA:150000000ns。

    代碼和數據需要結合在一起嗎?典型的OO將代碼和數據放在一個類中,為什么呢?回憶“代碼放在哪里”的問題:

    // this?
    class ThingThatAvoids
    {
        void AvoidOtherThing(ThingToAvoid* thing);
    };
    // or this?
    class ThingToAvoid
    {
        void MakeAvoidMe(ThingThatAvoids* who);
    };
    
    // why not this instead? does not even need to be in a class
    void DoAvoidStuff(ThingThatAvoids* who, ThingToAvoid* whom);
    

    數據優先:“所有程序及其所有部分的目的都是將數據從一種形式轉換為另一種形式”,“如果你不了解數據,你就不了解問題所在”——邁克·阿克頓。這是尼克勞斯·沃思1976年的一本經典著作。有人可能會說,“數據結構”也許應該是第一位的,請注意,它根本不談論“對象”!

    有一個,就有很多。你多久吃一次某樣東西?在游戲中,最常見的情況是:有幾件事,任何代碼都可以在這里使用,事情太多了,必須小心性能。

    virtual void Update(double time, float deltaTime) override
    {
        /* move one thing */
    }
    
    // 將上面的改成下面
    
    void UpdateAllMoves(size_t n, GameObject* objects, double time, float deltaTime)
    {
        /* move all of them */
    }
    

    面向數據的設計(DOD):理解數據解決這個問題所需的理想數據是什么?它是怎么布置的?誰讀什么,誰寫什么?數據中的模式是什么?常見情況下的設計,很少有“一”的東西,為什么你的代碼一次只處理一件事?DOD相關資源:

    傳統的Unity GO/組件設置是ECS嗎?傳統的Unity設置使用組件,但不使用ECS。組件解決了“來自地獄的基類”問題的一部分,但不能解決其它問題:邏輯、數據和代碼流難以推理,一次只對一件事執行邏輯(更新等),在一個類型/類中(“代碼放在哪里”問題),內存/數據局部性不是很好,一堆虛調用和指針。

    實體組件系統(ECS):實體只是一個標識符,有點像數據庫中的“主鍵”?是的。組件就是數據。系統是在具有特定組件集的實體上工作的代碼。

    ECS/DOD樣例:回憶一下簡單的“游戲”:400行代碼,100萬個精靈,20個泡泡:330毫秒更新時間,470ms啟動時間,310MB內存占用。

    第1步:糾正愚蠢。GetComponent在GO each和每一次中搜索組件,我們可以找到一次并保存它!330毫秒→ 309ms。

    GetComponent在Avoid組件的內部循環中,也緩存它。309ms → 78ms。

    現在時間花在哪里?使用探查器(Mac上是Xcode Instruments):

    讓我們做一些系統:AvoidanceSystem,避免和避免現在這些組件幾乎只是數據,系統知道它將操作的所有東西。

    struct AvoidThisComponent : public Component
    {
        float distance;
    };
    // Objects with this component "avoid" objects with AvoidThis component.
    struct AvoidComponent : public Component
    {
        virtual void Start() override;
    };
    
    // "Avoidance system" works out interactions between objects that have AvoidThis and Avoid
    // components. Objects with Avoid component:
    // - when they get closer to AvoidThis than AvoidThis::distance, they bounce back,
    // - also they take sprite color from the object they just bumped into
    struct AvoidanceSystem
    {
        // things to be avoided: distances to them, and their position components
        std::vector<float> avoidDistanceList;
        std::vector<PositionComponent*> avoidPositionList;
        // objects that avoid: their position components
        std::vector<PositionComponent*> objectList;
        
        void UpdateSystem(double time, float deltaTime)
        {
            // go through all the objects
            for (size_t io = 0, no = objectList.size(); io != no; ++io)
            {
                PositionComponent* myposition = objectList[io];
                // check each thing in avoid list
                for (size_t ia = 0, na = avoidPositionList.size(); ia != na; ++ia)
                {
                    float avDistance = avoidDistanceList[ia];
                    PositionComponent* avoidposition = avoidPositionList[ia];
                    // is our position closer to "thing to avoid" position than the avoid distance?
                    if (DistanceSq(myposition, avoidposition) < avDistance * avDistance)
                    {
                           /* … */
                    }
                }
            }
        }
    };
    

    以上是系統的邏輯代碼。78ms → 69ms。類似的,讓我們做一個移動系統:MoveSystem。

    // Move around with constant velocity. When reached world bounds, reflect back from them.
    struct MoveComponent : public Component
    {
        float velx, vely;
    };
    
    struct MoveSystem
    {
        WorldBoundsComponent* bounds;
        std::vector<PositionComponent*> positionList;
        std::vector<MoveComponent*> moveList;
        
        void UpdateSystem(double time, float deltaTime)
        {
            // go through all the objects
            for (size_t io = 0, no = positionList.size(); io != no; ++io)
            {
                PositionComponent* pos = positionList[io];
                MoveComponent* move = moveList[io];
                // update position based on movement velocity & delta time
                pos->x += move->velx * deltaTime;
                pos->y += move->vely * deltaTime;
                // check against world bounds; put back onto bounds and mirror the velocity component to "bounce" back
                if (pos->x < bounds->xMin) { move->velx = -move->velx; pos->x = bounds->xMin; }
                if (pos->x > bounds->xMax) { move->velx = -move->velx; pos->x = bounds->xMax; }
                if (pos->y < bounds->yMin) { move->vely = -move->vely; pos->y = bounds->yMin; }
                if (pos->y > bounds->yMax) { move->vely = -move->vely; pos->y = bounds->yMax; }
        }
    };
    

    以上是移動系統的邏輯,69ms→ 83ms, 什么!!!???再次分析之:

    迄今為止的經驗教訓:由于意想不到的原因,優化一個地方可能會讓事情變得更慢,亂序的CPU、緩存、預取等等。C++RTTI(dynamic_cast)可以非常慢,我們在GameObject::GetComponent中使用它。

    // get a component of type T, or null if it does not exist on this game object
    template<typename T> T* GetComponent()
    {
        for (auto i : m_Components) 
        { 
            T* c = dynamic_cast<T*>(i); 
            if (c != nullptr) 
                return c; 
        }
        return nullptr;
    }
    

    那就停止使用C++RTTI吧。如果有一個“類型”枚舉,并且每個組件都存儲了類型…83毫秒→ 54毫秒!!

    enum ComponentType
    {
        kCompPosition,
        kCompSprite,
        kCompWorldBounds,
        kCompMove,
        kCompAvoid,
        kCompAvoidThis,
    };
    // ...
    ComponentType m_Type;
    
    // was: T* c = dynamic_cast<T*>(i); if (c != nullptr) return c;
    if (c->GetType() == T::kTypeId) return (T*)c;
    

    到目前為止,更新性能:提高6倍(330毫秒)→54毫秒)!內存使用:增加310MB→363MB,組件指針緩存,在每個組件中鍵入ID…代碼行:400多行→500,讓我們試著移除一些東西!

    Avoid和AvoidThis組件,誰需要它們?沒錯,沒有人,直接用AvoidanceSystem注冊對象即可。54毫秒→ 46ms,363MB→325MB,500→455行。

    實際上,誰需要組件層次結構?只需在GameObject中包含組件字段。46毫秒→43ms更新,398→112ms啟動時間,325MB→218MB,455→350行。

    // each object has data for all possible components,
    // as well as flags indicating which ones are actually present.
    struct GameObject
    {
        GameObject(const std::string&& name)
            : m_Name(name), m_HasPosition(0), m_HasSprite(0), m_HasWorldBounds(0), m_HasMove(0) { }
        ~GameObject() {}
        
        std::string m_Name;
        // data for all components
        PositionComponent m_Position;
        SpriteComponent m_Sprite;
        WorldBoundsComponent m_WorldBounds;
        MoveComponent m_Move;
        // flags for every component, indicating whether this object "has it"
        int m_HasPosition : 1;
        int m_HasSprite : 1;
        int m_HasWorldBounds : 1;
        int m_HasMove : 1;
    };
    

    停止分配單個游戲對象,vector<GameObject*>vector<GameObject>,43ms更新,112→99ms啟動,218MB→203MB。

    典型布局是結構數組(AoS):一些對象,以及它們的數組。易于理解和管理,太好了…如果我們需要每個物體的所有數據。

    // structure
    struct Object
    {
        string name;
        Vector3 position;
        Quaternion rotation;
        float speed;
        float health;
    };
    // array of structures
    vector<Object> allObjects;
    

    數據在內存中是什么樣子的?

    struct Object // 60 bytes:
    {
        string name; // 24 bytes
        Vector3 position; // 12 bytes
        Quaternion rotation; // 16 bytes
        float speed; // 4 bytes
        float health; // 4 bytes
    };
    

    如果我們不需要所有數據呢?如果我們的系統只需要物體的位置和速度…嘿,CPU,讀第一個物體的位置(下圖上)!當然,就在這里…讓我幫你從內存中讀取整個緩存行(下圖下)!

    如果系統只需要物體的位置和速度…卻最終從內存中讀取了一切,但每個對象只需要60個字節中的16個字節,多達74%的內存流量浪費!

    翻轉:數組結構(SoA)。每個數據成員都有單獨的數組,數組需要保持同步, “對象”不再存在;通過索引訪問的數據。

    // structure of arrays
    struct Objects
    {
        vector<string> names; // 24 bytes each
        vector<Vector3> positions; // 12 bytes each
        vector<Quaternion> rotations; // 16 bytes each
        vector<float> speeds; // 4 bytes each
        vector<float> healths; // 4 bytes each
    };
    

    數據在內存中是什么樣子的?

    struct Objects
    {
        vector<string> names; // 24 bytes each
        vector<Vector3> positions; // 12 bytes each
        vector<Quaternion> rotations; // 16 bytes each
        vector<float> speeds; // 4 bytes each
        vector<float> healths; // 4 bytes each
    };
    

    在SoA中讀取部分數據:如果我們的系統只需要物體的位置和速度…嘿,CPU,讀讀第一個物體的位置!(下圖上)。當然,就在這里…讓我幫你從內存中讀取整個緩存行!(旁白)所以接下來4個對象的位置也被讀入了CPU緩存(下圖下)。

    SoA數據布局轉換:相當普遍, 不過要小心,不要做得過火!在某些情況下,單個數組的數量可能會適得其反,結構數組的結構(SoAo等)。

    回到組件數據的SoA布局。不再是一個游戲對象類,只是一個EntityID。43毫秒→31ms更新,99→94ms啟動,350→375行。

    // "ID" of a game object is just an index into the scene array.
    typedef size_t EntityID;
    
    // /* … */
    
    // names of each object
    vector<string> m_Names;
    // data for all components
    vector<PositionComponent> m_Positions;
    vector<SpriteComponent> m_Sprites;
    vector<WorldBoundsComponent> m_WorldBounds;
    vector<MoveComponent> m_Moves;
    // bit flags for every component, indicating whether this object "has it"
    vector<int> m_Flags;
    

    結果:100萬個精靈,20個泡泡:330ms→ 31ms的更新時間,快10倍!470ms→ 94ms的啟動時間,快5倍!310MB→ 203MB內存使用率,節省了100MB!400→ 375行代碼,代碼甚至變得更小了!甚至都還沒有開始線程化、SIMD…

    Rendering Technology in 'Agents of Mayhem'分享了游戲Agents of Mayhem所采用的渲染技術,包含話題OIT、照明計算、全局照明等。

    在OIT上,之前的半透明渲染是從后向前在CPU排序alpha,大量排序的alpha意味著CPU渲染效率低下,按“對象”排序,而不是按像素排序,排序跳變,低分辨率alpha不與高分辨率排序。解決方案:OIT?但許多OIT技術在GPU上效率低下。可以嘗試加權混合OIT(Weighted Blended OIT,WBOIT)

    加權混合OIT的好處是反向的CPU成本,現在可以按渲染狀態(即材質/著色器)而不是深度對alpha進行排序,在GPU上高效,alpha著色器中添加了一些數學,簡單的全屏合成步驟,低分辨率和高分辨率alpha無縫地排序,永遠沒有跳變,排序問題*滑過渡,不太復雜。缺點是到處都是神奇的數字,非常不透明的alpha表現不好,總是“錯”的。加權混合OIT用加權*均代替有序混合:

    加權函數(McGuire)的權重是“魔法”數據,權重高,覆蓋率高,權重更接*物體本身。

    自發光的Alpha是主要問題,可以考慮具有相同自發光Alpha值E的n層:

    主要思路:積累更多信息,“相加性”≈ 添加層的數量,通過相加性放大加權*均值。新WBOIT的視覺摘要:

    其中WBOIT的公式和可相加性描述如下:

    新的組合方式:

    加權函數與自發光:純自發光Alpha的不透明度為零,在計算重量時必須包括自發光,必須允許權重為零。

    實現:簡單的2個RT的MRT設置。第2個MRT將*均值存儲在R通道,可加性存儲為Alpha通道,對alpha通道使用單獨的混合控制。

    在光照方面,大量(昂貴的)照明功能已實現,如多個照明模型(所有PBR)、PCF陰影、可變半影陰影(PCSS)、投影紋理、紋理發射器區域燈光、泛光燈、“逼真”的管狀燈、方形或圓形聚光燈、黑色(負光)、光源裁減*面、光源遮擋和門戶。光照泄露是熟悉的問題,標準的解決方案:

    采用光照遮擋體,使用有限光剪裁*面:


    工作原理是在遮擋體“陰影”視錐體體上剔除tile(下圖左),列出需要對每個燈光進行逐像素檢查的遮罩(下圖右):


    實現的流程圖概覽:

    在GI方面,使用了LPV,改進了光照遮擋效果,在室內使用了固定的局部體積,質量更高,無需每幀注入和傳播。以往的LPV光照遮擋存在粗糙離散化的光照溢出和幾何缺失等問題:

    不管怎樣,藝術家們放置遮擋體也可以用于GI!

    傳播過程中的遮擋體:光照遮擋體注入體積時,存儲為“軸向”遮擋(沿每個軸的遮擋量),在傳播過程中遮擋光線,生成GI“陰影”(下圖左)。針對4x4x4宏觀單元格剔除的光照遮擋體,減少每個LPV單元格中考慮的一組遮擋體,應用過程中遮擋三線性采樣的光照,消除來自粗糙網格的漏光(下圖右):

    Rebuilding Your Engine During Development: Lessons from 'Mafia III'分享了重構游戲Mafia III所用的引擎的過程、技術和經驗教訓。該引擎重構的目標是更容易使用的工具,能夠處理大量數據,數據驅動,主要變化在于新世界編輯、新對象系統、構建系統、局部迭代、可視化腳本、中間件集成、物理、動畫、導航、用戶界面、音頻等。

    新的世界編輯器使用C#/WPF和DevExpress中的新工具,在新編輯器中集成舊編輯器插件,使用C++/CLI進行引擎通信,盡早讓用戶參與。

    新對象系統包含資產和文件管理、繼承和分組、賦予內容創作者權力等。資產和文件管理的目標是輕松跟蹤依賴關系,支持二進制和文本格式,省力,按ID標識對象,前后兼容。使用C++反射進行序列化,對于工程師來說,公開數據非常簡單,基于宏的內部框架,合理的前后兼容性,不需要版本控制系統,強大的代碼數據依賴性。每個對象都有一個唯一的標識符,資產的自由移動,服務讀取TOC和跟蹤ID,易于查詢依賴項。繼承系統提高資產的可重用性,易于工程師們和內容創作者使用,超越一切的能力,在序列化代碼中處理,基于反射和唯一ID,對可以修改的內容沒有限制。結果很好,但是…在保存上與父節點相比,子節點是碎片化的。

    重新保存依賴,但不知道如何在生產中修復,引入了一項“功能”來重新保存依賴,很難理解何時使用它。賦予內容創作者權力。合成對象的能力,將對象分組在一起,使用可視化語言編寫對象級腳本。每個對象都有唯一的ID很好,基于比較的泛型繼承系統,有子節點的父節點很棘手,賦予用戶的權力超出了我們的預期,技術允許的一切都將被使用。

    總之,升級到現代引擎,為新用戶提供更快的學習曲線,編輯器的一致控制,在生產過程中部署并不有趣。


    在過去的20年里,GPU廠商和游戲開發者都在努力追求“更好的幀速率”的圣杯。然而,人們對*滑度的問題知之甚少。The Elusive Frame Timing: A Case Study for Smoothness Over Speed探討了一個非常奇特、不為人所知但相當重要的原因,即可怕的“微結巴”,即使在單個GPU的情況下也經常發生,也將討論潛在問題、問題的歷史以及解決問題的原型方法。在應用程序中,口吃(stuttering)的情況比完美情況“更快”?是的,詳見下圖:

    Stutter的發生是因為游戲不知道它的顯示速度有多快!為什么游戲“認為”它運行得比較慢?上世紀80/90年代,8位/16位紀元,是固定硬件,總是相同的時間沒有問題,還記得不同的NTSC/PAL版本嗎?到了90/00年代,軟件渲染/圖形加速器,開始計時和插值,但沒有管線也沒問題。那么,當今怎么了?

    理論:“APl的/驅動的錯“。真正的GPU負載對游戲是“隱藏”的,該“功能”可能是在21世紀初的“驅動基準測試大戰”中引入的,由當時的Flush()和Finish()行為的改變,組合器也幫不上忙。試圖彌補這一點的內部機制?這就是為什么很難找到它!無論如何,使用流水線硬件來發揮其潛力是不可避免的,“緩沖空閑時間”是可以有的,但我們需要知道!

    錯誤時機的兩面,首先是錯誤的時間反饋,心跳口吃的主要原因(但有時也有其它原因)。其次是錯誤的幀調度,是導致從“緩慢”恢復到“完美”時口吃的主要原因。建議,必須知道過去的畫面持續了多久,異步統計,需要使用啟發式,必須承認它并不完美,理想情況下,知道下一幀將持續多長時間,但這是不可能的。必須能夠安排下一幀的顯示時間,更快并不總是更好!一定知道還有多少時間剩余,實際上是最有問題的部分。老的邏輯算法通常是以下的模樣:

    frame_step = 16.67 ms    // (assuming 60fps as initial baseline)
    current_time = 0
    while(running)
        Simulate(frame_step) // calculate inputs/physics/animation... using this delta
        RenderFrame()
        current_time += frame_step
        PresentFrame()        // scheduled by the driver/OS
        frame_step = LengthOfThisFrame() // calculated by the game
    

    新的邏輯算法改成以下代碼:

    frame_step = 16.67 ms // (assuming 60fps as initial baseline)
    current_time = 0
    pending_frames_queue = {} // (empty)
    frame_timing_history = {}
    while(running)
        Simulate(frame_step) // calculate inputs/physics/animation... using this delta
        RenderFrame()
        current_time += frame_step
        // current_time是新增的時間戳
        current_frame_id = PresentFrame(current_time)
        AddToList(pending_frames_queue, current_frame_id)
        // 查詢幀信息
        QueryFrameInfos(pending_frames_queue,frame_timing_history)
        // 幀時序啟發式
        frame_step = FrameTimingHeuristics(pending_frames_queue, frame_timing_history)
    

    上面的FrameTimingHeuristics()的內部結構:

    • 輪詢所有在pending_frames_queue隊列中已經可用的幀,將它們各自的計時記錄到frame_timing_history中。
    • 如果看到任何一個未完成其時間表的幀,返回其長度以用于frame_step(這就是我們降低幀率的方式)。
    • 如果看到recovery_count_threshold連續幀都是早且它們的margin < recovery_margin_threshold,返回用于frame_step的長度(這就是我們提高幀率的方式)。

    OpenGL+VDPAU原型:2015年8月在塔洛斯原則(The Talos Principle)中實現,作為概念證明。使用NV_present_video的OpenGL擴展,最初用于視頻播放,因此具有計時功能。差不多了:合理安排未來的幀,獲取過去幀的計時信息,但沒有邊緣信息,很難恢復。僅適用于Linux下OpenGL*臺的某些NVIDIA板,應用不是很廣,但證明了這一點。

    Vulkan和VK_GOOGLE_display_timing:在塔洛斯原則和Serious Sam Fusion中實現,什么信息都有:安排未來的幀、過去幀的計時信息、有邊緣信息(模棱兩可?)。僅可用于安卓,Metal、DX12尚沒有。

    考慮因素:何時決定從緩慢恢復到完美?如何(以及是否?)當下降到慢速時,如何校正計時?我們能預測速度并做出完美的下降嗎?如果可能的話,這將是“神圣的*滑”,可能會做得更好!VRR顯示器呢?如果Vsync關閉了怎么辦?這兩個都是可行的,但還沒有實現!

    主觀性:最終是一個感知的問題,也許有些人有不同的看法,不同的開發者會有不同的方法,向用戶公開不同的選項?

    Performance and Memory Post Mortem for Middle earth: Shadow分享了Monolith在《中土世界:戰爭的陰影》中實現30fps和內存的工程策略,主要分為性能優化和內存優化。

    線程的同步原語:不再使用內核原語,轉而使用輕量級原子自旋鎖來保護原子數據訪問,當需要上下文切換時,仍然使用內核原語,上下文切換的真正成本是緩存逐出。引入了一個輕量級的多讀單寫的原語,從多個線程讀取物理模擬的狀態就是一個例子。微軟*臺有一個超輕讀寫(SRW)鎖原語,索尼也有類似的原語。可以使用兩個原子自旋鎖實例和一個原子計數器來跟蹤讀取數量,第一個原子自旋鎖用作讀取鎖,第二個原子自旋鎖用作寫入鎖。

    線程的CPU集群:在控制臺上的兩個CPU集群之間分離工作負載,以利用共享的L2$。第一個CPU集群執行游戲邏輯,第二個CPU集群執行渲染邏輯。由于Shadow of War的復雜性增加,性能提高了10%,要求群集不同時接觸相同的緩存行,黑魔法。

    線程的整體方法:保持大量工作,盡可能多地保持單線程代碼,降低初級工程師的上手門檻,降低并發帶來的bug數量,需要更少的整體同步,通常會更好地使用CPU緩存。將整個系統移到后臺線程,為大系統設置硬親緣關系。用優先級較低的線程來填補CPU空閑,類似于異步計算的概念,但適用于CPU核心,例如文件I/O、流式傳輸和異步光線投射。

    線程的管線:通過使用循環命令緩沖區,操作以管線的方式進行,管線將操作推向一個方向,然后這些命令緩沖區中的每一個都在一個運行在專用內核上的專用線程上執行,允許每個管線階段獲得每幀33.3ms的完整數據。

    線程的動態幀調整:當管線化線程時,保證管線中的下一階段不超過1幀(33.3ms)。理想情況下,所有管線階段都是并行運行的,每個階段之間有幾毫秒的偏移。如果整個管線都被管線中的第一個階段所約束也是真實的。如果整個系統受到最后一個階段的約束,那么我們最終會顯示幾幀前模擬的幀,是由管線的伸縮性造成的,我們總是被v-sync的最后階段所束縛。

    輸入延遲成為一個問題,因為輸入是在模擬線程上評估的,比屏幕上顯示的幀早200毫秒。解決方案是動態幀調整系統,該系統監控GPU上的顯示隊列,如果隊列已滿,那么將受GPU限制,當受GPU限制時,人為地暫停第一階段,即SimulationThread,這樣它所花費的時間比分配的時間稍多,例如Sleep(33.3 TimeOfSimulationThread + 1),會導致望遠鏡收縮。

    在已知的最壞情況下,模擬線程現在下降到50毫秒:

    渲染器的常量緩沖區:游戲到引擎抽象層之間的復制太多,每個常數被單獨設置,然后組裝成一個常數緩沖區,每次drawcall都會重新生成常量緩沖區。根據更新頻率打破了常量緩沖區,訪問渲染器中的常量會返回指向實際常量緩沖區的指針,通過訪問器將常量緩沖區直接暴露給游戲。跟蹤臟狀態和幀代碼,一旦發送到GPU,就會生成一個新副本,并清除臟狀態,在GPU清除幀代碼之前,內存不會被重用。綁定命名常量緩沖區只是設置指針,材質常量緩沖區在構建階段烘焙到資產中。

    以骨骼變換為例:

    每個常量緩沖常數都是通過獲取關于常量使用頻率的啟發式方法手動排序的,很少使用的常量和通常設置為0的常量被排序到常量緩沖區的底部。分配的常量緩沖區僅足以容納已使用的常量和不為0的常量,大大降低了訪問的內存量。在GPU上讀取超過緩沖區末尾的數據只返回0,導致圖形API錯誤信息,但可以抑制。緩存的常量緩沖區帶有渲染節點,如果它們沒有改變,可以在不同階段重用,例如G-Buffer階段和CSM階段的骨骼。

    渲染器的常規優化:切換到更快的第1方圖形API,如具有快速語義/LCUE的D3D11.X。已刪除的所有幀到幀的參考計數器,僅使用幀代碼管理生命周期。緩存整個圖形API狀態,刪除對第1方圖形API的冗余狀態更改。通過在緩存行邊界上分配動態GPU內存并填充到緩存行的末尾,然后使用幀代碼跟蹤該內存以進行CPU訪問,減少了CPU和GPU緩存刷新。動態CPU負載縮放,根據CPU負載推出LOD以降低網格數,暫停高mip流以降低CPU使用率、內存壓力和物理頁面映射的成本。在已知的最壞情況下,渲染線程現在降至45毫秒:

    在內存方面,Shadow of War的一個巨大性能勝利是將所有分配切換到大型2MB頁面,超過64KB的頁面,性能提升20%。大頁面減少了代價高昂的翻譯查找緩沖區(TLB)丟失,在創建流程時預先分配所有大頁面。雖然PC幾乎總是受限于GPU,但它也在PC上實現了這一點。

    在已知的最壞情況下,模擬線程現在下降到40毫秒,渲染線程現在下降到36毫秒:

    開發構建:DLL用于改進工程迭代,所有項目都啟用了增量鏈接,調試:一些工程師可以使用FastLink,可執行文件是加載和運行這些DLL的一個小存根。零散構建:DLL現在編譯為LIBs,增量鏈接已禁用,可執行文件仍然是一個小存根,現在鏈接到LIBs中,將運行時性能提高約10%。LTCG在所有*臺和所有項目上都是啟用的,包括所有中間件,微軟*臺上的LTCG給了另外10%的改進,而PS4上的LTCG則提高了大約5%。PGO在所有*臺上都已啟用,PGO在所有*臺上的性能都提高了約5%,確保在Microsoft*臺上禁用COMDAT折疊,將抵消PGO帶來的收益。在已知的最壞情況下,模擬線程現在下降到33毫秒,渲染線程現在下降到30毫秒:

    GPU虛擬內存:現代GPU和CPU一樣使用虛擬內存,物理內存不必是連續的,物理內存可以按頁面粒度進行映射和取消映射,一個物理內存頁可以映射到多個虛擬內存地址。

    64K頁:使用64KB頁面的優點是它們比較大的頁面小,并允許更大的共享和重用。使用64KB頁面的缺點是,由于TLB未命中率增加,訪問速度較慢。

    Mipmap流:Shadow of Mordor不斷地流加載mipmap,但從未卸下,主要是為了縮短加載時間。如果使用了紋理,其高mip會被流化。Shadow of War為了節省內存,需要流入和流出高級別的mipmap,使用了一個固定的mipmap內存池,高mip是紋理2D內存的66%。在構建時間,分析每個網格,并確定具有最大紋理密度的最大三角形,把它保存下來。在運行時,渲染網格時,使用CPU將該三角形投影到屏幕空間,并計算*似的mipmap值。

    CPU系統由多個網格/材質控制,這些網格/材質可以每幀(64)進行分析,每個網格由一個幀代碼控制,并由一個阻尼器控制,以避免顛簸。每秒測試1920個網格,mipmap分析的CPU成本固定為每幀0.1ms。高mips使用64KB頁面池。這個頁面池是在進程創建時預先分配的,高MIP被加載,直到池耗盡,所有支持高mip流的紋理都是在沒有物理內存支持的情況下創建的。

    以上做法為高mips使用內存池可節省約1.0 GiB的內存。

    《戰爭陰影》使用了大量的紋理數組,如地形、角色模型、大多數結構、FX序列幀等。好處是Texture2Darray中的切片都在同一級別進行采樣,這對于混合非常有用。與采樣多個2D紋理時相比,通常更容易避免著色器中的分支來采樣Texture2Darray。但存在的問題是填充(padding)、復制。為了避開填充,手動將64KB頁面綁定到GPU實際讀取的紋理部分,不支持使用物理內存進行布局填充。壓縮MIP是小于64KB頁面并共享64KB頁面的MIP,它們總是由物理內存頁支持。

    通過在切片之間共享64KB物理內存頁來解決復制問題,Texture2DArray僅包含對Texture2D切片的引用,在運行時,切片的物理頁面被映射到Texture2Darray上,每個切片都是引用計數的,切片的物理內存在所有引用消失之前不會被釋放。壓縮的MIP比單個64KB頁面小,并且保持重復。該解決方案還允許我們將切片用作常規紋理2D,并將其與內存中的Texture2DArray共享,構建時間也減少了,因為我們一次只需要做一個切片。

    結論:避免重復*均節省了大約300M,視場景而定,避免填充允許藝術家創建非2的N次方的數組。

    The Challenges of Rendering an Open World in Far Cry 5闡述了Far Cry 5開發世界的渲染技術。

    對于水體渲染,深度多分辨率處理使用水的深度,好處是藝術家更喜歡水上的SSAO和SSS(屏幕空間陰影),由于陰影、大氣散射和霧在水面上延遲,因此有優化。缺點是延遲的陰影出現在水面上,分塊的燈光剔除不能正確剔除水下的燈光,但QC沒有報告任何錯誤,所以這是一個很好的折衷方案。如果我們想看到水下的透明物體呢?最終FC5將透明通道一分為二:

    for each transparent object
        find water plane at XY location
        if above water plane
            render after water
        else if below water plane
            render before water
        else // if intersecting water plane
            render before and after water
        end        
    end
    

    對于剔除,After water使用深度測試來裁剪水,Before water在頂點著色器中對水裁剪*面進行剔除。

    在一天的時間周期方面,挑一個你喜歡的日子!太陽或月亮總是存在的,不斷循環。計算今天和昨天太陽和月亮的位置,隨著時間的推移,從今天的位置過渡到昨天的位置,所以明天12:00和今天12:00是一樣的。

    CalculateSunPosition( timeToday, latitude, longitude, &azimuthToday, &zenithToday );
    CalculateSunPosition( timeYesterday, latitude, longitude, &azimuthYesterday, &zenithYesterday );
    
    float dayBlendFactor = secondsFromMidnight / (24.0f * 60.0f * 60.0f); // seconds in a day
    float azimuth = LerpAnglesOnCircle(azimuthToday, azimuthYesterday, dayBlendFactor);
    float zenith = LerpAnglesOnCircle(zenithToday, zenithYesterday, dayBlendFactor);
    

    像計算滿月一樣計算月光,可見的月相仍在發生,將月亮光的方向改為來自月亮的發光部分,以防止類似這樣的bug:

    但存在的問題還有不少,無法支持夜間物理照明值的對比度范圍,夾緊光照半徑只會增加對比度范圍,燈光藝術家將燈光調暗以支持夜晚,在一天中的其它時間導致不正確的行為。借鑒了電影使用照明設備來模擬月球照明的方法,解決方案:增加月亮的亮度,把月亮光源改成淺藍色,用于模擬填充燈光并降低對比度的最小環境條件。明亮的月亮會沖淡黎明/黃昏,因為它與衰落的太陽競爭。

    FC5還采用了局部色調映射,色調以不同的方式映射圖像的不同區域,減少明亮區域,使其進入更小的動態范圍。嘗試了后處理局部色調映射,但存在光暈等瑕疵。新想法:主要問題是黑暗的內部和明亮的外部,手動標記屏幕上需要調整曝光的區域,如窗戶、門等,我們稱之為曝光門戶。雙面幾何體,乘法混合使后面的顏色變暗或變亮,靠*相機時淡出。

    曝光門戶的局部色調映射。

    還有一種是基于GI的局部色調映射。雙邊模糊用于局部曝光,區分屏幕上的以下區域:2D空間的閉合區域、3D空間的遠距離區域,提供照明值的局部*均值,如果已經有了3D空間中照明值的局部*均值呢?事實上,有這些信息!這是全局照明系統,它存儲來自局部燈光和太陽的間接照明、天空遮擋。算法:從當前場景曝光創建一個參考中灰色,計算當前像素處的*均照明亮度:天空照明加上間接照明并忽略所有直接光(包括太陽光),比較值并相應地調整像素照明(發生在所有照明著色器中)。

    The Road toward Unified Rendering with Unity’s High Definition Render Pipeline分享了Unity的HDRP管線的實現過程和涉及的技術。

    高清晰度渲染管線(HDRP)的設計目標是跨*臺,如PC(DX11、DX12、Vulkan)XBox One、PS4、Mac,始終基于物理的渲染;統一照明,同樣的照明功能可用于不透明、透明和體積;一致性照明,所有燈光類型都適用于所有材質和全局照明,盡可能避免雙重照明/雙重遮擋。在光照架構方面,延遲、前向、混合的對比如下:

    也可以切換到所有都用前向渲染:

    材質架構如下:

    貼花的渲染架構如下:

    HDRP的BRDF如下:

    • 光照著色器。HDRP中的材質ID:材質特征的位掩碼,例如標準+半透明、標準+清漆+各向異性、標準+彩虹+次表面散射。
    • GBuffer約束。存儲空間帶來的獨特的材質特性,如虹彩、各向異性和次表面散射/半透明。

    標準著色器的GBuffer布局如下:

    另外,HDRP支持各向異性、清漆、GGX多散射、彩虹(Iridescence)、次表面散射等效果:

    在光照方面,支持LPV的全局光照:

    硬件和支持實時光線跟蹤的API的最新進展為新的圖形功能讓路,這些功能可以大幅提高最終幀的質量。Leveraging Real-Time Ray Tracing to build a Hybrid Game Engine將重點討論在將實時光線跟蹤功能(與現有圖形API)集成到生產游戲引擎中時應考慮的問題,深入研究實時光線跟蹤功能的實現細節,并提供開發過程中的經驗教訓。然后,還概述幾種在現代渲染管線中利用實時光線跟蹤的算法,例如反射和隨機區域光陰影,切深入了解在生產引擎中實現商品消費類硬件快速性能所需的重要優化。

    傳統的渲染管線如下圖上所示,其中藍色部分和間接光無關,可以忽略。下圖下的紅色是和間接光相關的階段。

    對于下圖的黃色步驟,解決方案是多次反彈或*似。接下來要看的是透明度,它似乎是光線追蹤的一個很好的候選者,對嗎?

    事實證明,屏幕空間照明問題同樣適用于透明材質(多維性、性能、過濾)。當前已經在探索SSS的體積解決方案,但沒有正確的SSS體積解決方案。混合渲染管線的流程如下:

    對于非直接光照,分裂和*似Karis 2013有助于減少方差,藍色是預先計算的,使用光柵化或光線跟蹤進行評估。

    在RTX的渲染流程如下:

    隨機化的區域光渲染流程如下:

    數據驅動架構是視頻游戲中非常常見的概念,因為團隊中的非編程部分需要輕松添加新功能。這種模式在每一個流行的游戲引擎中都得到了實現,它是增強內容創作者能力和釋放他們創造力的關鍵技術。Content Fueled Gameplay Programming in 'Frostpunk'介紹了11 bit studios內部技術的數據驅動的游戲玩法架構,它如何影響Frostpunk的游戲玩法代碼以及游戲的整體制作過程。

    數據驅動架構:基于實體組件系統(ECS),靈活、易于擴展的配置,由設計和藝術團隊創建的內容,程序員只提供工具,架構可以進行多次迭代和實驗。沒有數據驅動的體系結構,創建獨特的社區建設者是不可能的。

    RTTI類是運行時類型標識,可序列化為XML節點或二進制的C++類,編輯器中使用XML,游戲中使用二進制格式。模板是RTTI類定義游戲中對象的文件,由GUID識別,編輯器支持。

    模板示例:實體模板-定義游戲中的對象類型、組件模板-定義組件、網格模板-定義網格并導入數據、UI樣式-UI元素/UI屏幕定義。ECS實現示例如下:

    在組件中保存的狀態/數據,在系統中實現的邏輯,系統可以保持全球狀態,ECS之外的一些功能(UI、輸入…)。組件和組件模板是RTTI類,實體模板有一個組件模板列表,組件保留對其模板對象的引用,模板對象中保存的常量數據,組件對象中保存的可變數據。每個實體一個類型的組件,清晰的配置,更少的配置錯誤。每個組件類型一個系統,單一責任原則,簡化架構。沒有系統/組件類繼承,每個組件類型規則一個系統的后果,可能的位掩碼ID(下圖),通過添加新組件類型擴展功能。

    支持組件依賴和初始化順序:RequireComponent<Type>AddAfterComponent<Type>。組件集:設計師定義的組件批次,包含定義最常用實體類型的組件,例如建筑物、公民、資源堆,可以嵌套,如果重復,則使用最高級別的組件定義。

    class GeneratorComponent : public ComponentImpl<GeneratorComponent, GeneratorComponentTemplate>
    {
        DECLARE_PROPERTIES(GeneratorComponent, ComponentBase)
        {
            DECLARE_ATTRIBUTES(Attr::RequireComponent<BuildingComponent>());
            DECLARE_ATTRIBUTES(Attr::AddAfterComponent<HeaterComponent>());
            DECLARE_PROPERTY(SteamGenerationUpkeep, 0);
            DECLARE_PROPERTY(SteamPower, 0);
            
            ...
        }
        
        ...
            
        Map<EntryLink<ResourceEntry>, int> SteamGenerationUpkeep;
        float SteamPower = 0.0f;
        friend class GeneratorSystem;
    };
    

    架構預覽:

    《DirectX 12 Optimization Techniques in Capcom’s RE ENGINE》講述了Capcom公司的RE引擎使用及優化DX12的技術。用到的工具包含RGP、RGA等。

    RE Engine使用“中間繪圖命令”,獨立于*臺的命令,允許程序員在沒有*臺知識的情況下編寫繪圖命令,對多*臺開發有用,能夠在多個線程上創建繪圖命令,這些“中間繪圖命令”在創建后進行排序,然后轉換為API命令,使用優先級變量(uint 64位值)控制繪圖順序,允許用戶自行決定批處理,用于控制UAVOverlap和異步調度的同步定時。

    控制臺優化適應PC的措施有使用MultiDraw進行遮擋剔除、UAVOverlap、Wave指令、深度界限測試,針對DirectX 12的優化有減少資源屏障、緩沖區更新、根簽名、內存管理等。ExecuteIndirect總體而言,沒有想象的那么多改善,但對于基于GPU的遮擋剔除的實現非常有用。基于GPU的遮擋剔除的實現可以用ExecuteIndirect和預測指令(Predication command)兩種實現方式。ExecuteIndirect是4字節對齊,使用CountBuffer控制間接參數執行的次數,預測命令是8字節對齊,與控制臺不兼容。使用“VisibleBuffer”管理可見性,實際上,它是RE引擎中的緩沖區,字節地址緩沖區,元素數等于場景中的最大網格數,每個元素包含每個網格的可見性,0xffff表示可見,0x0000表示不可見。可見性測試時,用EarlyZ繪制([earlydepthstencil]屬性),將0xffff存儲到VisibleBuffer中,盡量減少以波為單位的同一地址的寫入[dorobot16]:

    [earlydepthstencil]
    void PS_Culltest(OccludeeOutput I)
    {
        // 減少以波為單位的同一地址的寫入.
        uint hash = WaveCompactValue(I.outputAddress);
        [branch]
        if (hash == 0)
        {
            RWCountBuffer.Store(I.outputAddress, 0xffff);
        }
    }
    

    然后應用可見性測試結果:按網格應用繪圖,使用MaxCommandCount指定繪圖數量,VisibleBuffer作為CountBuffer,CountBuffer 0xffff:啟用繪圖(計數為MaxCommandCount),計數緩沖區0:禁用繪制。

    void ExecuteIndirect(
        ID3D12CommandSignature *pCommandSignature,
        UINT MaxCommandCount,
        ID3D12Resource *pArgumentBuffer,
        UINT64 ArgumentBufferOffset,
        ID3D12Resource *pCountBuffer,
        UINT64 CountBufferOffset);
    

    還有另外一些改進的余地:有效針對道具和角色網格,剔除方法對較小的AABB單元有效,但對大網格無效,大網格始終可見,需要精細分割網格以獲得更好的效果。可以自動細分大的網格,剪取256個三角形作為一批,每批由連續的間接參數組成,每批創建AABB。然而這種方法也存在一些問題:幾乎所有繪制都低于768個索引,大量批次導致性能不佳(取決于硬件),如果相鄰間接參數連續,則合并命令。

    分區z-prepass(Partial Z-prepass):運行盡可能少的片段著色器,每個網格的Z-prepass都很昂貴,成本可以超過收益,將Z-prepass限制為相機附*的網格,重用自動劃分模型。不同裁剪方法的性能對比:

    但發現基于GPU的遮擋剔除無法獲得明顯的性能提升:

    UAVOverlap:DirectX12無依賴的著色器可以并行執行,UAV屏障的依賴性不明確,不清楚是讀還是寫,如果每個批寫入單獨的位置,則可以并行地執行,如果WAW(write-after-write)危險是可以避免的。用于每次計算著色器dispatch的可控的UAV同步,通過禁用UAV的同步,使并行執行成為可能,在DirectX 11中,可以使用AGS和NVAPI引入等效函數。

    // uavResourceSyncDisable就是禁用UAV的同步。
    void dispatch(u32 threadGroupX, u32 threadGroupY, u32 threadGroupZ, bool uavResourceSyncDisable = false);
    void dispatchIndirect(Buffer& buffer, u32 alignedOffsetForArgs, bool uavResourceSyncDisable = false);
    

    啟用UAVOverlap有略微的提升:

    wave指令:著色器標量化可以提高線程并行工作的速度,用于照明、基于GPU的遮擋剔除、SSR…Wave Intrinsic通過消除不必要的同步來提高標量化的效率。在DirectX 11和DirectX 12中受支持,使用AGS內置著色器模型5.1,也可與Shader Model 6.0一起使用。

    深度邊界測試:夾緊深度達到特定深度范圍,主要用于消除無關的像素著色器,可與DirectX 12(創作者更新)和DirectX 11.3一起使用,帶有AGS和NVAPI的DirectX 11,在RE ENGINE中,它用于貼花和光束。處理貼花時,在深度測試失敗的像素上運行,完全遮擋時,最好跳過處理,使用深度邊界測試解決。

    在沒有對資源屏障進行優化的原始構建中,批量插入了資源屏障,在執行繪圖命令之前,立即轉換當前批次所需的資源屏障:

    導致大量資源屏障,是基于GPU的遮擋剔除并沒有顯著提高性能的原因之一:

    資源屏障較多的階段運行效率不高:

    減少資源屏障:通過考慮每個資源的子資源進行優化,很難通過所有中間繪圖命令手動創建最佳資源屏障,困難表現在獲得最大的GPU性能和保持無Bug。為命令分析添加pre-pass,自動計算資源屏障的位置,分析中間繪圖命令,中間繪圖命令按優先級排序,可以按時間順序跟蹤每個資源的繪圖命令使用情況,通過改變優先級順序,分析具有依賴性的批次可以輕松提高GPU的效率。壓縮打包資源屏障的過程如下:

    上:對不同時間點的Barrier向前搜尋前面資源的Barrier;中:找到這些Barrier的共同時間點;下:遷移后面Barrier到同一時間點,執行合批。

    優勢是不必對內部實現和緩存如此敏感,減少不必要的資源屏障。劣勢是需要命令解析時間,但PC超級速!

    在更新緩沖區時仍然存在效率低下的部分如下圖:

    DMA傳輸中驅動程序造成的大量資源屏障:

    發生了什么事?圖形隊列上的緩沖區更新,復制緩沖區,如GPU粒子緩沖區更新和更新蒙皮矩陣,CopyBufferRegion作為DMA傳輸執行。在執行DMA傳輸時,強緩存刷新正在運行,一級緩存、二級緩存、K級緩存,批處理資源屏障沒有任何影響!可能的解決方案是如果每幀只有一次更新,則使用CopyQueue進行更新,使用計算著色器進行更新,前提是使用了計算著色器。

    StructuredBuffer<uint> fastCopySource;
    RWStructuredBuffer<uint> fastCopyTarget;
    
    [numthreads(256,1,1)]
    void CS_FastCopy( uint groupID : SV_GroupID, uint threadID : SV_GroupThreadID )
    {
        fastCopyTarget[(groupID.x * 2 + 0)*256 + threadID.x] = fastCopySource[(groupID.x * 2 + 0)*256 + threadID.x];
        fastCopyTarget[(groupID.x * 2 + 1)*256 + threadID.x] = fastCopySource[(groupID.x * 2 + 1)*256 + threadID.x];
    }
    

    固定緩沖區更新的優化:通過上傳堆更新所有常量緩沖區,更新同一固定緩沖區需要資源屏障和CopyBufferRegion(DMA傳輸),將新值存儲到上傳堆中,并獲取上傳堆偏移地址,使用ConstantBuffer的著色器只需要參考偏移地址,不再需要資源屏障和復制緩沖區。復制緩沖區縮減比較,成功消除了低效率:


    根簽名:DirectX12使用與DX11和控制臺類似的RootSignature,在運行時確定,而不是在著色器構建時確定,為每個IHV提供定制優化,對于AMD,使用RootParameter作為表格,對于NVIDIA,使用RootParameter優化ConstantBuffer訪問。

    內存管理:在第一個實現中,內存回收從大約50%的內存使用率開始,相當保守,游戲過程中出現了許多尖峰。在《生化危機2》中,每次角色移動時,控制每個房間的裝載和處理都會導致尖峰,甚至在加載暫停菜單的UI時發生。在內存耗盡之前不要逐出,防止微型逐出,當內存使用率超過90%時,未引用的內存將被逐出。

    經過以上所有優化之后,性能得到了24%的提升(優化實屬不易啊!!):

    GPU Driven Rendering and Virtual Texturing in 'Trials Rising'介紹了Trials Rising團隊在所有目標*臺(PS4、Xbox One、Switch、PC)上實現60 FPS恒定性能的過程,這些*臺的世界復雜性大大增加,包含GPU驅動的渲染實現及其與虛擬紋理的集成、主要創新、優化和性能結果的詳細信息,致力于介紹任天堂交換機*臺上GPU驅動的渲染可擴展性和技術效率的改進。

    GPU驅動的渲染將可見性測試轉移到GPU,直接在GPU上使用測試結果,GPU上的批處理實例,將不同網格的實例合并在一起,GPU能夠感知場景狀態,而不僅僅是通過frustrum測試的部分,GPU為自己提供渲染功能。

    GPU數據結構:兩個GPU緩沖區用于存儲幾何體(即頂點和索引池),一個大型GPU緩沖區,用于保存實例參數(即實例數據池),實例描述符表示的實例,一個GPU緩沖區,用于存儲場景中所有描述符的數組,CPU和GPU主要使用此列表中的索引進行操作。實例描述符包含內部和外部數據,幾乎是一個指針表,可以表示場景中的任何實例,和描述集非常接*:

    CPU和GPU實例狀態同步:CPU負責實例狀態模擬并上傳到GPU,GPU等待信號讀取實例數據,直接的狀態再生每一幀都不起作用,根據第一次實施結果,數據流量相當高,并且在未來有增加的趨勢。數據傳輸改進:實例更新率不是恒定的,狀態參數*均更新率不同,通常是穩定的,“固定”參數適用于特定實例類別,大量“閑置”組件,同步狀態的最小值。

    微型合批:寫一個包含緩沖區偏移量和要寫入的數據的簡單列表,合批程序的順序很重要:

    GPU實例表合批:

    池合批:

    微型合批結果:延遲同步,所有GPU結構只有一個顯式同步點,不與GPU爭用以訪問/修改緩沖區,合批數據收集顯著提高了CPU性能,合批池數據散射影響GPU性能,在“臟”狀態下的實例要少得多。

    可變同步速率:使用可變動畫速率的想法,基于“重要性”因素同步實例狀態,保持確定性(用于回放、調試等),盡可能自動計算“重要性”,整體同步問題看起來與網絡同步非常相似。

    文中還使用異步計算來并行處理物理模擬:

    虛擬紋理:一次訪問所有紋理數據,通常比普通紋理流技術的內存使用和數據傳輸壓力更低,極大地提高了GPU驅動渲染的“批處理”效率,替換某些*臺/API可用的無綁定紋理。

    VT使用了專用通道:使用專用攝像頭查看VT頁面請求通道,流預測、紋理預加載等,需要額外的視口剔除。光柵化幾何體兩次以生成頁面請求RT,較小的渲染目標(1/4分辨率+抖動),存儲頁面請求(x坐標、y坐標、mip),分析CPU并生成加載請求。In place PR:用兩個UAV緩沖區(Bloom過濾緩沖區、頁面請求緩沖區)替換渲染目標,將PR過濾、排序等移動到GPU,并將其委托給GBuffer中的每個像素,允許從CPU上節省一些時間,對透明通道、alpha混合通道等的頁面請求。Trials Rising“In最終使用了“In place PR”,這一變化使得每一個奇數幀(Xbox One計時)的CPU時間減少了約35毫秒,可能會導致額外的紋理加載延遲,必須加以考慮。半透明可以放置在Mega紋理中,很棒的著色器代碼簡化。虛擬紋理可伸縮性:每幀的頁面請求決定了系統的“性能”,高度依賴于渲染分辨率和VT頁面緩存大小,這兩個參數都可以在“在線”和“離線”設置中控制。

    GPU驅動的渲染可伸縮性:GPU管線的計算部分速度非常快,剔除和幾何組合可根據GPU時鐘和內存帶寬/延遲進行縮放,光柵化基于分辨率和比例,GPU管線需要CPU/GPU的特定數據路徑,實例同步對內存帶寬的影響,具有少量實例開銷的批處理比實際工作量要高。對低端*臺的遠距離對象使用更激進的消隱設置,根據CPU/GPU負載調整運行時的詳細級別。

    Snowdrop是一款AAA級游戲引擎,由育碧開發,為The Division及其續集提供動力。Efficient Rendering in 'The Division 2'闡述了The Division 2中使用的一些技巧,以在PC和控制臺上實現最佳性能,如構造幀、異步計算、多線程、內部函數、命令列表提交等。

    Snowdrop使用了異步計算和圖形管線相結合的計算,管線總覽如下(上半部分是圖形管線,下半部分是異步計算):

    整體管線使用CPU/渲染/GPU工作交錯,盡早提交,頻繁提交,沒有渲染圖或幀布局的先決信息,自動資源過渡跟蹤,但可以選擇不自動追蹤。每幀50到60次提交,200次過渡/100次屏障,3到6k個繪制調用,3到6百萬個圖元,(一些供應商)提交的時間比構建即時命令列表的時間要多。渲染核心處理非命令列表操作,包含渲染狀態/PSO和資源創建:緩沖區、像素存儲、紋理、RT。。。管理渲染上下文,圖形/計算/延遲/DMA。

    更新緩沖區時,所有瞬態數據復制到上傳緩沖區,然后復制到GPU局部緩沖區,著色器僅從GPU本地緩沖區讀取!沒有更快,而是可以獲得更穩定的幀率。命令列表鏈接(Command list chaining)在一些控制臺上可用,允許在記錄命令列表時執行命令列表,將CPU和GPU停頓的風險降至最低,但在DirectX?12中不可用…解決方案:模仿!!!進入隊列管理器,處理命令列表操作,如ExecuteCommandLists、關閉、重置,隱藏這些操作的CPU成本,有自己的工作線程和任務隊列,實際上是一個自定義驅動程序線程。

    隊列管理器提交它可以提交的內容,原子追蹤命令列表狀態,如錄制、打開,每個上下文一個隊列,按優先順序的俄羅斯轉盤(Round robins)方式執行計算機、圖形、DMA等。隊列管理器細節:

    原生和隊列管理器對比:

    隊列管理器可以消除大多數CPU停頓,預測性地準備命令列表,避免命令列表創建/重置的停頓,消除多余的信號/等待。

    異步計算的提交也由隊列管理器處理,2種類型的計算工作負載:依賴于gfx狀態、獨立的,用于不需要很快完成的工作負載。異步計算示例:深度下采樣和光源剔除、霧與體積計算、雨和雪的GPU粒子、天空/覆蓋采樣、草地/植被更新、陰影(可變半影預計算)、GI重新照明等。異步計算可能會讓gfx管線停頓,可能會導致GPU使用不足,控制臺上:限制異步計算占用,在PC上不支持D3D12_COMMAND_QUEUE_PRIORITY。

    High Zombie Throughput in Modern Graphics講述了Saber引擎的渲染管線(高級GPU工作流、著色概述、Vulkan優化)和僵尸渲染(特寫和中檔角色、遠距飛機群、貼花)等內容。Saber引擎渲染管線的特點是GPU驅動的可見性系統、完整的深度prepass、前向+PBR著色、烘焙GI:RNM+主導方向、2幀延遲、多線程命令緩沖區記錄。GPU工作流如下:

    GPU可見性系統:基于“游戲的實用、動態可視性”[Hill11],簡單高效,無需PVS/門戶等,主攝像機:HZB遮擋剔除與手工遮擋器,PSSM拆分:僅是追究剔除,0.7ms(XBox One)的GPU預算,用于100k+對象的場景,CPU回讀需要1幀延遲。

    D3D11和Vulkan的渲染管線對比:

    Vulkan vs D3D11:GPU時間最多減少10%,wave指令(GL_KHR_shader_subgroup),前向+照明/反射循環在著色過程中進行標量化,異步計算,半精度數學。無輔助驅動程序線程:渲染CPU總負載減少40%,由于MT命令緩沖區,CPU關鍵路徑減少20%。渲染目標內存重疊使得內存減少30%,并支持動態分辨率。

    僵尸渲染:每幀超過5k個可見僵尸,大多數都是非互動背景,300多個前景“真實”游戲內的實體/角色,只有50個僵尸大腦發育完全,實例被用來降低繪制調用壓力,靈活的每實例可視化定制系統。自定義網格:3種基本原型/骨架(男/女/“大”男),每具骨骼約50根骨頭,每個模型4個網格區域(腿/軀干/頭/頭發),每個區域2-5k個三角形,每個區域4-8個網格變體,總共70多種獨特的網格組合,再加上10個獨特的僵尸模型(化學制品、尖叫器等)。

    定制顏色和染色口罩:每個網格區域(襯衫/牛仔褲/鞋子/頭發):顏色掩蔽紋理(ARGB8紋理,低分辨率)、污漬掩蔽紋理(ARGB8紋理,低分辨率);共享污漬紋理集(血跡/雪/灰塵):反照率/法線/粗糙度紋理,所有紋理都是tile的,存儲為紋理數組;每實例常數,如顏色:通道遮罩+純色,著色:通道掩碼+紋理數組索引。

    僵尸渲染的實例化:從可見性系統中獲取單個僵尸,將相同的網格收集到實例化桶中,一個區域中變化相同的網格進入一個桶,不同的LOD模型將分離存儲桶。將累積的存儲桶處理到渲染隊列中,按可見性遮罩對桶進行排序(每個活動攝像頭都有自己的位), 將具有相同可見性遮罩的網格分批次收集(最多30個網格),對于每個批次,使用每個實例數據填充連續常量緩沖區,為每個過程/攝像頭(Z、SM、著色)每批生成1個繪圖調用。

    僵尸渲染的背景群體:超過5000個僵尸,非交互式:本質上只是一個GPU驅動的動畫,共8種預焙變體:2種網格類型 x 4種獨特外觀,每個網格約400個三角形,靈感來源于The Technical Art of Uncharted 4” [Maximov16]。利用現有的草地渲染解決方案,添加紋理烘焙頂點動畫,沿著預先建模的軌道移動。

    Halcyon Architecture - "Director's Cut"分享了迷你游戲PICA PICA的渲染架構和相關技術。Halcyon的渲染概覽如下:

    渲染句柄:由句柄關聯的資源,輕量級(64位),恒定時間查找,類型安全(即緩沖區與紋理),可以序列化或傳輸,次代之間安全,如雙重刪除、刪除后使用。可以在同一過程中混合搭配后端,使得調試VK實現變得更加容易,DX12位于屏幕左半部分,VK位于屏幕右半部分。

    渲染命令:指定的隊列類型,規格驗證,允許運行?例如利用計算,自動調度,它能跑哪?異步計算。

    渲染圖:幀圖 -> 渲染圖:沒有“幀”的概念,全自動轉換和分割屏障,單個實現,不考慮后端,從高級渲染命令流轉換,渲染圖中隱藏的API差異。對多GPU的支持,大多是隱式且自動的,可以指定計劃策略。多個圖在不同頻率下的組合,相同的GPU:異步計算,mGPU:每個GPU的圖形,核心外:服務器集群、遠程流。

    渲染圖有兩個階段:圖形構造(指定輸入和輸出,串行操作)和圖形評估(高度并行化,記錄高級渲染命令,自動屏障和過渡)。它還支持多GPU的渲染調度:

    另外,Halcyon使用了虛擬多GPU的技術。大多數開發者只有一個GPU,不適用于2臺GPU機器,對于3+GPU來說很少見,適用于展廳,最高可達11個,不適合常規開發。

    Halcyon的著色器跨*臺方案如下:

    Not-So-Little Light: Bringing 'Destiny 2' to HDR Displays介紹了為支持《命運2》的HDR顯示而采取的方法,探討了如何改變“命運”渲染管線以支持這項新技術,包括將SDR創建的內容引入HDR世界所面臨的挑戰。還將介紹在《命運2》中實現高質量HDR輸出所使用的技術,以保持命運的獨特外觀,并反思從工作中吸取的經驗教訓。

    Destiny 2的渲染管線如下:

    色調映射效果對比:

    顏色分級使用的LUT是SDR,使用數學映射:我們總是想要線性輸入,先是色調映射,然后轉換。忽略變換,則是可逆的。

    SDR Color = TonemapForSDR (Input Color);
    LUT Color = LutLookup (SDR Color);
    ...
    Transform = Input Color / max (SDR Color , 0.001);
    ...
    LUT Color *= Transform;
    

    當InputColor為0時,LutColor也為0。其中UI的渲染流程如下:

    新的渲染管線如下:

    EOTF是電-光的傳輸函數,從電壓到光強度。OETF是光-電的傳輸函數,從光強度到電壓。sRGB的EOTF如下:

    BT.709 OETF:

    技術陷阱:增強型HDMI,在著色器中小心使用飽和,fp16緩沖區表示負數,SDR空間中的AA,更深的黑色會加重屏幕噪音。文中采用了HDR的LUT,在用戶界面中使用色度/亮度,利用ICtCp或其它,最后一步是色調映射。離線的HDR管線如下:

    經驗教訓:內容驗證,做好改變SDR管線的準備,盡可能長時間地維護HDR緩沖區,探索RGB的替代方案,使用光度單位,做一個比較工具。

    6 Years of Optimizing World of Tanks: Making the Game a Great Experience on All Systems from Laptops to High End PCs分享了坦克世界的多年優化經驗,包含并行渲染、并發渲染、坦克履帶、Havok/AVX2、光線追蹤陰影等。

    優化Ultrabook - 陰影:使用共享視頻內存正確檢測英特爾硬件,靜態對象的自適應陰影貼圖(ASM),動態對象的級聯陰影貼圖(CSM)。

    優化Ultrabook - 性能優化:動態分辨率根據性能的不同,場景以動態分辨率渲染,且不縮放,UI始終以完全分辨率渲染,PC游戲的首批實現之一,在游戲中允許穩定的30 fps幀速率。硬件特定優化(2015年),用于在英特爾GPU上進行植被渲染的兩次模具寫入,以獲得最佳模板+clip()的性能。

    2018年,引入了TBB,讓引擎為現代多核CPU做好準備,第一個問題是選擇一個好的作業系統。如何選擇一個好的作業系統?WoT工程團隊標準:易于使用,兩種類型的并行:功能/任務和數據,功能豐富且強大,很好的支持。線程構建模塊(TBB)是并行算法和數據結構、線程和同步,可擴展內存分配和任務調度,是一種僅限于庫的解決方案,不依賴于特殊的編譯器支持,支持C++、Windows、Linux、OS X、Android和其它操作系統。

    WoT 1.0多線程渲染架構如下:



    到了2018年,WoT使用了SIMD(AVX2、ISPC)等技術進一步加速并行,并且使用TBB來加速Havok的破壞系統。

    到了2019年,WoT使用光線追蹤來改進陰影的質量。

    RT陰影的實現細節:

    • CPU端:兩級加速結構。

      • BLAS BVH。適用于所有坦克網格,在網格加載期間構建一次,并上傳至GPU,網格中的硬蒙皮部分拆分為多個靜態BVH,跳過軟蒙皮部分。
      • TLAS BVH。多線程,使用英特爾Embree和英特爾TBB,重建每一幀并上傳到GPU。
    • GPU端:像素著色器或計算著色器。

      • 基于均勻錐分布的時間射線抖動。
      • BVH遍歷和射線三角形交點。
      • 時間積累。
      • 降噪器(基于SVGF)。
      • 時間抗鋸齒。

    CPU BVH性能:CPU幀時間占2.5%,TBB線程,SSE 4.2(比原始WoT內部BVH builder快5.5倍),每幀更新多達5mb的GPU數據,高達72mb的靜態GPU數據。相關優化:RT陰影只能由坦克投射,不支持alpha測試的幾何體,BLAS的LOD,每像素1射線,如果出現以下情況便停止追蹤光線像素:NdotL<=0、如果像素已被陰影貼圖遮擋、距離攝像機超過300米。

    Software-based Variable Rate Shading in Call of Duty: Modern Warfare涵蓋了《使命召喚:現代戰爭》(2020)中使用的一種新型渲染管線。它可以實現高度可定制的基于軟件的可變速率著色;以與圖像頻率匹配的分辨率渲染部分渲染目標的方法。與僅限于可用設備子集的基于硬件的解決方案不同,其基于軟件的實現使得在廣泛的消費類硬件上實現更高的質量和性能成為可能。文中介紹了設計過程、最終實現,以及對管線每個階段的深入檢查,包括預過程渲染、可變速率估計器、用于計算著色器內核的新穎且獨特的像素打包方案,以及forward+渲染器中的最終幀渲染,還討論了其它渲染器類型和作業類型中的潛在實現及前瞻性擴展,以更好地滿足質量和性能范圍內的許多不同用例。

    2020年的不少硬件已經支持硬件可變速率著色(Variable Rate Shading,VRS),支持硬件中的可變速率著色,如DX12 Ultimate[DX19] [DX20],包含AMD-RDNA2 GPU、英特爾-11代APU、英偉達-圖靈GPU等。

    在不同的Tier支持不同的特性:

    • Tier 1:設置每次繪制的著色速率。
    • Tier 2:設置每個屏幕tile的著色率(和其它頻率),最小為8x8像素的tile大小。

    可以選擇適合圖像頻率的著色速率[LEI19]。

    由此催生了多分辨率渲染管線 [DRO17]:

    在標記為低分辨率的材質上進行3x–3.8x性能伸縮,方差取決于渲染目標微分塊的命中量、屏幕上全分辨率和低分辨率粒子之間的重疊。0.3ms–0.4ms的組合:向上采樣/解析/重建過程,方差來自需要所有子樣本的微分塊的數量。可能因GPU MSAA效率而異,對某些高三角形透明圖形上的quad占用的擔憂。

    COD團隊以4xMSAA(?分辨率以匹配原始分辨率目標)渲染prepass,在4xMSAA中渲染不透明,每次繪圖允許1s、2s、4s模式,發現對prepass渲染的巨大加速,與DX12 VRS Tier 1.0相匹配的靈活性。然而本次的實驗結果是失敗的:未達到IQ目標(性能模式下),未達到性能目標(最壞情況下無變化)。

    原因是有132k幸存的三角形,886k幸存的像素,*均每個三角形有6.7個像素。

    令人耳目一新的概念:光柵化和quad占用、MSAA與quad占用的交互、MSAA與內存的交互(交錯渲染)、與MSAA和quad占用的模板交互。

    光柵化和quad占用的關系。

    quad占用和分辨率的關系。

    quad占用和MSAA的關系。

    quad占用和MSAA、模板的關系。

    MSAA重組和交錯渲染的關系。

    COD團隊的計劃是把所有東西都設成白色,使用模板進行VRS遮蔽。早期研究表明,在最壞的情況下,負面的性能損失可以最小化,如聚合采樣率、交錯渲染、利用VRS遮蔽優化的CS/PostFX作業。與硬件VRS相比,軟件VRS的主要優勢是可在所有*臺上使用,由于tile較小,因此可能具有更高的性能,由于較小的tile和隱含的重建方法,圖像質量可能更高。軟件VRS的管線如下:

    常規設置:幀渲染時對繪制和CS作業考慮了4xMSA重組/反交錯,VRS以像素quad粒度(2x2)運行,與在MSAA的tile大小(16x16)上工作的硬件實現不同。Swizzle圖案是帶旋轉的方形,從VRS著色速率貼圖手動創建模板遮罩。

    VRS圖像遮罩:逐quad粒度,僅基于非圖像數據的2位掩碼,用于圖像著色速率估計和VRS渲染遮罩的生成。4種模式:單一樣本、Delta H、Delta V、所有樣本,在所有樣本模式下都保留激發像素。

    梯度檢測:

    // Image Base Gradient Codes
    #define VRS_IB_0   ( 0x0 )    // No grad
    #define VRS_IB_DH  ( 0x1 )    // Dir Hor
    #define VRS_IB_DV  ( 0x2 )    // Dir Ver
    #define VRS_IB_ALL ( 0x3 )    // All dirs
    #define VRS_IB_MASK( VRS_IB_ALL )
    
    // Local Weber-Fechner JND k calculation
    float tileLuma = min3( colorM, colorNW, 
                     min3(colorSW, colorSE, 
                     min3(colorNE, colorW, 
                     min3( colorE, colorN, colorS ) ) ) );
    float visiblityThreshold = vrsQualityThreshold * max(tileLuma, deviceBlack);
    
    // Horizontal
    float dh = max( abs( colorW - colorM ), abs( colorE - colorM ) );
    float ddh= max( abs( colorNW - colorNE ), abs( colorSW - colorSE ) );
    dh = max( dh, ddh );
    
    // Repeat for Vertical
    
    // VH direction and delta
    result.vrsRate  = VRS_IB_0;
    result.vrsRate |= dh < visiblityThreshold ? 0 : VRS_IB_DH;
    result.vrsRate |= dv < visiblityThreshold ? 0 : VRS_IB_DV;
    
    // Merge quads – promoting through individual directions to all directions
    result.vrsRate |= ddx( result.vrsRate );
    result.vrsRate |= ddy( result.vrsRate );
    

    梯度歷史滾動(FIFO隊列):保留4個重投影的幀。

    // VRS history roll during gradient detection at quad resolution
    // Reprojection happens in main VRS tile shader.
    if(((dispatchThreadID.x | dispatchThreadID.y) & 0x1) == 0)
    {
      uint vrsHistoryRate = vrsRateRWTex[dispatchThreadID >> 1];
      vrsRateRWTex[dispatchThreadID >> 1] = (freshResults.vrsRate & 0x3) | ((vrsHistoryRate<<2) & 0xFC);
    }
    

    VRS渲染遮罩生成:

    • CS作業消費者:速度緩沖器,4幀VRS圖像遮罩歷史,帶重投影和不一致拒絕[JIM17],來自prepass的FMask–生成VRS幾何遮罩。

    • CS作業生產者:

      • 重新投影4幀VRS圖像遮罩歷史,8位–4 x 2位圖像遮罩一次性重投影,稍后在梯度檢測過程中旋轉。
      • VRS渲染遮罩(逐quad),在最后4幀中,8位重新排序的FMask或2位圖像掩碼,如果VRS圖像掩碼3個樣本且FMask3,則首選FMask,優化重幾何體的不連續渲染,如alpha測試的草,FMask是按樣本順序重新排序的,以避免在采樣時進行間接排序,僅用于解塊(de-blocking)和重建。
      • 4XMSA模板更新到點畫渲染模式。源于VRS渲染模板,直接寫入重疊的texture2D的模板目標。
      • VRS波前緩沖區(每像素)。

    此外,軟件VRS的繪制過程涉及了prepass、Forward+等過程,而后續的圖像重建涉及解塊、解析等過程,文中還對CS作業進行了優化,包含*面迭代、wave壓縮、太陽可見性作業。經過這些復雜的操作之后,在森林如此復雜的情況下,可以獲得超過1ms的提升:

    而在室內這種輻照度稍低的場景,收益更明顯:

    挑選軟件VRS管線的部分:

    For the Alliance! World of Warcraft and Intel Discuss an Optimized Azeroth分享了2020年的魔獸世界的渲染優化。

    WOW對Direct X 12(和Metal)所做的工作包含多線程命令列表生成、異步管線創建、異步紋理上傳,幀率從50提升到了80。此外還使用了硬件的VRS,減少像素著色器工作的硬件功能,其工作原理是對像素組而不是每像素進行像素著色器調用,可以認為是MSAA的擴展,類似于著色速率下的LOD。控制VRS的方法有:按繪制調用、通過屏幕空間遮罩、從頂點/幾何體著色器。


    屏幕空間的大三角形是VRS的最大受益者:

    VRS的過渡可能相當不明顯,2x1利用假定的16:9屏幕比例,實現更*滑的過渡:

    使用VRS的最佳位置:查找重像素著色器和較低,視覺質量可能在感知上不明顯,如地形、運動模糊、DOF、遠處的物體等。VRS的優點是保留邊緣/輪廓,與基于邊緣的AA(如CMAA)配合良好,控制哪些系統應用了VRS,尤其適用于基于深度的文本效果,不需要放大通道。渲染比例優點是可以降低頂點和帶寬成本,*滑的強度范圍,如1920到3840之間的所有值。VRS的限制是如果相對屏幕空間的三角形密度較高,則效益最小,在粒子系統上使用VRS可能看起來有點笨重。

    Elwynn Forrest場景的性能,1x1時為61FPS,2x2時是68FPS,4x4時是76FPS,在*衡視覺質量時,通常有5%到10%的提升。總之,按繪制調用的VRS非常容易集成(在DX12引擎中),可以無縫地動態打開,可以降低像素著色器的成本,同時將質量降低到最低。


    隨著新的強大GPU IP的發布,Imagination Technologies一直在與Roblox密切合作,研究低水*的性能改進,為玩家提供最佳的游戲體驗。Rendering Roblox Vulkan Optimisations on PowerVR探索了Roblox游戲內的體素照明系統、EVSM陰影貼圖實現、著色器優化,以及為PowerVR優化的Roblox渲染器的其它最新功能,還將闡述在移動設備上進行分析和優化的過程,并研究如何使用異步計算來提高PowerVR的性能。

    Roblox的光照系統的目標是不要烘焙,每個物體都可以在任何時間點移動,不能完全禁用照明——對視覺和游戲性的影響很大,想要從內容中獲得最小的“性能”影響,想要在低端筆記本電腦/手機上運行(D3D9/GL2/GLES2類),想要在高端筆記本電腦/手機上獲得好看的照明效果。

    照明系統的高級概述,已有的特征包含光源(太陽/月亮、天空、局部燈光),幾何體和光源是動態的,所有光源都可以投射陰影,粗糙體素照明無處不在,產生高效柔和的照明,陰影貼圖支持從中端到高端和高質量的太陽陰影。將來會引入前向+:高端、高品質的局部光源,更好的體素照明:更好的天空光。

    照明系統的相位0是體素:大部分工作都是在CPU上完成的,體素化動態幾何,計算每個體素的燈光影響,如陽光、天空光、累積局部光RGB,將信息上傳到GPU(RGB:燈光 + 天空光 x 天空顏色,A:陽光),片元著色器中最終照明。管理CPU性能:體素網格被分割成塊,只有少數塊會每幀更新,固定質量,但更新可能“過時”,更新內核使用手工優化的SSE2/NEON。管理GPU性能:使用雙線性過濾的單個3D紋理查找,GLES2使用2個2D紋理圖譜查找模擬3D紋理查找,完全解耦的幾何體與光源復雜度。

    照明系統的相位1是更好的體素:保持系統的整體設計,HDR光源顏色使用RGBM編碼以節省空間,單獨存放天空光,更好地將sky集成到BRDF中,各向異性占用率,體素化器為每個體素保留3個軸向值,對內容兼容性至關重要,結果更接*陰影貼圖/前向+。GLSL的優化如下:

    // 分組標量算法
    vec_1 = (float_1 * vec_2) * float_2;  -->  vec_1 = (float_1 * float_2) * vec_2;
    
    // inversesqrt比sqrt開銷更低
    vec_1 = sqrt(vec_2);  -->  vec_1 = vec_2 * inversesqrt(vec_2);
    
    // abs() neg() and clamp(…, 0.0, 1.0)可以免費
    float_0 = max(float_1, 0.0);  -->  float_0 = clamp(float_1, 0.0, 1.0);
    vec_0 = vec_1 *vec_2 +vec_3;  -->  vec_0 = clamp(vec_1 *vec_2 +vec_3, 0.0, 1.0);
    

    使用的寄存器太多,導致單個cluster中處理的線程更少,導致利用率降低。允許PowerVR使用16位浮點,降低寄存器壓力,增加占用率(高達100%)。對于PBR著色器,A系列的周期數減少9%,Rogue(Oppo Reno)的周期數減少12%,利用率提高33%。對于PBR+IBL著色器,A系列的周期數減少9%,Rogue(Oppo Reno)的周期數減少20%,利用率提高36%,使用PVRShaderEditorto分析周期數和匯編。

    在PowerVR上分析Roblox。

    照明系統的第2階段是陰影圖:體素陰影太粗糙,陰影圖來救援!優良的品質,挑戰是“柔和”陰影、重新渲染成本高、許多陰影投射光源,僅釋放太陽陰影以簡化。渲染陰影圖:級聯分塊影貼圖,1-4個級聯,取決于質量水*,遠級聯被分割成塊,以便能夠阻止陰影更新;使用了CSM滾動,是級聯陰影貼圖渲染的加速技術;當需要更新級聯時,只需在一個通道完成,如果多個tile被標記為臟,將在這些tile中重新渲染幾何體,CPU進行逐塊的視錐體剔除。燈光方向更改會使緩存無效,還沒有找到解決這個問題的好辦法。陰影柔和度:想要大范圍的陰影貼圖“柔和”,寬PCF內核在高分辨率下成本昂貴,由于透明的幾何結構和*鋪機性能,屏幕空間過濾不切實際,Roblox使用EVSM,將陰影渲染與采樣完全解耦,但存在漏光…縮小Z范圍!將陰影*截頭體緊密貼合到接收幾何體,渲染投射者幾何體時的“*移(Pancaking)”(VkPipelineRasterizationStateCreateInfo::depthClampEnable),采樣EVSM時過度變暗,接著使用標準的兩通道模糊(水*和垂直)來模糊陰影圖。移動高斯模糊進行計算?

    可分離核高斯模糊使用兩個1D過程執行2D高斯模糊,數學等效(秩1矩陣),2n的紋理獲取,而不是\(n^2\)的紋理獲取。

    計算高斯模糊收集算法:PowerVR上的最佳工作組大小為32,處理8x8區域的8x4工作組大小,多次嘗試循環著色器(此處為每線程2個紋素):

    將包括周圍區域在內的深度值讀取到共享內存中,減少紋理獲取的次數。

    Morton順序:使用VK_IMAGE_TILING_OPTIMAL創建的圖像使用Morton排序進行尋址優化,通過加載對齊的texel區域(512位緩存行)提高緩存效率。

    任務打包:在每個幀的兩個隊列之間交替提交命令,允許獨立于幀的調度并增加任務打包!可能需要在兩組資源之間交替使用。更多開發者推薦Imagination Tech Doc

    使用計算的改進:5x5模糊的幀速率提高40%!(MeizuPro 7 Plus PowerVR 7XTP)

    idTech 7: Rendering the Hellscape of Doom Eternal分享了idTech 7的渲染技術。idTech 7完全使用前向渲染,uber著色器仍然很少,更大的關卡和更復雜的環境,更多流加載,所有*臺上的低級API,在相同分辨率的控制臺上仍保持60 fps。

    在Doom中使用了混合裝箱(Hybrid Binning)[Olson12],面臨的挑戰是Doom中更大的場景,遠處的小光源和貼花最終形成一個大簇,敵人有輕型裝備,在戰斗中,彈丸和沖擊貼紙的動態體積更大,藝術家們想要放置更多的燈光和貼花,想要轉移到GPU剔除以減少CPU負載,分簇與分塊的新混合。

    混合裝箱(Hybrid Binning)的靈感來源于“改進的拼貼和集群渲染挑選”[Drobot2017],問題是某些場景中有數千個燈光或貼花,由于狀態更改,硬件光柵tile裝箱效率低下,預算非常緊張(<500us)。

    計算著色器光柵化:為了簡單起見,只裝箱六面體,低分辨率,需要保守光柵化,許多邊緣案例,藝術家們會找到它們,反向浮點深度以實現精度。Binned光柵化[Abrash2009]:設置和剔除、粗糙光柵、精細光柵,將位域解析為列表,這些過程都涉及很多細節,由于已經有不少章節闡述過此技術,故此處略過。

    幾何體細節:藝術家想要幾何定義的小貼花,創作網格的一部分。挑戰是幾乎沒有額外的幀預算,前向渲染器:無法在G緩沖區上混合,G緩沖混合太慢,即使切換到延遲。

    來自網格UV的投影矩陣,世界空間->紋理空間,每個投影使用2x4的矩陣(8個浮點數=32字節),在磁盤上存儲對象->紋理的空間,與模型矩陣相乘得到世界->紋理空間矩陣,每個實例的幾何貼花都需要內存。將索引渲染到R8緩沖區:后深度通道,大于等于的深度比較,深度偏移以避免z-fighting,貼花需要共面,此通道要耗費50us。片元著色器中每個子網格投影列表的遍歷:綁定到實例描述符集的列表,任意混合,因為沒有G緩沖區傳遞。

    限制:每個子網格最多254個投影,一個貼花可能需要多個投影,不能很好地做曲線幾何,每個實例需要投影存儲,目前每個關卡最多1000個貼花,裝箱貼花始終位于頂部,每幀計算的動畫幾何體投影。

    幾何緩存:基于[Gneiting2014],從Alembic緩存編譯,改進的壓縮,預測幀,分層B幀,前向和后向運動預測,用于比特流壓縮的Oodle Kraken,如果緩存相同,實例可以共享流數據塊,可帶動畫的顏色和UV。

    自動蒙皮:位置流的大數據速率降低[Kavan2010],骨骼矩陣像其它流一樣被量化和壓縮,僅適用于某些網格,仍然支持頂點動畫。

    3字節切線坐標系:為每個幀計算切線坐標系并流式傳輸,儲存兩個用于法線和切線的向量非常昂貴。想法:僅存儲切線的法線+旋轉,用叉積重構副切線,2字節用于八面體標準編碼(對于頂點法線已足夠好),1字節用于旋轉。解碼:需要確定的正交向量來旋轉切線,法線與向量(如\((1,0,0)^T\))的粗糙叉積會導致奇點,而羅德里格斯的旋轉公式[Euler1770]可計算出切線:

    \[T = T_b \cos \alpha + (N \times T_b) \sin \alpha \]

    因為向量是正交的,所以上一項被抵消。

    此外,idTech 7還支持材質混合、GPU三角形剔除、GPU幾何合并。其中幾何合并的實現細節如下:

    • 所有資源都是全局可索引的。在單個全局池中分配的所有頂點緩沖區,與幾何流一致,所有紋理和緩沖區描述符的全局數組。
    • 在幾何體集中使用相同的PSO將最多256個可見網格分組。
    • 剔除著色器為每個幾何體集生成自定義間接索引緩沖區。在每個32位索引中打包頂點ID和實例ID,每個幾何體集一次繪制調用,在一次緊湊的繪制調用中渲染256個網格。
    • VS:使用實例ID+幾何圖形集ID檢索實例數據。幾何圖形集ID作為內聯常量發送,實例ID從索引值中提取,頂點緩沖區獲取、實例描述符集等的偏移量。
    • 著色器編譯器生成“可合并的”著色器變體。不同紋理/緩沖區獲取的序列化,實例數據的附加間接尋址,SSBO獲取頂點數據,而不是頂點屬性。
    • 異步計算上的計算剔除/合并與陰影渲染并行。

    從左到右:場景樣例、網格、幾何體集。

    結果:通過三角形剔除+合并,在密集場景中節省高達5毫秒的GPU,在VS中幾乎沒有浪費,基本上擺脫了固定的功能瓶頸。類似的CPU節省:對深度和不透明通道重復使用相同的間接索引緩沖區,在CPU可見性通道期間完成的設置,已經令人尷尬地并行。對于已經在執行標量優化的代碼非常有用,在排列數較低的情況下效果最好,無論如何,都盡可能地降低著色器計數。理論上的權衡:實例數據獲取現在是不同的,通常發生在VS內部,最終難以察覺。

    文中還涉及水體渲染、半透明表面等技術。

    隨著硬件的最新發展,在GPU上追蹤光線的功能比以往任何時候都更容易實現,游戲引擎已經利用了這一點,將光線追蹤效果(如反射、軟陰影、環境遮擋或全局照明)集成到實時管線中,在可能的情況下替換屏幕空間中更*似的光線追蹤效果。大多數(如果不是所有的話)效應都依賴于二維蒙特卡羅積分。沿著這條路,我們可以想象冒險進入更高的維度,添加更多間接反彈,或更多分布式效果(自由度、運動模糊),更接*完整的路徑追蹤。需要調整采樣方案,以確保最佳利用通常有限的實時/交互式預算。From Ray to Path Tracing: Navigating through Dimensions便闡述了如何逼*上述的效果。

    光線追蹤從離線到實時的幾個重要方面:

    • 明智地選擇光線。采用蒙地卡羅積分法、方差、重要性采樣、多重重要性采樣。
    • 仔細選擇你的(非)隨機數。域扭曲、準隨機序列、低差異、分層。
    • 把你的射線預算花在最有用的地方。自適應采樣。
    • 理解并防止錯誤。強度夾緊、路徑正則化。

    蒙特卡羅快速回顧:

    準蒙特卡羅(QMC):確定性、低差異序列/集合(Halton、Hammersley、Larcher-Pillichshammer)比隨機的收斂速度更好,例如Sobol或(0-2)序列不需要知道樣本數量,奇妙的分層特性。

    見下圖,我們有一個大面積的光在傾斜,漫反射地面上。相機正前方是一片薄玻璃片,折射率為1(因此完全透射)。這將是一個相當常見的場景的調試版本,其中場景的大部分(如果不是全部的話)都在一個窗口后面。

    因為我們對光線有一個固定的分裂因子,索引也很容易跟蹤:對于每個光采樣計算,我們使用樣本i到i+3。誠然,如果每個照明位置都與對應位置非常不同,就像康奈爾盒子的情況一樣,它不會有太大的區別(但考慮到屬性或我們的順序,至少會同樣好)。然而,如果位置更相似(或者甚至與我們當前場景中的位置完全相同)。。。

    在地面上使用64個相機直接可見的光源樣本。

    下圖是一個稍微不同的場景,使我們的采樣預算更容易測試。場景中的薄玻璃片更粗糙,為了更好地欣賞這種粗糙的效果,對地面進行紋理處理。

    若使用之前一樣的采樣方式,由粗糙玻璃產生的BSDF射線的相干度當然不如以前,由此獲得了下圖那樣更加毛躁的圖像:

    我們遇到了一個像素之間相關性很差的情況,4D序列中的一些維度在一個像素內的相關性很差。為此,我們想查看4D點的不同2D投影,遵循Jarosz等人(在正交陣列論文中使用的可視化約定:可以在軸上看到對應于每個維度的索引,在數組的每個單元格中,將顯示相應的2D切片,是尺寸(0,1)和(2,3)的2D切片,是在采樣例程中使用的切片,它們其實是一樣的。

    現在讓我們看看下圖的“診斷”切片,(0,3)和(1,2)也是相同的。剩下的(0,2)和(1,3)相當于(0,0)和(1,1)。。。

    實際上,需要使用高緯度的Sobol序列。有許多可能的Sobol序列,[Grünschlo?]和[Joe 2008]的Sobol序列優化了低維2D投影的分層特性,用于低樣本數量:

    雖然樣本數量少,但任何維度配對都會產生非常好的結果!在我們之前的各種填充嘗試中,我們注意到的問題已經消失了。

    如果我們在維度上上升,我們確實可以找到質量明顯最差的2D切片(==>確保對被積函數的最重要部分使用最低維度):

    額外的改進包括將Owen指令應用于所有維度,如前所述,它有助于打破序列特征的對齊模式,并提高收斂速度。由于這不是一個快速的過程(特別是對于實時),它可以預計算并存儲大量樣本==>這是HDRP中的操作,256D中有256個點。

    錦上添花:屏幕空間中的藍色噪點:

    白噪點和藍噪點的對比:

    最終維度:當進入高維時,同時考慮所有維度,避免考慮遞歸的低維積分(即使這樣開始更自然)。實時(預計算)選擇序列:一種低差異采樣器,將蒙特卡羅方差作為藍色噪聲分布在屏幕空間[Heitz 2019],漸進式多抖動樣本序列[Christensen 2018]。*期在高維集合方面值得注意的工作:蒙特卡羅渲染的正交數組采樣[Jarosz 2019]。實時路徑跟追蹤的未來?實時重構光照傳輸[Wyman 2020]。

    Real-Time Samurai Cinema: Lighting, Atmosphere, and Tonemapping in Ghost of Tsushima闡述了游戲Ghost of Tsushima中所使用的光照、天空大氣、色調映射等渲染技術。

    光照:藝術方向呼喚“程式化現實主義”,光照模型是基于物理的,用物理上合理的值編寫的材質,一些攝影測量,從動態天空和局部光照傳輸數據計算運行時的間接照明,基于物理的天空模型,用于天空、云、薄霧和霧粒子,使用自定義技術以HDR和色調圖渲染,照明可能會偏離物理正確性,藝術家可以在全局范圍內進行調整,以獲得理想的外觀。間接照明包含漫反射和鏡面反射,大氣體積照明包含天空與云彩、薄霧、粒子等,色調映射包含局部色調映射算子、自定義色調映射顏色空間和白*衡、Purkinje shift(微光視覺模擬)。

    間接光的漫反射:Tsushima的大小、動態時間和天氣需要一種新的方法:二次SH探針的規則網格,每個200m方形tile使用16x16x3,總共20x80個tile。四面體網格用于更復雜的情況,如城鎮、村莊、農莊、城堡等。通過位置流入,覆蓋常規網格,在交界處混合。

    運行時重新照明:需要在運行時更新輻照度探針,離線階段捕獲天空可見度,編碼為2度SH(單通道),不捕獲輻照度。在運行時將天空投射到SH,將天空SH乘以天空可見度,與蘭伯特余弦瓣卷積。

    反彈的天空光:是可行的,但只提供直接的天空照明,無反射光。完全轉移矩陣?內存太多(9倍),捕獲時間太長。假設整個半球的天光是大致恒定的,用統一的白色天空照亮世界,捕捉“反彈天空可見度”。乘以*均天空顏色(從第0階的SH開始)。

    添加太陽/月亮反彈:在假想的地面和墻壁上反射光線,投影\(-\hat{r}_g\)\(-\hat{r}_w\)到SH;窗口化以避免負波瓣,按RGB燈光強度縮放(由動態云陰影調制),乘以反彈的天空SH,并將其添加到之前的結果中。

    方向性增強:2級SH的頻率相對較低,間接照明在很多地方看起來都很*淡,沿線性SH最大值方向向三角形的Lerp,計算便宜,直接部分和反彈部分分別計算,在所有天氣狀態下有著25%的增強。

    去振鈴(deringing):不能保證最終輻照度在任何地方都是正的,對天空能見度應用固定的去振鈴會降低方向性和保真度,相反,在運行時計算天空亮度和最終輻照度,一個牛頓迭代找到最小值,使用在CPU上計算的深度為3的二叉搜索樹。

    去振鈴關閉(左)和開啟(右)對比:

    漏光解決方案:將四面體網格探針分為內部探針和外部探針,為表面指定一個0-1的內部遮罩\(w_{interior}\),幾何體屬性或著色器參數,頂點繪制,延遲貼花。按正常情況計算重心,然后乘以重量:\(w_{interior}\)用于內部探頭內部,\(1-w_{interior}\)用于外部探頭。重新規范化重心(如果全部為0,則使用原始重心)。

    間接光的鏡面反射:Seattle in Infamous使用了230個靜態反射探頭,已經在突破內存限制了。而Tsushima使用了235個,重新照明所需的額外數據,隨需應變,每生物群落默認探針,實例化內部探針,增加了對嵌套探針的支持,一次最多128個重照明探針。

    離線捕獲的數據:反照率立方體圖(BC1格式),法線+深度立方體貼圖(BC6H格式),RG:八面體法線編碼,深度(雙曲線),所有立方體貼圖256x256x6。

    反射探針新照明:在異步計算中執行循環(每幀一個),用遠距陰影圖集著色陰影,每tile的陰影圖,每個tile有128x128個紋素。間接照明用單SH樣本,也用作反射探針亮度的標準化,使用過濾的重要性采樣(filtered importance sampling)進行預過濾,使用GPURealTimeBC6H壓縮至BC6H,減少內存占用并提高采樣性能。

    立方體圖陰影追蹤:遠陰影圖不足以對內部進行陰影處理,低陰影分辨率和LOD的組合。觀察:深度立方體貼圖有很多遮擋信息,重新照亮每個立方體貼圖紋理時:求*行光線的交點,使用立方體貼圖體積,交叉點處的采樣深度,天空深度? 未遮擋,使用4x4 PCF。相當粗糙,但又便宜又有效。

    水*遮擋:解釋由于法線貼圖法線\(\hat{n}_m\)相對于頂點法線\(\hat{n}_v\)的傾斜而導致反射錐上基礎幾何體的遮擋,快速、*似、合理的結果,對于GGX粗糙度,包含能量分數的錐半角如下所示:

    \[\tan \theta_c = \alpha \sqrt{\cfrac{u_e}{1-u_e}} \]

    \[\begin{eqnarray} \theta_o &=& \min \Big(\theta_r + \theta_c - \cfrac{\pi}{2}, \ 2\theta_p \Big) \\ u_o &=& u_e \ \text{smoothstep}(0, \ 2 \theta _c,\ \theta_o) \end{eqnarray} \]

    天空大氣光使用了預計算的3D LUT,對Mie輻照度應用陰影,對瑞利輻照度應用環境遮擋,Haze使用AO的*均天空可見度,云渲染為拋物面紋理(paraboloid texture),每幀將當前太陽和月亮角度的3D LUT重采樣為2D,使用立方濾波進行上采樣,以避免日出和日落時出現鋸齒,使以后的查找更便宜。此外,還自定義了瑞利顏色空間:

    云體渲染為768x768拋物面紋理,用云密度來抗鋸齒,對Mie散射使用Henyey-Greenstein相位函數,實現了體積霧度(局部光、抗鋸齒)。此外,還闡述了粒子光照。

    色調映射方面,在Infamous的游戲中,努力維持物理上合理的漫反射反照率值,創建了漫反射顏色參考圖:

    切換到主要基于照度的曝光系統,僅當高光亮度超過閾值時才使用亮度。處理動態范圍時,的室內照明比較暗,天空驅動了大部分照明。人類視覺系統能夠感知非常大的動態范圍,雙邊過濾器可用于保持圖像細節,同時降低整體對比度。

    色調映射對顏色空間也進行了處理。渲染顏色中的每通道色調映射,空間裁剪飽和顏色為青色、品紅色和黃色只應用每通道的“Reinhard”操作符:

    \[c_i^{out} = \cfrac{c_i^{in}}{1+c_i^{in}} \]

    嘗試了部分顏色空間:ACES2065-1 (AP0)、ACEScg (AP1)、Rec.2020、DCI-P3,ACEScg是其中最好的,但仍然讓紅色過于偏向黃色,調整后的紅色主x坐標(0.713? 0.75)。


    效果對比:

    此外,色調映射還處理了白*衡、Purkinje Shift等效果。

    Experimenting with Concurrent Binary Trees for Large Scale Terrain Rendering分享了Unity中使用并發二叉樹進行大規模地形渲染的新技術的結果,包含并發二叉樹的基礎和在計算大規模地形幾何體的自適應細分方面的好處,深入探討將原始技術集成到Unity游戲引擎中的最新努力。下圖的綠色節點是二叉樹表示的葉節點,對應于想要為細分方案顯示的三角形:

    這是一個和歸約樹,存儲在并發二叉樹的所有其它級別。它逐步將位字段中設置為一個(或葉節點)的位的數量相加,直到根節點,提供了由位字段編碼的二叉樹中葉節點的總數。然后,通過遍歷這個和歸約樹,提供了一種在忽略零的情況下循環設置為1的位的方法,或者換句話說,在葉節點上循環。回到實際例子中,編碼的二叉樹代表最長的邊二等分,使用它為細分中的每個活動三角形分配一個線程。

    初始化曲面細分:選擇最大細分深度,CBT編碼完全可能的二叉樹,直到一個設定的深度。如果改變,重新編碼CBT,初始化CBT緩沖區,無符號整數數組,和歸約樹,位字段打包為uint。

    更新曲面細分:

    渲染曲面細分:

    同步更新渲染:

    結論:CBT是一種具有低存儲成本和良好性能的大規模地形幾何繪制方法,高度可定制的LOD標準。


    14.5.3.2 光影技術

    Deferred Lighting in Uncharted 4分享了神秘海域4的延遲光照。

    目標和動機是許多需要讀取/修改材質數據的屏幕空間效果(粒子、貼花),以及更重要的SSR、立方體圖、間接陰影等,無法避免保存材質數據。緊湊型GBuffer的在高密度幾何物體下開銷大,前向著色器中照明的復雜性增加了難以置信的寄存器壓力,進一步拖累了幾何通道的速度。

    神秘海域4最終采用了完整的延遲(fully deferred)。GBuffer必須支持游戲中的所有材質,不過當時的硬件有很多內存和帶寬。GBuffer是每像素16位無符號緩沖區,在生產過程中不斷地在特性之間移動位,大量的視覺測試來確定各種特性需要多少位,大量使用GCN參數封裝內部函數。

    第三個可選GBuffer用于更復雜的材質,根據材質的類型對其進行不同的表達,使用可選GBuffer的材質包括布料、頭發、皮膚和絲綢。GBuffer的表達是相互排斥的(即不能將布料和皮膚放在同一個像素中),此約束在材質編寫管線中強制執行。如果材質不需要,則可選GBuffer既不寫入也不讀取。

    問題是延遲著色器很快就會變得非常臃腫,因為必須支持皮膚、布料、植物、金屬、頭發等等,更不用說所有的光源類型。可以通過材質分類(classification)來改進:保存材質“ID”紋理,不是真正的材質ID,只是使用的著色器功能的位掩碼,12位壓縮為8位(通過考慮特征互斥性)。對于每個16x16的tile,使用整個tile的材質遮罩將其索引到查找表中,查找表是預計算的,擁有最簡單的著色器,支持tile中的所有特性。

    uint materialMask = DecompressMaterialMask(materialMaskBuffer.Load(int3(screenCoord, 0)));
    
    uint orReducedMaskBits;
    ReduceMaterialMask(materialMask, groupIndex, orReducedMaskBits);
    
    short shaderIndex = shaderTable[orReducedMaskBits];
    

    原子地將tile坐標加入到該著色器將計算光照的tile列表,原子整數也是dispatchIndirect參數緩沖區的dispatch數量。

    if (groupIndex == 0)
    {
        uint tileIndex = AtomicIncrement(shaderGdsOffsets[permutationIndex]);
        tileBuffers[shaderIndex][tileIndex] = groupId.x | (groupId.y << 16);
    }
    

    分類使得性能已經有了巨大的進步,以前也有文獻曾使用過類似的技術。

    還可以進一步優化。以布料著色器為例,對于所有像素都是布料的tile(即將布料材質掩碼位設置為1),該分支所做的只是增加開銷,因為它應該始終評估為真。創建另一個預計算表,當tile中的所有像素具有相同的材質遮罩時使用該表——“無分支”排列表。在分類過程中檢查該情況,并使用適當的表,不僅刪除了分支,還為全局編譯器優化提供了機會。

    // 之前
    short shaderIndex = shaderTable[orReducedMaskBits];
    
    // 之后
    bool constantTileValue = IsTileConstantValue( … );
    short shaderIndex = constantTileValue ? branchlessShaderTable[orReducedMaskBits] : shaderTable[orReducedMaskBits];
    

    改進前(左)后(右)的對比。

    在最壞情況下的性能改善昂貴的場景:4.0ms——無任何優化(“uber著色器”),3.4ms(-15%)——通過選擇最佳著色器,2.7ms(-20%,整體-30%)——使用無分支著色器。*均而言,無分支著色器可以提供額外的10-20%的改進,而且成本很低,而選擇最佳著色器*均可以提高20-30%。使得我們可以在不影響基本性能的情況下實現材質的復雜性和多樣性,為一個著色器(例如絲綢著色器)添加復雜性不會影響游戲的其余部分。經過幾次迭代之后接口實現依然干凈透明,額外的好處是分類計算著色器在異步計算上運行——幾乎不影響運行時。

    還可以更進一步改進,根據光源類型分配不同的計算著色器,少數光源類型增加了大多數復雜性和成本。迭代很難,真正了解一位(bit)的價值,最終達到了一個良好的系統。越簡單越好,可能會犧牲某些特性來獲得輕微的性能提升。

    接著聊鏡面遮擋。立方體圖不考慮局部遮擋,解決方案是有時在遮擋體內/周圍添加更多立方體圖,由于幾何體的排列方式,不總是可行的,更不別說性能/內存成本了。在采樣位置只能使用AO值,例如Frostbite的鏡面遮擋,效果很好,但方向性呢?

    為了獲得更精確的遮擋,Uncharted 4將以某種方式用某種編碼來遮擋鏡面反射波瓣,該編碼對采樣點的遮擋方式和方向進行編碼。

    左:反射波瓣;右:最小遮擋圓錐——彎曲圓錐。

    在離線處理過程中,用Bent Normals and Cones in Screen-Space中描述的方法生成彎曲圓錐,最小遮擋的方向定義為:

    其中如果光線的長度小于一定距離,則\(d(\overrightarrow{v})\)為1,而角度:

    對于反射圓錐,使用一個類似Drobot的匹配Phong/GGX lobe的想法,找到適合90%波瓣能量的圓錐。對于GGX,發現了一個簡單匹配:

    兩個圓錐體相交,求出交點的立體角。

    可以使用以下公式計算兩個相交圓錐體的立體角,給定兩個錐角\(\theta_1\)\(\theta_2\)及它們之間的角\(\alpha\),交點的立體角在右邊。

    稍微改變變量,將\(\cos/ \sin \theta\)作為查找紋理的輸入,將交點的立體角除以BRDF圓錐體的立體角\(\cfrac{1-\cos \theta_1}{2\pi}\),以獲得遮擋的百分比。

    float IntersectionSolidAngle(float cosTheta1, float cosTheta2, float cosAlpha)
    {
        float sinAlpha = sqrt(clamp(1.f - cosAlpha*cosAlpha, 0.0001f, 1.f));
    
        float tanGamma1 = (cosTheta2 - cosAlpha*cosTheta1) / (sinAlpha*cosTheta1);
        float tanGamma2 = (cosTheta1 - cosAlpha*cosTheta2) / (sinAlpha*cosTheta2);
    
        float sinGamma1 = tanGamma1 / sqrt(1 + tanGamma1*tanGamma1);
        float sinGamma2 = tanGamma2 / sqrt(1 + tanGamma2*tanGamma2);
    
        // Now we compute the solid angle of the first cone (This is divided by 2*kPi. That factor is taken into account in the texture)
        float solidAngle1 = 1 - cosTheta1;
    
        return (SegmentSolidAngleLookup(cosTheta1, sinGamma1) + SegmentSolidAngleLookup(cosTheta2, sinGamma2)) / solidAngle1;
    }
    

    當樣本被嚴重遮擋時,返回到方向光照圖的描述,保留能量,但反射細節丟失,大多數時候看起來都不錯!這種方法相對昂貴,如果一個參數是固定的,函數就會簡化。也許可以確定粗糙度參數,然后根據粗糙度調整最終結果。由于性能原因,它在某些級別上被禁用。不適用于動態對象(例如角色),可以使用前面提到的更簡單的遮擋方法。不是基于物理的,但它解決了很多遮擋問題。可以向前推進,使用其它表示法進行更精確的遮擋,走向完全不同的方向。

    Rendering Antialiased Shadows with Moment Shadow Mapping闡述了利用MSM(矩陰影圖)來實現陰影抗鋸齒的技術。

    陰影圖是實時渲染的基礎技術,但由于分辨率有限,會帶來嚴重的鋸齒。以往的解決方案有PCF(百分比漸進過濾)、VSM(方差陰影圖)等。PCF對每個像素執行樣本過濾區域、閾值、過濾,消耗大。

    對于深度分布,陰影是深度的函數,一步一個紋素,通常是單調的函數。

    VSM存儲\(z\)\(z^2\),存在冗余數據,直到過濾,有2個矩,用下邊界重建。

    此外,還有存儲傅里葉系數的CSM(Convolution shadow map,卷積陰影圖),存儲\(\exp(??\cdot ??)\)ESM(Exponential shadow map,指數陰影圖),存儲\(\exp(??_+ \cdot ??),\ \exp(??_+ \cdot ??)^2,\ \exp(???_? \cdot ??),\ \exp(???_? \cdot ??)^2\)EVSM(Exponential variance shadow map,指數方差陰影圖)。而文中提出的方法是MSM(Moment shadow mapping,矩陰影圖),有4個通道、4個矩、每個紋素64位,少量光照泄露。

    從矩到陰影,許多分布共享相同的4個矩,以避免表面的陰影粉刺。

    需要一種有效的方法來計算界限,基本思想是利用一個有用的數學定理,該數學定理指出,邊界總是通過非常特定的深度分布來實現,只使用三個不同的深度值來匹配四個給定的矩,其中之一是正在最小化的深度。因此,只需要計算另外兩個深度值和三個臺階的高度,正是矩陰影映射算法所做的。

    上一個示例中的邊界不夠清晰,而下圖所示的邊界非常窄,因此重建非常精確。過濾區域中的兩個表面,完美重建,涵蓋大多數情況。

    但有一個問題,負擔不起每紋素超過64位來過濾硬陰影,但若只存儲深度的冪,舍入誤差就太大了。基本上,陰影貼圖的四個通道將存儲底部顯示的四個基函數。

    可用的價值范圍利用率很低:

    解決方案:使用優化的多項式基,現在64位就足夠了。

    利用范圍的體積最大化:

    MSM的使用過程如下:

    • 生成矩陰影圖。

      • 渲染到多采樣的深度緩沖區。

      • 解析時計算矩。

      • 過濾,例如2通道的高斯+mipmap。

    • 恢復矩。

      • 給片元著色,
      • 對過濾的矩陰影圖進行采樣(使用mipmapping、各向異性等)。
      • 逆向量化變換:

    • 偏倚矩。示例:兩個矩存儲在2?3位。

      • 舍入誤差使矩無效。
      • 用插值恢復它們:\(?? = 0.15\)\(??′ ? (1 ? ??) ? ?? + ?? ? (0,\ 0.63,\ 0,\ 0.63)^T\)
      • 如果在4?16位,\(?? = 6 ? 10^{?5}\)
      • 漏光稍微多一些。

    MSM和EVSM、PCF的對比:

    MSM還支持半透明遮擋體的陰影、矩軟陰影和預過濾單次散射。


    結論:矩陰影圖優于其它可過濾陰影貼圖,內存需求遠低于卷積陰影圖,與方差陰影圖、指數陰影圖和指數方差陰影圖相比,漏光更少。在以下情況下,矩陰影圖比普通陰影圖更快:小的陰影圖分辨率(由于抗鋸齒而可行),大的輸出分辨率(即4k和VR),較大的過濾區域(適用于柔和陰影),表面粉刺幾乎不是問題。

    Real-Time Area Lighting: a Journey from Research to Production闡述了區域光源的研究到產品應用的整個鏈路技術。文中提到想要所有頻率(即不同粗糙度)的區域光源的正確響應:

    想要各向異性,可由線性變換余弦作為入口:

    利用LTC可以實現余弦、粗糙度、各向異性、斜切、隨機等光照效果:

    預先應用要給矩陣之后可以獲得線性變換的余弦,若將粗糙度和視角等因素預先計算,可以得到約80kb的查找表數據:

    通過LTC的逆向變換,可以恢復原來的余弦項:

    區域積分實際上可以轉換成邊緣積分:

    相關的代碼:

    float EdgeIntegral( float v1, float v2, float n) 
    {
        float theta = acos(dot(v1, v2));
        float3 u = normalize(cross(v1, v2));
        return theta * dot(u, n);
    }
    
    float PolyIntegral(float3 v[4], float3 n) 
    {
        float sum;
        sum += EdgeIntegraL(v[0], v[1]); 
        sum += EdgeIntegral(v[1], v[2]);
        sum += EdgeIntegral(v[2], v[3]);
        sum += EdgeIntegral(v[3], v[0]);
        return sum/(2.0 * pi);
    }
    

    實現過程:

    • 根據粗糙度和視角查找\(M^{-1}\)。當粗糙度較大,會產生過胖的照明區域:

      需要標準化矩陣的4項:


      渲染效果變得穩定且正確:

      查找LUT時有更簡單的方式:

    • \(M^{-1}\)變換多邊形。

    • 將多邊形裁剪到上半球。可以去掉乘以法線,用向量形狀因子代替:

      而多邊形可用代理球體*似:

      效果對比:

    • 計算邊積分。如果用以下代碼計算,會出現亮處的條帶瑕疵:

      float EdgeIntegral( float v1, float v2, float n) 
      {
          float theta = acos(dot(v1, v2));
          float3 u = normalize(cross(v1, v2));
          return theta * dot(u, n);
      }
      

      修改成以下代碼:

      float EdgeIntegral( float v1, float v2, float n) 
      {
          float theta = acos(dot(v1, v2));
          // 改了此行
          float3 u = cross(v1, v2) / sin(theta); // 原來:float3 u = normalize(cross(v1, v2));
          return theta * dot(u, n);
      }
      

      看起來好一些,但acos將邪惡潛伏其中!需要修改acos的實現,利用合理的擬合:

      float EdgeIntegral( float v1, float v2, float n) 
      {
          float x = dot(v1, v2);
          float y = abs(x);
          
          float a = 5.42031 + (3.12829 + 0.0902326*y)*y;
          float b = 3.45068 + (4.18814 + y)*y;
          float theta_sintheta = a / b; 
          
          if(x < 0.0) 
              theta_sintheta = pi*rsgrt(1.0 - x*x) - theta_sintheta; 
          
          float3 u = cross(v1, v2);
          
          return theta_sintheta*dot(u, n);
      }
      

      新舊曲線的對比:

      最終效果終于好了:

      然而還有更低成本的擬合:

      float EdgeIntegral( float v1, float v2, float n) 
      {
          (...)
          
          // float a = 5.42031 + (3.12829 + 0.0902326*y)*y;
          // float b = 3.45068 + (4.18814 + y)*y;
          // float theta_sintheta = a / b; 
          float theta_sintheta = 1.5708 + (-0.879406 + 0.368609*y)*y;
          
          (...)
      }
      

    此外,這種LTC的方式還支持紋理區域光源:

    總結:數值問題被解決,在PS4@1080p的漫反射+鏡面反射的性能:0.9ms。

    Volumetric Global Illumination At Treyarch是暴雪呈現的關于體積全局光照的技術,包含體紋理中的GI、Lean紋理數據、由探針烘焙的IBL、凸混合形狀。傳統的反射探針、輻照度體積都存在可見性的問題,即沒有考慮每個探針的可見性項。

    如果按體素渲染反射探頭,需要的時間將是巨大的數字:

    從反射探針收集顏色,重投影立方體圖,組合起來填充孔洞。

    實際上,每個體素4096條射線,考慮15個鄰域樣本,漏掉的光線被涂上了顏色。

    另外,從已存在的探針重投影:

    重投影時,基于距離排序的鄰居候選對象,角度和到曲面的距離定義了立方體貼圖中的立體角,根據深度金字塔驗證樣本區域,如果可見,則進行適當的mip采樣。

    distFromUnitCube = sqrt( 1 + u^2 + v^2 ); // Compensation for cube-map shape.
    angleOfVoxel = 4 * PI / numSamples; // Solid angle from voxel.
    inSqrt = 1 + distFromVoxel^2 * angleOfVoxel * ( angleOfVoxel – 4*PI ) / ( 4 * PI^2 * distFromProbe^2 );
    angleOfProbe = 2*PI * ( 1 – sqrt(inSqrt) ); // Solid angle from reflection probe.
    cubeRes = 1.0f / sqrt( angleOfProbe * distFromUnitCube^3 ); // Resolution needed for sample.
    mipLevel = clamp( mipCount – log2( cubeRes ), 0, mipCount ); // Mip level to use.
    
    return mipLevel;
    

    最大的收益是硬件渲染、重新渲染以獲得反彈、只需光線跟蹤和重新投影一次。紋理編碼使用BC6H壓縮的環境立方體,好處是只有3個樣本,硬件三線性濾波:

    color = xVolume.SampleLevel( coord ) * normal.x * normal.x +
            yVolume.SampleLevel( coord ) * normal.y * normal.y +
            zVolume.SampleLevel( coord ) * normal.z * normal.z;
    
    // 評估
    color[n] = normal^2 * float3(Xsample[n], Ysample[n], Zsample[n]);
    

    另外,光照漏光是個很大的問題,傳統的解決方案是根據正常值調整三線性[Silvennoine15]。文中嘗試了更多的體素數據的方案:*面、SDF,但存在不良的瑕疵——墻壁黑點。于是使用了衰減體積,讓藝術家放置長方體,這些長方體定義了GI體積的剪輯形狀,長方體的每一面都有一個衰減GI的羽狀距離。對于復雜的房間,使用凸面體衰減形狀。運行時實現:

    • 對體積AABB進行剔除,以建立體積列表。
    • 每像素計算可見體積上的衰減,凸殼CSG,由六個*面組成的一組,可以是擴展的、合并的或減去的。
    • 根據法線采樣三個環境立方體值。
    • 在所有體積之間混合結果。

    需要解決的問題有體素內的幾何體、光照裂縫等問題。另外,反射使用了反射*面。

    Tiled shading: light culling – reaching the speed of light介紹了一種新的分塊和剔除分塊燈光的技術,該技術利用光柵化器進行免費的粗糙剔除,并進行早期深度測試以快速剔除工作。此外,還介紹了利用顯式多GPU編程功能的技術。

    分塊著色的優點是照明階段一次性使用所有可見光,缺點是使用tile粒度進行不太精確的剔除,*截頭體原始測試要么太粗糙,要么太慢。為什么要關心剔除?因為剔除本身可能是一項成本高昂的動作,精確的剔除可以加快照明速度,引入“誤報”會顯著降低照明性能!剔除挑戰是盡量減少在剔除階段獲得的“誤報”的燈光數量,提高分塊著色渲染中的燈光著色性能。

    球體和截錐體*面:永遠不要用!最常用的測試,事實上,是截錐體和包圍盒測試,對于大球體來說非常不準確。

    如何提高剔除精度?將tile的最小最大z與所有交點中的最小最大z進行比較,4條射線效果更好。


    但對計算著色器的剔除很糟糕,這是一個簡單的枚舉:總操作數 = X * Y * N,其中X–分塊網格寬度、Y–分塊網格高度、N–光源數量。如何提高剔除性能?減少枚舉的順序,將屏幕細分為4-8個子屏幕,根據子屏幕截錐體粗略剔除燈光,在剔除階段選擇相應的子屏幕,少量光源時最多可提升2倍,但需要更多!我們受到計算能力的限制,試著將一些工作從著色器轉移到特殊的硬件單元!讓我們從計算切換到圖形管線!就像過去的好時光一樣!

    使用圖形進行燈光剔除:使用光柵化器生成燈光片元,空的tile將被原生地跳過,使用深度測試來遮擋,遮擋分塊的無用工作將被跳過,在PS中使用圖元-光線相交進行精細剔除和燈光列表更新。想法概覽:剔除階段分塊→ 1像素,光源體積→ 代理幾何體,粗XY剔除→ 光柵化,粗Z剔除→ 深度測試,精細剔除→ 像素著色器。

    如何集成?不要使用uber著色器,始終將分塊著色分為3個階段:減少、剔除(用新方法)、照明。新剔除方法的概覽:相機截錐剔除、深度緩沖區創建、光柵化與分類,它們的說明如下:

    • 步驟1:相機截錐剔除。根據相機截錐剔除燈光,將可見光源分為“外部”和“內部”。

    • 第2步:創建深度緩沖區。對于每個tile:查找并復制“外部”燈光的最大深度,查找并復制“內部”燈光的最小深度,深度測試是高性能的關鍵!在著色器中使用[earlydepthstencil]。

    • 步驟3:光柵化和分類。使用深度測試渲染燈光幾何體,“外部”——最大深度緩沖區,正面直接深度測試,“內部”——最小深度緩沖區,反向深度測試的背面,使用PS進行精確剔除和逐tile燈光列表創建。

    常見燈光類型:點光源(全向)、*行光(聚光燈),燈光幾何體可以替換為代理幾何體。

    點光源的代理幾何體用幾何球體(2個細分,基于八進制,下圖左),離球體足夠*,低多邊形在低分辨率下工作良好,等邊三角形可以減少光柵化器的時間。聚光燈的代理幾何體(下圖右)易于參數化,從探照燈到半球,*面部分可用于處理區域光。

    光柵化的光源剔除的優勢:不適用于無光源的tile和被遮擋的光源,粗糙剔除幾乎是免費的!對小尺寸的光源性能提升驚人,可以使用復雜的代理模型!從數學上講,這是一個分支定界過程!性能對比如下,普遍有數倍的提升:

    光柵化剔除的結論:比同樣的CS版本快3-20倍,以較小的成本產生較少的“誤報”,具有更好的分辨率縮放,光柵允許我們使用復雜的光源體積。

    Decima Engine: Advances in Lighting and AA分享了Decima Engine的光照和抗鋸齒技術。其中Decima引擎最初是為Killzone系列開發的,現在為Horizon:Zero Dawn和Death Stranding提供動力。該文將介紹的渲染技術包括通過彎曲單點光源的光矢量來*似球形區域光源的改進方法、高度霧的實際大氣散射、針對1080p的兩幀時間消除鋸齒解決方案,以及在PS4 Pro上使用的優化的2160p棋盤渲染和“七巧板”解決策略。

    在區域光方面,在形狀上積分GGX是很困難的,不同的*似方法,均衡性能與質量,特定形狀(例如[Hill16]用于多邊形燈光),對于球形燈光:廉價的“扭曲”點光源技巧[Karis13],可能會導致失真,但可以改進。

    如何使用每像素1個點光源來*似區域光?通過將點光源移向像素的反射向量[Karis13](下圖左),非常適合Phong模型[Picott92],但對于微*面模型來說不是很好:峰值響應仍然可能被“忽略”。想法:將點光源移動到周邊,以最大限度地提高其響應,主導因素:N·H(下圖右)。

    [Karis13]的方法是向反射向量方向彎曲L,相反,彎曲L最大化N?H:

    求解使N最大的φ?H很難,可以解決等效問題:

    然后使用牛頓迭代法求解:

    效果對比如下:

    文中涉及了高度霧的實現,其主要步驟和算法如下:



    高度霧的效果:

    對于Decima引擎而言,擁有復雜的自然場景來,光靠形態學是不夠的,例如彎曲或亞像素寬度、所有植被都是Alpha測試的、半透明。開銷可能很貴,例如,SMAA[Jimenez12]對植被可能較慢,用TAA補充FXAA。下表是不同的AA在不同場景下的消耗:


    TAA需要之前的幀數據,但Horizon沒有,很多很薄的幾何體,AA應用于所有其它方面之后,低質量運動向量。沒有回饋循環的累積歷史,僅將當前和上一個原始渲染作為輸入。TAA的兩幀內每像素4倍邊緣采樣,具有自適應1D銳化:

    采樣模式類似于FLIPQUAD[Akenine02],但是比較銳利,沒有額外的mipmap邏輯,無需MSAA/EQAA硬件,在去除線條和網格上的鋸齒方面較差(由FXAA解決)。

    棋盤渲染和其它AA在效果、性能方面的對比如下:

    PS4 Pro上的2160p棋盤渲染:

    • 典型棋盤渲染。

      • 每幀渲染50%的像素。
      • 每幀交替采樣位置。
      • 渲染原生分辨率提示以“智能地”填補空白:深度緩沖區、三角形索引緩沖區、阿爾法測試覆蓋率。
      • 在2160p@30Hz的頻率下,原生分辨率對地*線來說太大開銷,只有UI和最終backbuffer是原生分辨率,一切都在2160p棋盤上運行,沒有原生分辨率提示,打破標準決心…
    • 每幀渲染50%的所有像素角點(下圖左),以及*均可用的角落,所有像素的處理都是一樣的。不需要原生分辨率提示,沒有形態的技巧,沒有抖動模式(下圖中)。兩幀中每個原生像素的4個樣本,AA穩定性與1080p的TAA相似(下圖右)。

    *均兩個最*的像素角點本質上沒有鋸齒:

    單幀對角線仍然存在問題,FXAA沿對角線方向施加:

    棋盤渲染總結:以棋盤分辨率進行渲染和后期處理(1920x2160),轉換為(YCoCg)的七巧板(2160x2160),其中YCoCg用于下一個通道:單個gatherRed()的4個亮度樣本。應用常規的FXAA,由于七巧板的緣故,消除了對角線上的鋸齒。混合當前七巧板和重投影的歷史七巧板,輸出到2160p。

    PBR Diffuse Lighting for GGX+Smith Microsurfaces講述了基于微*面的BRDF、GGX+Smith微面模型的漫反射模擬、與其它漫反射BRDF的比較等內容。

    漫反射微表面:數值求解積分,希望找到好的*似值,與Oren Nayar論文的方法相同,多達一半的光照不見了!不能忽略多次反彈。。。(完整的Oren Nayar還包括第二次反彈)。

    文中對G項進行了校正(下圖左未校正,右已做校正):

    改進后的Smith*似方法在成本和效果上都有提升:

    效果對比(Lambert、Disney、新模型):

    Improved Culling for Tiled and Clustered Rendering分享了暴雪的COD使用的分塊和分簇光照算法的改進。

    以往的光照算法在數據結構分支的性能問題:

    • 分塊:在前向,三角形可以穿過多個分塊;在延遲,波前可以匹配分塊大小。
    • 分簇:在前向,三角形可以跨越多個簇,在延遲,波前可以穿過多個Z切片。
    • 體素樹:波前可以跨越多個體素
    • 三角形/紋素:波前可以跨越多個三角形/紋理。

    分支的性能問題:VMEM和VALU成本高,超過分支(即tile內)的所有計算均按向量進行,所有的內存加載,即使是一致的,也會發生在每個向量上,從而大量占用TCC單元,內存運算將按向量進行,增加了ALU消耗。高VGPR消耗,因為所有操作都是基于向量的,所以所有常量數據(如實體描述符)都必須加載到VGRS中。

    標量化:一次僅在一個分支項上執行波前,通過波前對所有采樣項進行循環,遮罩所有波前線程以僅對選定項起作用,轉到下一個。可用于前向和延遲。

    數據容器:層級。指向葉子簇的指針,葉子簇存儲指向每個條目中可見實體的所有指針,不受entity編號的約束,間接尋址導致的昂貴遍歷,可變內存存儲成本。

    標量化的層次容器:著色器是完全標量化的,更好的VMEM/SMEM和VALU/SALU*衡,VGPR使用率低(類似于前向恒定加載)。性能高度可變,如果相同的實體在不同的容器中,波前可以多次處理實體,根據數據一致性/冗余性,可能會導致速度減慢。理想情況下,在實體級別進行標量化,這需要有序的容器——扁*位數組。

    數據容器:扁*。扁*位數組是位的集合–表示全局列表中第n個實體可見性的第n位,簡單遍歷——遍歷位,內存/實體綁定–主要用于每個視錐體上下文。

    位掩碼標量化:著色器完全標量化,每個實體執行一次,VGPR使用率低,顯著高于基線,可以說是更優雅的代碼。合成測試著色器的標量化結果(應用于密集照明環境中燈光查找的標量化):

    分層數據容器可以在容器級別上進行標量化,如分塊、分簇、體素地址,扁*數據容器可以在存儲實體級別上進行標量化,如光源/探頭/貼花索引。

    此外,在深度箱化(Z-Binning)的過程做了改進:

    前向+渲染器的Z-binning算法步驟:

    • CPU:
      • 按Z排序燈光。
      • 在可能的總深度范圍內設置均勻分布的箱。
      • 在每個箱子邊界內生成帶有最小/最大燈光ID的2 x 16位的LUT。
    • GPU(PS/CS):
      • 向量加載Z-BIN。
      • 波均勻光源的最小/最大ID。
      • 波均勻加載來自最小/最大范圍內的光源位。
      • 從燈光最小/最大ID創建向量位掩碼。
      • 用向量Z-Bin遮罩來遮罩均勻的光源。
        
        // Flat Bit Array iterator scalarized on entity with Z-Bin masked words
        wordMin = 0;
        wordMax = max(MAX_WORDS - 1, 0);
        address = containerAddressFromScreenPosition(screenCoords.xy);
    
        zbinAddr = ContainerZBinScreenPosition(screenCoords.z);
        zbinData = maskZBin.TypedLoad(zbinAddr, TYPEMASK_NUM_DATA(FORMAT_NUMERICAL_UINT, FORMAT_DATA_16_16));
        minIdx = zbinData.x;
        maxIdx = zbinData.y;
        mergedMin = WaveReadFirstLane(WaveAllMin(minIdx)); // mergedMin scalar from this point
        mergedMax = WaveReadFirstLane(WaveAllMax(maxIdx)); // mergedMax scalar from this point
        wordMin = max(mergedLightMin / 32, wordMin);
        wordMax = min(mergedLightMax / 32, wordMax);
    
        // Read range of words of visibility bits
        for (uint wordIndex=wordMin; wordIndex<=wordMax; wordIndex++)
        {
            // … //
        }
    
        // Read range of words of visibility bits
        for (uint wordIndex=wordMin; wordIndex<=wordMax; wordIndex++)
        {
            // Load bit mask data per lane
            mask = entityMasksTile[address + wordIndex];        
            // Mask by ZBin mask
            uint localMin  = clamp((int)minIdx - (int)(wordIndex*32), 0, 31);
            uint maskWidth = clamp((int)maxIdx - (int)minIdx+1, 0, 32);
            // BitFieldMask op needs manual 32 size wrap support
            uint zbinMask  = maskWidth == 32 ? (uint)(0xFFFFFFFF) : BitFieldMask(maskWidth, localMin);
            mask &= zbinMask;
            // Compact word bitmask over all lanes in wavefront
            mergedMask = WaveReadFirstLane(WaveAllBitOr(mask));
            while (mergedMask != 0) // processed scalar over merged bitmask
            {
                bitIndex = firstbitlow(mergedMask);
                entityIndex = 32*wordIndex + bitIndex;
                mergedMask ^= (1 << bitIndex);
                ProcessEntity(entityIndex);
            }
        }
    

    前向+渲染器的內存性能:

    前向+渲染器的Z-Bin性能:


    文中還使用了保守光柵化剔除、解決原子爭用、光源代理等方法來優化光照的效果和性能。其中光源代理將代理擴展到更多渲染實體,改進光柵化批處理。

    分塊光柵化的不同方法的性能。

    分簇光柵化的不同方法的性能。

    光源代理的效果對比。

    光源代理的性能對比。

    Precomputed lighting in Call Of Duty: Infinite Warfare闡述了COD的預計算光照涉及的各項技術,其在預計算光照過程中涉及的技術有光照圖、探測照明、光照貼圖–大型結構幾何(代表、投影)、光探針——其它一切(將可見性與照明分離、可見度的表示、插值照明、用于存儲和訪問的輕型網格結構:生成、烘焙、可見性表示)等。

    結構化幾何體:光照圖用ADH(Ambient Highlight Direction)編碼獲得最大的性能提升,在切線空間中,半八面體編碼,BC6H的顏色,選項以禁用每個貼圖的壓縮。

    左:傳統的光照圖效果;右:COD預期的光照圖效果。

    僅具有遮擋的遠距離照明下漫反射曲面的渲染方程:

    兩個特征截然不同的項:入射光(*滑且表現良好)和可見性(快速變化,高頻定向組件),需要把兩者分離!COD在頂點處存儲的可見性,燈光存儲在對象上的一些點上。對于可見性,將可視性存儲為圓錐體:軸、錐角和比例(0到1),可見性在實例之間共享,只需烘焙一次(在轉化過程中)。

    烘焙可見性時,生成四階SH,表面均勻取樣,驗證樣本,非線性迭代*滑器,將線性SH的范數插值為額外通道,重新縮放。圓錐體的最小二乘擬合:最佳線性軸,非線性擬合角度,但僅1d,比例只是圓錐體上積分的可見性與圓錐體自身積分的比率。

    光照:單一的照明環境不夠,大型對象的照明可能會發生劇烈變化,可以轉換為梯度[Annen2004],但質量/成本不滿意,生成多個采樣點,散落在物體周圍,數量取決于對象大小,將入射光存儲在這些點上,并對整個對象進行插值。確定采樣位置:算出N——探頭的目標數量——基于對象大小,均勻地、相當密集地采樣對象幾何體,執行k-means分簇,將樣本分配給N個分簇,松散后的最終分簇中心成為采樣位置。

    插值照明:計算采樣位置集的協方差矩陣,特征值的相對大小決定插值模式,特征向量定義了插值的局部空間。不同維度的插值方式如下:

    效果對比:

    對于光照探針網格,使用了非均勻網格。首先對level的體積進行非常粗略的體素化,四面體化體素化,僅輸入長方體=漂亮且常規的TET。細分四面體(來自[Schaeffer04]的方案),確保相鄰區域之間最多相隔1個細分級別,細分*幾何體、navmesh和手動放置的感興趣的區域。

    計算層次底部點的粗略照明,對于層次中的每個細分,計算誤差,給定級別和更低級別的插值照明之間的*方差,分析單元格體積上的積分。從層次結構的頂部開始再次優化,基于誤差、接*感興趣區域等的優化優先級。最后一組點是規則的,與幾何圖形和信號對齊,然而,TET單元格不僅僅是重建網格的單元格。

    左:四面體網格(Tetrahedral Mesh);右:光照網格。

    光照網格可見性:燈光穿透墻壁,即使從查找位置看不到探頭,也要使用探頭。使用一些可見性信息來增強探針,以限制其影響,對于每個探頭,它接觸的每個tet存儲一個三角形深度圖,第一個交點的重心:0-1范圍。

    不同預計算方法的效果對比:

    ADH投影過程:

    總之,分別考慮渲染方程的不同組件,隨著照明變得更加詳細,表示變得更加復雜——重新采樣到更簡單的結構以降低成本,最小二乘法是你的朋友,但別忘了規則化。

    The Lighting Technology of 'Detroit: Become Human'闡述了底特律-變人的光照技術,包含PBR、直接照明(解析光、陰影、體積光照)、間接照明等。

    光度單位(Photometric Units)是照明一致性是主要目標,更容易與現實生活中的參考資料進行比較,藝術家可以使用現實生活中的輸入值,防止它們將照明信息烘焙為反照率,允許更好的場景對比度/范圍。

    光度單位有以下幾種:

    • 發光功率(Luminous power ,lm):發出的總光量。*行光之外的其它光源以lm為單位。
    • 發光強度(Luminous Intensity,cd):每立體角方向lm。
    • 照度(Illuminance,lux):落在表面上的光量。*行光以lux為單位。
    • 亮度(Luminance ,cd/m2):特定方向上每單位面積的cd。

    強制二次衰減,照度非常高,接*正點光(punctual light)。

    發光表面:所有材質上的發射強度參數(+顏色),以曝光值(EV)表示,cd/m2是一個線性刻度,但人眼無法感知,cd/m2在感知上是線性的,+1 EV是感知光強度的兩倍。

    場景曝光:需要在關卡編輯器中正確曝光場景,不建議自動曝光,使用典型照明條件下的測量曝光量,關卡分為場景區(Scene Zone,SZ),攝影總監為每個“SZ”提供曝光,是沒有過渡的固定值,當相機進入SZ時應用曝光。曝光以EV100表示,表示相機快門速度和光圈數的組合,EV100是ISO100傳感器靈敏度的曝光值,為我們提供了一個框架,以確保一致的照明范圍,場景曝光對于預曝光(pre-expose)積累緩沖區來說非常好,游戲中曝光需要更多控制。

    相機曝光:場景曝光的曝光補償,提供動態更改曝光的控制,藝術家可以選擇4種相機曝光類型,自動曝光=游戲階段(主要)、手動曝光=剪切場景(主要)。相機曝光類型:手動(EV100中的曝光值可由動畫曲線控制)、相機(根據物理相機的設置來計算,如f光圈、ISO、快門時間)、自動*均(根據場景的對數*均亮度計算)、自動區域(根據手動放置在場景中的“場景區域”+EV“貼花”中提供的曝光值計算)。

    光度單位(調試):

    • 虛擬光點計(Virtual Spot Meter)提供像素絕對亮度,以cd/m2和EV100為單位,RGB和sRGB值,真的很有用,可以調整發射表面,調試鏡面反射的高值。

    ?

    • 錯誤顏色調試菜單用于檢查場景是否曝光良好。綠色是中灰色(18%),粉紅色是膚色,紫色是死黑,紅色是死白:

    材質校準:現在我們有了一個很好的照明框架,現實生活中的參考資料和相干值,材質需要同樣的處理,無法掃描我們所有的材質。捕獲一些對象和材質樣本,設置一個環境受控的房間,建立一個有三個白熾燈泡的暗房,易于在引擎中復制,捕獲的材質有助于驗證照明環境。圍繞這一點,建立了一個“凍結工具”,提供一些經過校準的照明環境,包含暗房和在各種照明環境下捕獲的其它全方位IBL[LAG16],材質屬性可視化,與對象/材質參考的比較,所有道具都可以通過該工具進行驗證。

    高亮顯示材質屬性超出范圍的值。紅色:基礎色不對,電介質材質必須在[30 240]的sRGB內,金屬材質必須在[186 255]的sRGB內;藍色:錯誤的玻璃著色器反射率,菲涅耳反射率必須在[52 114]的sRGB范圍內;黃色:金屬參數錯誤,金屬值應接*0或1,介于兩者之間的值通常是錯誤的。

    在直接光方面,所有的光源都是準時的:定向光、點光源、聚光燈、投影燈(“定向的”光,被限制在具有衰減的盒子中)。

    在陰影方面,陰影圖具有8個樣本的PCF+時間超級采樣,使用藍色噪點以抖動,3倍默認模糊半徑,最高可達15倍。根據幾何體法線計算的自動陰影偏移(定制的[HOL11])。嘗試過PCSS,由于寄存器壓力過大,不實用,僅用于牙齒著色器。

    陰影圖集的陰影以16位精度存儲在8192x8192x的圖集上,拆分為256x256塊,藝術家可以在3種不同尺寸之間選擇分辨率:256、512、1024,根據相機距離調整陰影大小,分辨率最多可以減半,在4步中降低分辨率以防止像素條紋,當達到256px的倍數時,重新打包在圖集中。僅當有東西在光的視錐中移動時更新,可以單獨排除點光源陰影面,裁減*面附*的可調整陰影,有助于數字精度和光線定位,可以與裁減*面附*的燈光解相關(無旋轉)。

    方向級聯陰影映射具有時間超級采樣的PCF(8個樣本),使用抖動和TAA,每個高達1440px的4次分割和16位精度,大多數場景使用2或3個分割,自動分割分配。

    靜態陰影根據相機距離切換到靜態陰影,只有1個樣本具有雙線性比較,靜態陰影圖集,圖集尺寸:2048x2048,每個陰影64x64,最多1024個陰影。定向靜態陰影是一個帶有關卡所有靜態幾何圖形的大紋理,尺寸:8192x8192。

    特寫陰影(Close-up Shadow)增加接觸和自陰影的精確度,以15362像素的速度增加最多兩個陰影,藝術家在場景中選擇相關對象,例如角色,只有這些對象接收特寫陰影。對象選擇:半徑10米以內的所有可見標記對象,從蒙皮點云計算蒙皮對象邊界體積。*/遠*面適合*距離接收器選擇的邊界體積,截錐體外部的對象投影在*陰影*面上。

    特寫陰影類似于UE的逐物體陰影。特寫陰影開啟(下圖左)和關閉(下圖右)對比:

    體積照明使用統一體積照明([WRO14] [HIL15]),匹配在燈組深度上,使用棋盤渲染,藍色噪音抖動的TAA,由直射光和漫反射探針光柵照亮,霧對GI烘焙的影響,偽多次散射。體積光可能會透過表面泄漏(下圖上),修復泄漏步驟(下圖下):逐tile存儲的最小/最大深度,使用“最大深度”在燈光評估時夾緊體素厚度,對體積紋理采樣應用Z偏移(TileDepthVariance > threshold)。

    在間接照明方面,使用了HL2環境多維數據集,靜態幾何的頂點烘焙,用于動態幾何的光源探針,偽間接鏡面照明,需要靜態和動態的統一解決方案。基于探針的解決方案非常適合基于鏡面反射圖像的照明(IBL),捕捉場景立方體貼圖,烘焙GGX NDF[WAL07] [KARIS14],過濾重要采樣以防止螢火蟲,藝術家控制影響盒和視差盒。另外,還使用了輻照度稀疏八叉樹,一個八叉樹單元格有8個探針,每個角一個,空間點始終由8個探針包圍,不丟棄任何探針,實際上是偏移探針。其中八叉樹級不連續,下圖的橙色點是計算的探針,淺藍色點是插值的探針:

    下圖上在每個葉子節點存儲2x2x2的數據,有很多冗余;下圖下以3x3x3的父節點存儲數據,更少冗余數據。

    球諧函數使用二階SH,4系數,使用Geomerics重建[Geomer15],3個RGBA16F體積紋理(R、G、B),24055個探針→ (105x105x3)x 3紋理約3MB。

    GI顯示時永不丟棄探針允許采樣體積紋理,只需使用硬件(3D)雙線性濾波,從3d文字中找到八叉樹單元哈希鍵(Morton鍵,O(1),限制為32位),對紋理坐標緩沖區使用預計算的哈希:X坐標在前15位編碼、Y坐標按以下15位編碼、Z坐標在最后2位編碼。這樣做的好處是使用計算著色器混合多個GI集非常簡單,支持GI開關(內部光源開關、閃電等)和GI轉換(一天中的時間、限制/關閉等)。

    GI過渡:避免場景區域之間的硬GI過渡,如內部? 外部,設置入口周圍的距離,通過GI的動態對象,基于到入口的距離和法線方向。

    GI過渡關閉和開啟的對比圖:

    大多數靜態對象都位于唯一的場景區域中,如內墻與外墻,那么門、窗、窗框呢?指定給場景區域,但從其它區域也可見,未指定給場景區域。

    Cluster Forward Rendering and Anti-Aliasing in 'Detroit: Become Human'介紹了Quantic引擎從Playstation 3到Playstation 4的演變,從延遲照明到集群前向照明的轉換、好處以及如何解決遇到的問題,還介紹了TAA及TAA的應用,例如SSR、SSAO、PCF陰影、皮膚次表面散射和體積照明。


    分簇前向渲染使用GPU更加靈活高效,新的照明算法:分塊渲染、前向+渲染、分簇前向渲染,它們的對比如下:

    分簇前向渲染有3個通道,它們的過程如下:

    分簇前向渲染的優化:強制燈光循環使用標量寄存器而不是矢量寄存器,并對燈光進行排序,確保所有東西盡可能使用相同的空間(視圖空間)。對TAA使用較少的陰影紋理樣本(僅8個),強制編譯器使用帶有2x4紋理陰影樣本的循環,在一定距離內只使用一個紋理陰影樣本的烘焙陰影紋理。深度通道是必要的,分簇可用于逐像素照明和逐頂點照明!如果可能,將基于圖像的照明轉移到延遲通道。

    光照循環優化:有4種燈(點光源、聚光燈、定向燈和投影儀),陰影和投影紋理,第一個版本使用4個循環(每種燈類型一個),后面改成到1個循環處理所有類型的燈光:

    For each light:
        ComputeLightAttenuation(...);
        ComputeShadow(...); // → Higher register usage for sun shadow
        ComputeProjectedTexture(...);
        ComputeFinalLightingColorWithMaterialBRDF(...);
    
    // 優化太陽陰影,增加各種可見性測試
    ComputeSunShadow(...); // → Lower register usage
    For each light:
        VisibilityTestBitField(...); // → Early exit
        ComputeLightAttenuation(...); // → Early exit
        TestNDotL(...); // → Early exit
        ComputeShadow(...); // → Early exit
        ComputeProjectedTexture(...);
        ComputeFinalLightingColorWithMaterialBRDF(...);
    

    經過以上步驟的優化之后,光源的數量隨之減少:

    半透明優化:透明度可能是性能殺手,玻璃僅基于圖像的照明,粒子:每質心、球諧函數、半分辨率。

    底特律游戲實現了TAA的著色抗鋸齒,可以使用GPU導數進行多次著色,效果不錯,但代價高昂。可以使用法線分布函數 (NDF) 過濾(Filtering Distributions of Normals for Shading Antialiasing及優化版本[Error Reduction and Simplification for Shading Anti-Aliasing](https://yusuketokuyoshi.com/papers/2017/Error Reduction and Simplification for Shading Anti-Aliasing.pdf)),與TAA配合得很好,雨水細節更為明顯。

    從上到下:TAA關閉、TAA開啟、TAA+NDF過濾。

    TAA還可用于陰影、HBAO、SSR、皮膚的屏幕空間次表面散射、體積光照。和TAA常搭檔的有Blue Noise(藍色噪點),具有最小低頻成分且無能量集中峰值的噪音。

    左邊是白噪點,右邊是藍噪點,下排是*鋪(tiling)模式。

    對于帶時間采樣的SSR,擁有TAA通道,使用棋盤夾緊鄰域:

    Precomputed Global Illumination in Frostbite講描述了Frostbite為“FIFA”、“Madden”、“前線”和未來游戲開發的靜態GI技術,包含路徑跟蹤、球面諧波光照貼圖和高效的光照貼圖打包算法。

    Flux是Frostbite的路徑追蹤器,具有下一事件估計的單向路徑跟蹤(蠻力),烘焙的CPU實現(Intel Embree、IncrediBuild XGE),實時藝術家工作流程的GPU實現。

    Frostbite中路徑追蹤的SH光照圖包含漫射照明、高效編碼、*似鏡面照明等方面的技術。烘焙的GI是一個數據庫/緩存,由位置??產生鍵值,存儲渲染方程的部分解:

    其中眼睛向量????未知(烘焙期間沒有攝像頭),著色法線??烘烤時可能不知道,運行時可以使用法線貼圖或幾何體LOD,光通量(Flux)烘焙光照圖和探針中的球面輻照度函數,假設基本漫反射BRDF(如\(??_??=\cfrac{??}{\pi}\))和\(??_{?????????????}=??_{????????}\),在運行時使用每像素曲面法線進行評估??和底色??,使用球諧函數。

    使用球諧函數的原因:不需要切線坐標系,與RNM或?-Basis不同,色度分離的RGB方向照明,不同于環境光+高光方向(AHD),高對比度漫反射照明,*似間接鏡面照明,RGB L1的SH的良好壓縮選項。烘焙球諧函數過程:


    為了簡化輻照度的SH,需要對輻射率等公式進行推導:

    輻照度SH光照圖編碼:使用4個RGB紋理存儲12個SH系數,L0 HDR中的系數(BC6H紋理),L1 LDR中的系數(3倍BC7或BC1紋理),RGB SH光照貼圖的總占用:高質量模式32位(4字節)/texel,用于BC6+BC7;低質量模式20位(2.5字節)/texel,用于BC6+BC1。例子:4096 x 4096光照圖,32位/texel時為64MB,20位/texel時為40MB。

    在鏡面光照上,*似鏡面反射的一般思想是在光照貼圖中使用L1球面諧波數據,從SH推導出主光源方向(僅規范化L1數據),根據L1波段的長度估計“傳播”或“聚焦”:

    float focus = lightmap.L1 ); // [0..1]
    

    使用估計的參數創建一個假想光源,將這些數字插入標準GGX鏡面反射公式。*似鏡面反射光照貼圖著色器:使用L1幅度來估計燈光的聚焦程度,與非線性漫反射SH照明類似的原理,接*0.0意味著光線來自多個方向,接*1.0意味著光主要來自一個方向。將表面粗糙度調整為快速區域光*似值,當啟發式建議全方位照明時,使高光更柔和,純粹基于視覺上令人愉悅且可信的結果的任意經驗*似:

    // Approximate an area light by adjusting smoothness/roughness
    
    float lightmapDirectionLength = length(lightmapDirection); // value in range [0..1]
    float3 L = lightmapDirection / lightmapDirectionLength;
    float adjustedSmoothness = linearSmoothness * sqrt(lightmapDirectionLength);
    
    // Proceed with standard GGX specular maths
    

    文中還涉及了很多技巧,諸如半球和紋素采樣、渲染收斂檢測、處理重疊幾何體、確保正確的雙線性插值、高效的光照圖集打包等。有興趣的童鞋可以閱讀原文。

    HDR Image Based Lighting: From Acquisition to Render闡述了HDR下的IBL從需求到實現的過程和涉及的技術、優化。基于圖像的照明(IBL)具有真實照片、物理正確、光度單位、環境匹配照明等特點,下面分別是HDR和LDR的IBL效果對比圖:


    為什么HDR和LDR有如此顯著的差異呢?IBL作為點光源的總和:

    和的函數不等于函數的和:

    IBL獲得正確照明的關鍵是亮度,色調映射的輸出,而不是資產,色調映射是任何真正的HDR渲染的基本部分。作為輻射的環境貼圖:全亮度范圍,沒有后期處理,沒有白*衡,線性的。真實世界的亮度值,99%的可能物體位于10000nit的前方,但剩余物體會對照明產生很大影響:

    靠數碼相機捕捉的環境圖仍然比太陽暗約350倍,數碼圖像質量又取決于分辨率、鏡頭光斑、噪點等因素。輻射貼圖的組裝:原始預處理,PTGui/HDR存儲,逆轉的響應曲線,光度校正。

    其中PTGui是全景拼接和HDR組裝軟件,使用真正的HDR選項,使用預定義的反向響應曲線。


    存儲時,以絕對值存儲,如果不能使用浮點格式,則可以進行更精確的打包,每個環境的照明設置基本相同。對于太陽亮度,用照度計測量照度,如果沒有硬件,在顏色檢查器上拍攝白色目標以恢復顏色或亮度,執行一些數學運算。太陽亮度的照度計測量傳感器何時正常朝向太陽,使用“*面”傳感器。

    太陽的亮度計算過程:讓目標暴露在陽光下拍攝,在陽光被小物體遮擋的情況下拍攝,以獲得天光,從第一個減去第二個,除以反照率,獲取照度:

    利用HDR、攝影學校正、IBL光照渲染之后,以下是渲染器(Maxwell Render)和數碼相片的對比:

    Material Advances in Call of Duty: WWII闡述了COD: WWII的高級材質特性,例如法線和光澤映射(有理函數擬合、結合細節光澤)、材質表面遮擋、多散射漫反射BRDF(能量守恒擴散)等。

    法線和光澤度(NDF)表示不同比例的幾何信息,當法線后退到一定距離時,像素足跡下的法線變化應該用光澤度(NDF)表示。


    MIPMAPPING的處理過程(將縮短的法線長度轉換為光澤):

    法線變化用MIPs編碼,較低的MIP編碼較高的正常變異,顏色較深。

    法線圖生成高度圖,再從高度圖生成遮擋圖:

    在處理AO時,被遮擋的方向具有相同的漫反射反照率,并且被遮擋的方式相似:

    原始AO和改進AO的對比:

    另外,將遮擋轉換為等效錐角:

    不同的遮擋方式的效果對比:

    間接鏡面遮擋采用了基于錐體的方法,環境BRDF擴展到三維:錐角θ,32x32x8紋理查找表,最左邊的切片與二維查找表相同:完全未包含的圓錐體,最右邊的切片是scale=0、bias=0:完全閉塞的圓錐體。

    為了讓漫反射達到能量守恒,使用了ENVBRDF查找表。EnvBRDF查找表表示聚集并反射到眼睛的鏡面反射光能量的分數,同一個查找表還表示準時光源表面散射的光能的分數。

    普通蘭伯特和能量守恒蘭伯特的對比:

    另外,加強了掠射角的反光效應:

    蘭伯特和完全多散射漫反射BRDF的對比:

    使用了BRDF切片的2D有理函數擬合:

    完全多散射漫反射與擬合多散射漫反射的對比:

    隨著實時著色技術的最新進展,已經可以用復雜的區域光源照亮基于物理的存在。一個關鍵的挑戰仍然存在:精確的區域光陰影。DXR的出現為通過光線追蹤解決這個問題打開了大門,但正確的公式并不明顯,而且存在幾個潛在的陷阱。例如,最流行的策略包括追蹤光線到光源上隨機分布的點,并*均可見性,但這是不正確的,并且會產生視覺失真。相反,Real-Time Ray Tracing of Correct* Soft Shadows提出了一個軟陰影的定義,可以計算正確的結果,以及一個與現有分析區域照明解決方案一起工作的高效實現。

    之前的LTC并不能處理遮擋的光照,但更真實的光影應該具備:

    之前有文獻提出了僅光追的軟陰影,做法是*均可見性:

    但如果使用BRDF獲得直接光,再乘以光追的*均可見性的軟陰影,將得到錯誤的結果:

    正確的做法應該如下圖右邊所示:

    也可以采用隨機化的方式,但必須強制BRDF的所有項都是隨機化的:

    隨機化的結果是過多噪點和過于模糊:

    所以僅光追的軟陰影和完全隨機化的兩種方案都將獲得錯誤或不良的結果。正確的軟陰影算法應該如下所示:

    從數學上講,我們可以看到事情顯然是正確的:\(a·b/a=b\)

    對應的正確隨機化公式:

    文中提出的方法如下:


    正確降噪的各個頻率的函數如下:

    降噪圖例:

    在采樣方面,使用了多重要性采樣:

    對于電介質(非金屬),使用了電解質多重要性采樣:

    最終效果對比:

    渲染通道和流程如下:

    總之,比率估計器:無噪聲有偏分析+無偏噪聲隨機,作為穩健噪聲估計的總變化(非方差),由分析著色驅動的陰影多重要性采樣,實時光線追蹤GPU的注意事項:活動狀態、延遲和占用率、多重要性采樣的分支、波前與內聯,混合的光線+光柵圖形示例。

    光線跟蹤最終進入了實時圖形管線,與光柵化、著色和計算緊密結合。跟蹤光線的能力提供了最終能夠在實時圖形中準確模擬全局光散射的希望。正如*年來基于物理的材質徹底改變了實時圖形一樣,基于物理的全球照明也有機會對圖像質量以及開發者和藝術家的生產力產生類似的影響。然而,要做到這一點并不容易:目前,在每個像素上只能追蹤到少量光線,需要圖形程序員非常謹慎和創造性。10年前廣泛采用光線跟蹤的離線渲染有一些經驗教訓,Adopting lessons from offline ray tracing to real-time ray tracing for practical pipelines討論了離線管線使用該技術的經驗,并強調在該領域開發的各種關鍵創新,這些創新對于采用實時光線追蹤的開發人員來說是值得了解的。

    離線渲染的經驗:大約10年前,多通道光柵化達到了臨界點,對于藝術家來說,迭代時間長,工作流程笨拙,從可視性角度渲染工件
    *似值,預烘焙和緩存照明通常有效…直到它不起作用(╯°□°),無法按預期準確模擬光照傳輸。采用路徑追蹤:處理一切的統一光照傳輸算法,圖元包含曲面、頭發、體積測量…反射:所有類型的BSDF、BSSRDF…燈光:點光源、區域光源、環境圖光源…

    文中涉及四個主題:明智地選擇光線(以及為什么必須選擇),仔細地生成(非)隨機數,把射線預算花在最有用的地方,理解并防止誤差。首先了解一下方差的概念和公式:

    所有采樣技術都基于將隨機數從單位*方扭曲到其它域,再到半球、球體、球體周圍的圓錐體,再到圓盤。還可以根據BSDF的散射分布生成采樣,或選擇IBL光源的方向。有許許多多的采樣方式,但它們都是從0到1之間的值開始的,其中有一個很好的正交性:有“你開始的那些值是什么”,然后有“你如何將它們扭曲到你想要采樣的東西的分布,以使用第二個蒙特卡羅估計”。

    對應采樣方式,常用的有均勻、低差異序列、分層采樣、元素區間、藍噪點抖動等方式。低差異類似廣義分層,藍色噪點類似不同樣本之間的距離有多*。過程化模式可以使用任意數量的前綴,并且(某些)前綴分布均勻。

    方差驅動的采樣:根據迄今為止采集的樣本,周期地估計每個像素的方差,在差異較大的地方多采樣,更好的做法是在方差/估計值較高的地方進行更多采樣,在色調映射等之后執行此操作。離線(質量驅動):一旦像素的方差足夠低,就停止處理它。實時(幀率驅動):在方差最大的地方采集更多樣本。計算樣本方差(重要提示:樣本方差是對真實方差的估計):

    float SampleVariance(float samples[], int n) 
    {
        float sum = 0, sum_sq = 0;
        for (int i=0; i<n; ++i) 
        {
            sum += samples[i];
            sum_sq += samples[i] * samples[i];
        }
        return sum_sq/(n*(n-1))) - sum*sum/((n-1)*n*n);
    }
    

    樣本方差只是一個估計值,大量的工作都是為了降噪,MC渲染自適應采樣和重建的最新進展。總體思路:在附*像素處加入樣本方差,可能根據輔助特征(位置、法線等)的接*程度進行加權。

    高方差是個詛咒,一旦引入了一個高方差樣本,你就有大麻煩了,例如考慮對數據進行均勻采樣:

    6個樣本:(1, 1, 1, 1, 1, 100 ) ≈ 17.5,再取6個樣本:(1, 1, 1, 1, 1, 100, 1, 1, 1, 1, 1, 1 ) ≈ 9.25,回想一下,方差隨樣本數呈線性下降…面對這種高方差樣本,最hack但也最有效的方式是clamp,如下圖所示:

    更復雜的選擇:基于密度的異常值剔除[Decoro 2010],保存所有樣本,分析并過濾異常值;[Zirr 2018]:根據亮度將樣本分成幾個單獨的圖像,然后根據統計分析重新加權。

    Real-Time Reflections in 'Mafia III' and Beyond闡述了游戲Mafia III實現實時反射的方案,包含現有解決方案、GPU上的光線投射
    反射渲染、粗糙表面上的反射、結果等。

    當時已經存在的解決方案有SSR、預過濾立方體圖查找、SSR+預過濾立方體圖查找、SSR+視差校正立方體貼圖(預過濾)、錐體追蹤等。現有的解決方案均未滿足所有要求:相機運動的穩定性、良好的性能和內存成本、在所有環境(室內、城市、景觀)中無縫工作、合理的內容創作成本、實時更新(場景更改)等。

    GPU上的光線投射:網格/BVH有分支、非相干存儲器存取、如何計算著色等問題,體素有內存繁重、重要實現等問題,深度紋理對GPU友好,普通的實現,不是完美的空間覆蓋等。

    文中采用的立方體圖設置:8個活動幾何體CM、1個天空CM,每個512像素的分辨率,帶完整的MIP鏈,無法離線預渲染CM,因為要支持動態時間和天氣,如果可以預渲染,則不需要單獨的天空CM。立方體圖離線預計算最大視距(每一側):

    用于所有側面的CHull場景查詢,使用幾何體著色器輸出到受影響的邊,有限的特性集。在更新上,天空CM每隔幾幀更新一次(云、ToD),幾何CMs定期更新動態照明(循環),緩存G緩沖區和靜態照明,找到更好的CM后渲染新的。

    活動立方體貼圖選擇:每個項目可能會有所不同,使用8個離玩家最*的,但有2個特殊情況:至少一個室外CM,在垂直軸上分開樓層的不利影響。可能的改進是使用邊界框(內/外、距離),使用遮擋查詢,預計算體積的最佳CM集。

    反射的渲染步驟:

    • 下采樣G-Buffer,應用NDF。檢測深度不連續性,如果檢測到邊緣,丟棄“小樣本”,選擇隨機樣本(利用時間過濾器),抖動法線(應用NDF),輸出(均為半分辨率)RT0(深度)、RT1(抖動的法線和粗糙度)、RT2(原始法線和粗糙度)。

    • 追蹤屏幕,輸出距離。追蹤屏幕空間深度,輸出行進的距離、“完成”標志,“完成”標志的模板遮罩。

    • 追蹤立方體圖,輸出距離和索引。基于粗糙度(HQ/LQ)的2次通道,從SSR終點開始,使用最佳CM,如果追蹤失敗,切換到鏈中的下一個CM并繼續,如果所有CM都失敗,使用回退,輸出行進的距離和CM索引(發現命中的位置)。

    • 解析顏色。半分辨率通道:解析SSR顏色,解析CM顏色。全分辨率通道:放大半分辨率的解析緩沖區,生成低粗糙度的模板掩模,在低粗糙度像素上解析SSR,在低粗糙度像素上解析CM。

    • 放大。輸入半分辨率的顏色、半分辨率的無抖動法線、半分辨率的深度、全分辨率的法線、全分辨率的深度,輸出:全分辨率的顏色(高粗糙度像素)、模板掩模,從半分辨率顏色中選擇一個最匹配全分辨率的法線和深度的樣本。

    對于粗糙表面的反射,當前的解決方案是混合所有3+一些技巧:50%重要性采樣、50%使用預過濾MIP(SSR和CM)、5樣本的BRDF加權屏幕空間模糊、修正的樣本分布、時間過濾,數學基于Blinn Phong(尚未轉換為GGX)。

    在實現反射的過程中,重用了鄰域樣本。4個鄰域的采樣深度和法線,與像素分類相同的模式,使用未抖動的法線,計算加權*均數:中心樣本:1、深度/粗糙度不連續性:0,否則評估BRDF。

    A Journey Through Implementing Multiscattering BRDFs and Area Lights闡述了多散射的光照模型和區域光的技術。

    在多散射的鏡面反射上,非金屬不明顯,但金屬有著較明顯的區別:

    目標是對Lambertian漫反射的改進,考慮多散射,漫反射會對表面粗糙度產生反應,漫反射取決于法線的分布,漫反射和鏡面反射都是能量守恒的。以往的光照模型如下:


    存在的問題是無多散射間接鏡面反射,頭發上沒有多散射鏡面反射,無多散射間接漫反射,皮膚上無多散射,無多散射。在鏡面多散射上面,公式的推導、*似過程如下:

    單次散射的能量實際上是環境BRDF中紅色和綠色通道的總和:

    考慮到\(F_{avg}\)可以解析計算,且多重散射光是漫反射的,我們得到以下公式:

    float2 FssEss = envBRDF.x + F0 * envBRDF.y;
    float  Ess    = envBRDF.x + envBRDF.y;
    float  Ems    = 1.0f - Ess;
    float  Favg   = F0 + (1.0f / 21.0f) * (1.0f - F0);
    float  Fms    = FssEss * Favg / (1.0f - Favg * (1.0f - Ess));
    float  Lss    = FssEss * radiance;
    // irradiance改成了radiance。
    float  Lms    = Fms * Ems * radiance;
    
    return Lss + Lms;
    

    效果對比:

    至此,可以更新不同材質使用的光照模型:


    對于多散射鏡面反射,LTC幅度和菲涅耳依賴于F0的線性依賴關系(下圖上),但該文的多散射BRDF具有非線性依賴性(下圖下):

    有一個多散射BRDF的公式,E(μ)是在幅度和菲涅耳LUT中的紅色通道:

    效果對比:

    文中還涉及了組合扭曲漫反射和LTC、組合預計算SSS和LTC等方法。

    It Just Works: Ray-Traced Reflections in "Battlefield V"闡述了游戲Battlefield V的光線追蹤相關的技術,包含GPU光線追蹤管線、DXR的引擎集成、GPU性能等。

    (簡單)光線跟蹤管線:

    生成管線階段,讀取GBuffer的紋理,使用隨機光柵化來生成光線:

    float4 light(MaterialData surfaceInfo , float3 rayDir)
    {
        foreach (light : pointLights)
            radiance += calcPoint(surfaceInfo, rayDir, light);
        
        foreach (light : spotLights)
            radiance += calcSpot(surfaceInfo, rayDir, light);
        
        foreach (light : reflectionVolumes)
            radiance += calcReflVol(surfaceInfo, rayDir, light);
        
        …
    }
    

    然而這種簡單的光追管線渲染出來的畫質存在噪點、低效、光線貢獻較少等問題:

    現在改進管線,在生成射線時加入可變速率追蹤:

    可變速率追蹤的過程如下:

    可變速率追蹤使得水上、掠射角有更多光線。但依然存在問題:

    可以加入Ray Binning(光線箱化),將屏幕偏移和角度作為bin的索引。




    依次可以加入SSR混合(SSR Hybridization)、碎片整理(Defrag)、逐單元格光源列表光照、降噪(BRDF降噪、時間降噪)等優化。

    SSR Hybridization的過程和結果。

    逐單元格光源列表光照。

    BRDF降噪過程。

    最終形成的新管線和時間消耗如下:

    渲染效果:

    DXR基礎:

    DXR的性能優化包含減少實例數、使用剔除啟發法、接受(一些)小瑕疵。剔除啟發法假設遠處的物體并不重要,除了橋梁、建筑等大型物體物,需要一些測量。投影球體包圍盒,如果\(\theta\)小于某個閾值,則剔除:

    不同閾值的效果:

    剔除結果:使用4度剔除,5000-->400 BLAS重建每幀,20000-->2800個TLAS實例,TLAS+BLAS構建(GPU):64毫秒-->14.5毫秒,但引入了偶爾跳變及物體丟失等瑕疵。

    BLAS更新優化:BLAS更新依舊開銷大,可以采用以下方法優化:

    • 錯開完整和增量BLAS重建。在完全重建之前N幀增量。
    • 使用D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_BUILD。
    • 避免重復重建。檢查CS輸入(骨骼矩陣),400 --> 50,將BLAS更新與GFX重疊,如Gbuffer、陰影圖。

    結果TLAS+BLAS構建(GPU):14.5毫秒-->1.15毫秒,RayGen(GPU):0.71毫秒-->0.81毫秒(交錯重建+標志)。

    不透明總結:總是使用ClosestHit著色器,僅對Alpha tested物體使用Any Hit著色器,對蒙皮、破壞使用計算著色器。

    射線有效載荷(RAY PAYLOAD)在ray交點出返回,與Gbuffer RTV的格式相同,包含材質數據、法線、基礎色、*滑度等。

    struct GbufferPayloadPacked
    {
        uint data0; // R10G10B10A2_UNORM
        uint data1; // R8G8B8A8_SRGB
        uint data2; // R8G8B8A8_UNORM
        uint data3; // R11G11B10_FLOAT
        float hitT; // Ray length
    };
    

    驗證正確性:光柵化輸出,向場景中發射主要光線,將有效載荷與Gbuffer進行比較,非零輸出?有bug!修正錯誤。

    The Indirect Lighting Pipeline of 'God of War'闡述了God of War的間接光渲染,包含整個管線的概述、動機和權衡、推動決策的背景/情況、技術概述等。

    Gl體積針對低頻靜態間接照明,每個體素1米以上的粒度,松散地放在Maya中,額外烘焙。編碼用4種3D紋理:2階球諧函數、RGB反彈和天空可見性+單色反彈,用Float16格式。分割天空和反彈可以提高方向性,天空不可知,單獨表示,在運行時與Gl合并:

    大的體素產生自遮擋和漏光,通過體素法線偏移Gl的采樣位置,兼容硬件過濾。


    對于移動物體,使用法線偏移移動對象可以獲得閃亮的外觀,不要在動態對象上用法線偏移,因為它們不在烘焙中。間接鏡面反射可選屏幕空間反射,立方貼圖(mip鏈中的光澤卷積),盒子視差校正,手動放置(包括盒子碰撞),從cubemap深度緩沖區中查找最佳擬合框碰撞的實用程序,盒子對有機環境不是很好的選擇。

    立方映射標準化:目標是在漫反射和鏡面反射環境照明之間保持自然*衡,將立方體貼圖用于角度細節和空間細節。在構建過程中從cubemap生成球諧函數,使用球面諧波消除低頻細節,替換為GI的低頻細節:

    立方體標準化關閉(上)和開啟(下)的對比:

    環境遮擋:SSAO、AO圖、角色AO膠囊類似于Last of Us的固定項 + 方向項,戰神用了布娃娃膠囊,在生產后期幾乎超過了極限。

    光照注入:最初的技術在光照圖中積累了中間結果,這是在持久的surfel集合中完成的,創建surfel時,照明保持發光狀態。針對每種光線(添加到emissive)的每一個表面評估直射光,使用與主渲染相同的照明計算/模型。

    射線處理:N條射線:將surfel投影到鏈接列表中,對列表進行排序,遍歷列表,在surfel之間傳輸光線,解析到GI體積。構建列表:對于每個光線方向,將表面投影到鏈接列表的紋理中,每個texel存儲當前列表頭指針,原子交換將新的surfel附加為新的頭(下圖左)。排序列表:對于每個光線方向,排序并壓*,每個線程可以是高度不同的、唯一的鏈表,處理過程中最慢的部分,使用冒泡排序(下圖右)。


    光照傳輸:對于每個光線方向,遍歷排序的列表,將鄰域的照明貢獻累積到surfel中,使用渲染器的共享照明代碼,以同樣的方式積累天空,但首個surfel將天空視為光源(下圖左)。解析體積:隨著處理的進展,立即積累SH編碼,對于每個光線方向,對于每個體素中心,投射到頭部紋理中,遍歷直到在兩個surfel之間,編碼surfel貢獻(下圖右)。

    文中還涉及體積霧、半透明、可變分辨率等技術進行了分享。

    Scalable Real time Global Illumination for Large Scenes講述了用于大規模場景的可擴展的實時GI解決方案。其解決方案是場景的體素表示(如體素圓錐體跟蹤),相機周圍的初始體素化照明場景盡可能好,盡可能快,如碰撞幾何、實體的較低LOD、高度圖數據,來自屏幕GBuffer的反饋體素化燈光場景,可見光輻照度volmap(體積圖)部分地在每一幀重新計算,使用真實的強力光線投射(無光)。

    輻照度圖:將輻照度存儲在相機周圍的嵌套體積貼圖中(3d clipmap),每個級聯為約64x32x64,單元格大小為\(0.45m \cdot 3^i\)(或在低端設備上為\(0.9m \cdot 3^i\),i是級聯索引),選擇了HL2環境立方體基,正交基,但GPU對樣本非常友好,可以很容易地更改為其它基。

    基本場景參數化:將場景存儲在相機周圍的嵌套體積貼圖(3d clipmap)中,每個級聯為128x64x128,單元大小為\(0.25m \cdot 3^i\)(或在低端設備上為\(0.5m \cdot 3^i\),i是級聯索引),要么存儲完全照明的結果,要么存儲兩個體積紋理中的照明+反照率。

    Sponza場景參數化:

    初始場景填充:當相機移動時,“以環形方式”填充新的體素(類似于紋理包裹),用高度貼圖數據和碰撞幾何體(頂點著色)或實體的低級LOD填充新的體素,然后立即用太陽光、間接光輻照度和該區域最重要的光照亮這個新的體素。

    場景反饋循環:在中等設置下,場景不斷更新,隨機選擇32k GBuffer像素,對于每個隨機選擇的GBuffer像素,使用它的反照率、法線和位置以及直接光和間接輻照度貼圖來照亮它,使用移動*均更新這個新的發光顏色的體素化場景表示。這提供了反饋循環,因為使用重照明GBuffer像素更新場景體素,使用當前輻照度體積圖更新它們,并用當前場景體素更新輻照度體積圖。它不僅提供多次反彈,還解決了體素化問題(墻比2個體素薄,精度高),此外,環境探針(在渲染時)提供了“主”攝像頭無法捕捉到的更多數據。

    輻照度貼圖初始化:當攝像機移動時,我們填充新的紋理(探針),對于更精細的級聯,從更粗糙的級聯復制數據,對于“場景相交”和最粗糙的級聯貼圖,跟蹤64條光線以獲得更好的初始*似,用magic(“不是真正計算的”)值來標記它們的時間收斂權重,所以一旦它們變得可見,它們就會被重新激活。

    輻照度圖-計算循環:在輻照度體積圖中隨機選擇幾百個可見的“探針”(位置),選擇的概率取決于“探測”的可見性和探測的收斂因子(上次變化的程度),對于選定的“探針”,在場景中投射1024到2048條光線(取決于設置),用移動*均法累積結果。

    輻照度圖-計算隊列:為了快速收斂,在輻照度圖中為不同的“探針”設置不同的隊列非常重要,第一次看到,從未計算過要盡快計算,即使質量較低。使用256條射線,但隊列中有4096個探針,不相交的場景探針不參與光照傳輸,但仍然需要為動態對象、體積測量和粒子計算它們。使用1024條射線,隊列大小只有64到128個探針。

    初始化光照-雞和蛋問題:當攝像機傳送時,它周圍的所有級聯都是無效的,所以不能用輻照度(第二次反彈)來照亮初始場景,也不能計算沒有初始場景的初始輻照度。分兩次完成:體素化場景,僅使用直射光照亮,計算天光和第二次反彈的輻照度,然后重新縮放場景,很少發生(剪影)。

    使用輻照度貼圖進行渲染:選擇最好的級聯,根據法線符號,從六個輻照度體積貼圖紋理中抽取三個進行采樣(請參見HL2 Ambient Cube)。在邊界上,將其與下一個級聯混合,延遲通道和向前通道(以及體積光照)也一樣。

    另外,使用凸面偏移過濾來解決光照泄漏:

    對于室內的過暗問題,添加“孔/窗”體積(永遠不會用體素填充)來解決:

    GI結論:具有多個光反彈的一致間接照明,可調質量,從低端PC到超高端硬件支持,但不影響游戲性,可擴展的細節大小,以及光線跟蹤質量。動態的(在某種程度上),炸毀一堵墻,摧毀一棟建筑,光照可以照進,建造圍墻產生反射和間接陰影,快速迭代。

    Precomputed Lighting Advances in Call of Duty: Modern Warfare分享了2020年的COD的預計算光照技術,包常規烘焙改進、球諧編碼(關注關于球諧函數的見解)、一種新的線性SH重建算法、動態光集實現細節。

    常規烘焙改進包含射線導向、回溯太陽、大地圖(天窗、流)、光照一致性(縫合模型、合并模型、薄模型)、光照圖編碼等。

    新的光照圖編碼對比及公式。

    文中采用了SH差異函數來編碼:

    SH重建*行光時,使用線性的SH的效果并不怎么好:

    可以將SH轉換成ZH基:

    ZH計算:

    效果對比如下:

    編碼數據:LG和LGV都很簡單,SH編碼最初僅用于此目的,LM更具挑戰性,讀修改寫資源問題,大部分數據。新的LM格式最初僅適用于DLS,Texel尺寸為當時LM的一半。

    VRS Tier 1 with DirectX 12 From Theory To Practice介紹了DX12 Tier 1的可變速率著色、虛擬現實引擎4中的VRS及騎士精神II實踐中的VRS。

    VRS邊緣保持解釋:以2x2渲染的16x16三角形,28個2x2粗像素+8個1x1像素=120像素,邊緣像素在粗像素的中心進行采樣,覆蓋區域外的像素沒有著色,邊緣保持是分辨率縮放的一個優勢。

    VRS Tier 1的限制:如果SV_Coverage被聲明為VRS Tier 1的著色器輸入或輸出,則著色速率將降低到1x1。SampleMask必須是完整的遮罩,如果將SampleMask配置為其它類型,則著色速率將降低到1x1。EvaluateAttributeAt[ Centroid|Sample|Snapped ]與tier 1 VRS不兼容,如果使用這些內置函數,著色率將降低到1x1。著色器中引用的HLSL的sample關鍵字將使著色速率降低到1x1。

    虛幻引擎4集成延遲和前向著色,在DirectX 12 RHI集成,RHICheckVRSSupport確定硬件支持,并在初始化時調用,RHISetVRSValues采用控制結構,使用D3D12CommandList5將著色速率值傳遞到命令列表中,控制臺變量的實現用于網格通道的著色速率,針對每種材質著色速率的編輯器集成。

    VRS網格通道控制GBuffer放置期間的著色速率。由EMeshPass枚舉定義的多個網格過程用于GBuffer放置,如BasePass、TranslucencyStandard、TranslucencyAfterDOF、TranslucencyAll,為像素限制的工作負載提供性能提升,BasePass和半透明通道是最無失真的通道,用其它通道進行的試驗沒有達到可接受的質量。由于邊緣保留,網格通道的視覺質量高于等效渲染比例,但性能將取決于內容。

    VRS網格通道:分辨率縮放不也有類似的好處嗎?分辨率縮放是在低功耗圖形部件上*滑幀率的常用技術,分辨率縮放會在多個管線階段影響整幀的質量。通過控制網格通過著色速率,可以更好地控制以質量換取性能的位置。分辨率縮放可以提供更高的性能,但網格通道著色速率可以保留三角形邊,而分辨率縮放則不能,網格通道著色速率可用于DRR的性能提升。

    SSR+VRS實驗:將VRS應用于SSR會導致視覺偽影,但是我們能提高視覺質量嗎?啟用TAA時,右上角的對角線瑕疵會閃爍,設置r.SSR.Temporal為1,并通過著色速率X/著色速率Y縮小r.TemporalAAFilterSize可以減少閃爍,也會增加清晰度。在ScreenSpaceReflections.usf通過著色速率縮放ScreenSpacerReflectionsPS中的StepOffset值可以減輕視覺損壞,但邊緣瑕疵仍然存在。

    該文作者不建議將其用于最終產品,但它指出了SSR+VRS可能可行的未來。

    VRS材質:子通道材質系統。新的材質屬性——著色率,具有實例覆蓋的材質實例支持,“材質”視口中的實時更新,將著色速率應用于共享材質的多個資源,創建實例以智能地控制網格子集的著色速率,使用最高的著色率1x1保存高質量資產。對于不太重要的資產,使用較低的著色速率,如2x2或4x4。混合和匹配材質著色速率,以選擇性地保持單個網格的質量。

    混合和匹配材質著色速率:使用兩種材質在每個模型的基礎上有選擇地改變著色速率(下圖左)。在某些情況下,使用2x2或4x4可能會導致視覺質量差(下圖中)。通過混合著色速率,可以保留高保真的內容(下圖右)。

    VRS的LOD:為每個LOD級別創建VRS材質,定義*(1x1)、中(1x2)、遠(2x2)。在“材質編輯器”中,將LOD材質插入材質槽。使用材質窗將VRS材質應用于自定義LOD。

    VRS速度和攝像機旋轉:

    VRS體積和粒子:

    材質限制:一切都要適度…過度使用VRS材質可能會導致性能不佳。盡量減少著色率的變化,以避免API性能損失和Ice Lake架構上的部分管線Flush。驅動程序中的重復著色速率被刪減,但切換速率仍有開銷。可以在渲染器中靜態緩存著色速率,以減少開銷,但太多的材質排列可能會導致性能不佳。帶有不透明遮罩的材質(即Masked材質)不會在透明區域保留邊緣,請考慮半透明。

    游戲Chivalry II的Base Pass對比:

    VRS和渲染縮放對比:



    Large-Scale Global Illumination in Call of Duty闡述了COD的大規模GI,包含通過Activision的預計算照明管線、扭曲輻照度體積、采樣體積照明、球諧函數的約束投影、照明數據的壓縮、預計算光傳輸的移動基分解等內容。

    扭曲輻照度體積:從上方和下方拍攝光線,以獲得高度包絡(下圖左)。 自適應無塊邊界問題,如果已經有高度圖可用,則可能是“免費的”(下圖右)。

    采樣體積照明:光照泄漏是體積光表示的一個基本問題,大體素和小幾何特征之間的不匹配。解決方案:將室內和室外劃分為不同的體量[Hill15,Hooker16],使用幾何感知重建過濾[ST15]。對大體素和薄幾何體,每一條光線都構成了hat內核的三線性足跡,基于可見性的樣本驗證,生成空間樣本,發射可見度光線并使低可見度樣本無效。

    效果對比:

    約束球諧函數:在SH域中,關于逐點約束的推理并不容易,示例:非負性在線性SH中很簡單,但推廣到更高階需要搜索窗口的系數[Sloan17],空間域與頻率域:空間域是關于逐點約束推理的自然選擇。同樣的方法也適用于一般的凸約束,選擇球體上的一個點集,例如64-128個斐波那契點,根據空間域編寫投影操作符,示例:半球形域約束。無需導出自定義半球形底座,無需應用基礎運算符的更改,最終結果是SH基礎,適用于任意域,例如球形帽等。

    接下來聊壓縮。使用移動基分解(Moving Basis decomposition,MBD)的效果更*滑,尺寸更小:


    直接光傳輸對比:

    間接光傳輸對比:

    總體效果預覽:

    結論:基于可見性的樣本驗證,概率視角,最大限度地減少超出預期視角位置的誤差,處理約束SH投影的RKHS框架,顏色約束、可見性約束、域約束等。移動基分解高效無縫地壓縮高維數據。


    14.5.3.3 移動*臺

    Architecting Archean: Simultaneously Building for Room-Scale and Mobile VR, AR, and All Input Devices涵蓋了在支持Archean廣泛的VR和AR設備方面所學到的經驗和技術,包括Rift、Vive、GearVR、Cardboard、Tango以及大量第三方物理、光學和傳統輸入。重點討論代碼庫體系結構,旨在通過創建抽象層將硬件支持與游戲功能分離,開發人員可以添加更多*臺,也可以添加更多游戲功能,同時既不為其它*臺添加任何額外工作,也不為編輯器擴展和SDK管理器自動處理特定于*臺的場景和項目設置,允許在各種各樣的*臺上立即構建。文中提出的VR分層架構如下:

    • 特定于SDK的輸入類:每個硬件/SDK一個類,沒有特定于項目的邏輯!監聽設備輸入,調用抽象處理程序,調用工具的down/hold/up,調用常規輸入的down/hold/up。

    • 輸入組件:SDK特定組件類包含對硬件功能的特定引用:

      public class ViveControllerComponents : WandComponents 
      {
          public SteamVR_Controller.Device viveController;
      }
      

      特定于類別的組件類包含硬件類型的公共屬性:

      public class WandComponents : InputComponents 
      {
          public Transform handTrans;
          public override Vector3 Position { get { return handTrans.position; } }
          public override Vector3 Forward { get { return handTrans.forward; } }
          public override Quaternion Rotation {get{ return handTrans.rotation; }}
      }
      

      InputComponents基類包含大多數抽象數據:

      public class InputComponents 
      {
          public virtual bool Valid { get { return true; } }
          public virtual Vector3 Position { get { return Vector3.zero; } }
          public virtual Vector3 Forward { get { return Vector3.forward; } }
          public virtual Quaternion Rotation { get { return Quaternion.identity; } }
      }
      
    • 工具基類。

      // 每個SDK的向下/保持/向上掛鉤
      public virtual void DoToolDown_Sixense(SixenseComponents sxComponents) 
      {
          DoToolDown_Wand(sxComponents);
      }
      public virtual void DoToolDown_Leap(LeapComponents leapComponents) 
      {
          DoToolDown_Optical(leapComponents);
      }
      public virtual void DoToolDown_Tango(TangoComponents tangoComponents) {
          DoToolDown_PointCloud(tangoComponents);
      }
      
      // 每個類別的向下/保持/向上掛鉤
      public virtual void DoToolDown_Wand(WandComponents wandComponents) 
      {
          DoToolDown_Core(wandComponents); 
      }
      public virtual void DoToolDown_Optical(OpticalComponents opticalComps) 
      {
          DoToolDown_Core(opticalComps); 
      }
      public virtual void DoToolDown_PointCloud(PCComponents pcComponents) 
      {
          DoToolDown_Core(pcComponents); 
      }
      
      // 工具基礎函數
      DoToolDown_Core
      DoToolDownAndHit*
      DoToolHeld_Core
      DoToolUp_Core
      DoToolDisplay_Core
          
      public virtual void DoToolDown_Core(InputComponents comp) 
      {
          if (Physics.Raycast(comp.Position, comp.Forward, out hit, dist, layers)) 
          {
              DoToolDownAndHit(comp);
          }
      }
      
    • 特定工具類型。

      // 可以覆蓋DoToolDown_Core等,實現完全*臺無關邏輯
      public class MoveTool : Tool 
      {
          protected override void DoToolHeldAndHit(InputComps comps) 
          {
              selectedTrans.position = hit.point;
          }
      }
      
      // 可以覆蓋任何類別或特定于SDK的掛鉤,以實現更定制的行為
      public class MoveTool : Tool 
      {
          // ...
          protected override void DoToolHeld_Optical(OpticalComps comps) 
          {
              // Move mechanic that’s more appropriate for optical control
          }
      }
      
    • 硬件輸入基類。非工具抽象輸入,適用于一般游戲功能和一次性交互,三種處理方法:

      // 比如Unity的原生輸入類
      bool HardwareInput.ButtonADown/Held/Up
      // 當想要那個觀察者的時
      event HardwareInput.OnButtonADown
      // 集中輸入/游戲邏輯
      void HardwareInput.HandleButtonADown()
      
    • 游戲性/一般輸入類...。

      // 一次性輸入
      public class GameplayController : MonoBehaviour 
      {
          void Update() 
          {
              if(HardwareInput.TriggerDown) 
              {
                  WorldConsole.Log("Fire ze missiles!");
              }
          }
      
          void Awake() 
          {
              HardwareInput.OnButtonADown += HandleButtonA;
          }
      
          void HandleButtonA() 
          {
              WorldConsole.Log("Boom!"); // Btw: use a “world console”!
          }
      }
      
      // 組件的一般輸入, 更新的三種方法:
      // 添加硬件輸入/位置/向前等
      HardwareInput.ButtonADown/Held/Up
      // 傳遞包含組件的事件參數
      HardwareInput.OnButtonADown(args)
      HandleButtonADown(components)
      

    所有SDK都以Libs/dir的形式存在于項目中:

    SDK太多了!SDK之間的AndroidManifest和插件沖突,在某些情況下,可以通過合并清單來解決(例如Cardboard+Nod),在許多情況下,只需要將沖突的SDK移入或移出Asset文件夾,可以連接到構建管線中。對于多SDK場景設置,將場景設置為支持所有SDK,Player對象包含用于ViveInput、GamepadInput、CardboardInput、NodeInput、LeapInput、TangoInput的組件...好處是場景之間沒有重復的工作,所有設備都可以同時啟用(例如Vive+Leap)。好處是讓多種設備類型交互意味著新的設計挑戰,更多*臺==更復雜的場景,可以將播放器拆分為更易于管理的預置體,并在運行時或使用編輯器腳本組裝這些預置。對于SDK管理器編輯器腳本,在編輯器中或在構建時啟用/禁用每個*臺的組件和對象。

    public void SetupForCardboard() 
    {
        Setup(
            // Build settings
            bundleIdentifier: "io.archean.cardboard",
            vrSupported: false,
            // GameObjects
            cameraMasterActive: true,
            sixenseContainerActive: false,
            // MonoBehaviours
            cardboardInputEnabled: true
        );
    }
    

    此外,可以自定義輸入模塊將允許向uGUI添加新的硬件支持。塊狀和點擊用戶界面(Block-and-pointer UI),點擊或按下時,<設備>將光線投射輸入用戶界面(例如Vive),或簡單的碰撞(如Leap)。自定義按鈕組件:

    ButtonHandler類中有一個巨大的switch語句來映射所有動作:

    switch(button.action) 
    {
        case ButtonStrings.Action_TogglePalette: TogglePalette(button, state); break;
        case ButtonStrings.Action_ChangePage: ChangePage(button, state); break;
        case ButtonStrings.Action_ChangePagination:ChangePagination(button); break;
        case ButtonStrings.Action_SelectProp: SelectProp(button, state); break;
        case ButtonStrings.Action_SelectTool: Tool.HandleSelectToolButton(button); break;
        …
    

    還有一個文件,里面有用于操作的常量字符串:

    public const string Action_TogglePalette = "togglePalette";
    public const string Action_ChangePage = "changePage";
    public const string Action_ChangePagination = "changePagination";
    public const string Action_SelectProp = "propSelect";
    public const string Action_SelectTool = "toolSelect";
    ....
    

    通過參數字段可以獲得更高級和可重用的功能,用戶界面代碼集中,按鈕可以傳遞任何數據類型,非常方便。所以你想做一個多*臺的虛擬現實應用…你確定嗎?SteamVR&Cardboard加入Unity的原生VR支持,從一開始就計劃多*臺。

    Advanced VR Rendering Performance闡述了2016年的VR渲染優化技巧,包含多GPU、注視點渲染和徑向密度遮罩、重投影、自適應質量等。

    單GPU情況下,單個GPU完成所有工作,立體渲染可以通過多種方式完成(本例使用順序渲染),陰影緩沖區由兩只眼睛共享。多GPU親和API,AMD和NVIDIA有多個GPU親和API,使用關聯掩碼跨GPU廣播繪制調用,為每個GPU設置不同的著色器常量緩沖區,跨GPU傳輸渲染目標的子矩形,使用傳輸柵欄在目標GPU仍在渲染時異步傳輸。2個GPU時,每個GPU渲染一只眼睛,兩個GPU都渲染陰影緩沖區,“向左提交”和“應用程序窗口”在傳輸氣泡中執行,性能提高30-35%。4個GPU時,每個GPU渲染一只眼睛的一半,所有GPU渲染陰影緩沖區,PS成本成線性比例,VS成本則不是,驅動程序的CPU成本可能很高。

    從上到下:1個、2個、4個GPU渲染示意圖。

    投影矩陣與VR光學:投影矩陣的像素密度分布與我們想要的相反,投影矩陣在邊緣每度像素密度增加,VR光學在中心像素密度增加,我們最終在邊緣過度渲染像素。使用NVIDIA的“多分辨率著色”,可以在更少的CPU開銷下獲得額外約5-10%的GPU性能。

    徑向密度遮蔽(Radial Density Masking):跳過渲染2x2像素方塊的棋盤格圖案,以匹配當前的GPU架構。

    重建濾波器的過程如下:

    徑向密度遮蔽的步驟:

    • 渲染時Clip掉2x2的像素方塊,或使用2x2的棋盤格圖案填充模板或深度,然后進行渲染。
    • 重建濾波器。

    在Aperture Robot Repair中節省5-15%的性能,使用不同的內容和不同的著色器可以獲得更高的增益,如果重建和跳過像素的開銷沒有超過跳過像素四元體的像素著色器節省,那么就是一次wash。在低端GPU上幾乎總是能節省很多工作。

    處理漏幀,如果引擎未達到幀速率,VR系統可以重用最后一幀的渲染圖像并重新投影:僅旋轉重投影、位置和旋轉重投影,用重投影來填充缺失的幀應該被視為最后的安全網。請不要依賴重投影來維持幀率,除非目標用戶使用的GPU低于應用程序的最低規格。

    僅旋轉重投影:抖動是由攝影機*移、動畫和被跟蹤控制器移動的對象引起的,抖動表現為兩個不同的圖像*均在一起。

    旋轉重投影是以眼睛為中心,而不是以頭部為中心,所以從錯誤的位置重投影,ICD(攝像機間距離)根據旋轉量在重投影過程中人為縮小。

    好的一面:幾十年來,人們對算法有了很好的理解,并且可能會隨著現代研究而改進,即使有已知的副作用,它也能很好地處理單個漏幀。所以…有一個非常重要的折衷方案,它已足夠好,可以作為錯過幀的最后安全網,總比丟幀好。

    位置重投影:仍然是一個非常感興趣的尚未解決的問題,在傳統渲染器中只能獲得一個深度,因此表示半透明是一個挑戰(粒子系統),深度可能存儲在已解析顏色的MSAA深度緩沖區中,可能會導致顏色溢出。對于未表示的像素,孔洞填充算法可能會導致視網膜競爭,即使有許多幀的有效立體畫面對,如果用戶通過蹲下或站起來垂直移動,也有需要填補空白。

    異步重投影:理想的安全網,要求搶占粒度等于或優于當前一代GPU,根據GPU的不同,當前GPU通常可以在draw調用邊界處搶占
    ,目前還不能保證vsync能夠及時重新發布。應用程序需要了解搶占粒度。

    交錯重投射提示:舊的GPU不能支持異步重投影,所以需要一個替代方案,OpenVR API有一個交錯重投影提示,如果底層系統不支持始終開啟的異步重投影,應用程序可以每隔一次請求僅限幀旋轉的重投影。應用程序獲得約18毫秒/幀的渲染。當應用程序低于目標幀率時,底層VR系統還可以使用交錯重投影作為自動啟用的安全網,每隔一幀重新投影是一個很好的折衷。

    維持幀率很難,虛擬現實比傳統游戲更具挑戰性,因為用戶可以很好地控制攝像機,許多交互模型允許用戶重新配置世界,可以放棄將渲染和內容調整到90fps,因為用戶可以輕松地重新配置內容,通過調整最差的20%體驗,讓Aperture Robot Repair達到了幀率。

    自適應質量:它通過動態更改渲染設置以保持幀率,同時最大限度地提高GPU利用率。目標是減少掉幀和重投影的機會和在有空閑的GPU周期時提高質量。例如,Aperture Robot Repair VR演示使用兩種不同的方法在NVIDIA 680上以目標幀率運行。利益是適用于應用程序的最低GPU規格,增加了藝術資產限制——藝術家現在可以在保真度稍低的渲染與更高的多邊形資產或更復雜的材質之間進行權衡,不需要依靠重投影來維持幀率,意想不到的好處:應用程序在所有硬件上都看起來更好。

    在VR中,無法調整的:無法切換鏡面反射等視覺功能,無法切換陰影。可以調整的內容:渲染分辨率/視口(也稱為動態分辨率)、MSAA級別或抗鋸齒算法、注視點渲染、徑向密度遮蔽等。自適應質量示例(黑體是默認配置):

    測量GPU工作負載:GPU工作負載并不總是穩定的,可能有氣泡,VR系統GPU的工作量是可變的:鏡頭畸變、色差、伴侶邊界、覆蓋等。從VR系統而不是應用程序獲取計時,例如,OpenVR提供了一個總的GPU計時器用于計算所有GPU工作。

    GPU定時器-延遲,GPU查詢已經有1幀了,隊列中還有1到2個無法修改的幀。

    實現細節——3條規則,目標是保持70%-90%的GPU利用率。

    • 高GPU利用率=幀的90%(10.0ms),大幅度降低:如果最后一幀在GPU幀的90%閾值之后完成渲染,則降低2級,等待2幀。
    • 低GPU利用率=幀的70%(7.8毫秒),保守地增加:如果最后3幀完成時低于GPU幀的70%閾值,則增加1級,等待2幀。
    • 預測=幀的85%(9.4ms),使用最后兩幀的線性外推來預測快速增長,如果最后一幀高于85%閾值,線性外推的下一幀高于高閾值(90%),則降低2個級別,等待2幀。

    10%空閑的規則:90%的高閾值幾乎每幀都會讓10%的GPU空閑用于其它處理,是件好事。需要與其它處理共享GPU,即使Windows桌面每隔幾幀就需要一塊GPU。對GPU預算的心理模型從去年的每幀11.11ms變為現在的每幀10.0ms,所以你幾乎永遠不會餓死GPU周期的其它處理。

    解耦CPU和GPU性能,使渲染線程自治,如果CPU沒有準備好新的幀,不要重投影!相反,渲染線程使用更新的HMD姿勢和動態分辨率的最低自適應質量支持重新提交最后一幀的GPU工作負載。要解決動畫抖動問題,請為渲染線程提供兩個動畫幀,可以在它們之間進行插值以保持動畫更新,但是,非普通的動畫預測是一個難題。然后,可以計劃以1/2或1/3 GPU幀速率運行CPU,以進行更復雜的模擬或在低端CPU上運行。

    總之,所有虛擬現實引擎都應支持多GPU(至少2個GPU),注視點渲染和徑向密度遮蔽是有助于抵消光學與投影矩陣之爭的解決方案,Adaptive Quality可上下縮放保真度,同時將10%的GPU用于其它進程,不要依靠重投影來達到最小規格的幀率!考慮引擎如何通過在渲染線程上重新提交來分離CPU和GPU性能。

    Higher Res Without Sacrificing Quality, plus Other Lessons from 'PlayStation VR Worlds'講述了PS的VR渲染技術,包含紋理流、無綁定、SRT、繪制校驗、分辨率漸變、源碼級別的著色器調試、自適應分辨率等。

    由于不再使用像素坐標來進行查找,也就是說,在視圖空間中進行有效的分塊,只需使用組合*截頭體對兩只眼睛進行一次即可,已用于貼花分塊:

    混合的分塊前向、延遲的缺點:失去了前向的優勢,對MSAA采用延遲方法也很重要,虛擬現實世界中沒有MSAA。但結果很好,為重新照明模型提供了靈活性。性能良好,將燈光分塊分辨率與圖像分辨率分離是一個巨大的勝利。

    部分駐留紋理,硬件提供了一個內著色器機制,通知使用者讀取了未映射的內存,然后可以用這些信息做些事情,例如,可以把這個失敗信息記錄下來,然后在后面里把那頁帶進來。

    虛擬內存系統:充分利用Aaron MacDougall提出的系統,紋理按兩個字節大小的冪進行分組,每兩種大小的冪都有自己的插槽分配器,每個插槽分配器都有4GB的虛擬內存,總共使用36GB。然后,游戲會提供一大袋物理頁面供用戶使用,可以根據需要在運行時改變頁面數量。

    隨著紋理質量的提高,然后映射到64KB的頁面中,紋理的最小/最大mip字段立即更改,新的波前將在這一幀中看到這種變化。隨著紋理質量的降低,最小/最大mip字段會立即更改,但下一幀頁面將被取消映射。由于頁面映射不變,因此沒有明顯的卡頓。無需復制或碎片整理,我們不關心虛擬內存碎片,物理碎片并不重要,紋理映射的繪制調用驗證很容易。總之,虛擬內存使紋理流更容易!并帶來許多其它好處,在著色器中,PRT是一場噩夢,但基于mip的PRT非常容易管理,另外,別忘了PRT也適用于CS和VS。

    繪制調用校驗:當事情不可避免地出錯時,將投入了大量的時間來制作東西來幫助解決問題,這就就是繪制調用校驗。在發送繪制之前,嘗試捕捉明顯的錯誤,GPU崩潰和超時很難調試,此外,并不是所有的錯誤都會導致如此明顯、直接的錯誤。當GPU崩潰時,將會花相當多的時間進行調試,當時的工具讓這變得非常困難,哪個繪制失敗了?為什么?可能花了一周的時間來追蹤,幾分鐘來修復:

    GPU Protection fault. Access Read: Unmapped page access: Addr(VA)0x000000000433e000
    

    大多數故障都很微妙(不是崩潰),不可見的物體、損壞的材質、空白紋理,如果沒有看到這個物體,就會錯過問題,或者可能會認為是藝術家有意設計的。但它仍然在崩潰:GPU Protection fault. Access Read: Unmapped page access: Addr(VA)0x000000000433e000,沒有程序計數器,沒有著色器ID,沒有繪圖ID。啟動命令緩沖區后,狀態可能已損壞,內存可能無法映射,鋒利的東西可能會被扔掉,代碼仍然可能被破壞。如果可以在指令發生時捕捉崩潰,那么就不需要那么多猜測了。可以檢查激活的狀態,看看出了什么問題,就像使用普通的調試器一樣,需要在著色器中進行驗證。

    打包著色器代碼:向量加載示例,用跳轉到新代碼片段替換向量加載。

    新代碼片段:從V#計算基址和緩沖區大小,檢查范圍是否已映射,檢查每個頁面的權限,如果滿意,運行原始指令并返回,否則,將PC寫入主存儲器,并向CPU和用戶發送信號。

    總之,驗證是值得做,很容易做到,而且會節省時間!捕捉異步頁面錯誤很棘手,但在關鍵時期之前開發一個基礎設施是值得的。

    接下來聊分辨率漸變(Resolution Gradient)。VR畫面變形的流程和預期分辨率的關系如下

    更高的內部分辨率:對于PlayStation VR,建議瞄準\(1.4^2\)的面板分辨率,相當于是原來2倍的像素數量!需要60fps。

    \[\begin{eqnarray} &&(960 \times 1.4) \ \times \ (1080 \times 1.4) \times 2 \text{只眼睛} \\ &=& 1512 \times 1344 \times 2 \\ &=& 406萬像素/幀 \\ &\approx& 2.5億像素/秒 \\ &\approx& 原生4k\ 30Hz像素數/秒 \end{eqnarray} \]

    因此,需要找到一種方法,以可變分辨率渲染中心的多個像素,外圍區域較少,沒有硬件支持。一種方法是使用兩個通道:以高分辨率渲染中心部分,以低分辨率渲染其余部分,組合和混合它們(下圖)。但是要求我們在每個視圖中使用額外的幾何通道,只有兩個級別:高或低,形狀不是很靈活。

    構造抖動模板遮罩以應用于圖像,更多控制,沒有額外的幾何通道(因為需要高分辨率插入方法)。在中間照常著色,然后,隨著離中心越來越遠,逐漸遮蔽了更多的像素。在圖像的外部,僅以1/4個像素進行渲染(下圖右)。


    你可能已經注意到,屏蔽了2x2的像素網格,為了阻止quad overdraw讓整個事情變得毫無意義。Quad粒度的遮蔽:如果在Quad中遮罩一些像素,同樣適用。因此,為了獲得任何好處,需要以Quad(2x2的像素塊)的粒度遮蔽,而不是像素。然后,用一個放大通道來填充孔洞,只需在最*的著色像素上進行復制(下圖)。

    在許多情況下,填充“按需”調整著色器中的采樣位置以選擇最*的有效像素會更快,不再需要單獨的擴張通道,多通道中的額外ALU通常比額外的讀/寫便宜,需要修改所有著色器代碼!

    Prepass之后遮蔽:根據場景的幾何和深度復雜性,有時不在預處理過程中遮罩會更快。這會使預處理稍微慢一點,但可能不會太多,因為預處理著色是輕量的,并且避免了擴大深度的需要,可以節省成本。

    時間魔法:改變抖動模式每幀TAA累積貢獻,確保所有像素以較低的時間頻率做出貢獻,以時間分辨率換取空間分辨率,對現有TAA的改動很少。

    結果出人意料的好,即使在1/4分辨率的區域!即使以Quad粒度遮蔽!在16.6ms的幀上節省約4ms!


    MSAA濫用:Quad粒度遮蔽確實會加劇高頻區域的不穩定性,在運動中更引人注目,但找到了一種在像素粒度上屏蔽的方法!沒有過繪制問題,多虧了Mark Cerny的insight。以4XMSAA的采樣頻率渲染(調整MSAA樣本位置)和1/4分辨率(1/2 x 1/2),仍然有相同數量的樣本,但是,組成quad的樣本現在在分辨率圖像中相距更遠。像

    素粒度的遮蔽過程。A:2x2的像素網格,每個右4個樣本。B:使用每個像素中的相應樣本構建的像素Quad,第一個quad由2x2像素網格中每個像素的第一個樣本組成。C:其它像素類似。D:像以前一樣遮蔽掉綠色和黃色標記的quad,就得到了像素粒度遮蔽!


    MSAA技巧難題:處理它的代碼開始滲入所有著色器和運行時代碼,尤其是按需擴展,圖形調試工具更難使用,圖像分為4個樣本,需要下定決心把它正確地可視化。

    高分辨率和低延遲圖形是為VR用戶提供真正沉浸式體驗的關鍵。High quality mobile VR with Unreal Engine and Oculus分享了UE4上移動虛擬現實的技巧、最佳實踐、UE4的最新渲染技術,以及圖形管線如何從移動外形因素中獲得最大收益,還討論移動虛擬現實技術是如何發展的以在未來可以期待什么。

    VR最佳實踐:與PC和控制臺相比,移動設備有更多的限制,最易訪問的開發環境,最具挑戰性的*臺。電池續航和散熱是主要問題,快速峰值性能,但不能無限期地運行,優化比PC和控制臺更復雜,僅僅保持幀速率不夠,安卓N持續性能模式,保證以較低的性能水*無限期運行。

    資產預算和建議:整個場景的*均三角形數為50-60k,最多10萬,每只眼睛50次DC,合并材質和網格,使用實例,多視圖改進了這一點!聚合性LOD,考慮對內存的影響。新的自動LOD生成示例輸出,維護頂點數據,以便共享材質和光照貼圖(下圖)。材質應不超過125個指令,沒有動態燈光或陰影,烘焙和偽造,使用沒有后期處理的LDR,創建具有代表性內容的測試關卡,在打算發布的設備上配置文件,以驗證預算,測試預算會話時間的持續時間。

    內容建議:刪除用戶看不到的三角形,刪除背面,細分大尺寸的模型,將遠處的環境烘焙到skybox,使用最佳采樣的Oculus立方體環境圖層,Monoscopic可用于中間地帶。完全粗糙的存在,假的環境反射。不要渲染被遮擋的對象,浪費DC和圖元剔除時間,設計場景以最小化繪制距離,使用預計算的可見性體積,利用場景信息主動手動隱藏不在視野中的對象。最小化透明過繪制,仍然繪制100%透明的對象。設置可見性標志!使用MSAA,至少2倍,如果可能,至少4倍。避免后期處理消除鋸齒,使用ASTC進行紋理壓縮,盡量最大化塊大小,生成MIP映射,避免復雜的過濾選項。跟蹤tick對象數量,如果不需要就不要tick,創建對象極其昂貴,放到加載時創建,在多個幀上攤銷。考慮構建一個管理器來共享對象,嘗試藍圖原生化以減少腳本VM開銷。

    立體層:未在引擎中渲染,在組合器(compositor)中進行光線跟蹤,只有一次采樣!支持四邊形、圓柱體和立方體貼圖,有頭部鎖定、跟蹤器鎖定或世界鎖定等鎖定模式,立體層組件,與UMG一起協作!

    當時的UE新增了單視遠場(Monoscopic Far Field)渲染和移動端多視圖渲染。

    Monoscopic渲染:渲染兩只眼睛,位置差造成雙眼視差,投影差異造成雙眼視差,深度一樣。存在性能問題,將CPU使用率提高一倍,將頂點/片元的使用增加一倍。

    隨著距離的增加,位置差異不那么顯著。添加第三個攝像頭,兩個立體攝像機有一個30英尺遠的*面,單鏡相機在飛機附*有一個30英尺的攝像頭,像素的嚴格排序,新的渲染管線。

    新管線的問題:單鏡相機渲染未使用的像素,立體相機繪制視錐剔除的遠距離目標,合成瑕疵(主要是透明度),運行第三個攝像頭的性能卡頓。結果:性能非常依賴于環境,在某些情況下增加20%以上,CPU和GPU的影響,也會導致性能下降,動態系統,包括開/關和視距,vr.FarFieldRenderingMode 0/1/2/3/4

    移動端多視圖(Multiview)渲染:圖元粒度上視圖之間的最小差異,以下是常規與多視圖模式的CPU-GPU時間線:

    消耗項:用于PC的實例化立體渲染,PS4實例立體的多視圖擴展,Nvidia的單通道的立體渲染,來自AMD的DirectX 11多視圖擴展,OpenGL ES的多視圖擴展。

    UE4的實現:PC/PS4實例立體和PS4多視圖,基于管線的標準圖形,實例化繪制調用、變換、剔除、剪裁頂點著色器,PS4的小擴展,以減少頂點著色器工作。移動多視圖:繪制調用實例和頂點工作完全由驅動程序完成,利用實例立體的視圖統一系統。多視圖的CPU性能:

    OpenGL ES相關的擴展:

    • GL_OVR_multiview:將gl_ViewID_OVR的使用限制為計算gl_position。
    • GL_OVR_multiview2:沒有限制使用gl_ViewID_OVR,gl_ViewID_OVR可以在片元和頂點著色器階段使用。
    • OVR_multiview_multisampled_render_to_texture:EXT多采樣渲染到紋理的多視圖版本。

    支持多視圖的頂點著色器:

    在應用程序中使用多視圖:

    UE4的材質編譯和渲染流程如下:

    驅動程序支持環境:多個GPU供應商,所有供應商的初始實現中都存在許多驅動程序錯誤,驅動程序更新和最終用戶設備上的可用性之間的長時間延遲,如果已知設備存在問題,會在應用程序初始化期間從著色器中刪除多視圖代碼,以確保驅動程序錯誤不會破壞應用程序。例如Samsung Galaxy S6、Samsung Galaxy S7 Mali (Android M and N)、S7 Adreno (Android N)。

    當前的開發工作流程:

    注視點渲染是4視圖的多視圖:

    使用4視圖的多視圖進行注視點渲染比2視圖的可以減少65%的工作量

    結果對比:

    可以使用Mali Graphics Debugger (MGD)進行詳細深入的多視圖和VR渲染調試:

    Shading of 'Spellsouls': Achieving AAA Quality on Mobile分享了如何在移動端實現3A級的渲染品質,包含PBR、特效、蒙皮、優化等方面的內容。PBR具有逼真的外觀,不同的照明條件,標準的GGX方法很昂貴,標準化的Blinn-Phong。其中標準化的Blinn-Phong計算如下:

    線性顏色空間是PBR的必要條件,但在2018年只有50%的移動設備支持它。在光照上,假設支持4個點光源,用逐光源計算還是Forward+?Forward+已由Spotty用計算著色器支持,但受限于GPU。如何優化?去掉深度的prepass,使用CPU剔除光源,用64x64像素tile,以球體*似光源面積:

    包圍圓在透視不強時可以使用,投影假設攝像頭是正交的,并增加安全半徑。如果投射很強,投影到AABB中。

    在陰影方面,移動對象用動態陰影,環境用靜態陰影。動態陰影用動態的陰影圖,硬件4樣本PCF,靜態陰影直接用烘焙的光照圖。在渲染地形時進行組合,為每個像素評估光照圖顏色和動態陰影顏色。

    color = albedo.rgb * lightmap.rgb;
    color = color * lerp(lightColor, shadowColor, lightmap.a * min(NdotL, dynamicShadow);
    

    特效方面,去掉后處理,粒子系統很昂貴,使用精靈圖集!精靈圖集可以使用運動向量來優化:

    • 從相同的UV坐標讀取當前幀像素和運動矢量值。
    • 使用運動矢量確定要從下一幀讀取的UV坐標。
    • 在當前像素和下一幀像素之間插值。

    左:精靈圖集的顏色;右:精靈圖集的運動向量。

    精靈圖集的運動向量的數據:R、G通道存儲了下一個像素的UV,B通道存儲了UV的縮放因子。

    對于蒙皮,CPU蒙皮占用了2個完整的核心,每幀向GPU上傳網格都會影響性能。可以嘗試GPU蒙皮,無GPU網格上傳、支持實例化、快速。也可以使用基于紋理的矩陣調色板(Texture Based Matrix-palette,TRS)GPU蒙皮:定期采集骨骼TRS樣本,將骨骼TRS烘焙成紋理,3x4個浮點需要3個紋理,每個實例數據1個浮點、U坐標。

    TRS的GPU蒙皮的插值過程:讀取兩個關鍵幀,重建矩陣,插值。其中每個骨骼影響6個紋理讀取!

    需要解決的問題:6個紋理讀取、3個紋理、插值數學。可以使用基于紋理的雙四元數GPU蒙皮(Texture Based Dual Quaternion GPU Skinning)進行優化,其使用雙四元數,2個紋理(第3個縮放紋理可選),雙線性過濾,在低采樣率(15FPS)下看起來不錯。但如果要實現混合動畫,需要雙倍的紋理讀取。

    針對幀率、熱量、電量續航等方面進行了優化。其中對于著色器指令,不使用固定精度,在精度之間進行轉換時要小心謹慎,小心指令數量(No-ops)。

    // 250條指令
    float4x4 sum = (boneMatrices[index0] * weight0) + (boneMatrices[index1] * weight1);
    
    // 180條指令,減少了28%。
    float4x4 matrix0 = boneMatrices[index0];
    float4x4 matrix1 = boneMatrices[index1];
    float4x4 sum = (matrix0 * weight0) + (matrix1 * weight1);
    

    熱量保護(Thermal throttling):

    • 等待設備進入穩定狀態(25FPS)。
    • 確定目標幀率——在這種情況下為30FPS。
    • 設置圖形質量,使FPS比目標高30%——40FPS。
    • 限制幀率至目標–30FPS。

    熱量保護的好處:我們沒有使用設備的所有計算資源,攤銷幀時間峰值,根據GPU確定的質量設置。

    性能跟蹤:分析跟蹤*均FPS、電池耗電,電池耗電與熱量有關。

    MOBILE GAME DEVELOPMENT闡述了移動游戲開發的部分技術,如開發移動游戲的原因、方法、階段、引擎等。2020年的移動端市場份額,達到770億美元,在新冠的陰霾下依然逆勢同比增長13.3%:

    游戲開發過程的多學科性質結合了音頻、視覺藝術、動畫、控制系統、人工智能(AI)和人為因素,使得軟件游戲開發實踐不同于傳統的軟件開發。用于游戲開發和設計的許多方法,敏捷方法是目前管理數字游戲開發最流行和最常用的軟件工程框架,這種開發方法基于迭代和增量方法。生產階段分為幾個小的迭代,主要關注最關鍵的特性,前期制作、制作和后期制作是手機游戲開發的主要階段。

    適合移動游戲開發的引擎包含Unreal Engine、Unity、Monogame、Solar2D、Titanium、Amazon Lumberyard、Cocos2d-x、Haxe、Gideros、Godot、CRYENGINE、Phaser、Defold、Starling等。

    Optimizing Roblox: Vulkan Best Practices forMobile Developers闡述了用Vulkan優化游戲Roblox的技術,加載/存儲操作、subpass、管線屏障、MSAA、Roblox CPU優化、命令緩沖區管理、渲染通道、管線狀態、描述符管理等。下面是立即模式和分塊模式的GPU架構對比圖:

    Vulkan的RenderPass和Subpass的關系如下:

    使用Vulkan實現多pass的延遲渲染圖例如下:

    利用Subpass可以顯著減少額外的內存讀取和寫入:

    利用屏障依賴標記,可以提升各個著色器階段的重疊度,從而減少延遲:

    使得幀時間可以減少56%:

    利用tile內解析MSAA數據(下圖左),可以顯著降低內存的讀取和寫入(分別降低261%、440%!!)。

    優化繪制調用:Vulkan在公共渲染界面上實現,我們如何通過合理的努力獲得最大的性能?關注穩定狀態的性能,緩存所有易于緩存的內容。采用常規幀結構,最大限度地減少繪制調用,在易用性和性能之間找到折衷方案,盡可能優化實現、線程友好的實現,允許每個線程單獨錄制繪制調用。

    // 1. Command buffer management
    DeviceContext* ctx = device->createCommandBuffer();
    PassClear passClear;
    passClear.mask = Framebuffer::Mask_Color0;
    // 2. Render passes
    ctx‐>beginPass(fb, 0, Framebuffer::Mask_Color0, &passClear);
    // 3. Pipeline state
    ctx‐>bindProgram(program.get());
    // 4. Descriptor management
    ctx‐>bindBuffer(0, globalDataBuffer.get());
    ctx‐>bindBufferData(1, &params, sizeof(params));
    ctx‐>bindTexture(0, lightMap, SamplerState::Filter_Linear);
    // 5. General optimizations
    ctx‐>draw(geometry, Geometry::Primitive_Triangles, 0, count);
    ctx‐>endPass();
    device->commitCommandBuffer(ctx);
    

    命令緩沖區管理:看似簡單…createCommandBuffer() => vkAllocateCommandBuffers,commitCommandBuffer() => vkQueueSubmit…但實際上很復雜。每個線程都需要一個單獨的VkCommandPool進行分配,如果從VkCommandPool分配的命令緩沖區正在運行,則不能使用VkCommandPool,vkAllocateCommandBuffers不是免費的,vkFreeCommandBuffers并不總是回收命令內存,vkQueueSubmit可能很昂貴。命令池:createCommandBuffer()在關鍵部分下竊取(或創建)VkCommandPool,我們從不釋放命令緩沖區,并重用分配的命令緩沖區,批量命令緩沖區提交,commitCommandBuffer()將命令緩沖區添加到幀列表并返回池,一個vkQueueSubmit,位于submitCount=1的幀末尾。命令池回收:錄制幀后,從全局池中刪除所有具有掛起命令緩沖區的池,幀完成后,將所有池放回全局池,別忘了運行vkResetCommandPool,它會自動重置所有分配的命令緩沖區,并將其置于就緒狀態。

    常規優化:驅動比典型的GL驅動輕量得多,暴露了以前微不足道/不可察覺的事情!除非需要,否則不要調用vk*函數,緩存所有易于緩存的內容,過濾冗余狀態綁定。積極消除緩存未命中,減少抽象中的分配和間接操作,例如,使用的GeometryVulkan類似于OpenGL VAO–struct的所有幾何體狀態。通過vkGetDeviceProcAddr獲得的指針調用大多數函數,volk loader為我們做這件事,可獲得些許性能的提升。

    結果:與GLE相比,所有供應商的CPU性能提高了2-3倍,端到端渲染幀,真實內容。移動測試關卡,840次繪制調用、單核、在2.4 GHz Cortex-A73、Mali-G72,GLES花費38毫秒,Vulkan單核:13毫秒,良好的多核擴展功能!小心big vs LITTLE core。


    14.5.3.4 并行技術

    計算機硬件越來越并行,需要理解并發性才能利用并行性,需要了解硬件和內核才能理解并發編程!Parallelism and Concurrent Programming詳盡地介紹了現代并行計算硬件、并發編程技術,以及如何將其應用于構建高性能游戲引擎。

    并發可以定義為共享數據文件的轉換,由多個控制流共享進程、線程、纖程等,有多個共享數據的讀取和/或寫入。數據“文件”可以是任何東西,線程之間共享的全局布爾變量、共享隊列,數據文件可以存放在任何地方,如多線程進程中的虛擬內存空間、多個進程在一臺計算機內共享虛擬內存頁、GPU和CPU共享物理RAM、兩個進程之間的管線、存在于多臺計算機可訪問的網絡驅動器上的文件。如果數據沒有被共享,那就不是并發性,它只是“同時”計算:


    并發意味著在共享數據上運行的多個控制流,并行性是指多個硬件組件同時運行,即使在單核CPU上,并發也是可能的,例如先發制人的多任務處理。同樣,并行硬件甚至可以提高單線程代碼的性能,隱式并行(流水線或超標量CPU架構)。

    并行分為隱式并行和顯式并行,當前的硬件架構基本都是顯式并行。顯式并行將并行計算硬件的存在暴露給:程序員和/或編譯程序,自2002年起用于消費電子產品,包含多處理器計算機、多核CPU、x86(SSE)中的SIMD矢量處理單元、多核GPU(SIMT),顯式并行支持并發。

    指令集架構(Instruction Set Architecture,ISA):每個CPU都提供不同的ISA,ISA定義:CPU識別的一組操作碼、寄存器的數目及名稱、CPU支持的尋址模式、公開了硬件的一些細節:CPU是否包含FPU?VPU?I/O是如何完成的?內存映射?基于寄存器?它能在每個時鐘上發出多條指令嗎?(VLIW)是否支持特權模式?有多少個保護環?其中CPU的執行上下文如下圖:


    內存緩存層次架構通過利用時間和空間的局部性減少了*均內存訪問延遲。時間局部性:數據往往在短時間內被重復訪問,如果一個程序訪問地址x,它很有可能在不久的將來再次訪問地址x。空間局部性:數據往往是按順序或按塊訪問的,如果程序訪問地址x,它很有可能也會訪問地址x+n(對于較小的|n |)。

    緩存的工作原理是將主內存分成幾行,典型的緩存行是64字節或128字節。主RAM的行可以讀入緩存,一旦進入緩存,CPU就可以更快地訪問數據。

    一旦緩存中有一行,我們如何知道它來自哪個內存行?可根據行索引(64字節緩存:地址 & 0x3F,128字節緩存:地址 & 0x7F)和標簽(64字節緩存:地址 >> 6,128字節緩存:地址 >> 7),將標簽與每個緩存行一起存儲,以跟蹤其在內存中的原始位置(下圖)。

    當CPU讀取數據項(單字節或更大)時:轉換為行索引的地址,內存控制器檢查一級緩存:緩存是否已經包含該行?如果命中(Hit),則從該行獲取數據,如果未命中,請從下一級緩存(L2)中提取……刷新并重復,直到達到主存儲器。在多核系統上,讀請求也可以由同一級別的其它核來完成。

    當CPU寫入數據項(單字節或更大)時:轉換為行索引的地址,內存控制器檢查一級緩存:緩存是否已經包含該行?如果命中,將項目寫入行中,標記行已修改(在多核機器中,這會變得更復雜…)。如果未命中,則從L2、L3……主存儲器中提取行,寫入并標記已修改,寫入緩存行不一定立即寫回主RAM。下次讀取緩存線或使緩存線失效時觸發寫回,直寫操作可以繞過緩存。

    上面描述的是直接映射緩存,內存中的每一行都映射到緩存中的一行可能存在沖突(例如地址0x80、0x100和0x180)。全關聯緩存是內存中的任何行都可以放在緩存中的任何位置,需要對標記進行線性搜索才能在緩存中找到行。n路集關聯緩存是內存中的每一行映射到緩存中的n行,兩全其美:將線路沖突減少n倍,有限搜索(只搜索路徑,不搜索整個緩存)。例如2路集合關聯緩存,內存中的每一行映射到緩存中的兩行:

    當緩存滿了會發生什么?必須刪除以前的數據才能騰出空間,緩存行替換策略:先進先出(直接映射緩存中的唯一選項),NMRU(不是最*使用的):每組1位,LRU(最*最少使用):n>2時成本較高,LFU(使用頻率最低),偽隨機。

    那么我們為什么要關心緩存呢?了解緩存是一種優化工具,構造數據以避免過度緩存未命中,將數據打包到非稀疏數組中,避免在內存中跳轉。

    該文還詳細介紹了內核、進程、線程、虛擬內存等概念。其中虛擬內存是每個進程都有的私有內存“視圖”,用戶空間程序根據虛擬地址執行所有內存訪問,CPU和內核協同工作,以便在運行時將虛擬地址映射到物理地址。


    線程和進程的關系:

    線程調度的幾個狀態:

    線程上下文切換:


    進程上下文切換:


    早期的CPU是串行(serially)執行的,一次一條指令,CPU內部的組件沒有很好地分開,執行指令時,許多組件處于空閑狀態。現代CPU是流水線的(pipelined),每個指令都經過明確定義的階段,每個階段對應于CPU內核中的一個組件,組件之間的清晰分隔,通過允許多條指令同時“運行”,可以讓所有組件保持忙碌,每個都在核心中使用不同的組件。

    不同的CPU對其階段的定義不同,從四到五個階段,在深度流水線CPU中多達30多個階段。基本階段:

    • Fetch:從內存中提取指令字(F)。
    • Decode:將指令解碼為操作碼和操作數(D)。
    • Register:寄存器訪問(R),假設D和R是組合的(D/R)。
    • Execute:在ALU上執行指令(E)。
    • Memory:內存讀/寫(M)。
    • Register writeback:寄存器寫回,例如將結果存儲在寄存器中(W)。

    為了讓每個階段保持忙碌,每個時鐘發出一條新指令:

    流水線是指令級并行(ILP)的一種形式,衡量性能的指標有吞吐量(每單位時間失效的指令數)和延遲(使一條指令失效所需的時間長度)。

    停頓(stall)被定義為并非所有階段都保持忙碌的情況,換句話說,由于某種依賴關系,無法發出下一條指令。CPU管線中有三種依賴關系:

    • 數據相關性:一條指令中使用的寄存器在下一條指令中再次使用。
    • 分支依賴性:指令的操作取決于前一條指令的結果(例如BZ)。
    • 資源依賴性:某些類型的指令只能在某些硬件上執行(例如整數ALU)。

    依賴是指令流的屬性,依賴關系可能導致危害——對CPU管線的影響(停頓):

    • 數據依賴導致數據危害:發出本指令需要之前指令的結果。
    • 分支依賴導致控制危害:需要在條件表達式的結果已知之前預測分支。
    • 資源依賴導致結構危害:想發出(例如整數加法),但ALU正忙。

    數據危害示例。

    OOO執行允許CPU對指令A、B、C和D(取自add之前)進行重新排序,以便它們位于add和mul指令之間。

    內存延遲隱藏:請注意,由于內存延遲,也可能發生停頓,mov r3, [r1+8]可能需要4、40或400個周期!(L1、L2、主RAM)。OOO執行還有助于隱藏內存延遲,在等待內存控制器響應時,在延遲槽中執行其它獨立指令。多線程是另一種隱藏內存延遲的方法,當一個線程正在等待內存時,可以安排其它線程使用CPU內核。

    當遇到條件分支指令時,CPU應該做什么?問題發生時尚不清楚的情況的結果,管線深度可達10+級,因此,條件分支引入了10+周期停頓,這被稱為分支損害(branch penalty),控制危害,這是原始數據危害的特例。

    分支損害問題的解決方案:

    • 軟件:
      • 循環展開以增加運行長度(如果測試較少)。
      • 重新排列代碼以最小化“分支”。
    • 硬件:
      • 延遲槽(Delay Slot)。選擇stall期間要運行的指令(OOO執行)。
      • 從兩個分支中選擇一個,然后進行預測性地執行。如果預測是正確的,就不會延遲。如果預測是錯誤的,則刷新管線,返回中間結果,然后在正確的分支上重新開始。

    CPU應該如何猜測?簡單的想法:假設向后的分支總是被執行,向前的分支永遠不會被執行。for循環中非常常見的模式:

    for (i = 0; i < count; ++i)     
    {
        // do work...
        if (special_case)
            break; 
        // forward branch (rare)    
    } // backward branch (common)
    

    另一個想法是依靠人工標記if語句:

    for (i = 0; i < count; ++i)
    {
        // do work...
        if (UNLIKELY(special_case))
            break; 
    }
    
    // gcc definitions:
    #define LIKELY(x)       __builtin_expect((x), 1)
    #define UNLIKELY(x)     __builtin_expect((x), 0)
    

    另一個想法是:在CPU芯片上包含分支預測邏輯,記錄哪些分支被取走的歷史。邏輯可能會變得相當復雜,一些CPU可以檢測像Y,N,Y,N,Y,N等模式…顯著增加晶體管數量和電路復雜性,預測執行需要將計算結果臨時存儲。

    還有一個想法:根本不要分支!相反,始終執行分支的兩側,但在每條指令上標記與其關聯的條件(預測),當條件(預測)為真時,CPU才實際提交謂詞指令的結果,這種方法稱為預測(predication)。更簡單的形式:部分預測(Partial Predication),條件移動,條件選擇指令。

    條件選擇,例如PowerPC的fsel和isel指令:isel RR, RA, RB, predicate_code,如果預測位為1,則移動RA -> RR,如果預測位為0,則移動RB -> RR。

    完整的預測如下:

    超標量(Superscalar):

    管線化和超標量的CPU對比如下:


    弗林分類法(Flynn’s Taxonomy)在1966年由斯坦福大學的邁克爾·J·弗林提出,將并行性分為指令級并行(ILP)和數據級并行(DLP):

    • 單指令單數據(SISD)。
    • 多指令多數據(MIMD)。
    • 單指令多數據(SIMD)。
    • 多指令單數據(MISD)。
    • 單指令多線程(SIMT),用于GPU。

    串行、超線程、多核CPU的架構對比如下:

    GPU架構是SIMD和MIMD的混合體,SIMD內核將單個指令流(線程)并行應用于多個數據“通道”,為了隱藏延遲,許多線程通過時間切片共享一個SIMD內核,Nvidia調用此單指令多線程(SIMT)。

    此外,計算機集群:多臺同質計算機,機架安裝在附*,通過快速本地網絡連接。網格計算:松散耦合的異構計算機(例如SETI@home)。云計算:按需計算和存儲,軟件即服務(SaaS),靈活、可擴展、經濟高效,只使用你需要的,隨用隨付。

    并發編程比順序編程困難得多,在順序程序中運行良好的許多假設、技術和數據結構在并發情況下會崩潰,新種類的bug:新問題、新的限制。需要以數據為導向的思維方式,需要了解目標硬件,同時數據訪問是并發問題,解決這個問題需要新技術!

    競爭條件是軟件(或硬件)系統的行為取決于事件的順序或時間的情況。并非所有的比賽條件都有不利影響,例如在單幀中,武器射線投射的處理順序無關緊要,一個關鍵的競爭條件(critical race condition)是導致不正確的行為。數據競爭是一種特定的競爭條件:在共享數據上運行的兩個或多個指令流(線程),根據具體的時間,讀取或寫入共享數據的結果會變得不一致,數據競爭是并發問題!

    細粒度的數據競爭:增量競爭可以在多核機器上以真正的并行性發生,但原因稍有不同。現在,同一條指令可能會在兩個或多個線程中同時執行,指令需要多個時鐘周期才能運行,所以指令之間的重疊可能是完整的,也可能是部分的。

    粗粒度的數據競爭:讀取/寫入共享磁盤文件的多個進程之間也可能發生數據競爭:1個讀1個寫、1個寫多個讀、多個寫,例如,兩個進程將一個32kib的塊寫入一個共享文件,進程A打開文件,寫入其第一個4kb塊,進程B然后打開文件,寫入它的第一個4kb塊,與此同時,A繼續寫入導致數據損壞!

    增加共享計數器的問題的關鍵是什么?增量操作可能會中途中斷,為了避免數據競爭,我們需要一種使某些關鍵操作原子化(不可中斷)的方法。使用這些原語稱為基于鎖的編程,可靠但也昂貴(互斥體涉及內核調用),也可以編寫無鎖算法,要想做對要困難得多,可以比基于鎖的方法執行得更好,更容錯。

    文中還涉及了原子(atomic)、互斥量(mutex)、臨界區(critical section)、信號量(Semaphore)、范圍鎖(Scoped Lock)、條件變量(condition variable)、線程安全的函數等技術。其中線程安全函數是一種在內部鎖定互斥體的函數,以提供原子操作的假象。雖然方便,但是可能會導致程序員傾向于長時間持有鎖。這樣的函數是不可重入的:a()調用b()調用c()…調用a()會導致死鎖,因為a()已經持有鎖!緩解線程安全函數問題的想法:保持鎖的時間最短,目標是盡可能接*“無鎖”。

    // 不良的代碼
    void NotGreat(args)
    {
        g_mutex.lock();
        // do huge amount of work...
        g_mutex.unlock();
    }
    
    // 良好的代碼
    void Better(args)
    {
        g_mutex.lock();
        WorkItem workItem(args);
        g_workQueue.enqueue(workItem);
        g_mutex.unlock();
    }
    
    void ProcessWorkItems()
    {
        for (workItem : g_workQueue)
            // do huge amount of work...
    }
    

    如果操作需要以原子方式完成,請編寫一個不安全版本,然后從安全版本調用它,這支持可重入調用或使用可重入鎖:

    // 無鎖的函數(不安全)
    void DoWorkNoLock()
    {
        if (conditionA)
            return;
    
        for (auto x : collection)
        {
            if (!IsValid(x))
                return;
            DoStuff(x);
        }
    }
    
    // 有鎖的函數(安全)
    void DoWork()
    {
        g_mutex.lock();
        // 調用無鎖的函數
        DoWorkNoLock();
        g_mutex.unlock();
    }
    

    此外,文中還涉及加載鏈接/存儲條件、LL/SC和緩存行、屏障、內存屏障、原子變量、內存順序、自旋鎖(Spin Lock)、讀寫鎖(Reader/Writer Lock)、可重入鎖(Reentrant Lock)等技術。

    基于鎖的并發是在共享內存系統中同步線程的最簡單、最可靠的方法。但是,它很容易出現單線程程序中不會出現的問題(使用無鎖或無等待算法也可以避免):死鎖(Deadlock)、活鎖(Livelock)、饑餓(Starvation)、優先級反轉(Priority inversion)等。

    還有餐飲哲學家問題(Dining Philosophers Problem):五位哲學家圍坐在餐桌旁,桌子上有五把叉子,每個哲學家之間各有一把,每個哲學家可以處于兩種狀態之一:思考或吃。為了進入進食狀態,哲學家必須獲得兩只叉子,一只手一把,一有食物,就可以抓住任何一個叉子,離開進食狀態時,他放下兩個叉子,哲學家們從不互相交談。問題陳述:設計一種行為模式(并行算法),讓所有哲學家在不餓的情況下無限地思考和進食。天真的解決方案:總是先抓住左叉子,然后抓住右叉子,如果每個哲學家都同時抓住左叉子…那么他們誰也抓不到正確的叉子!導致一種被稱為死鎖的情況。


    我們可以通過繪制連通圖來解釋死鎖,節點代表線程(\(T_i\))和資源(\(R_j\)),有向邊表示鎖,T -> R:線程T正在等待資源R,R -> T:線程T持有/鎖定的資源R。資源圖包括\(T_s\)\(R_s\),等待圖僅包括\(T_s\),等待圖中的循環=死鎖。

    資源圖和等待圖。

    活鎖(livelock)是一種線程可以取得局部處理,但無法取得全局處理的情況。在多線程程序中,當線程試圖通過后退和重試來避免死鎖時發生,例如,它們都會退出,然后在相同的時間后重試。換句話說,系統的狀態可以提高,但沒有線程可以做任何真正的工作,線程將所有時間都花在后退和重試鎖定資源上。

    活鎖示例。

    死鎖問題的解決方案總是需要消除四個必要和充分條件中的一個或多個:

    • 消除互斥。例如,為每個線程提供自己的資源。
    • 取消等待。例如,一次不要持有超過一把鎖,例如,無鎖算法。
    • 允許搶占鎖。例如,內核或主管線程可以檢測死鎖,并通過搶占鎖來“修復故障”,迫使線程重新嘗試。
    • 移除循環等待。例如,設定全球排序/資源優先級。

    優先級反轉:如果低優先級線程L獲得了資源a的鎖,但在等待資源B時進入睡眠狀態,會發生什么?高優先級線程H將無法獲得資源A上的鎖,因此,線程L的優先級似乎高于線程H的優先級,它們的優先次序被顛倒了。如果中等優先級線程M在持有資源a的鎖時搶占了線程L,也可能發生優先級反轉,線程L鎖定資源A,線程M搶先線程L,使L進入睡眠狀態,線程H再次無法獲得資源a上的鎖。

    優先級反轉的可能后果:

    • 沒有。如果線程L快速釋放資源,線程H將能夠運行,而反轉可能不會被注意到。
    • 感知到的系統放緩。如果線程H長時間缺乏對資源的訪問,系統可能會顯得遲鈍(例如,如果線程H響應用戶輸入)。
    • 系統故障。線程H可能會因無法訪問資源而導致系統故障(例如,錯過實時截止日期)。

    游戲引擎中的并發從初級到高級有以下幾種形式:



    節點圖可以有依賴,每個依賴項都會創建一個同步點,同步點是一個時間點,在這個時間點上,多個并行任務必須同步,以便進行排序。第一個要完成的任務必須等到最后一個任務完成,每個同步點都可能導致系統資源使用效率低下(硬件空閑時間)。

    并發地更新游戲對象:讓我們將這些想法應用到游戲對象更新中,我們如何同時更新游戲對象?大多數游戲引擎根本解決不了這個問題,因為它是以單線程方式編寫的遺留代碼,因為這很難!游戲對象通常相互依賴:

    處理游戲對象依賴關系的一種方法:水體化(Bucketed)更新。

    enum Bucket
    {
        kBucketVehiclesPlatforms,
        kBucketCharacters, 
        kBucketAttachedobjects,
        kBucketCount
    };
    
    void UpdateBucket( Bucket bucket) 
    {
        // ...
        
        for (each gameObject in bucket)
            gameObject.PreAnimUpdate(dt); 
    
        g_animationEngine.CalculateIntermediatePoses(bucket, dt);  
    
        for (each gameObject in bucket) 
            gameObJect.PostAnimUpdate(dt);
        
        g_ragdollSystem.ApplyskeletonsToRagDolls(bucket); 
        g_physicsEngine.Simulate(bucket, dt) // ragdolls etc.  
        g_collisionEngine.DetectAndResolveCollisions(bucket, dt);
        g_ragdollSystem.ApplyRagDollsToSkeletons(bucket);
        g_animationEngine.PinalizePoseAndMatrixPalette(bucket);
        
        for (each gameObject in bucket) 
            gameObject.FinalUpdate(dt);
        
        // ...
    }
    
    void RunGameLoop() 
    {
        while (true)
        {
            // ... 
            
            UpdateBucket(kBucketVehiclesAndPlatforms);
            UpdateBucket(kBucketCharacters);
            UpdateBucket(kBucketAttachedobiects);
            // ... 
            
            g_renderingEngine.RenderSceneAndswapBuffers();
        }
    }
    

    并發操作總是涉及延遲,同時思考需要我們將每個操作分解為調用和響應(完成)。

    系統的吞吐量要求是什么?通常可以用延遲換取吞吐量,延遲增加 -> 增加吞吐量,如果客戶端可以等待結果(更高的延遲),那么:鎖的爭用減少了,少等待,更少的同步點,更高的吞吐量。

    文中還涉及作業系統并行、無鎖隊列、非阻塞保證、原子快照對象、SIMD與GPU編程等等技術,可謂是并發編程的大而全。其中GPU編程模型:編寫幾何體、頂點、像素著色器(用于圖形)或編寫計算著色器(GPGPU),對大量數據執行單個相對簡單操作的循環適用于GPU,考慮一個循環,它迭代并處理N個數據項。典型工作流程:編寫CPU循環,測試,讓它工作;將循環體轉換為GPU內核;GPU將在一個或多個CU內的SIMD單元上并行運行循環體N次。程序員編寫“單線程”內核,啟動它在GPU上執行,單線程內核由編譯器/GPU矢量化,因此它可以在CU中的SIMD上并行運行,每個“lane”都被稱為一條線程,在SIMD上運行的一組線程稱為波前(wavefront)。

    CPU和GPU編程模型對比。

    GCN Wavefront:Radeon GCN的波前有64線程(lane)寬,但我們知道每個SIMD實際上只有16線程寬,這是怎么回事?答案:循環指令處理!一個16 lane的SIMD在4個時鐘周期內執行由64 lane組成的波前,通過運行4組16個線程,每個時鐘周期一個。

    總結:SIMD單元在數據的 4、8或16 lane上并行執行單個指令流,可以循環處理更多通道(例如,通過循環處理4組16個線程,每個SIMD 64個線程)。計算單元(CU)有多個SIMD,每個CU時間在多個線程之間切片以隱藏停頓,使用預測處理分支,循環是可能的,但整個wavefront / warp必須循環。GPU有多個CU。


    傳統上,CPU上的SIMD矢量化是使用特定于*臺的內部函數完成的。有了英特爾ISPC,這已經成為過去。基于行業標準LLVM編譯器,英特爾ISPC是一種易于編寫矢量代碼的語言。它為許多不同的指令集生成高性能矢量代碼——SSE、AVX、AVX-512甚至ARM NEON。它使用簡單,易于與現有的代碼庫集成,外觀和行為非常像C。

    Intel? ISPC in Unreal Engine 4: A Peek Behind the Curtain闡述了英特爾ISPC現在在虛幻引擎4中的實現,可以用了解英特爾如何與Epic Games的合作,優化物理和動畫系統,以及任何虛幻開發者如何使用它讓游戲變得更好。

    利用并行性對于在Chaos和動畫中獲得最佳性能至關重要,即使在現代高端系統上也是如此,任務并行性包含多線程、多核,SIMD并行性有SIMD向量指令,自動向量化很難控制,如果每個人都在學習如何編寫向量內部函數,就沒有時間去創建任何東西,讓你不用成為一個忍者程序員就可以輕松獲得所有的失敗。這些難題可以使用英特爾的SPMD程序編譯器(ISPC)來解決!Ice Lake U(第10代)四核CPU,黃色圓圈代表神奇發生的CPU執行單元!

    ISPC是什么?英特爾SPMD程序編譯器,SPMD==單程序、多數據編程模型,它是一個編譯器和一種用于編寫向量(SIMD)代碼的語言。針對許多SIMD的基于LLVM的開源語言和編譯器架構,為許多向量ISA生成高性能向量代碼,如SSE/AVX/AVX2/AVX 512/NEON…(實驗),這種語言看起來很像C語言,簡單易用,易于與現有代碼庫集成,C函數調用。

    為什么是ISPC?使用SIMD快速加速現有C++代碼,內部函數是硬件加速的,并且是特定于指令集的,添加AVX意味著另一種排列。Unreal在*臺抽象中使用SSE2本質,集成到引擎中有利于所有基于它構建的游戲,適用于虛幻的任何地方(Win、Mac、Linux、PS4、Xbox、ARM),通過其它引擎集成的成熟技術,如Embree Lightmass(靜態照明)、ISPC紋理壓縮器(BC6H/BC7/ASTC)。

    ISPC編程模型:ISPC不是“自動矢量化”編譯器,不會通過分析和轉換標量循環來生成向量代碼,更像是一個所見即所得的矢量化編譯器。程序員告訴ISPC什么是向量、什么是標量,向量類型是顯式的,SIMD向量和任務編程模型的清晰分離,適用于現有的C/C++內存分配、數據緩沖區。

    ISPC看起來很像C,所以很容易閱讀和理解,代碼看起來是順序的,但并行執行,輕松混合標量計算和向量計算,使用兩個新的ISPC語言關鍵字uniform和varying進行顯式矢量化,同樣,ISPC不是一個自動矢量化編譯器。

    export void rgb2grey(uniform int N, uniform float R[], uniform float G[], uniform float B[], uniform float grey[])
    {
        foreach(i=0 ... N)
        {
            grey[i] = 0.3f*R[i] + 0.59f*G[i] + 0.11f*B[i];
        }
    }
    

    uniform用于標量數據,結果在標量寄存器中(eax、ebx、ecx等),所有SIMD線程共享相同的值。Varying用于向量數據,結果在SIMD向量寄存器中(XMM、YMM、ZMM等),默認是Varying,每個SIMD線程都有一個唯一的值,寬度取決于目標。

    ISPC包含內部變量、控制流、數組、結構體等概念和關鍵字。在內存與性能方面,ISPC在生成的代碼非常棒,但它不能重新排列代碼,不能加速內存訪問,所以數據布局很重要,數據需要在緩存中,并且需要在正確的布局中。收集/分散指令可能很痛苦,首選SoA或AoSoA內存布局,它們將生成向量裝載/儲存,Mike Acton,面向數據的設計和C語言++。

    ISPC提供了豐富的操作標準:邏輯運算符、位操作、數學、夾緊與飽和算法、超越運算、RNG(不是最快的!)、遮罩/跨線程操作、減少,還不止這些!ISPC提供了此處未涉及的其它功能:指針和內存分配、AoS到SoA輔助函數、類似C++的引用、結構(+、--、*、/、<<,>>)的二進制運算符重載、內置任務系統、多個數學庫(標準、快速、SVML、系統)語言功能。

    虛幻的ISPC集成:ISPC在4.23版的《虛幻》中提供,在4.25中增加了控制臺支持,用于Chaos物理和動畫系統,支持自定義使用。在build.cs中包括ISPC模塊,將ispc文件添加到項目中,include生成的C++頭,虛幻構建工具處理其余的問題。

    什么時候在虛幻中使用ISPC?適用于密集的計算負載,沉重的數學負擔,比如物理交叉測試、布料或CPU頂點變換。最好使用連續內存加載、操作、存儲,如虛幻的TArray。最好操作之間沒有數據依賴關系,與并行和批處理結合使用時特別有用。

    ISPC對Chaos的好處:ISPC提供了一個類似于著色器語言的簡單界面,用于使用SIMD進行性能優化,跨*臺工作,無需特定于*臺的內在代碼,在Fortnite和Destruction中都被Chaos所積極使用。

    剛體蒙皮:使用輸入頂點緩沖區生成變換頂點,并根據骨骼陣列進行變換。骨骼經常重復,使用foreach_unique減少變換操作所需的數量,短向量數組被壓縮,使用aos_to_soa消除聚集。

    包圍盒:定制減少,問題是,對于要求和的大型包圍盒數組,有時求和會是負數,做reduce_min和reduce_max似乎是正確的,如果一個盒子無效呢?然后將鉗制為零(默認初始化),而不是希望Foreach_active的值,運算符+序列化并處理這種情況。

    場景查詢:同時處理列表中的多個元素,可以直接從ispc調用cpp代碼,根據列表的大小,性能可能會有所不同。伸縮性很好,但確實有開銷,所以更多的元素做得更好。

    Rigid Chains:簡單的數學和繁重的計算,將所有數據轉換為uniform,以便獲得最佳的性能,與原生指令相比沒有成本,總計提高15到20%的角色蒙皮速度。

    UE4中的動畫:Unreal Engine目前支持8個*臺,其中10個帶有下一代控制臺,在所有目標中維護SIMD代碼是一項巨大的任務,動畫團隊歷來沒有時間去完成,性能改進的重點是高級算法更改或多線程處理,以前,動畫中唯一的矢量化來自UE4中的通用庫代碼。

    Fortnite中的動畫:Fortnite正在推動整個引擎的角色性能改進,大量角色(BR中有100個),LTM(如Team Rumble/50v50 tend)傾向于接*的大團隊玩家,妨礙正常性能的“技巧”。隨著新游戲玩法(如NPC)的推出,對動畫的性能要求不斷提高。如果可以運行2倍的角色,那會解鎖什么新游戲。

    ISPC:性能對于動畫來說總是至關重要的(更復雜的角色,屏幕上同時出現更多的角色等等),能夠一次編寫代碼并達到所有目標*臺是一個巨大的勝利。讓團隊專注于構建動畫技術,而不是維護相同邏輯的N個版本。

    在UE4動畫中使用ISPC,首先關注運行時熱點:姿勢混合、附加姿勢轉換、規范化旋轉、解壓動畫數據、構建組件空間變換、為渲染器準備骨骼變換。姿勢混合是ISPC的理想場景,給定兩個骨骼變換數組和一個權重,創建第三個變換數組,之前的優化工作意味著已經在運行連續的轉換數組,在測試中,可以看到ISPC的性能提高了*兩倍。

    解壓縮:在UE4中,解壓是動畫性能成本的很大一部分,引擎中支持的每種壓縮格式都有為其編寫的ISPC代碼。在測試中觀察到,根據使用的壓縮類型,性能提高了1.5倍到2倍。



    14.5.3.5 特殊技術

    Math for Game Programmers: Voxel Surfing詳細地介紹了體積表達、體素的知識、應用及優化。

    對于任意連續的函數\(f(x, y, z)\),隱式地將體積定義為\(f(x, y, z) > 0\),表面是\(f(x, y, z) = 0\)的水*集。

    只需要一個連續的函數,任意的代數函數、有向距離場(CSG樹,在網格三線性采樣)、密度函數(在網格三線性采樣)。

    使用密度(Density)要容易得多(局部更改),但在距離場上的一些有用操作(如放大、縮小體積、更高質量的漸變計算)上會失敗,可以將密度視為距離場,夾緊距離約為一個采樣單元格。

    將隱式曲面或參數化網格體素化的過程:1、在網格上采樣。2、*似每個單元格中的表面。3、確保表面與單元邊界對齊。體素化的理想特征是:易于實現、局部獨立、*滑、自適應/適合LOD、最小化三角形條形、保留銳利和薄的特征。

    對于簡單的立方體,在每個網格單元的中心采樣\(f(x, y, z)\),在具有不同符號的單元格之間繪制一個面。

    這種表達方式在體素化的理想特征的優劣如下表:

    易于實現 局部獨立 *滑 自適應LOD 最小化三角形 銳利
    ++ + - - + - -

    步進立方體(Maring Cube):在每個單元格的角落采樣\(f(x, y, z)\),在三角形拓撲中使用角的符號,沿著邊緣在插值的0點處定位頂點。

    這種表達方式在體素化的理想特征的優劣如下表:

    易于實現 局部獨立 *滑 自適應LOD 最小化三角形 銳利
    + + + - - - -

    超體素(transvoxel)算法:在每個單元格的角落采樣\(f(x, y, z)\),允許細分一次單元格的邊(以縫合相鄰的LOD級別),為三角形拓撲使用采樣點的符號,沿邊在插值的0點處定位頂點。

    Transvoxel是一種允許行進立方體跨越不同LOD級別的方法。總共71種拓撲方式,用于處理任意邊組合的細分。

    這種表達方式在體素化的理想特征的優劣如下表:

    易于實現 局部獨立 *滑 自適應LOD 最小化三角形 銳利
    + + + + - - -

    雙重輪廓(Dual Contour):在每個單元格的角落采樣\(f(x, y, z)\),在每個邊交叉點位置采樣\(f'(x, y, z)\),在輪廓上的每個單元格內找到一個理想點,連接相鄰單元格的兩點(支持多個LOD分辨率)。

    這種表達方式在體素化的理想特征的優劣如下表:

    易于實現 局部獨立 *滑 自適應LOD 最小化三角形 銳利
    - - + + + + ~

    雙重行進立方體(Dual Marching Cube):在精細網格上采樣\(f(x, y, z)\),找出誤差最小的點(QEF),如果誤差 > $\varepsilon $,則在該點細分八叉樹。重復前面的步驟,直到索引的誤差 < \(\varepsilon\)。構造此八叉樹的拓撲對偶(topological dual),當成雙重輪廓曲面細分之。

    這種表達方式在體素化的理想特征的優劣如下表:

    易于實現 局部獨立 *滑 自適應LOD 最小化三角形 銳利
    - - + + + + +

    立方體行進正方體(Cubical Marching Square):構建一個帶有誤差細分的八叉樹(類似于DMC),對于任何體素(在任何八叉樹級別):展開體素,分別觀察每一側;使用行進立方體創建曲線,如果出現誤差,則進行細分;將兩邊折疊在一起,形成三角形。

    易于實現 局部獨立 *滑 自適應LOD 最小化三角形 銳利
    - + + + + + +

    Windborne的體素化概覽:\(F(x, y, z)\) = 合成的、軸對齊的密度網格,每個塊(chunk)都有重疊的特征列表,在計算時,使用布爾運算累積到塊。

    組合時,每個特征都是一個層,每一層要么減去,要么疊加,Alpha混合密度:\(\alpha_a + \alpha_b (1-\alpha_a)\)

    除了組合,還有減去、疊加、并集、輪廓指示器、網格密度(可用于動態光照,如光流、AO)、暴露參數、利用特征等等操作或應用。具體可參閱原文。

    Aggregate G-Buffer Anti-Aliasing in Unreal Engine 4由Nvidia呈現,講述了在UE4實現的用聚合GBuffer來達成抗鋸齒的特殊技術。

    從左到右:4xTAA、4xSSAA、4xAGAA。其中AGAA快了1.7倍。

    TAA大幅提升質量[Karis 2014],然而存在鬼影、閃爍、過度*滑視覺特征(如鏡面反射高光),需要與SSAA結合以獲得最佳質量。

    AGAA(Aggregate G-Buffer Anti-Aliasing,聚合幾何緩沖抗鋸齒)將著色率與G-Buffer采樣數量分離,使用動態預過濾,以4x或8x MSAA/SSAA進行光柵化,光照最大為2倍/像素。

    預過濾、超采樣、后過濾的目標是捕捉或再現亞像素細節的外觀,用于過濾各種幾何比例的各種工具。其流程如下:

    AGAA管線的高級別視圖:

    光照預過濾聚合:目標是聚合足跡上的*似*均反射率,為每個聚合獨立預過濾著色函數的輸入,靈感來自紋理空間和體素空間預過濾方案,屬性去相關(decorrelation)假設,遠距離場(far field)假設。逐聚合統計信息:*均多數著色參數、建立法線分布函數(NDF)、陰影的*均衰減。

    遠距離場假設。

    聚合創建:分簇(cluster)。目標是將相關屬性導致的著色誤差降至最低,距離度量有光照模型、法線、深度/位置。其中分簇樣本可以跨圖元 + 離散的表面。

    接著是聚合。一旦有了樣本到聚合映射,剩下的步驟就是使用超采樣G緩沖區中的數據,執行每個聚合的著色屬性過濾。總而言之,對大多數著色屬性進行*均,并將每個樣本的法線和粗糙度信息轉換為與BRDF模型相關聯的法線分布表示,使得可以線性組合它們,并*似它們在聚合足跡上的方差。

    在UE4中,有多個可能的渲染路徑來執行著色,其中之一是分塊延遲著色。文中將引擎配置為盡可能使用分塊延遲路徑,并在分塊延遲著色計算著色器中實現AGAA聚合和照明。為了獲得最佳性能,在該計算著色器的著色階段開始時實現了G-Buffer聚合步驟,以便重用當前分塊的所有已處理光源的聚合屬性。

    分簇通道獲取每個像素的所有ShadingModel ID、深度和法線,并計算從樣本ID到聚合ID的映射。因為每個像素只有2個聚合,所以每個樣本聚合ID可以存儲為每個樣本一位。接下來,對于每個GBuffer樣本,基于GBuffer數據重建著色參數(以用于聚合著色的格式),在每個聚合中的所有樣本中*均這些著色參數。但有一個例外:對于世界空間位置,首先計算每個聚集的*均視圖空間深度,然后通過假設該位置的屏幕空間XY坐標位于像素中心來重建世界空間位置,速度要快得多。這種*似不會在測試中引入任何可見的瑕疵。

    預過濾G-Buffer樣本用到了法線貼圖預過濾(Toksvig 2005)和幾何曲率過濾(Kaplanyan 2016)。

    float3 GetAgaaAverageQuadNormal(FMaterialPixelParameters MaterialParameters, float3 N)
    {
        int2 PixelPos = MaterialParameters.SVPosition.xy;
        N -= ddx_fine(N) * (float(PixelPos.x & 1) - 0.5);
        N -= ddy_fine(N) * (float(PixelPos.y & 1) - 0.5);
        return N;
    }
    
    // 幾何曲率過濾(Kaplanyan 2016)
    float2 GetAgaaKaplanyanFilteringRect(FMaterialPixelParameters MaterialParameters)
    {
        // Shading frame
        float3 T = MaterialParameters.TangentToWorld[0];
        float3 ShFrameN = normalize(MaterialParameters.TangentToWorld[2]);
        float3 ShFrameS = normalize(T - ShFrameN * dot(ShFrameN, T));
        float3 ShFrameT = cross(ShFrameN, ShFrameS);
    
        // Use average quad normal as a half vector
        float3 hppW = GetAgaaAverageQuadNormal(MaterialParameters, ShFrameN);
    
        // Compute half vector in parallel plane
        hppW /= dot(ShFrameN, hppW);
        float2 hpp = float2(dot(hppW, ShFrameS), dot(hppW, ShFrameT));
    
        // Compute filtering region
        float2 rectFp = (abs(ddx_fine(hpp)) + abs(ddy_fine(hpp))) * 0.5f;
    
        // For grazing angles where the first-order footprint goes to very high values
        rectFp = min(View.AgaaKaplanyanRoughnessMaxFootprint, rectFp);
        
        return rectFp;
    }
    
    float GetAgaaKaplanyanRoughness(FMaterialPixelParameters MaterialParameters, float InRoughness)
    {
        float2 rectFp = GetAgaaKaplanyanFilteringRect(MaterialParameters);
    
        // Covariance matrix of pixel filter's Gaussian (remapped in roughness units)
        // Need to x2 because roughness = sqrt(2) * pixel_sigma_hpp 
        float2 covMx = rectFp * rectFp * 2.f * View.AgaaKaplanyanRoughnessBoost; 
    
        // Since we have an isotropic roughness to output, we conservatively take the largest edge of the filtering rectangle
        float maxIsoFp = max(covMx.x, covMx.y);
    
        return sqrt(InRoughness * InRoughness + maxIsoFp);          // Beckmann proxy convolution for GGX
    }
    

    GBuffer NDF預過濾關閉(左)和開啟(右)的對比。

    4xAGAA的顯存消耗如下:

    AGAA Pass Render Target Video Memory Bytes
    AGAA Clustering AGAA MetaData WxHx1 R16_UINT
    AGAA Lighting & Reflections Per-Aggregate Lit Colors WxHx2 R11G11B10F
    Merge Emissive Per-Pixel Lit Color + Emissive WxHx4 R11G11B10F
    Total VRAM overhead: 26 bytes / pixel

    對于解析,自發光保持逐樣本解析,始終解析色調映射顏色[Karis2014]。

    4xAGAA的性能(GPU時間,單位是ms)如下:

    階段 4xSSAA 4xAGAA 4xAGAA / 4xSSAA
    Z PrePass 0.13 0.13 1.0x
    GBuffer Fill 1.26 1.26 1.0x
    Lighting 4.71 2.85 1.65x
    PostProcessing 0.55 0.55 1.0x
    Frame 6.65 4.79 1.39x

    另外,使用可能很復雜的材質圖來控制材質定義(下圖),可以實現4xMSAA。這些圖形可以包含許多非線性操作,例如丟棄調用、冪函數、夾緊、遮罩、條件,以及這些操作的各種組合,會破壞紋理空間的預過濾。

    使MSAA更可行。在GBuffer填充中使用MSAA可以產生比超級采樣更好的性能提升(約2倍),但是,如果像素著色器使用丟棄或非線性數學,則逐片元著色可能會引入瑕疵。提議的解決方案:1、鼓勵藝術家避免非線性材質節點(pow、clamp等);2、對具有非線性的GBuffer屬性進行選擇性超級采樣。

    AGAA的限制是只加快了光照通道的速度,尚未測試UE的非標準著色模型,也未測試每個像素超過2個著色模型的情況。總之,AGAA加速了超采樣光照,4x AGAA照明比4x SSAA快1.7倍,8x AGAA照明比8x SSAA快2.6倍,可與TAA結合使用或單獨使用。

    自2015年發布DirectX 12和Vulkan等現代圖形API之后,游戲引擎、游戲應用等開始支持和兼容它們。Explicit Multi GPU Programming with DirectX 12舊闡述了在DirectX 12下如何實現多GPU的顯式編程。

    以往的多GPU是隱式編程,理想的狀態是驅動施展了魔法,很管用,開發者不必在意驅動的細節。然而事實是,驅動需要很多提示,如清除、丟棄,特定于供應商的API,開發人員需要了解驅動程序試圖做了什么,它仍然不總是能讓性能起飛。。。

    顯式多GPU可以控制跨GPU傳輸,沒有意外的隱性傳輸,控制每個GPU上完成的工作,不僅僅是交替幀渲染(AFR)。基于DirectX 12的顯式多GPU不再有驅動魔法,AFR沒有驅動程序級別的支持,現在應用層開發任意可以自己做得更好,甚至好得多,并且不需要特定于供應商的API。DX12有鏈接節點適配器(下圖上)和多適配器(下圖下)兩種模式:

    兩種模式下的命令隊列、資源管理、同步和應用方式等都有所不同。

    對于交叉幀渲染,老的管線可能存在依賴。新的幀流水線在一個GPU上開始幀,將工作轉移到下一個GPU以完成渲染和呈現,GPU和復制引擎形成一條管線:

    允許使用時間技術而不損害性能:

    總之,不再有驅動魔法,可以盡情地控制著AFR,嘗試使用時間技術進行管線化,充分利用復制引擎,可以用額外的GPU做任何想做的事情!

    Real-time BC6H Compression on GPU講述了在GPU中實時用BC6H進行壓縮的技術。BC6H是為FP16 HDR紋理設計的基于塊的有損壓縮,自DX11和當前次代的控制臺以來支持的硬件,4x4固定尺寸的紋素塊,沒有Alpha,6:1壓縮比(每個紋素8比特),很好地替代了像RGBE、RGBM、RGBK這樣的黑代碼。。。

    有一些關于紋理的技巧:有符號或無符號半浮點數,三種算法的混合:端點和指數、增量壓縮、分區,每個塊選擇不同的壓縮模式。

    BC6H端點和指數:每個塊存儲兩個端點和16個索引,端點在RGB空間中定義線段,索引定義塊中每個紋素在該段上的位置(下圖左)。BC6H增量壓縮:第一個端點以更高的精度存儲,存儲端點之間的有符號偏移量,而不是第二個端點,提高具有相似值的塊的質量(下圖右)。

    BC6H分區:每個塊允許兩條單獨的線段,提高包含較大顏色變化的塊的質量,使用32個預定義分區中的一個將當前紋理指定給兩條線段中的一條(下圖上)。BC6H帶增量壓縮的分區:一個基本端點直接存儲,其它3個存儲為自基準端點的有符號偏移,有助于有限的端點精度下圖下)。

    BC6H有14種不同的模式:1種不使用分區或增量壓縮的模式,3種僅使用增量壓縮的模式,1種僅使用分區的模式,9種使用分區和增量壓縮的模式。模式在端點精度和偏移精度之間選擇不同的折衷。最佳壓縮是困難的,有10種分區模式,每個分區都需要找到4個端點和16個最優指標(有32個分區),巨大的搜索空間。

    GPU上實時壓縮的典型案例:動態環境圖、運行時從其它格式轉錄的HDR紋理、用戶生成的內容,基于GPU的壓縮避免了CPU-GPU同步以及CPU和GPU之間的數據傳輸。文中的用例包含動態光照條件、程序化世界幾何、在相機移動期間生成附*的地圖、實時將生成的環境圖壓縮到BC6H、啟用密集環境貼圖放置。

    實時壓縮需要權衡性能和質量。性能至關重要,質量不一定是一流的,質量很重要,壓縮可能需要幾毫秒。“快速”預設用哪種模式?分區模式太慢,僅使用增量壓縮的模式速度很快,但僅在少數特定情況下有所改善。模式11只有終點和指數,量化為10位浮點的兩個端點,16個索引(每個索引4位)。端點計算塊的紋理的RGB邊界框[Waveren06],將其最小值和最大值用作端點。

    RGB BBox插圖,小比例插入RGB BBOX,以提高精度(下圖上)。HDR數據會導致非常大的偏移量(下圖下)。

    RGB BBox細化,使用第二小和第二大的R、G和B重建bbox:

    需要在端點之間的線段上選擇16種插值顏色之一,在段上投影并選擇最*的索引(下圖左),不像插值權重分布不均勻那么簡單(下圖右)。

    簡單的*似方法是擬合最小誤差的方程:

    修正索引:第一個索引的MSB隱式假定為零,并且未存儲,需要通過交換端點來確保這個屬性。

    快速和質量預設的效果對比:

    DX11的實現:渲染到比R32G32A32A32小16倍的臨時目標,運行像素著色器并將壓縮塊作為像素輸出,將結果復制到BC6H紋理(CopyResource),使用現代API:跳過復制步驟,實現為異步計算。著色器優化:使用“聚集”獲取源紋理,將16位整數數學替換為浮點數學,將查找表緊密位打包為無符號整數。

    Rendering Rapids in Uncharted 4講述了神秘海域4的激流的渲染技術。在水體模擬方面,使用了FFT、波粒子、流向圖等技術,水體網格使用了CDLOD(Continuous Distance-Dependent Level of Detail for Rendering Heightmaps,渲染高度圖的連續距離相關細節級別)

    從河流幾何體中的一組固定四邊形開始,按范圍確定四邊形LOD級別,使用高度貼圖范圍bbox進行相機過渡,根據相機的xz位置細分每個基礎四邊形,使用高度貼圖+波浪向上置換頂點。對于物體變形,例如水體上的船只,對每個相交的四邊形頂點使用對象邊界框和過濾,基于遮罩偏移四邊形中的頂點。

    粒子的變形相對更復雜,其管線如下:

    Crest: Novel ocean rendering techniques in an open source framework闡述了Crest——一個在Unity3D中實現的開源海洋渲染器,展示了網格生成、形狀表示和表面著色方面的許多新技術。為了生成幾何圖形,使用連續的細節級別擴展clipmapping,并將一條踢腳線幾何圖形擴展到地*線,該表面由支持視錐體剔除的非重疊分塊組成。為了保持屏幕空間的網格密度,展示了如何在相機改變高度時水*縮放網格,而不會出現明顯的跳變。還實現了一個簡單的啟發式算法,用于將細節中心放置在查看器前面,這對所有視圖位置和方向都是魯棒的。在GPU上將海洋形狀渲染為置換紋理,以匹配幾何體LOD的密度和位置。如果每個形狀紋理的波長太短,會限制添加的Gerstner波列的數量,以避免鋸齒和不必要的工作,還使用乒乓渲染目標來模擬波動方程,以添加動態運動層。對于著色,法線貼圖UV可以使用幾何體LOD環進行縮放,以實現接*觀察者的細節,同時防止在中到遠距離出現細節不足的玻璃般*坦外觀,并消除可見重復。對于泡沫,在形狀紋理旁邊添加了額外的渲染目標,并使用反饋渲染過程來模擬耗散。最后,還展示應用深度剝離來實現通過多個表面的非*凡光傳輸路徑的結果。

    裁剪圖的運動可視化。

    CD裁減圖的運動可視化。

    水*縮放可視化。

    對比CDLOD:用于驅動細節的歐幾里德距離,四叉樹以放置幾何體分塊,*滑幾何過渡,神秘海域4:Gonzalez-Ochoa

    CDLOD CDClipmap
    歐幾里得距離 出租車(Taxicab )距離
    四叉樹 最小化CPU
    裙子幾何? 裙子幾何
    無形狀連接 形狀連接

    左:CDLOD的歐幾里得距離;右:CDClipmap的出租車(Taxicab )距離。

    在形狀方面,GPU管線如下:

    可以渲染任何其它可以進行數學建模的內容:

    另外,還支持泡沫、深度剝離(Depth Peeling):

    深度剝離過程:獲取最前面水層的深度,渲染后續層,累積散射/吸收,渲染最前面的層,典型的折射著色器,但具有正確的散射,可以應用于水下后處理。

    渲染層后,有最后面的表面深度,與場景顏色相結合,計算從最后面的深度和水內的吸收和散射,只能應用水下后期處理效果,例如焦散。


    最終效果:

    Data Binding Architectures for Rapid UI Creation in Unity闡述了在Unity中實現數據綁定來創建UI的架構和技術。舊的UI制作方法是藝術家在Photoshop中工作、UI開發者應用魔法、QA發現了漏洞之間循環:

    這種方式存在諸多問題:”不是我的問題“的態度,開發者扮演UI藝術家,未經測試的意大利面條(混亂的)代碼,開發周期長。Lost Survivor中的架構:藝術家直接在Unity中工作,開發/藝術資源解耦,開發/藝術并行工作。相關的技術有MVC模型:

    Object更易于重用,其接口也更易于定義。原生iOS SDK支持,關注點分離,XCode中的可視化設計器。還有MVVC模型:

    ViewModel為視圖提供服務,每個模型視圖一個視圖,基于數據綁定。Lost Survivor的架構如下:

    類圖:

    數據綁定示例:

    // bind toggles to method calls
    Subscribe<SettingsConfigurationModel>()
        .BindToggle(MusicToggle, _audioService.MuteMusic, true)
        .BindToggle(SfxToggle, _audioService.MuteSfx, true)
        .BindToggle(PushNotificationToggle, SetPushEnabled)
    

    還可以自定義數據綁定:

    Subscribe<CharacterModel>()
        .BindModelChangeAction(UpdateBuffObjects)
        .BindButton(HealButton, CurrentBuffSubview, (model, script) => ...)
        ...
    .Finish();
    
    ...
    
    private void UpdateBuffObjects(CharacterModel model)
    {
        for (int i = 0, count = model.ActiveStageBuffs.Count; i < count; i++)
        {
            InstantiateNewBuffGameObject(CurrentBuffSubview, model.ActiveStageBuffs[i]);
        }
        ...
    }
    

    性能影響:基于反射,僅限初始階段,無垃圾回收壓力。可以實現分離測試,快速迭代,少依賴,一切都可以模擬。總之,可以實現開發/藝術可以各自專注、模擬對象、團結協作、單元測試、用戶界面測試等。

    The Destiny Particle Architecture闡述了Destiny引擎粒子特效的架構,包含表達能力和靈活性、亞秒級迭代和性能。

    Destiny的粒子采用了腳本化、可視化的架構:

    component c_emitter_shape:ring
    {
        c_initial_velocity_type:* initial_velocity_type;
        
        float radius;
    
        #hlsl
            float3 calculate_initial_position()
            {
                float2 circle_point= random_point_on_unit_circle();
                return float3(0, circle_point * radius);
            }
        #end
    }
    

    其數據層次結構如下:

    粒子的內存布局如下:

    表達式和字節碼轉換過程及亞秒級迭代如下:

    那么執行速度夠快嗎?對于CPU粒子(其最大值通常在3k左右),足以用于開發目的:

    此外,還嘗試了GPU字節碼和烘焙的HLSL的方案,它們在速度和迭代的關系如下圖:

    《Evolution of Programmable Models for Graphics Engines》講述了Unity引擎的架構演變史。一個(糟糕的)方法是在每個資產、每個*臺的基礎上編寫代碼,顯然無法擴展,因此不是真正要做的方式:

    可以使用應用層的特定接口的映射來解耦:

    有助于創新的原則:易于共享可復制的執行行為,廣泛的基礎設施支持服務,游戲引擎很容易做到這一點,框架應該具有靈活性,沉重的基礎設施層不應阻礙創新。當時的引擎普遍分為低級別API層、引擎抽象層、渲染通道抽象層:

    渲染管線的復雜性已經上升了幾個數量級,基于邏輯的渲染控制包含游戲邏輯、內容控制的渲染選擇:

    對于圖形開發的迭代,希望在低級別API和引擎抽象層盡量不修改,而將修改放在渲染通道和游戲邏輯層。圖形引擎編程的范式轉換,每個特性有一個小的C++內核,暴露APl給C#層,在C#語言中實現細節。Unity希望渲染框架的預期是:前瞻性:最小表面積、易于測試、松散的耦合;以用戶為中心:放置在用戶的項目空間,易于快速調試,易于擴展和修改;最優的:性能非常快,針對特定*臺應用程序類型的優化允許用戶僅執行必要的操作以實現其預期目標;明確的:它完全按照你說的做,沒有更多,沒有更少,沒有魔法,清晰的API。為此,Unity提出了新的SRP(腳本化渲染管線):

    SRP高級概念:從腳本中調用已過濾的繪制調用,可以為特定的渲染管線設計著色器,與可見對象列表、燈光等結合使用,大大簡化了渲染管線的高級代碼。其原則是:深度配置、沒有隱式假設、可發現性、靈活、快速開發迭代、高性能。其中C++層負責性能關鍵區域:剔除、對對象集進行排序/批處理/渲染、GPU命令緩沖區的生成、內部圖形*臺抽象、低級別資源管理。C#負責高層指令:攝像機設置、照明/陰影設置、幀渲染通道設置/邏輯、后處理邏輯。著色器負責GPU的工作:材質定義、光線和應用、陰影生成、全屏過程、計算著色器。

    應該是數據(圖形/配置文件)還是代碼(C#/Lua)?有些決定取決于分支和游戲狀態,Unity更喜歡C#,已經是Unity的核心設計原則,代碼是圖形程序員的舒適區,腳本熱加載的快速迭代非常好。

    SRP優勢:新算法的超快速迭代,Unity引擎框架的所有好處,關注算法本身而不是搭建架構,低級別的精益執行的所有性能優勢,腳本中熱重新加載的所有好處。命令緩沖區調度,一切都入隊排著,Submit會倍自動調用,按添加操作的順序執行,每個命令都是一個作業。SRP非常適合于延遲/前向//前向+(例如G緩沖區布局/包裝的變化)、照明架構的變化、陰影算法、燈光類型研究(區域燈光等)、材質模型、對后處理的更改、分簇的變化。

    可編寫腳本的渲染管線的好處是建立在偉大的引擎框架之上,圖形技術開發的快速迭代,內置的性能分析,高效的多*臺執行,輕松分享技術,開發新管線的偉大起點。對用戶的限制是需要觸及底層*臺APL或引擎層抽象的更改需要C++源代碼,這些改變并非不可能,但需要對核心引擎進行改變。


    4K Checkerboard in Battlefield 1 and Mass Effect Andromeda闡述了Battlefield 1的4K級別的棋盤渲染。

    EQAA(AMD)是MSAA的超集,可以存儲比深度片元更少的顏色片元,顏色<=ID<=深度。4K棋盤利用了這種配置:1x顏色片元、2x ID片元、2倍深度片元。著色分塊有兩種:2x顏色4x深度、1x顏色2x深度:

    EQAA的棋盤布局如下:

    另外,COD使用了物體和圖元ID緩沖區、梯度調整、重心評估、Alpha展開等技術。在優化上,使用PS調用、FP16、EQAA深度解析等。在后處理上,使用了棋盤解析、空間組件、顏色包圍盒、不同的混合操作、物體和圖元ID、時間組件、基于速度的重投影、銳化過濾等技術。

    棋盤解析過程。

    此外,文中還涉及渲染目標重疊、動態分辨率縮放及性能等內容。

    A Life of a Bokeh分享了UE的Bokeh景深效果的實現、步驟和相關優化,具體地說包含分散即收集、混合散射、孔洞填充、輕微失焦、膜片模擬、性能結果和可擴展性。UE的景深目標是高端品質、硬件可擴展性、實時渲染。UE的DOF處理流程如下:

    在采樣模式上,均勻的模式過于分散。嘗試小樣本CoC,使用更大的內核,因為附*的大屏幕無法聚焦,只有一些像素會與之相交,取決于隨機內核偏移量,mip級別越低,顯示越多。

    收集采樣密度改變,讓密度變化發生在環之間,無每個樣本權重改成完全修改之前的桶狀權重:

    擴大BGD-MIN-COC和擴大BGD MIN INTERSECTABLE COC的對比:

    間接散射通道:用于前景和背景,分散獨立精靈,在受支持的*臺上使用矩形列表拓撲進行優化,要分散的精靈數量各不相同,間接繪制調用[Kawase15.3]:

    鄰域對比:在Reduce pass中將顏色與鄰域進行比較,使用四分之一分辨率緩沖區,聚集分散像素的顏色為黑色,RGB加法渲染目標混合。

    重組通道的流程如下:

    用于前景的鏡像孔填充:反映每個樣本的貢獻[Jimenez14],僅限前景收集通道,在2個相反的環形樣本的CoC中取小值,在相交之前執行。

    專用于填孔的收集通道:僅適用于前景不透明度不等于0且不等于1,發現距離較*前景的不透明度,使用背景*滑過渡,輸出RGBA,允許在重組過程中進行不透明度修改。

    半分辨率焦點收集:收集所有輸入樣本,3個半分辨率像素的半徑,顏色和不透明度,已知從擴張通道收集什么分塊。

    重組:RGBA夾緊全分辨率聚集,使用最接*的2x2半分辨率暴力收集輸出,RGB用于穩定閃爍的高光,不透明度可在頭發等抖動材質上保持穩定。隨機收集重組中的全部資源,基于Sobol的隨機發生器[OlanoXX],將正方形分布映射到磁盤[Reynolds17],對于交叉點模型,必須達到亞像素精度。使用主TAA通道作為后過濾器。最終的效果和性能對比如下:

    Efficient Screen-Space Subsurface Scattering Using Burley’s Normalized Diffusion in Real-Time闡述了利用Burley的標準化擴散來實現高效高質量的屏幕空間次表面散射的效果。

    以往的可分離次表面散射用可分離的高斯卷積,速度很快,僅在*面上過濾時可分離,雙邊濾波使卷積“偽可分離”,結果在視覺上是可信的。但是,高斯混合公式顯然是不可分離的,除非每個高斯函數執行2次卷積通道,可能讓SSSSSS導致必須在內容端修復的瑕疵,在實踐中,用2個高斯進行4次通道代價太高。高斯混合不適合藝術家,高斯只是一個數學概念,藝術家被迫使用測量數據或“眼球”的數值擬合,太多自由度,大多數組合都無意義。

    Burley標準化擴散模型(又叫迪士尼SSS)由Brent Burley和Per Christensen開發【Burley 2015年】,參考了MC數據的曲線擬合(1995年),參與介質中的單次和多次散射。

    擴散剖面是標準化的,可以用作PDF格式:

    實現次表面散射:漫反射BRDF是SSS的遠場*似,使用迪士尼漫反射BRDF公式定義漫反射傳輸行為[Burley 2015],物理上不合理,但比假設蘭伯特分布更好。

    不支持無鏡面反射傳輸(僅單次和多次散射)。漫反射BRDF是SSS的遠場*似值,使用表面反照率作為體積反照率,提供2種紋理選項
    :后處理散射紋理(出射位置的反照率)、預處理和后處理散射紋理。

    它們的效果對比:

    擴散剖面被歸一化,實際上是沿幾何表面的能量守恒模糊,在屏幕空間中使用雙邊過濾器進行*似計算。

    采樣擴散剖面:根據擴散曲線進行圓盤采樣,使用斐波那契序列Hannay均勻采樣角度,通常比分布在球體和磁盤上的其它低差異序列表現更好。然后,與擴散曲線的卷積就是樣本值和權重的點積:


    雙邊濾波允許跨非*面曲面進行過濾:

    修改PDF并不是那么簡單,因為樣本位置已經根據舊的“*面”PDF分布,相反,只需執行能量守恒(使權重標準化以滿足單位分割):

    另外,還支持薄及厚的半透明物體的模擬:


    優化:SSS實現為通過R11G11B10緩沖區的全屏CS通道,SSS pass CS針對GCN的高占用率進行了大量優化,如32 VGRS、38 SGPR、用于16x16線程組大小6656字節LDS。將LDS用作20x20鄰域的L0緩存,以減少VRAM傳輸,在SoA中存儲輻射度和線性深度,以減少bank沖突,沿Z順序曲線排列線程[Morton 1966],以匹配GCN渲染目標的內存布局。G-Buffer通道用材質類型標記模板,提取HTile信息,以便盡早剔除,照明通道使用材質分類[Garawany 2016],SSS材質使用擴展的標準照明著色器,標記SSS緩沖區,以避免在SSS通道期間讀取模板,在照明通道中,同時應用入射點和出射點傳輸,雖然概念上是錯誤的,但視覺上的差異很小。實現一個內部著色器離散LOD系統,最大半徑在1像素足跡內的圓盤——無卷積,最大半徑在4x4像素范圍內的圓盤——使用較少的樣本,最大半徑超過4x4像素足跡的圓盤——使用更多樣本,始終如一的視覺效果,沒有LOD跳變。帶有隨機核旋轉的欠采樣[Jimenez 2014]的對比(左邊是Jimenez,右邊是Burley):

    性能方面,在1080p的基礎PS4上進行測試,每像素21個樣本,CS執行卷積和組合,使用鏡面反射照明的結果,GPU時間為1.16毫秒。局限性:采樣不足會導致視覺噪點(下圖),將散射距離設置為較大的值可能會降低性能,對于較大的厚度值,厚對象半透明不是很精確。


    你的精靈會讓你的內存爆掉嗎?使用精靈動畫的游戲面臨的一個常見問題是每幀使用的紋理內存量,這對移動設備來說是一個特別嚴重的限制。為了解決這個問題,可以使用sprite打包和圖集來更有效地利用內存,但這些解決方案可能會導致圖集中的空間浪費,無法完全填滿。Grabthar's Hammer是秘密實驗室團隊為“森林之夜”的移動端口編寫的工具,通過使用sprite dicing以最小的浪費最大限度地利用atlas,以更高效的方式解決了這個問題。By Grabthar's Hammer, What a Savings: Making the Most of Texture Memory with Sprite Dicing介紹了這項技術,討論《森林之夜》是如何被改編來使用它的,以及如何通過使用視頻壓縮技術來進一步利用精靈切分技術,從而從內存預算中獲得最大數量的精靈。

    對精靈紋理的常規優化包含:

    • 紋理壓縮。對不同紋理格式的不同支持,如PVRTC、ASTC。
    • 向量追蹤。非常緊湊,無法很好地保存小細節,通常需要手動調整以獲得最佳效果。
    • 紋理圖集。非常適合PVRTC壓縮,打包不完整時浪費空間。

    文中使用了特殊的方法,即精靈切分(Sprite Dicing),步驟如下:

    • 把精靈切成小塊。
    • 丟棄任何透明小塊。
    • 把這些小塊打包成圖集。

    精靈切分有更好的圖集覆蓋率,透明區域移除,并非所有小塊都被保存。具體的過程圖例如下:

    結合上圖,按從左到右、從上到下依次編排序號,它們的說明如下:

    1、如果精靈是凹面的,它包含許多簡單修剪無法移除的透明像素。

    2、這里的綠色區域是我們可以通過簡單的修剪去除的;紅色區域是透明的,但無法觸及,因為它位于裝飾框內。

    3、這里所有標記為綠色的東西都是我們可以丟棄的。請注意,這里有一些不必要的透明區域,可以丟棄——這是因為這些部分是32x32。

    4、沒有理由讓每一塊都是16x16,所以也把它們修剪成最小的單個矩形。

    5、這是一張720x720的圖片,所有透明的部分都被移除,每個部分都被單獨修剪,并打包成一張圖集——它可以放進一個512x512,還有很多剩余的部分。它必須是512x512,因為PVRTC需要兩個紋理的冪。

    6、缺點是網格密度增加了一點——每個頂點大約有18個字節。然而,額外的網格成本遠低于存儲更少像素的紋理數據所節省的成本。

    這種方式的優勢是更好的圖集覆蓋率,圖集紋理可壓縮,更低的著色器復雜性。缺點是預處理通道,圖集大小限制,更高的多邊形數,需要填充,在Unity中沒有內置的支持。

    另外,可以進一步優化,那就是查找并刪除冗余。冗余的類型包含空間冗余(同一圖像中的多個副本)和時間冗余(一系列圖像的多個副本)。查找空間冗余的過程如下:

    1、我們利用了這樣一個事實,即兩個完全相同的片段相距甚遠的可能性很小。所以,我們只對靠得很*的成對小塊進行測試。

    2、它們是一樣的嗎?

    3、只檢查唯一對。

    查找時間冗余的過程如下:

    我們將檢查兩幅圖像中相同位置的塊;對所有區塊位置和所有圖像對重復上述操作(或者,如果我們想加快速度,在序列中相互跟隨的圖像對之間重復)。

    接下來是尋找分簇(cluster)。

    1、現在有一個區塊圖…

    2、…及其連接-這些連接可以是同一圖像上的塊之間的連接,也可以是多個圖像上的塊之間的連接。如果相似性分數足夠高,認為一個區塊與另一個區塊“相連”。然后可以找到塊的“強連通集”;在每一組中,所有的塊都會非常相似,所以選擇其中任何一個,讓其余的塊使用該塊的紋理數據,使用Tarjan算法來尋找強連通集。

    Altas紋理問題:最大紋理大小為8192x8192像素,圖集必須是方形的,無法始終將所有區塊放入一個圖集,浪費空間。

    結果是23個720x720的RGBA精靈,PVRTC 4bpp壓縮,16x16小塊尺寸,原來的裁剪版2.68mb,切塊版1.5mb(原版的55%)。若是106個1024x512的精靈,原來的裁剪版9.76mb,切塊版1.5mb(原版的15%)。

    Bungie's Asset Pipeline: 'Destiny 2' and Beyond分享了命運引擎的資產管線、變化、迭代改進等。命運引擎的資產管線流程如下:

    使用的依賴圖如下:

    原始的資產管線流:

    存在的核心問題是大量任務和數據統計,Destiny 1全球有910萬個任務、1930萬個數據元素,跟蹤結構非常龐大,難以檢查/理解,粒度強調緩存機制。

    到了命運2,工作流程的目標是制作更多內容,速度更快。現在是時候做出更大改變的機會,但是,仍然需要源內容的向后兼容性,最小化生產成本。命運2的資產管線的重大修改是將圖表拆分為許多較小的逐資產圖表,隨著任務數量的減少,上下文工作的范圍會更快,并行運行資產圖操作,更容易檢查和理解較小的單一資產圖,更合理的緩存粒度,更難在資產之間引入數據依賴關系,更容易理解工作流的影響,允許在加載時修復資產引用。DESTINY 2的資產管線流:

    DESTINY 2的資產管線的并行:

    限制:向后兼容性/最低生產成本:

    總之,單個大規模依賴關系圖的擴展問題,引入了資產級粒度,引入了帶有加載時間修正的資產引用。這些新工具能夠構建高效的工作流,繼續完成并升級/優化現有工作流,刪除/減少代價高昂的數據依賴關系,定義新資產,調整資產邊界。可以結合每資產和基于依賴關系圖的數據管線的主要優點,在添加數據依賴項之前了解影響至關重要。

    Terrain Rendering in 'Far Cry 5'闡述了Far Cry 5的地形渲染,包含高場渲染(基本原理、GPU管線)、著色、懸崖著色、超越高度場、屏幕空間著色、基于地形的效果。

    高度場的基礎和過程如下:

    地形四叉樹:分區(Sector)64m x 64m,世界160 x 160個分區、共10km x 10km,地形分辨率0.5m x 0.5m。

    四叉樹的節點存儲在圖集上:

    地形的渲染過程如下:

    流傳輸四叉樹節點圖例如下:

    遍歷和剔除節點的過程如下:

    上圖的3個步驟都在GPU中執行,動機是數據僅由GPU使用,消除CPU成本,降低GPU成本!實現最大頂點剔除,使地形數據可用于其它GPU系統樹木、巖石、草地等。GPU中的數據結構包含:

    • 地形四叉樹。加載/卸載節點時的持久更新。
    • 地形節點列表。四叉樹遍歷生成的節點列表,每幀構建一次。
    • 地形LOD圖。世界上每個分區使用的幾何體LOD,從地形節點列表中每幀重建一次。
    • 可見渲染批次列表。要渲染的最終剔除節點列表,每個渲染視圖生成一次。




    對于地形著色,使用了濺射圖:

    前景著色步驟如下:

    虛擬紋理的處理過程如下:

    對于懸崖著色,使用了隨機化采樣和著色:

    屏幕空間的地形著色是根據視距采用不同成本的著色方案:

    屏幕空間的地形著色的通道如下:

    Interactive Wind and Vegetation in 'God of War'闡述了God of War的關于風的逼真模擬,包含風流模擬、風力觸發器、風力接收器、網格數據和計算細節、創作和最佳實踐等內容。

    風力場體積。

    風力場體積分辨率。

    風還支持和音頻、布料、粒子、網格等物體的交互。

    風接收器由下圖的5個部分組成。逐頂點數據定義模型的哪些部分移動,哪些部分不移動,模型參數描述了運動部件的行為。

    風的網格參數包含:密度、運動縮放、彎曲、伸展、僵硬、搖擺彈簧、搖擺阻尼、樹模式、樹彎曲、樹運動縮放、樹葉滯后等。

    風的網格伸展參數對比。

    風采用了分形噪聲函數:


    文中還涉及了樹的簡化技術,測試已知坐標系中的所有卡片*面,展**的面,好處在于最大最小扭曲面=最大總*面面積,為最好的卡片獲取面,重復之(下圖左)。6D到2D:卡片的方向到卡片的距離,將卡片空間離散化,測量半球方向的距離,批量并行卡片處理,卡片必須與模型邊界相交下圖中)。渲染到紋理:來自正交相機的材質通道,將攝像頭匹配到卡邊界,剪裁*面匹靠*度,Python圖像庫+Maya UV自動布局,最后卡片重新獲取面(下圖右)。

    UE版本的實現源碼在"Interactive Wind and Vegetation in 'God of War'" in Unreal Engine 4


    Strand-based Hair Rendering in Frostbite分享了基于線條的頭發渲染和模擬。

    人類頭發是蛋白質絲,*似圓柱形,覆蓋著稱為角質層的鱗片,頭發纖維非常薄,約17-181μm,主要由白色/透明的角蛋白制成,深色頭發是頭發中黑色素濃度的影響,人類頭上有大約10萬根頭發。以往的游戲通常使用頭發卡片或殼狀物,存在昂貴的創作、有限的發型、真實感模擬差、著色簡單等缺點。基于線條的頭發模擬可以避免以上問題,實現更復雜、逼真的頭發模擬:

    基于線條的頭發概覽如下:

    頭發模擬:線條被模擬為一系列帶有約束的點,歐拉和拉格朗日模擬的結合,使用網格計算的鏈-鏈相互作用,摩擦、體積保持、空氣動力學,在每個點上分別進行積分,然后通過迭代約束和碰撞器來求解點。渲染概覽如下:

    單次吸收使用Marschner03的著色模型,考慮R、TRT、TT等組成部分:

    可以實現頭發的吸收、*滑等特征:

    期間還會考慮縱向散射\(M_p\)和方位散射\(N_p\)

    透視通過一個3D的LUT來*似:

    多重散射對真實感很重要,考慮從燈光到攝像機的所有可能路徑,不可能實時完成,使用雙散射*似[Zinke08],局部散射解釋了靠*著色點的散射,全局散射解釋了光在頭發體積中的傳播。

    深度不透明度貼圖[Yuksel08]用于估計全局散射光路徑上的頭發數,它們也用于陰影。

    渲染時,頭發被曲面細分成三角形條紋,寬度通常小于像素大小,細分必須考慮像素大小。

    僅僅啟用MSAA并沒有真正的幫助,需要Visibility buffer + MSAA:

    著色時,對相似樣本只進行一次著色,約2倍的性能提升:

    渲染總覽如下:

    通過“Forza Horizon 4”,游樂場游戲公司開發了一種基于分光光度法的新型物理校準(PBC)管線,以前所未有的精度匹配*900種真實世界汽車涂料的游戲外觀。Physically-Based Calibration: Accurate Material Production in 'Forza Horizon 4'的新方法克服了計算機圖形學中的一個重大挑戰,其靈感來自汽車行業的顏色管理技術,可以輕松地應用于各種渲染系統中的各種材質表示。基于之前在分光光度法和比色法方面的研究,新的校準方法科學地消除了材質復制中的大多數系統誤差,并能可靠地找到準確復制真實世界“真實”材質外觀的最佳著色模型輸入。新的基于物理的校準技術被認為是一項重大突破,對未來在虛擬環境中精確復制材質具有重要意義。

    汽車漆是“看不見的”:

    文中由基于感知的方式轉變程物理基礎的方式:

    現實中和游戲內的測量對比:

    都有關反射分布,匹配反射率分布,你就能匹配材質。蘋果對蘋果的對比——定量顏色分析:

    使用“Digital Masters”XML來*似SPD:

    ASTM E 308標準:從光譜功率分布(SPD)到顏色信息,SPD --> XYZ:

    從SPD到XYZ顏色:

    同色異譜:

    從XTZ到Lab:

    Lab和sRGB顏色空間的對比:

    從游戲測量到實驗室:

    校準管線:

    新舊對比:


    新的渲染:

    Procedurally Crafting Manhattan for Marvel’s Spider-Man講述了游戲蜘蛛俠的過程化打造曼哈頓場景的技術。

    蜘蛛俠的場景有6公里×3公里,9個區,544條路,1202條小巷,8300棟建筑,3250棟大樓預制件,350個店面,3000個罪犯,3000個插曲,包含故事、任務、英雄人物、惡棍等。程序系統計劃:定義道路和小巷、地面修改、分塊及ID、地面、地面細節、建筑物等。

    場景中還包含大量的物體、特效和技術:

    程序化系統管線集成:

    按程序生成大部分內容:

    傳統手工處理:

    效果一覽:

    Creating the Atmospheric World of Red Dead Redemption 2: A Complete and Integrated Solution介紹了Rockstar的Red Dead Redemption 2(荒野大鏢客2)游戲中的天空渲染技術,包括云/霧渲染、體積效果以及由此產生的表示天空間接照明的環境照明模型。這套技術共同體現了團隊如何在Red Dead Redemption 2的開放世界中創造一種自然氛圍。這些技術的構思和設計既符合光傳輸的物理特性和規律,但重要的是,也提供了強大的直接性手段,允許藝術家精心設計我們的自然環境,以創造一種強烈的氛圍,始終支持和增強游戲的故事、任務和情緒。

    在地形處理中使用了霧體積、環境光、彩虹渲染、局部光、閃電等,下面是光照散射的總覽:

    散射過程中使用了視錐體素網格、陰影體積、材質體積、散射光體積、視錐體積、光線行進等技術。

    材質體積。

    游戲回放在電子競技、學習和社交分享中變得越來越重要,因為它推動玩家參與核心游戲循環之外的活動。Bringing Replays to World of Tanks: Mercenaries概述實現服務器端游戲記錄和回放所需的基礎設施,包括當現有游戲引擎關于時間和空間流動的假設不再成立時,客戶端問題的架構解決方案。WoT將幀更新拆分成以下部分和路徑:

    并且對物體移動采用了插值:

    文中還對實現過程中的不少問題提出了可靠有效的解決方案,對回放系統感興趣的童鞋不妨點擊原文詳細閱讀。


    2014年初,Frostbite團隊開始為基于quad的Bioware角色模型開發內部LOD技術。如今,這項技術為工作室在電子藝術領域使用的工具提供了動力,從戰場到植物與僵尸。基于四邊形的模型在行業中被廣泛使用,藝術家們非常關注它們的拓撲結構和邊緣流。然而,四邊形網格的簡化在文獻中研究較少——流行的LOD工具對網格進行三角化,忽略拓撲。雖然LOD有時被認為是一次性的,但保持良好的拓撲結構有利于后續的手動編輯、照明和動畫。出于這個原因,Bioware在歷史上花了數天的時間來制作每個模型的角色LOD。這項工作的目的是在程序上生成LOD,大多數情況下看起來像藝術家創作的模型,為每個游戲節省數十萬美元。Quad mesh simplification in Frostbite將闡述如何實現以上目標。

    使用Garland和Heckbert二次誤差度量的邊折疊算子的前后對比:

    僅有復合弦折疊的LOD結果:

    使用筆刷可以改變頂點的優先級,下圖是優先級的可視化和對應的LOD對比效果:

    使用子復合弦折疊的LOD結果:

    優先級繪制可以覆蓋對稱性:

    使用對稱的子復合弦折疊的LOD結果:

    Lima Oscar Delta!: Scaling Content in 'Call of Duty: Modern Warfare'分享了COD的離線LOD處理、模型打包、運行時LOD處理、世界代理LOD等技術。LOD離線處理的第一部分:找出一個通用幾何縮減的解決方案(又名“簡單部分”),第二部分是其它的一切。

    需要多少LOD級別?支持最多5倍的額外離散條目,為所有模型使用所有插槽就是浪費。

    減少的目標是頂點、三角形、法線方差、輪廓保持、流重要性權重等。什么時候需要切換LOD?模型將如何在游戲中使用?英雄人物、簡單道具、邊緣、視圖模型、結構體等等。碰撞體需要匹配渲染LOD或其它內容。盡可能優化LOD計數、縮減目標、開關距離、游戲上下文、碰撞等。第一次嘗試:自動化一切!1、減少網格,直到其幾何偏差超過某個閾值。

    2、計算閾值消耗特定數量像素的開關距離。

    3、跳過生成的切換距離與前一個切換距離“太*”的LOD。

    4、重復此操作,直到在生成的開關距離處LOD“太小”。

    以上的初次實現(對程序員)有意義,確定性性能和視覺效果,對藝術家完全不敏感…

    第二次嘗試:混合解決方案。保持LOD設置的自動生成,但僅為方便起見。如果需要,允許藝術家覆蓋幾乎所有內容。1、從DCC工具導出一些模型幾何圖形。2.在資產管理工具中創建新的模型資產。3、啟動后臺流程,生成“技術上理想”的LOD數量、切換距離等。4、使用這些值在資產管理工具中植入GUI表單。5、如果藝術家喜歡他們看到的東西,就不需要再做什么了。否則,他們可以根據需要進行微調。

    第二次嘗試的混合解決方案提供直觀的視覺反饋,幫助評估給定的運行效率。

    LOD包裝:將LOD拆分為單獨的可流化塊:

    物體空間位置量化:

    將剛體動畫的表面升級為軟蒙皮,并且聚合合并幾何體和材質:

    在LOD運行時,LOD選擇標準:創作的開關距離,示意圖如下:

    LOD選擇標準:FOV變化。無論顯示縱橫比(4:3、16:9、16:10,…)如何,使用垂直視場確保相同的結果。

    LOD選擇標準:場景分辨率縮放。運行時計算一次,defaultFOV=65.0,defaultScreenHeight=1080.0。


    最終,LOD選擇標準有創作的開關距離、FOV、場景分辨率、頂點處理效率、通用GPU性能、基于預算的偏移等因素,計算公式如下:

    \[\text{LOD} = \cfrac{\text{lodDist}}{||D||} \times \text{invFovScale} \times \text{sceneResScale} \times \text{vertexProcessingScale} \times \text{gpuPerfScale} + \text{geoBias} \]

    此外,還可以考慮拒絕小物件、程序運動、靜態模型盡量保持公*、流加載等因素。

    代理LOD的“戰區”概述:200名玩家,地面和空中機甲、120倍于大多數10v10地圖的面積,但密度相同,長視線。代理LOD表示世界網格單元的單個模型,是對該單元中所有靜態幾何體組成部分的重新劃分。使用自動化FTW:

    輸入:1000個獨特的模型、其它世界幾何圖形和創作,千萬個三角形!輸出:一對模型,每個模型有一種材質,它們代表了世界cell的中遠距離版本。代理LOD's的困難:匹配材質模型、處理建筑內部、透明表面、樹、大型物體、小物件、構成大對象的小對象、緩存、新硬件(+規格)。

    總之,一套強大的工具對于創建幾何內容至關重要,它可以在運行時視覺和性能目標之間取得適當的*衡。這些工具的核心行為需要盡早解決,這不僅是為了提高效率,也是為了讓藝術家們相信,他們不是在與移動的目標合作。這些工具應該用可操作的反饋來指導藝術家,而不是強迫他們。最佳運行時幾何LOD選擇應基于多個動態標準,而不僅僅是距離相機的距離。大型世界LOD有一套不同的要求需要*衡。由于世界LODing的所有輸入都經過了微調,因此這一過程(幾乎)完全自動化是可取的。


    創造合作的角色行為是非常具有挑戰性的,而且對所有規模的工作室來說都是昂貴的。深度強化學習是一種很好的方法,因為開發人員可以清楚地表達期望的目標,讓機器學習最佳行為,可能會影響游戲開發的經濟性。然而,大多數DRL算法旨在解決單個角色的任務(因此角色之間沒有協作)。然而,協作DRL工具有了新的進展,例如集中評估,可以用來在游戲中創建協作角色行為。Creating Cooperative Character Behaviors using Deep Reinforcement LearningVincent將回顧這些主題,并舉例說明一個工作室使用深度強化學習在游戲中創建合作角色的真實示例。

    角色的合作行為一直是一個難題,行為的復雜性增加了,有多少個角色,角色之間的合作程度。

    傳統的AI和深度學習的AI對比:

    深度強化學習:

    集中學習:集中學習允許角色為隊友的狀態和行為賦予價值,而不僅僅是他們自己。

    同步決策和異步決策:

    代理j的優勢計算:

    Blowing from the West: Simulating Wind in 'Ghost of Tsushima'分享了游戲Ghost of Tsushima中使用的風模擬。風模擬的模型包含了向量 + 噪點 + 漩渦 + 位移,其中漩渦模擬過程如下:

    風可對粒子、樹葉、草、布料等物體進行交互。草的模擬流程如下:

    風和布料的交互過程如下:


    FidelityFX Super Resolution (FSR)闡述了AMD的高保真超分辨率的技術。

    邊緣自適應空間上采樣(EASU)是FSR 1.0中的上采樣算法。EASU的目的是提供比CAS+硬件擴展更好的擴展,質量改進,由于著色器中的縮放,成本更高,掃描輸出上的硬件縮放在視覺上與水*和垂直Lanczos匹配,EASU是一種局部自適應的類Lanczos橢圓濾波器。由于方向適應性,EASU要求輸入具有良好的抗鋸齒能力。EASU算法:使用固定的12-tap內核窗口,12-tap=良好上限,單通道算法(徑向/橢圓濾波),12 taps * 3 channels = 36 VGPRs (FP32),64 VGPR (good upper limit) - 36 = 28 VGPRs for logic,算法需要全部12-tap進行分析,然后進行過濾。對luma(r+2g+b)中內部2x2 quad的每個“+”圖案進行分析。

    分析是雙線性插值,并用于形成最終的濾波器核。

    EASU采樣:Vega/Navi/Xbox系列S |X/PS4Pro/PS5/etc硬件支持壓縮16位操作,將gather4用于FP32和壓縮FP16路徑,12-tap通過4個位置完成。

    EASU分析:根據中心差估計邊緣方向,特征長度是通過對梯度反轉量進行評分來估計的。

    EASU與色彩空間:大多數游戲機最終都會在邊緣上產生感知上均勻的梯度,因此,方向分析在游戲的感知空間中效果更好,因為從線性到感知的轉換非常昂貴,而且需要12 tab,強烈建議在感知空間運行EASU。

    EASU內核成形:插值后的分析產生{direction,length},X在{軸與對角線對齊}方向上從{1.0到sqrt(2.0)}縮放,在{small to Large feature length}上,Y從{1.0到2.0}縮放。

    EASU內核:對lanczos使用多項式*似,使用基礎*窗口的優化*似:

    EASU去振鈴:本地2x2 texel quad{min,max}用于鉗制EASU輸出,刪除所有振鈴,還刪除了12-tap限制窗口的一些瑕疵,或者內核自適應的一些瑕疵。Unity的HDRP后處理管線如下:

    EASU在集成進Unity時,在光柵化過程中輸出顏色、深度、運動矢量和法線之后,立即應用縮放。

    在unity中,有兩種處理鋸齒的方法,即查看一段內存的方法,這樣就可以以不同的分辨率對其進行光柵化。第一種是基于軟件的,適用于dx11、dx12、Vulkan、Metal和GNM等渲染*臺,基本上是HDRP支持的所有功能。第二個是基于硬件,處理渲染API的子集,這些API包含更高級的功能,如直接訪問資源描述符,以及紋理/資源重疊。

    對于基于軟件的應用程序,該技術非常簡單,所做的就是在采樣期間在x軸和y軸上應用縮放,只縮放從中采樣的任何紋理的UV,會處理好邊界。對于光柵化,在軟件中所做的是設置一個視口,它是最終目標大小的子集。在unity中,在渲染圖中有這樣的設置,在渲染圖(render graph)中,通常保留一個渲染目標,該目標是使用軟件縮放創建虛擬或軟件視圖的渲染目標中所有相機的最大分辨率。

    在使用紋理采樣器對軟件DRS中的某個部分進行采樣時,請確保夾緊UV:

    float2 borderUv(float2 uv, float2 texelSize, float2 scale) 
    {
        // texelSize = 1.0/resolution.xy
        float2 maxCoord = 1.0f - 0.5f * texelSize.xy;
        return min(uv, maxCoord) * scale;
    }
    
    float2 borderUvOptimized(float2 uv) 
    {
        //innerFactor => 1.0f / (1.0f - 0.5f*texelSize.xy)
        //outerFactor=> scale * (1.0f - 0.5f*texelSize.xy)
        return saturate(uv * innerFactor) * outerFactor;
    }
    

    對于基于硬件的鋸齒,所做的有點不同,在unity的C++端有一個非常大的堆,在那里基本上分配一個放置的資源。開始按需將這些資源放在堆上,取決于目前需要的解決方案,就是產生重疊的地方。這種方法唯一棘手的一點是,它比基于軟件的方法快得多,因為不必使用乘法器或視口,實際上是在硬件級別上處理這種原生分辨率。但問題是,正在為遇到的每個解決方案創建描述符,必須非常小心CPU的實現,以確保回收這些描述符,并且不會膨脹內存并導致CPU性能成本。

    對于效果鏈,要做的是啟動uber post流程,這是bloom之后輸出的所有內容,在色調映射之后。對渲染目標執行*方根,所以在*方根空間輸出,可稱之為空間感知色彩空間,這就進入了EASU算法,該算法消耗色彩空間并應用上采樣器。在上采樣器中輸出后,確保返回線性空間,然后應用RCAS算法,RCAS算法改進了邊緣和之前可能丟失的所有細節。

    效果對比:

    EASU運行時消耗:PS4 Pro上4k輸出分辨率的Unity Spaceship演示,為EASU和RCAS使用FP32路徑(可移植到PS4),EASU隔離計時0.43毫秒,隔離計時的RCAS為0.16毫秒。


    14.5.4 渲染技術

    14.5.4.1 Visibility Buffer

    4K Rendering Breakthrough: The Filtered and Culled Visibility Buffer介紹了一種新的渲染系統:過濾和剔除的可見性緩沖區,其性能將比具有更高分辨率的傳統系統好得多,至少與具有最常見分辨率的傳統渲染系統一樣好。它將三角形數據存儲在可見性緩沖區中,而不是將數據存儲在G緩沖區中,G緩沖區隨著屏幕分辨率的大幅增加而增加。為了優化該緩沖區中存儲的三角形的數量和質量,在預處理步驟中應用了三角形過濾和剔除。此外,三角形和消隱預處理步驟還準備了所有其它讀取視圖,如陰影圖渲染。

    前向渲染按三角形提交順序對所有片段進行著色,浪費對最終圖像沒有貢獻的像素的渲染能力,延遲著色通過兩個步驟解決此問題:首先,表面屬性存儲在屏幕緩沖區->G緩沖區中,其次,僅對可見片段計算著色。但是,延遲著色會增加內存帶寬消耗,例如屏幕緩沖區:法線、深度、反照率、材質ID,…G緩沖區大小在高分辨率下變得很有挑戰性。以下多圖是不同年代的延遲著色和可見性緩沖區的對比圖:



    以下是可見性緩沖區和GBuffer的顯存消耗對比:




    VisibilityBuffer的填充步驟:

    • 可見性緩沖區生成步驟。
    • 對于屏幕中的每個像素:
      • 將(alpha屏蔽位、drawID、primitiveID)打包到1個32位UINT中。
      • 將其寫入屏幕大小的緩沖區。
    • 元組(alpha掩碼位、drawID、primitiveID)將允許著色器在著色步驟中訪問三角形數據。

    VisibilityBuffer的著色步驟:

    • 對于屏幕空間中的每個像素,執行以下操作:
      • 獲取drawID/triangleID像素位置。
      • 從VB加載3個頂點的數據。
      • 計算三角形梯度。
      • 使用漸變在像素點處插值頂點屬性(可以進行三角形/對象空間照明)。
        • 屬性使用w從位置計算透視正確插值。
        • MVP矩陣被應用于位置。
      • 已經準備好所有數據:著色和計算最終顏色。

    VisibilityBuffer的優勢:更好地從著色中分離可見性,計算導數可以與著色階段分開進行(將其包括在當前階段),可以用不同的頻率或質量來著色。提高內存效率,提高緩存利用率,內存訪問是高度一致的(高速緩存命中率高),G緩沖區需要存儲每個屏幕空間像素的數據,與頂點/索引緩沖區相比,其中一些數據是冗余的,可以看到紋理、頂點和索引緩沖區的可見性緩沖區的99%二級緩存命中率。與g緩沖區相比,為復雜照明模型(如PBR)存儲的數據更少,可見性緩沖區的PBR數據是由頂點結構中的材質id索引的結構常量內存,該結構將索引保存到各種PBR紋理的紋理數組中,它還包含驅動BRDF所需的材質說明,每像素變化的任何數據都存儲在結構引用的紋理中。將緩沖區足印與屏幕分辨率解耦,在高分辨率下提高性能:2K、4K、MSAA…在帶寬有限的*臺上提高性能。

    怎么執行照明?可以選擇最喜歡的照明結構:延遲分塊、向前分塊等。向前分塊或Forward++似乎是一種自然的搭配,因為它將受益于通過剔除、過濾和可見性檢查減少的頂點數,并為不透明和透明對象提供一致的照明。三角形或物體空間的照明也是可能的。

    游戲的多邊形復雜性每年都在增加,有效剔除三角形非常重要。2個剔除階段:1、分簇剔除:在將三角形組發送到GPU之前剔除它們;2、三角形過濾:在發送到GPU后剔除單個三角形。

    分簇剔除:將三角形分組為256個具有相似朝向的三角形的小塊,塊有一個相關的模型矩陣(它們可以移動),每個塊在發送到GPU之前必須通過快速可見性測試:圓錐測試。

    快速分簇剔除的圓錐測試,如果眼睛在安全區域,我們就看不到任何三角形,因為它們是背向的:

    具體的過程和解析如下:

    1:找到分簇的中心。

    2:從分簇中心開始負向累加法線值。

    3:負向累加第一個法線值之后的第二個法線值。

    4:累加下一個。

    5:在累加最后一個之后,得到了排除體積的起點和方向。

    接著,結合下圖,分別是用于計算圓錐體張角的最嚴格的三角形*面、計算出的排除體積:

    分簇剔除效率:有效性取決于在簇中的面的朝向,方向越相似,排除/剔除體積越大。根據三角形,無法計算排除體積,用于分簇剔除的分簇無效,只需要略過。

    基于計算的三角形過濾:動機是在三角形進入圖形管線之前剔除它們,使用異步計算在圖形管線執行期間使用未使用的計算單元。基于計算的過濾,每個線程一個三角形,過濾的三角形包含退化三角形剔除、背面剔除、視錐剔除、小圖元剔除、深度剔除(需要粗糙的深度緩沖),通過這些測試的三角形索引將附加到索引緩沖區。基于計算的三角形過濾:

    • 退化三角形剔除。允許剔除不可見的零面積三角形。成本:快速測試(如果至少兩個三角形索引相等,則放棄),效果:低。

      cull = (indices[0] == indices[1] || indices[1] == indices[2] || indices[0] == indices[2] );
      
    • 背面剔除。允許剔除遠離觀察者的三角形,如果使用細分,則必須考慮最大面片高度。成本:計算3x3矩陣的行列式,效果:高(可能會剔除50%的幾何體)。

    • 視錐剔除。允許剔除投影在剪裁立方體外部的三角形,考慮**面和遠*面。成本:檢查所有頂點是否位于剪輯空間立方體的負側,效果:中高(取決于場景大小和眼睛位置)。

    • 小圖元剔除。允許剔除太小而看不見的三角形,投影后不接觸任何采樣點的三角形,不接觸任何樣本的細長三角形也會被剔除,更高效地利用硬件資源。成本:三角形接觸任何亞像素樣本,效果:中(取決于三角形的大小和屏幕分辨率)。

    • 深度剔除。允許剔除被場景遮擋的三角形,該測試需要一個粗糙的深度緩沖區。成本:從地圖加載深度值并檢查三角形/BB交點,效果:中高(取決于場景復雜度和三角形的大小)。

    基于計算的三角形過濾的概覽如下:

    三角形過濾在256個三角形(一批)-> 空繪制的組上執行,繪制批次壓縮來援救,可以在計算著色器中并行運行,從多間接繪圖緩沖區中刪除空繪圖。

    添加三角形/分簇過濾的幀步驟:

    • [CPU] 使用分簇剔除提前丟棄不可見的幾何。
    • [CS] 使用三角形過濾(每個線程一個三角形)生成未被剔除的索引和多繪制間接緩沖區。
    • 像之前一樣執行。

    添加三角形/分簇過濾的數據管理:對于這個靜態場景,一個大的頂點緩沖區和一個由三角形剔除和過濾生成的索引緩沖區。繪制批次,每個批次為一種材質保存一塊幾何體,只有兩種“材質”不透明和alpha遮罩的透明對象和其它材質將進入同一緩沖區。對于動態對象,將為每個對象使用專用的VB/IB對;是可選的。

    基于計算的三角形過濾的好處:允許在將三角形發送到圖形管線之前剔除它們,避免在圖形管線(光柵化器)中占據壓倒性的部分,圖形管線可以更好地利用可見三角形(光柵化效率、命令處理器等),可以利用異步計算與圖形管線重疊。

    可以對多個視圖/渲染過程重用三角形過濾結果。將算法推廣到不同的N視圖上進行測試,加載索引/頂點一次,變換每個視圖的頂點。

    添加三角形過濾數據的重用的步驟:

    • [CPU]使用分簇剔除提前丟棄從任何視圖中都看不到的幾何體。
    • [CS]對N個視圖使用三角形過濾測試生成N個索引和N個多繪制間接緩沖區(每個線程一個三角形)。
    • 對于每個視圖i使用(第i個索引緩沖區和第i個MDI緩沖區):
      • [Gfx]清除可視性和深度緩沖區。
      • [VS, PS]可視性緩沖區通道,[PS]輸出三角形/實例ID。
      • [PS]從梯度和著色像素插值屬性。

    結果:



    總結:

    虛擬現實呢?正在處理饑餓問題,它能以非常高的分辨率顯示大視場,可見性緩沖區將大大提高性能,可以一次完成所有視圖和陰影貼圖視圖的數據篩選和準備,提供forward+,因此不必處理透明度問題。

    總之,建立的渲染系統為不同視圖(如主視圖、陰影視圖、反射視圖、GI視圖等)分簇剔除和過濾三角形,優化后的三角形用于填充屏幕空間可見性緩沖區或更多視圖的更多可見性緩沖區。然后,使用基于可見性的優化幾何體渲染燈光、陰影和反彈燈光,可以區分幾何體的可見性和著色頻率,可以在每個三角形或所謂的物體空間中計算照明。

    14.5.4.2 Filmic SMAA

    Filmic SMAA: Sharp Morphological and Temporal Antialiasing由任職于暴雪的抗鋸齒的鼻祖Jorge Jimenez呈現,講述了電影級的SMAA的最新成果。

    SMAA的目標是銳利、魯棒性、高性能。形態抗鋸齒(SMAA 1x),增加了時間超采樣(SMAA T2x)、空間多采樣(SMAA S2x)和組合(SMAA 4x)不適用于暴雪的游戲。Filmic SMAA是Filmic過濾(Filmic變體):Filmic SMAA 1x、Filmic SMAA T2x–用于PS4和XB1,時間過濾 ≠ 時間超采樣。

    先重溫以下形態抗鋸齒基礎。結合下圖,先搜索線的左右兩端,然后獲得線兩側的交叉邊,有了距離和交叉邊,有足夠的信息來計算重建線下的面積。

    形態抗鋸齒改進包含質量(形態學邊緣抑制、輪廓線檢測、U形*滑)和性能(延遲隊列、使用LDS、雙面混合)。文中談及的質量方面的知識點包含局部對比度邊緣抑制、形態邊緣抑制、輪廓線檢測、U型*滑,性能方面的知識點包含形態多通道法、使用Compute簡化為一個通道、像素著色器效率低下、模板剔除效率低下、延遲隊列、重復模式搜索、SMAA用CS解決問題的方法、使用LDS、雙線性獲取和解碼等。

    局部邊緣抑制圖例。對于給定的邊,如右圖中紅色標記的邊,檢查附*的邊緣,如果發現另一個邊緣在對比度上占主導地位,就會抑制它。

    通過(左)和不通過(右)當前邊緣的圖案的對比。其中右邊的方法會檢查哪一個得分更高(強度)如果不通過當前邊緣的圖案獲勝,則抑制邊緣。

    三種基本的U型模式。小U型圖案在時間上是不穩定,想要軟化或擺脫它們,使用LUT,可以根據感知調整。

    SMAA形態多通道方法。

    延遲隊列圖例。上:檢測到的邊被附加到附加/消耗緩沖區,執行間接分派以使用該緩沖區,允許所有線程執行實際工作。下:避免讀取全屏模板緩沖區,在某些情況下,由于邊緣分布分散,層次模板并不總是最佳的。文中調整了SMAA以使用類似的方法,顯著提高了性能,也適用于時間。

    文中也涉及了TAA的基礎,諸如抖動、采樣模式、重投影、速度等,還涉及了不一致(Disocclusion)和速度加權。不一致是指新幀中的像素在前一幀中不存在,解決方案是如果速度相差太大,不要混合,不適用于沒有速度的像素(alpha混合)。

    顏色信息也可用于不一致,加上速度加權,無法比較當前幀和上一幀,但由于不同的抖動,即使在靜態圖像上也會經常被拒絕。解決方案是比較N幀和N-2幀,在沒有速度的情況下減輕對象上的重影(alpha混合)。

    指數歷史/指數移動*均:SMAA T2x和類似使用兩個幀的技術:\(c_{aa}=0.5c_i+0.5c_i?1\)

    可以利用指數累積緩沖區:

    類似于多幀移動*均[Karis2004],增加有效子樣本數,反饋循環。

    指數累加緩沖技術通常使用鄰域夾緊來消除混淆[Lottes2011],其基本形式:

    \[p = \max?(\min(p, n_{max}), n_{min}) \]

    \(n_{min}\)\(n_{max}\)是3x3虛線鄰域中的最小和最大顏色。

    當使用這種夾緊與時間抖動一起使用時,可能會導致閃爍。

    在這種情況下,考慮到幾何體非常小,它在第二幀中消失了,在幀之間,顏色的鄰域范圍也發生了變化。使得輸出不一樣,從而在靜態圖像上產生閃爍的偽影。

    最*的速度:指數歷史*滑且抗鋸齒,新的幀速度是帶鋸齒的(在重投影時引入鋸齒),解決方案是湖區最前面的鄰域速度[Karis2014]。

    有關TAA的知識點還有形狀鄰域的YCoCg裁剪[Karis2014]、軟鄰域夾緊[Drobot2014]、高階重采樣[Drobot2014]、方差剪裁[Salvi2016]...

    之前已有不少相關的研究,如SMAA 1TX [Sousa2013]、Unreal Engine 4 TAA [Karis2014]、HRAA [Drobot2014]。文中給出的關鍵要點是時間濾波與AA解耦。使用指數歷史是一個可以與亞像素抖動的超采樣分離的過程,從現在起將被稱為臨時過濾,它可以在時間上過濾(或模糊)圖像。可以認為,作為一種副產品,它可以模糊或*均時間亞像素抖動,即使在不抖動的情況下,運動中的對象也會在每幀中自然地落在不同的子采樣位置,即使不使用抖動,運動中的對象也會有AA。Filmic SMAA T2x和SMAA T2x的對比如下:

    Filmic SMAA T2x SMAA T2x
    子樣本:2x(可選對比度感知的Quincunx約4x)
    邊緣:形態學
    時間過濾:
    子樣本:2x
    邊緣:形態學
    時間過濾:否
    減少鬼影
    少量的性能預算
    銳利
    在靜態圖像中穩定
    在沒有速度的物體上鬼影
    在靜態圖像中穩定

    文中涉及的時間抗鋸齒改進:

    • 分離時間超采樣和時間濾波。
    • 時間過濾銳利。
    • 時空對比度跟蹤。
    • 基于FPS的時域過濾。
    • 用深度測試擴展鄰域夾緊。
    • 改進的時間超采樣重采樣。
    • 改進的時間Quincunx。
    • 超采樣導數。
    • 改進的顏色權重。
    • 低輪廓的速度緩沖區。
    • 更快的最*速度。
    • 時間上采樣。

    Drobot2014的單通道和Filmic的雙通道對比:

    它們的方法和效果對比:

    對于時間銳利度,使用了雙三次重采樣(Bicubic Resampling),優化的Catmull Rom使用9個雙線性樣本處理4x4區域。最終的方案是忽略4個角會產生非常相似的結果,從9個樣本減少到5個。小數值誤差,可能可以應用在其它領域。

    左:原始的Bicubic;右:減少到5個樣本的*似方法。

    兩種方法的誤差。

    // 5樣本的采樣方法shader代碼
    float3 SMAAFilterHistory(SMAATexture2D colorTex, float2 texcoord, float4 rtMetrics)
    {
        float2 position = rtMetrics.zw * texcoord;
        float2 centerPosition = floor(position - 0.5) + 0.5;
        float2 f = position - centerPosition;
        float2 f2 = f * f;
        float2 f3 = f * f2;
     
        float c = SMAA_FILMIC_REPROJECTION_SHARPNESS / 100.0;
        float2 w0 =        -c  * f3 +  2.0 * c         * f2 - c * f;
        float2 w1 =  (2.0 - c) * f3 - (3.0 - c)        * f2         + 1.0;
        float2 w2 = -(2.0 - c) * f3 + (3.0 -  2.0 * c) * f2 + c * f;
        float2 w3 =         c  * f3 -                c * f2;
     
        float2 w12 = w1 + w2;
        float2 tc12 = rtMetrics.xy * (centerPosition + w2 / w12);
        float3 centerColor = SMAASample(colorTex, float2(tc12.x, tc12.y)).rgb;
     
        float2 tc0 = rtMetrics.xy * (centerPosition - 1.0);
        float2 tc3 = rtMetrics.xy * (centerPosition + 2.0);
        float4 color = float4(SMAASample(colorTex, float2(tc12.x, tc0.y )).rgb, 1.0) * (w12.x * w0.y ) +
                       float4(SMAASample(colorTex, float2(tc0.x,  tc12.y)).rgb, 1.0) * (w0.x  * w12.y) +
                       float4(centerColor,                                      1.0) * (w12.x * w12.y) +
                       float4(SMAASample(colorTex, float2(tc3.x,  tc12.y)).rgb, 1.0) * (w3.x  * w12.y) +
                       float4(SMAASample(colorTex, float2(tc12.x, tc3.y )).rgb, 1.0) * (w12.x * w3.y );
    
        return color.rgb * rcp(color.a);
    }
    

    改進的時間超采樣重采樣:

    嘗試了不同的*滑曲線,選擇了下圖上排右邊的曲線,因為它具有最強的S形。接著要做的是用立方**滑來*似它,使用了夾緊和重縮放,也就是下排右邊的圖:

    效果如下:

    文中改進了Quincunx采樣。結合下圖,Quincunx的定義是:\(0.5 \times 藍色樣本 + 0.5 \times 橙色樣本\)

    其中橙色是單一樣本,可以用雙線性來獲取全部藍色樣本,Quincunx變成紋理坐標移量,與2x相同的性能。對比度感知的Quincunx的想法:根據局部對比度來調整紋理坐標的偏移量,低對比度(紋理細節)用2x,高對比度(邊緣)用Quincunx,使用Quincunx子樣本確定對比度,如果對比度低,則使用2x偏移重新獲取,開銷低:約0.02毫秒PS4@1080。

    文中還涉及了很多技術改進細節和對比,有興趣的童鞋可以點擊原文閱讀。總之,Filmic SMAA T2x的亮點是良好的銳利、高健壯性及高性能。

    14.5.4.3 High Dynamic Range Imaging

    高動態范圍(HDR)圖像和視頻包含像素,這些像素可以代表比現有標準動態范圍圖像更大的顏色和亮度范圍。這種“更好的像素”極大地提高了視覺內容的整體質量,使其看起來更真實,對觀眾更有吸引力。HDR是未來成像管線的關鍵技術之一,它將改變數字視覺內容的表示和操作方式。

    High Dynamic Range Imaging對HDR方法和技術進行了廣泛的回顧,并介紹了HDR圖像感知背后的基本概念,也回顧了HDR成像技術的現狀。它涵蓋了與用相機捕捉HDR內容以及用計算機圖形學方法生成內容相關的主題;HDR圖像和視頻的編碼和壓縮;用于在標準動態范圍顯示器上顯示HDR內容的色調映射;反向色調映射,用于放大傳統內容,以便在HDR顯示器上顯示;提供HDR范圍的顯示技術;最后是適合HDR內容的圖像和視頻質量指標。

    圖左:透明實體代表人眼可見的整個色域。在較低的亮度水*下,隨著顏色感知的降低,固體逐漸向底部傾斜。為了便于比較,內部的紅色固體代表標準sRGB(Rec. 709)色域,由高質量顯示器產生。圖右:與CRT和LDR監視器上顯示的亮度范圍相比的真實亮度值。大多數數字內容的存儲格式最多能保留典型顯示器的動態范圍。

    從應用角度來看,HDR圖像的質量往往要高于LDR,下圖是HDR和LDR視覺內容之間的潛在差異,而下表給出的數字只是一個例子,并不意味著是一個精確的參考。

    使用標準的高位深度編解碼器(如JPEG2000、JPEG XR或選定的H.264配置文件)對HDR圖像或視頻內容進行編碼。HDR像素需要編碼到一個亮度通道和兩個色度通道中,以確保顏色通道的良好去相關性和編碼值的感知一致性。標準壓縮可以選擇性地擴展,以便為鮮明對比的邊緣提供更好的編碼。

    下圖是向后兼容HDR壓縮的典型編碼方案,深棕色框表示視頻編解碼器的標準(通常為8位)圖像,如H.264或JPEG。

    基于僅向前的視覺模型的色調映射的典型處理管線如下圖。原始圖像使用視覺模型轉換為抽象表示,然后直接發送到顯示器。

    基于正向和逆向視覺模型的色調映射的典型處理管線見下圖。使用正向視覺模型將原始圖像轉換為抽象表示,可以選擇編輯,然后通過反向顯示模型轉換回物理圖像域。

    色調映射的典型處理管線解決了一個約束映射問題,使用默認參數對圖像進行色調映射,然后使用視覺度量將顯示的圖像與原始HDR圖像進行比較。然后,在迭代優化循環中使用度量中的標量誤差值,以找到最佳色調映射參數。請注意,在實踐中,解決方案往往被簡化,并制定為二次規劃,甚至有一個封閉形式的解決方案。

    驅動HDR顯示器中的低分辨率背光調制器和高分辨率前LCD面板所需的圖像處理流程:

    HDR-VDP-2度量的處理階段,測試圖像和參考圖像經過相似的視覺建模階段,然后在單個空間和方向選擇帶(BT和BR)水*上進行比較。該差異用于預測可見度(檢測概率)或質量(感知的失真程度)。

    為色調映射的圖片(底部)預測動態范圍無關度量(頂部),綠色表示可見對比度的損失,藍色表示不可見對比度的放大,紅色表示對比度反轉。

    14.5.4.4 Texture Streaming

    Efficient Texture Streaming in 'Titanfall 2'分享了Titanfall 2的高效紋理流技術。紋理流動態加載以提高圖像質量,概念上是一種壓縮形式,常見方法有手動分割、邊界幾何測試、GPU反饋。工作流程要求盡量減少設計和藝術方面的手工工作,藝術家可以自由映射紋理(無固定密度),可以在不損害其它紋理的情況下添加MIPs,預處理應該是穩定的,一些好的手動暗示,與“資產面包房”合作,包括熱插拔。

    算法概述:任何低于64k的MIP都是永久性的,可以逐個添加/刪除MIP,使用預先計算的信息建立重要/不重要內容的列表,每一幀都對著這個列表工作。

    什么是“直方圖”?想要根據mip在屏幕上覆蓋的像素數(覆蓋率)來區分mip的優先級,而不僅僅是“是/否”,‘“直方圖”是每種材質每個MIP的覆蓋率,16個標量“箱”(通常為浮點數)——每個MIP一個。假設屏幕分辨率為256 x 256的4k x 4k紋理,變換和縮放分辨率和移動模型,使用低密度紋理貼圖適當地加權小的、被遮擋的或背面的三角形。

    算法-預計算:計算每種材質的直方圖,對于靜態物體,使用GPU渲染世界的每列,放入文件。對于動態物體,每個模型在加載時:計算每個三角形的紋理梯度,將三角形區域添加到MIP的直方圖區域,計劃從不同的角度進行項目,但不值得,手動調整比例因子以匹配靜態數據。

    每幀會發生什么?從磁盤播放播放器的“列”,添加模型覆蓋率,將覆蓋率除以紋素數量,得到一個“指標”,生成最重要和最不重要的MIP列表,更精細的MIPs級聯(更粗糙的始終>=更精細)。加載最重要的MIP,刪除最不重要的MIP,捕捉運行數量上限和每幀丟棄的字節數,除非你正在加載更重要的東西,否則不要丟掉東西!

    如何選擇探針?運行“rstream.exe”,實例化模型,計算邊界,將幾何圖形切為16英尺x16英尺的列,探針位于向上三角形上方的眼睛高度,添加提示探測(在附*的列中也使用Z),使用k-means組合成每列最多8個探針,將探測位置存儲在日志文件中以供調試使用。

    如何渲染探針?將靜態幾何信息上傳到GPU一次,渲染N個探針的UAV:

    float2 dx = ddx( interpolants.vTexCoord ) * STBSP_NOMINAL_TEX_RES; // STBSP_NOMINAL_TEX_RES is 4096.0
    float2 dy = ddy( interpolants.vTexCoord ) * STBSP_NOMINAL_TEX_RES;
    float d = max( dot( dx, dx ), dot( dy, dy ) );
    // miplevel is log2 of sqrt of unclamped_d. (MATERIAL_HISTOGRAM_BIN_COUNT is 16.)
    float mipLevel = floor( clamp( 0.5f * log2(d), 0.0f, (float)(MATERIAL_HISTOGRAM_BIN_COUNT - 1) ) );
    InterlockedAdd( outHistogram[interpolants.vMaterialId * MATERIAL_HISTOGRAM_BIN_COUNT + (uint)mipLevel], 1 );
    

    每個立方體面做一次(累積結果),不透明通過寫入深度,透明僅測試,沒有幀緩沖區!

    編譯探針數據:現在,有每種材質每個MIP在探針上的覆蓋率,在每列內以max方式組合探針,記錄材質ID、MIP數量、覆蓋范圍(4字節),每列存儲512條最重要的記錄,將4x4列分組為約32k的可流化頁面,索引到穩定的全局材質ID和位置,每個關卡一個‘.stbsp’文件。

    管理紋理資源:每個壓縮(和旋轉)紋理文件可能有一個“可流化”的片段,在為一個關卡構建快速加載的“rpak”文件時,會聚集到第二個“starpak”文件中。對于發布版本,在所有級別使用共享starpak,磁盤上僅復制了<64k的MIP,Starpak包含對齊的、準備加載的數據。

    // Crediting World Textures
    Compute column (x,y integer), Ensure active page is resident (cache 4 MRU), or request it.
    totalBinBias = Log2(NOMINAL_SCREEN_RES * halfFovX / (NOMINAL_TEX_RES * viewWidthInPixels) )
    For each material represented in column,
        For each texture in that material
            For each record (<material,bin,coverage>) in column (up to 16)
                If texture->lastFrame != thisFrame,
                    texture->accum[0..15] = 0, and texture->lastFrame = thisFrame
                mipForBinF = totalBinBias + record->bin + Log2(textureWidthInPixels)
                mipForBint = floor( max( 0.0, mipForBucketF ) ), clamped to (16-1).
                texture->accum[mipForBin] += record->coverage * renormFactorForStbspPage;
    
    // Crediting Models
    float distInUnits = sqrtf( Max( VectorDistSqr( pos, *pViewOrigin ), 1.0f ) );
    if ( distInUnits >= CUTOFF ) continue;
    float textureUnitsPerRepeat = STREAMINGTEXTUREMESHINFO_HISTOGRAM_BIN_0_CAP; // 0.5f
    float unitsPerScreen = tanOfHalfFov * distInUnits;
    float perspectiveScaleFactor = 1.0f / unitsPerScreen;
    // This is the rate of pixels per texel that maps to the cap on bin 0 of the mesh info.
    // ( Exponentiate by STREAMINGTEXTUREMESHINFO_HISTOGRAM_BIN_CAP_EXPBASE for other slots )
    float pixelsPerTextureRepeatBin0 = viewWidthPixels * textureUnitsPerRepeat * perspectiveScaleFactor;
    Float perspectiveScaleAreaFactor = perspectiveScaleFactor * perspectiveScaleFactor;
    pixelsPerTextureRepeatBinTerm0 = (int32)floorf(-Log2( pixelsPerTextureRepeatBin0 ); // Mip level for bin 0 if texture were 1x1.
                                                   
    For each texture t:
        if first use this frame, clear accum.
        if high priority, t->accum[clampedMipLevel] += HIGH_PRIORITY_CONSTANT (100000000.0f)
        For dim 0 and 1 (texture u,v):
            const int mipLevelForBinBase = (i32)FloorLog2( (u32)textureAsset->textureSize[dim] ) + pixelsPerTextureRepeatBinTerm0 ;
            For each bin
                // Log2 decreases by one per bin due to divide by two. (Each slot we double pixelsPerTextureRepeatBin0, which is in the denominator.)
                const int32 clampedMipLevel = clamp(mipLevelForBinBase - (i32)binIter, 0..15 )
    t->accum[clampedMipLevel] += modelMeshHistogram[binIter][dim] * perspectiveScaleAreaFactor;
        If accum exceeded a small ‘significance threshold’, update t’s last-used frame.
                  
                                                   
    // Prioritization
    For each texture mip,
        metric = accumulator * 65536.0f / (texelCount >> (2 * mipIndex));
        If used this frame:
            non-resident mips are added to ‘add list’, with metric.
            resident mips are added to ‘drop list’ with same metric.
        If not used this frame:
            all mips added to ‘drop list’ with metric of ( -metric + -frames_unused.)
                (also, clamped to finer mips’ metric + 0.01f, so coarser is always better)
    Then partial_sort the add and drop lists by metric to get best & worst 16.
         
                                                   
    // Add/Drop
    shift s_usedMemory queue
    for ( ; ( (shouldDropAllUnused && tDrop->metric < 0.0f) || s_usedMemory[0] > s_memoryTarget) && droppedSoFar <16MiB && tDrop != taskList.dropEnd; ++tDrop ) { drop tDrop, increase droppedSoFar; }
    for ( TextureStreamMgr_Task_t* t = taskList.loadBegin; t != tLoadEnd; ++t ) { // t points into to add list
        if ( we have 8 textures queued || t->metric <= bestMetricDropped ) break;
        if ( s_usedMemory[STREAMING_TEXTURES_MEMORY_LATENCY_FRAME_COUNT - 1] + memoryNeeded <= s_memoryTarget ) {
            for ( u32 memIter = 0; memIter != STREAMING_TEXTURES_MEMORY_LATENCY_FRAME_COUNT; ++memIter ) {
                s_usedMemory[memIter] += memoryNeeded; }
            if ( !begin loading t ) { s_usedMemory[0] -= memoryNeeded; } // failure eventually gets the memory back
    } else for ( ;; ) { // Look for ‘drop items’ to get rid of until we'll have enough room.
        if ( planToLoadLater + memoryNeeded + s_usedMemory[0] <= s_memoryTarget ) {
            planToLoadLater += memoryNeeded; break; }
        if ( droppedSoFar >= 16MiB || tDrop >= taskList.dropEnd || t->metric <= tDrop->metric ) { break; }
        bestMetricDropped = Max( bestMetricDropped, tDrop->metric );
        drop tDrop, increase droppedSoFar;
        ++tDrop; } }
    

    如何調整紋理大小?在Windows/DirectX下,最初的CPU可寫紋理、貼圖、讀取新MIPs,創建GPU紋理,GPU復制新的和舊的MIPs,現在只需加載到堆中并傳遞到CreateTexture。控制臺下,直接讀取新的MIP進來,放入排隊3幀,以刷新管線。

    異步I/O:異步線程,運行中的2個請求,多優先級隊列,紋理優先級低,音頻優先級很高,為了提高可中斷性,讀取以64kb的數據塊進行。

    14.5.4.5 Frame Graph

    FrameGraph: Extensible Rendering Architecture in Frostbite闡述了2017年的Frostbite的演變歷史,最終采用了幀圖的方式。

    Frostbite引擎在07年(左)和17年(右)的渲染系統對比。

    渲染體系的簡化圖如下:

    其中WorldRenderer協調所有渲染,采用代碼驅動的架構,是主要的世界幾何(通過著色系統),照明、后處理(通過渲染上下文),掌管所有視圖和渲染通道,在系統之間管理設置和資源,分配資源(渲染目標、緩沖區)。

    WorldRenderer面臨諸多挑戰,例如顯式立即模式渲染,顯性資源管理,定制、手工制作的ESRAM管理,不同游戲團隊的多種實現,渲染系統之間的緊密耦合。有限的可擴展性,游戲團隊必須fork / diverge才能定制,從4k增長到15k SLOC,具有超過2k SLOC的單個功能,維護、擴展和合并/集成成本高昂。

    WorldRenderer模塊化的目標是高層次知識框架,改進的可擴展性,解耦和可組合的代碼模塊,自動資源管理,更好的可視化和診斷。新的架構組件如下:

    h3.png)

    其中幀圖(Frame Graph)是渲染通道和資源的高級表示,完全掌控整幀;臨時資源系統(Transient Resource System)負責資源分配、內存重疊。它的目標是建立整幀的高層次信息,簡化資源管理,簡化渲染管線配置,簡化異步計算和資源屏障,允許獨立且高效的渲染模塊,可視化和調試復雜的渲染管線。

    引擎資源的生命周期視圖,引用十分復雜。

    幀圖的設計是遠離立即模式渲染,將代碼拆分為通道的渲染,多階段保留模式渲染API:設置階段、編譯階段、執行階段,每一幀都是從零開始建造的,代碼驅動的架構。

    設置階段定義渲染/計算通道,定義每個通道的輸入和輸出資源,代碼流類似于立即模式渲染。

    // 資源示例
    RenderPass::RenderPass(FrameGraphBuilder& builder)
    {
        // Declare new transient resource
        FrameGraphTextureDesc desc;
        desc.width = 1280;
        desc.height = 720;
        desc.format = RenderFormat_D32_FLOAT;
        desc.initialSate = FrameGraphTextureDesc::Clear;
        m_renderTarget = builder.createTexture(desc);
    }
    
    // 設置示例
    RenderPass::RenderPass(FrameGraphBuilder& builder, FrameGraphResource input, FrameGraphMutableResource renderTarget)
    {
        // Declare resource dependencies
        m_input = builder.read(input, readFlags);
        m_renderTarget = builder.write(renderTarget, writeFlags);
    }
    

    高級的幀圖操作:延遲創建資源,盡早聲明資源,在第一次實際使用時分配,基于使用情況的自動資源綁定標志。派生資源參數,根據輸入大小/格式創建渲染通道輸出,根據使用情況派生綁定標志。移動子資源,將一種資源轉發給另一種資源,自動創建子資源視圖/重疊,允許“時間旅行”。

    移動子資源示例。

    編譯階段剔除未引用的資源和通道,在聲明階段可能會有點粗糙,旨在降低配置的復雜性,簡化條件傳遞、調試渲染等。計算資源生命周期。根據使用情況分配具體的GPU資源,簡單貪婪分配算法,首次使用前獲得,最后一次使用后釋放,延長異步計算的生命周期,根據使用情況派生資源綁定標志。

    上圖處于調試的特殊渲染模式,因此會剔除掉紅框內的通道和資源。

    執行階段為每個渲染通道執行回調函數,立即模式的渲染代碼,使用熟悉的RenderContext API,設置狀態、資源、著色器、繪制調用、派發,從設置階段生成的句柄中獲取真正的GPU資源。

    異步計算:可以自動從依賴關系圖派生,需要手動控制,節省性能的潛力很大,但是內存增加,如果使用不當,可能會影響性能。每次渲染通道選擇性加入,在主時間線上開始,第一次使用另一個隊列上的輸出資源時的同步點,資源生命周期自動延長到同步點。

    Async compute的同步點示意圖。

    // 異步設置示例
    AmbientOcclusionPass::AmbientOcclusionPass(FrameGraphBuilder& builder)
    {
        // The only change required to make this pass
        // and all its child passes run on async queue
        builder.asyncComputeEnable(true);
    
        // Rest of the setup code is unaffected
        // …        
    }
    

    渲染模塊:

    • 有兩種類型的渲染模塊:

      • 獨立無狀態函數。輸入和輸出是幀圖資源句柄,可以創建嵌套的渲染通道,Frostbite中最常見的模塊類型。

      • 持久化渲染模塊。可能有一些持久性資源(LUT、歷史緩沖區等)。

    • WorldRenderer仍在協調高級渲染。不分配任何GPU資源,只需在高級啟動渲染模塊,更容易擴展,代碼大小從15K減少到5K SLOC。

    模塊之間的通訊:模塊可以通過黑板進行通信,組件哈希表,通過組件類型ID訪問,允許受控耦合。

    void BlurModule::renderBlurPyramid(FrameGraph& frameGraph, FrameGraphBlackboard& blackboard)
    {
        // Produce blur pyramid in the blur module
        auto& blurData = blackboard.add<BlurPyramidData>();
        addBlurPyramidPass(frameGraph, blurData);
    }
    
    #include ”BlurModule.h”
    void TonemapModule::createBlurPyramid(FrameGraph& frameGraph, const FrameGraphBlackboard& blackboard)
    {
        // Consume blur pyramid in a different module
        const auto& blurData = blackboard.get<BlurPyramidData>();
        addTonemapPass(frameGraph, blurData);
    }
    

    UE的RDG沒有blackboard的概念,所有信息都放到FRDGBuilder(類似于FrameGraph)中。

    臨時資源系統(Transient resource system):Transient是活動時間不超過一幀的資源,如緩沖區、深度和顏色目標、UAV等。在1幀內盡量減少資源使用時間,在使用資源的地方分配資源,直接在葉子節點渲染系統中,盡快釋放分配,使編寫獨立功能變得更容易。是幀圖的關鍵組件。

    臨時資源系統的實現取決于*臺功能,物理內存中的重疊(XB1)、虛擬內存中的重疊(DX12、PS4)、對象池(DX11)。用于緩沖區的原子線性分配器沒有重疊,只用于快速傳輸內存,主要用于向GPU發送數據。紋理的內存池。

    以下是不同*臺的臨時資源分配機制圖:



    內存重疊注意事項:一定要非常小心,確保有效的資源元數據狀態(FMASK、CMASK、DCC等),執行快速清除或放棄/重寫資源或禁用元數據,確保資源生命周期是正確的,比聽起來更難,考慮計算和圖形流水線,考慮異步計算,確保在重新使用之前將物理頁寫入內存。

    資源的丟棄和清除:必須是新分配資源上的第一個操作,要求資源處于渲染目標或深度寫入狀態,初始化資源元數據(HTILE、CMASK、FMASK、DCC等),類似于執行快速清除,資源內容未定義(未實際清除),如果可能的話,寧愿放棄資源也不要清除。

    重疊屏障(Aliasing barriers):在GPU上的工作之間添加同步,添加必要的緩存刷新,使用精確的屏障將性能成本降至最低,可以在困難的情況下使用通配符屏障(但預期IHV分裂),在DirectX 12中批量處理所有其它資源屏障!

    重疊屏障示例。上:管線化CS和PS工作導致的潛在重疊危險,CS和PS使用不同的D3D資源,所以過渡屏障是不夠的,必須在PS之前刷新CS或延長CS資源生命周期。下:串行計算工作確保了內存重疊時的正確性,在某些情況下可能會影響性能,當重疊對性能至關重要時,使用顯式異步計算。

    720p下使用重疊內存前(上)后(下)的對比。其中下圖是DX12的內存重疊布局,可以節省*50%的內存占用,4k分辨率下可以節省超過50%。

    總之,整幀的信息有很多好處,通過資源重疊節省大量內存,半自動異步計算,簡化渲染管線配置,很好的可視化和診斷工具,圖形是渲染管線的一種有吸引力的表示形式,直觀而熟悉的概念,類似于CPU作業圖或著色器圖,現代C++功能減輕了保留模式API的痛苦。更多可參閱:

    14.5.4.6 Display Latency

    Controller to Display Latency in 'Call of Duty'詳細且深入地討論了控制器的顯示延遲:玩家按下按鈕和在屏幕上看到按下結果之間的最短持續時間,還介紹Call of Duty游戲中添加的動態調節功能,以控制影響輸入延遲的權衡,最終目標是減少控制器到顯示器的延遲。最先討論如何測量延遲,然后將深入研究引擎的特定方面,這些方面必須考慮到節流閥(throttle)。玩家按下控制鍵按鈕到看到畫面的流程實際上包含了以下方面的步驟或階段:

    上面的Controller/OS sample、Game engine query、Game logic and rendering、Video scan-out是在游戲引擎涉及的階段。

    首先要明白,延遲不等于性能,延遲只是對性能變化的適應性,延遲在整個渲染管線中的流向圖和簡化圖如下:

    COD將采用的減少延遲的策略是在輸入樣本之前引入一個節流閥(throttle)。通過在此處添加延遲,輸入樣本將被壓縮到更接*幀末尾的位置。

    為了更容易地考慮這個限制,可以將延遲持續時間分為兩類。首先是工作,工作是為這個特定的幀積極處理某些東西所花費的時間:例如游戲邏輯或渲染,就是下圖的彩色框中表示的時間。

    除了工作的其它一切,可稱之為“slop”:

    Slop不一定是空閑時間:它通常是一條時間線在前一幀上工作的片段,或者一條時間線在等待另一條時間線釋放共享資源的片段。如果你看一個通用的生產者-消費者系統,生產者執行一個工作單元,將結果傳遞給消費者,然后開始生產下一個工作單元。如果消費者始終比生產者慢,系統將變為“消費者受限”。消費者的時間線保持完整,試圖跟上生產者,但生產者可以盡可能領先。這就是slop存在的原因:生產者領先于消費者,所以生產者何時完成與消費者何時開始之間存在差距。生產者的領先程度取決于生產者和消費者之間允許的緩沖量,以及它們獲取和釋放對緩沖數據的訪問的確切時間。

    另一方面,當我們被生產者限制時,slop通常會消失。消費者的時間線被釋放了,所以一旦生產者完成了一個畫面,消費者就可以立即開始,我們不會得到一個空隙。

    延遲輸入采樣時,slop會從第一段開始擠出,直到消失,然后移動到第二段,依此類推。另一方面,工作會在時間上向前移動,而整個幀的總工作持續時間理想情況下保持不變。

    延遲 = 工作+Slop,更高的slop ? 更高的延遲,更低的slop ? 更低的延遲。

    讓我們先看看延遲持續時間結束時會發生什么。游戲將最終的圖像渲染到一個名為幀緩沖區的內存中。然后,視頻掃描硬件從上到下逐行讀取幀緩沖區,并通過電纜將像素數據傳輸到顯示器。掃描輸出以與顯示器刷新率相匹配的速率連續傳輸。如今,傳統顯示器的刷新率為60Hz,因此幀緩沖區通常也會以60Hz的頻率掃描,或者每16.6ms掃描一次。16.6ms的大部分時間用于主動傳輸可見像素數據,但在傳輸的數據與可見像素不對應的情況下,會有短暫的暫停。首先,在每一行的末尾有一個暫停,稱為水*空白(HBLANK),在最后一行之后有一個暫停,稱為垂直空白(VBLANK)這些暫停在舊的CRT監視器上是物理上必要的,但由于遺留原因和傳輸元數據,它們今天仍然存在。

    如果我們把它放在一個時間軸上,我們得到掃描接著是vblank,掃描接著是vblank等等,所有這些都以固定的60Hz頻率發生。

    現在我們知道延遲時間的結束是固定的:每16.6毫秒發生一次,意味著我們可以提前準確地預測這一幀的掃描何時開始。我們現在的目標是找出與固定掃描相關的其它時間線的位置。向上移動,GPU將圖像渲染到幀緩沖區。

    掃描輸出是不斷從幀緩沖區讀取數據并將數據發送到顯示器,意味著我們同時在同一個內存中讀寫:一種競爭條件。傳統的解決方案是雙緩沖:分配兩個幀緩沖區,并在每一幀渲染到備用緩沖區,在渲染到幀緩沖區A時,幀緩沖區B會被掃描出來,然后我們渲染到幀緩沖區B,然后從幀緩沖區A掃描出來。

    在GPU使用幀后立即翻轉是不夠的,比方說渲染完成,接著立即翻轉,A開始向外掃描,而B開始渲染。B在A的掃描結束之前完成渲染。如果翻轉現在發生,B完成渲染之后會發生什么?然后,掃描輸出將在從A讀取到從B讀取的中間切換,而無需重置掃描線位置。屏幕上生成的圖像將從緩沖區A掃描上半部分,而屏幕的下半部分將從緩沖區B掃描。這種偽影被稱為撕裂,當A和B之間有較大的水*移動時,最為明顯,比如游戲相機在旋轉。解決這個問題需要另一條規則:只在掃描完成后翻轉,換言之,僅在VBLANK期間翻轉,此規則稱為垂直空白同步(VSyync)

    簡而言之,中掃描渲(Mid-scan)染用雙緩沖修正,撕裂用等待vblank翻轉修正。使用60Hz和vsync的雙緩沖,在vblank期間每次掃描出后,幀緩沖區A和B之間會發生翻轉。

    結合下圖,假設GPU正在渲染到幀緩沖區A中,GPU不允許觸摸A的時間段是藍色大矩形中的區域。在第一個階段,GPU必須空閑等待幀緩沖區A可用,對A的掃描完成后,可以開始渲染到A。一旦GPU完成渲染,不能立即翻轉,因為會導致撕裂。相反,GPU“將翻轉排隊”:它表示A已準備好在下一次vblank中翻轉。這是slop(斜坡)的第一個來源:介于翻轉隊列和實際翻轉之間。

    如果計算slop持續時間,它是約16.6ms減去GPU的總工作時間。但請記住,我們希望盡可能多地使用可用硬件,GPU周期是一種特別有價值的資源,如果GPU的工作負載一直很低,就相當于浪費。如果我們工作做得好,可以以更高的分辨率繪制更多的圖形。如果在更重的GPU負載下,slop會發生什么?

    當GPU工作負載滿時,接*16.6ms,slop消失。但請記住,節流閥應該將slop擠出延遲持續時間。如果這里沒有斜坡,那么首先查看GPU進行掃描有什么意義?

    事實上,事情要復雜一點,實際上有一個巨大的slop源,即使GPU負載很重。原因是,幀的大多數繪制都是在其它地方渲染的,而不是在幀緩沖區中。幾乎所有的GPU時間都花在了渲染屏幕外緩沖區上,大部分3D繪圖都渲染到了第三個屏幕外緩沖區,稱為“場景緩沖區”。場景緩沖區的分辨率可以低于最終顯示分辨率,并且可以使用不同的顏色編碼。場景渲染完成后,場景緩沖區將上采樣到幀緩沖區中,通常在上采樣期間應用時間抗鋸齒。在上采樣之后,UI元素以顯示分辨率呈現到幀緩沖區中,然后是排隊等待翻轉的幀緩沖區。這里最重要的一點是,大部分GPU幀時間都花在渲染3D場景上,幀緩沖區直到幀后期的上采樣才被觸及。這并不完全是三重緩沖,因為場景緩沖永遠不會掃描到顯示器上。但是計時的結果類似于三重緩沖,所以我們稱之為“偽三重緩沖”。

    在最初的雙緩沖設置中,GPU需要在幀的最開始處等待幀緩沖,將GPU時間線與掃描輸出時間線同步。現在有了偽三重緩沖,GPU只需要在幀的后期,即上采樣之前等待幀緩沖。

    對于嚴格的雙緩沖,只允許在藍色矩形之間的時間段內渲染幀緩沖區A。

    現在,GPU工作負載分為兩部分:場景渲染(不使用幀緩沖區)和幀緩沖區渲染(使用幀緩沖區)。幀緩沖區渲染部分仍然必須位于兩個藍色矩形之間,以避免中間掃描渲染,但場景渲染部分可以隨時啟動。

    一切都被允許在時間上向后移動,GPU在完成最后一幀后立即開始場景渲染,幀緩沖區渲染仍然必須與掃描輸出同步,但它會被移回場景渲染留下的空間。

    在嚴格的雙緩沖中,slop僅為~16.6ms減去GPU的總工作時間,隨著GPU工作負載的增加,slop消失了。

    現在使用偽三重緩沖,slop被分成兩部分。第一個slop持續時間是幀緩沖區等待:介于場景渲染結束和幀緩沖區渲染開始之間,等于16.6ms減去GPU的總工作量,第二個slop持續時間介于翻轉隊列和實際翻轉之間,大約16.6ms減去幀緩沖區渲染工作負載。

    一個完整的GPU工作負載通常意味著更多的場景渲染:更多的3D對象和更高的場景分辨率,幀緩沖區渲染通常很短。因此,當GPU工作負載滿時,第一個slop仍然會消失,而第二個slop可以保持很長時間。

    即使充分利用,也可能有很多slop。

    但偽三重緩沖還有另一個后果。到目前為止,我們一直假設GPU的工作負載總是快于16.6ms。但是,如果你想讓GPU盡可能地忙碌,很容易不小心會有一點過度,使得幀變慢。假設在嚴格的雙緩沖情況下,幀B的渲染速度低于16.6ms。在即將到來的vblank間隔之前,幀尚未準備好掃描,因此它“錯過了vblank”。沒有幀緩沖區排隊等待翻轉,因此在下一次vblank期間不會發生翻轉。取而代之的是,scan out保持指向幀緩沖區A,A再次被掃描出來,B的掃描必須等到下一個vblank開始。現在,由于A正在被第二次掃描,GPU必須一直等到下一次翻轉開始渲染A。如果渲染幀A也很慢,它會錯過另一個vblank,以此類推。

    如果GPU渲染時間始終低于刷新速度,即使是微秒,也會錯過每一個vblank幀速率將固定在30Hz而不是60Hz。這是一個非常災難性的后果,尤其是如果我們試圖讓GPU的工作負載盡可能滿。

    現在在偽三重緩沖的情況下,假設第一個vblank仍然丟失:幀緩沖區A仍然需要掃描兩次。但現在允許立即開始A的場景渲染,即使A仍在被掃描,因為GPU可以渲染到屏幕外緩沖區的時間沒有限制。這與雙緩沖區的情況有很大不同,在雙緩沖區的情況下,GPU必須一直閑置到下一個vblank。即使GPU的工作負載仍然大于16.6ms,A也不會錯過下一個vblank。

    縮小并查看多個幀上的行為(假設固定的、低于16.6ms的GPU工作負載)。第一幀未命中vblank,A被掃描兩次,但是下一幀,下一幀,下一幀都會生成它們的Vblank。最終,一個vblank確實會再次被忽略,但在六個緩慢的幀通過之前,不會錯過它們的vblank。

    使用偽三重緩沖時,*均幀率不會達到30Hz。取而代之的是,*均幀速率從60Hz慢慢下降到50Hz

    看看底部的大括號:這些是測量翻轉隊列和每個幀的實際翻轉之間的slop。請注意,在第一次錯過vblank之后,slop是高的。當慢速幀通過時,斜率逐漸減小。每一個慢幀都會消耗可用的slop,直到slop最終降到零以下,vblank就會丟失。這說明了slop的一個重要方面:slop充當緩沖空間,讓慢速幀在不丟失vblank的情況下通過

    能夠保持在50Hz比降到30Hz要好,但它仍然會產生一種令人不快的效果,即每隔幾幀就會錯過一幀。我們更愿意保持在一個穩定的60,這就是動態分辨率的來源——當游戲注意到GPU時間低于閾值時,引擎就會啟動。

    但現在有了動態分辨率,讓我們再次看看偽三重緩沖的時間線。幾幀緩慢的畫面經過,一幀也沒有漏掉。Slop變得非常低,但最終分辨率下降。現在,GPU的幀時間快于16.6ms。請注意,隨著快速幀的流逝,slop會累積回安全水*。其想法是,慢幀會吞食slop,而快幀會將其重建。使用偽三重緩沖帶來的額外slop,慢幀閾值可以提高到接*16.6ms,并且有可能在該閾值以上的峰值中存活。這種特性對于動態分辨率成為一種可行的技術至關重要。

    低slop意味著低延遲,但在慢幀上更容易錯過Vblank。高slop意味著更高的延遲,有緩沖空間,以容忍幾個慢幀。

    1、在一般情況下要最大化slop。在緩沖方面權衡額外的內存,給定緩沖范圍,考慮最大化slop。

    下面是一個非常簡單的例子,從最*的一個任務召喚游戲中,可以在實踐中最大化slop。添加HDR支持后,場景緩沖區-幀緩沖區分割發生了變化。第一個場景渲染通常對場景緩沖區執行,場景緩沖區被上采樣到顯示分辨率,但這次以顯示分辨率進入另一個屏幕外緩沖區,UI以屏幕外緩沖區的顯示分辨率呈現,最后,在幀的最后,該緩沖區被轉碼到其最終顏色空間中的實際幀緩沖區。請注意,第一次接觸真正的幀緩沖區是在轉碼之前。幀緩沖區僅用于幀時間的一小部分,即1080p時約170微秒。

    在一個*臺上,幀緩沖區最初是在幀的最開始處獲取的,完全抵消了偽三重緩沖的好處,使其行為與嚴格的雙緩沖相同。任何慢幀都會導致vblank丟失,從而降低動態分辨率的效率。

    在另一個*臺上情況有所好轉:等待幀緩沖區是在場景渲染后插入的,但當添加HDR支持時,此等待未被移動。此設置的性能優于前一種情況,但仍會導致幀緩沖區部分的長度超出必要的長度。即使允許幀比嚴格的雙緩沖更早開始,slop也沒有最大化,導致丟失Vblank的幾率高于最佳值。

    獲取幀緩沖區的正確位置就在轉碼之前,這將最大限度地提高slop,并最小化丟掉VBlank的機會。

    建議檢查代碼,確保等待幀緩沖區的時間盡可能晚。它通常是一個單一的函數,但當幀的結構發生變化時,很容易忘記移動它,因為當vsync關閉時,它對性能或計時沒有影響。對于DX12,在幀緩沖區資源上執行從當前狀態到渲染目標(或其它寫入)狀態的轉換屏障時,會發生等待。

    D3D12_RESOURCE_STATE_PRESENT → D3D12_RESOURCE_STATE_RENDER_TARGET
    

    其它*臺上也有類似的功能。還建議對這些等待進行計時,可以在這些調用周圍卡住GPU時間戳,并將測量結果納入GPU計時系統。啟用vsync時,必須從GPU總時間中減去這些值,才能獲得動態分辨率的精確幀定時。

    CPU的很大一部分工作負載被用來告訴GPU該做什么,意味著記錄狀態更改、繪制和分派到命令緩沖區中供GPU使用,以及生成相關的渲染數據,如動態頂點和索引緩沖區。CPU寫入的緩沖區和GPU讀取的緩沖區通常是雙緩沖區或從環中分配的,在CPU建立了命令緩沖區之后,它會告訴GPU通過“kick”開始處理它:這個kick事件類似于GPU和掃描輸出之間的幀緩沖區翻轉。就像GPU進行掃描一樣,slop可以在CPU的啟動和GPU實際啟動這一幀之間累積。

    不過,與GPU掃描系統不同的是,CPU記錄命令的順序與GPU使用命令的順序大致相同。例如,CPU上的一個幀可能由錄制prepass命令、陰影命令、不透明命令等組成。在kick之后,GPU也會按這個順序繪制。

    我們利用這個事實在CPU和GPU之間以比一幀更細的粒度分割緩沖區。CPU不需要預先生成整個幀的所有數據,并一次性啟動整個幀,而是可以在每個幀上進行多個較小的啟動。例如,CPU可以在記錄prepass前半部分的數據后立即啟動。GPU開始處理prepass,而CPU同時記錄prepass后半部分的數據,依此類推。這允許CPU和GPU幀之間有明顯的重疊。為了正確解釋這種重疊情況下延遲的工作方式,需要修改slop和work的定義。

    在不重疊的情況下,slop是從CPU幀的末尾到GPU幀的開頭的段。回顧slop和work的定義:當節流閥時,工作是延遲持續時間的一部分,它會隨著時間向前移動,但不會收縮;Slop是延遲持續時間縮短的部分。

    現在有了重疊的情況,GPU可以在CPU第一次啟動后立即開始工作,Slop需要定義為CPU的第一次啟動和GPU啟動幀之間的范圍。工作就是其它一切:CPU幀開始到第一次啟動之間的持續時間,加上整個GPU幀時間。請注意,僅通過允許重疊,測量的工作就顯著減少了。

    這并不是說CPU和GPU被迫重疊。CPU幀的結束和GPU幀的開始之間可能仍有延遲。因為GPU可以更早地啟動,所以slop的測量值會高得多。通過允許重疊,所做的就是允許throttle比在非重疊情況下擠壓得更遠。

    讓我們來談談性能。GPU周期通常是最寶貴的硬件資源:在《使命召喚》中,更有可能在GPU上工作繁重,而不是CPU上。在理想情況下,CPU記錄命令的速度始終比GPU消耗命令的速度快,這樣GPU就永遠不會空閑和閑置。

    但如果其中一個CPU段被kick得太晚,GPU可能會閑置:這被稱為GPU氣泡。當GPU和CPU明顯重疊,并且GPU只稍微落后于CPU時,氣泡的風險會更高,尤其是在幀的早期。氣泡是低效的,意味著空閑的GPU周期,但它也會使用于動態分辨率的GPU幀時間測量發生偏差,導致不必要的分辨率下降。

    但是,如果GPU和CPU之間有一些slop,那么這個slop可以作為緩沖空間,以最小化CPU峰值,產生一個較小的氣泡或一起避免氣泡。

    低slop——重疊越多,延遲越低,易受泡沫影響。高slop——重疊越少,延遲越高,有足夠的空間吸收尖峰,避免起泡。避免氣泡:并行命令緩沖區生成,調整繪制列表拆分,仔細安排工作,避免爭用。減少繪制調用次數,啟用CPU剔除、實例化、多重繪制。減少繪制調用開銷,如材質排序、bindless。

    現在我們有了超快速的命令緩沖區生成,它可以在多個內核上廣泛運行。一個線程提取完成的命令緩沖區,然后將它們kick到GPU,在理想情況下,GPU的運行速度比CPU慢,因此它保持穩定,并為整個幀提供數據。

    不幸的是,理想的情況并不總是發生。也許其中一個繪圖作業開始晚了,或者它被系統線程踢出了一個內核,或者它只是有太多的工作要做。提交線程必須等待作業完成后才能啟動命令緩沖區,如果等待時間太長,GPU就會閑置并產生氣泡。

    但是在一個代碼庫中有一件很酷的事情可以避免出現氣泡。提交線程對繪制作業的等待有一個超時,如果作業沒有及時完成,就會發出中止信號,通知作業停止。作業會定期檢查信號,如果檢測到中止,它會停止迭代并關閉命令緩沖區。然后提交線程接管作業并執行作業本應完成的所有工作。但這一次它不止一次,而是在工作的最后一次。通過更頻繁地kick,GPU比提交線程等待作業完成的時間要早一點獲得填鴨式的工作。這是以更頻繁的kick帶來的額外CPU和CP開銷為代價的,但它可能會減少氣泡的影響或完全避免氣泡。

    單幀CPU工作涉及許多系統的交互,下面是有一個非常簡單的總結:首先,從服務器獲取權威的游戲狀態,以更新客戶端游戲狀態;然后,對輸入進行采樣,并將輸入因素納入客戶端游戲模擬;最后,完成了渲染游戲模擬結果所需的所有工作。讓我們關注渲染部分。

    渲染分為許多小任務,包括遍歷場景圖、剔除、為模型拾取LOD等等,所有這些作業的最終輸出都是一組可見表面列表。然后將作業分配給迭代每個單獨的列表,這些作業生成命令緩沖區和相關的渲染數據。最后,一個作業收集所有已完成的命令段,并將它們kick到GPU。此作業還記錄表面列表中未包含的命令:解壓縮表面、后處理和二維圖形。

    從概念上講,所有這些工作可以分為三組:場景準備、繪圖和GPU提交。

    一幀上的所有工作都是多線程的,并盡可能廣泛地運行。然而,*均而言,場景準備和繪圖是從廣泛運行中受益最多的部分。與可用的內核相比,幀的開頭和結尾實現了更低的利用率,留下了更多空閑的CPU周期。

    為了使CPU內核更加一致地飽和,繪圖和提交工作被分開。然后在這一幀的場景準備之后,下一幀就可以立即開始了。這樣一來,下一幀的開頭與該幀的繪圖和提交重疊,可以產生相當大的加速。

    從概念上講,整個場景準備過程中的一切都可以稱為“客戶端幀”,有一個專門的客戶端線程來協調所有相關的作業。第二個渲染線程執行所有命令緩沖區提交、PostFX、2D繪圖等,并包含第二個并行“渲染幀”。在這兩條時間線之間是生成命令緩沖區的所有繪圖作業,客戶端啟動這些作業,渲染線程等待它們并啟動它們的結果。

    與其它生產者-消費者對一樣,slop可以在客戶端和渲染幀之間累積。

    但讓我們關注一下這兩條時間線之間的同步,以及它如何影響slop。首先,客戶端必須與GPU同步,因為它會寫入與場景渲染共享的緩沖區。這些資源是雙緩沖的,因此在客戶端可以啟動其幀之前,它必須等待兩幀前的GPU場景完成。客戶端和渲染線程也共享此數據。最初,渲染線程在下一個客戶端幀開始之前不允許啟動幀。

    縮小之后,底部是GPU時間線。客戶端幀的開始與兩幀前GPU場景渲染的結束同步。然后,渲染幀的開始與下一個客戶端幀的開始同步,會在客戶端幀的結束和渲染幀的開始之間創建一些slop。如果管線沒有綁定到渲染幀上,為什么會存在這種slop?

    渲染幀不能從這里開始,沒有正確的理由:就在當前客戶端幀的末尾。進行此更改對總延遲沒有影響:整個幀的總slop保持不變,只是從渲染幀的左側移動到右側。

    2、稍后在幀中移動slop。

    記住,slop可以防止尖峰,但并不是所有的slop都能*等地防止所有可能的尖峰。

    例如,如果客戶端幀出現尖峰,幀中稍后的所有slop源都可用于吸收尖峰并避免丟失幀。

    但是如果GPU出現尖峰,只有GPU幀后的slop可以吸收尖峰。

    需要盡快開始所有工作。

    同步已更改為允許渲染幀在客戶端幀完成后立即開始。現在請記住為什么渲染幀和客戶端幀不允許同時運行:因為客戶端寫入渲染線程讀取的緩沖區。

    事實上,渲染幀可以更早開始,從而允許它在客戶端幀結束之前開始工作。這種重疊在一個代碼分支中實現,它不會像CPU-GPU重疊那樣顯著減少延遲,但它仍然可以收回幾毫秒。這種重疊是通過在客戶端和渲染器之間分割共享數據并最小化分割之間的依賴關系來實現的,重疊的程度取決于數據拆分可以在客戶端幀依賴關系中執行的距離。

    需要修改客戶端和渲染幀之間的slop定義,以考慮這種重疊:現在是在客戶端中較早喚醒渲染線程和渲染幀實際啟動之間的延遲。

    關于同步還有最后一件事。請記住,只有GPU在兩幀前完成場景,客戶端才能啟動,可以保護對CPU和GPU之間共享的雙緩沖數據的訪問。但是,這些緩沖區僅在渲染相關操作(場景準備、繪制和提交)期間由CPU寫入。在幀的前半部分,所有的客戶端模擬工作都不會觸及GPU可見緩沖區(下圖上)。這意味著等待可以在稍后的幀中移動,就在寫入任何共享緩沖區之前。(下圖下)

    通過此更改,客戶端幀被分為兩部分:一部分在與GPU同步之前,另一部分在與GPU同步之后。GPU現在等待在輸入樣本之后,因此在延遲路徑中引入了一個新的slop。

    現在我們已經查看了引擎中的所有主要時間線,并確定了延遲路徑中的所有工作和slop部分。


    現在,讓我們將所有這些整合到throttle實現中。我們在輸入樣本之前引入throttle,這樣樣本和后續工作就會延遲并向右移動。

    注意slop是如何從左邊開始被擠出的,但總的工作保持不變。


    現在,客戶端、渲染器和GPU之間不再有任何slop,throttle已經足夠長了。唯一剩下的slop在GPU和掃描輸出之間。

    如果我們再延遲一點,GPU工作的結束就會被推過vblank并錯過一幀。

    所以問題是:throttle應該等多久?throttle位于客戶端幀的中間,就在輸入示例之前。延遲持續時間的結束是在未來的vblank期間,此時該幀將被翻轉。從現在到預期翻轉之間的時間是我們正在解決的throttle加上幀剩余部分的工作和slop。求解throttle,我們得到從現在到vblank的持續時間減去總工作和slop。但請記住,throttle接*幀的開始,在它運行之前,我們不知道會有多少工作和slop。

    取而代之的是,我們必須根據之前的幀數據來估計此幀將有多少工作,而且必須事先決定要達到此幀的目標slop值。問題就變成了:給定一個目標量的slop,throttle應該休眠多長時間?

    讓我們先看看如何找到預期翻轉的時間,需要計算當打算翻轉時的vblank的絕對時間。因為每16.6ms出現一次vblank,所以未來幀的vblank可以根據之前的幀計時進行推斷。

    翻轉時間:專用的高優先級線程,等待GPU中斷事件,自己使用API提供的時間戳或時間戳。Xbox One的翻轉時間過程:1、在當前狀態下傳遞引擎的幀索引:

    DXGIX_PRESENTARRAY_PARAMETERS params
    params.Cookie = [internal frame index]
    ...
    DXGIXPresentArray( ..., &params )
    

    2、查詢幀統計信息以獲取時間:

    DXGIX_FRAME_STATISTICS stats[4]
    DXGIXGetFrameStatistics( 4, stats )
    

    然后獲取首個有效的stats[i].Cookiestats[i].CPUTimeFlip

    為了預測這一幀的工作量,需要從之前幀中收集工作段的時間戳。在throttle打開之前,需要收集并匯總最新的測量結果,以從前一幀中獲得總的工作估計。因為throttle在客戶機時間線中,所以客戶機工作測量不需要雙緩沖,而是可以直接從最后一幀讀取,因為它們保證是完整的。但是,由于客戶端幀與渲染和GPU幀同時運行,所以在收集時間戳時,工作持續時間可能會處于運行狀態,從而導致開始時間戳位于結束時間戳之后。相反,非客戶端時間線上的時間戳可以進行雙緩沖。每對時間戳中至少應有一個完整:通過比較兩對的開始和結束時間,可以確定最*的有效時間戳對。

    newEstimate = estimate * smooth + work * (1 - smooth)
    

    目標slop:減少延遲與漏幀風險?值判斷;工作有多緊張?從工作值的歷史中估算方差;可調整的slop,根據方差進行調整,如果slop低于閾值,則設置為無限(unthrottled))。

    減少方差:持續的問題,需要定期重新審視,引擎內測量,通常由計劃不周的工作引起:重新安排依賴關系,限制關鍵作業可以在哪些核心上運行,拆分/合并作業。

    我們現在已經討論了計算throttle所需的每一項:下一幀應該翻轉的時間可以從之前的翻轉推斷出來,通過*滑前一幀的測量值,可以預測該幀的總工作,在這個幀中允許的slop可以通過基于觀察到的方差的啟發式方法來決定。使用這些項,我們可以計算客戶端幀在采樣輸入之前需要睡眠多長時間。

    退一步,讓我們回顧一下為什么我們必須通過識別引擎中每一個工作源和slop的過程。

    有了工作和slop的正確定義,throttle對這些值的影響變得非常可預測。使得在給定目標slop的情況下,計算合適的throttle變得容易。然后,測量的slop在一幀內迅速收斂到目標附*。

    當該文獻作者第一次研究延遲時,將引擎視為一個大黑匣子,把大黑匣子里的一切都稱為工作,唯一的問題是從隊列翻轉到翻轉的持續時間,忽略了發動機內部所有額外的slop源,但它使功測量變得容易得多。

    問題是throttle不再是可預測的,一些最初的throttle擠壓是有效的,但不會影響slop。

    不再是線性的,需要搜索多個幀。


    結果:經審核的數據流和同步提高CPU密集型場景的幀率,修復錯誤,增加了throttle的測量。*臺1下,帶throttle:~*均幀可節省5毫秒延遲,提前移動幀緩沖區等待:避免重場景中的vblank未命中。*臺2下,帶throttle:約*均每幀減少22毫秒延遲,幀緩沖區等待已在幀中延遲。未來的工作:關注內容和解決方案的工作預測,可變刷新率支持,延遲輸入樣本重投影。建議:默認情況下打開Vsync的配置文件,使用引擎內定時器可視化延遲,映射從輸入樣本到掃描輸出的數據路徑,確保同步盡可能緊密,尋找重疊的機會,測量并減少方差。

    14.5.4.7 Mesh Shading

    Mesh Shading: Towards Greater Efficiency of Geometry Processing分享了Mesh著色器的技術,包含簡史、背景和動力、網格著色編程模型、新興應用和未來方向。圖形管線vs計算著色器是GPU的人格分裂:

    如果將計算管線化進光柵呢?

    基本網格著色模型:

    Meshlet是屏幕空間的標準化接口:

    網格著色器編程模型:應用程序定義的線程角色,比如計算、協同生成輸出網格,結合頂點和幾何體著色,假設面與頂點的比率是固定的,有限動態擴展。

    輸入表示是應用程序定義的,自定義壓縮、非B-rep方案…可使用網格著色器id直接尋址。管線頂部的固定功能消失了…無索引重復數據消除,無頂點屬性提取。避免序列化點—可擴展性,負責利用頂點重用的應用程序,可以預計算優化的圖元分簇,運行時沒有重復工作-節省功率。

    動態擴展:幾何合成需要支持放大,推廣了細分擴展模型,刪除固定功能拓撲生成。

    帶有任務和網格著色器的幾何體管線:

    任務和網格包含舊著色器階段:

    網格著色器剔除:任務著色器剔除圖元分簇,*截頭體、背面、亞像素,逐圖元的FF剔除。利用預計算,局部化圖元分簇,預計算法線分布等。更緊湊,內存比索引緩沖區少25-50%!

    動態加載*衡:英偉達的“小行星”演示,每幀5000萬以上三角形,使用任務著色器的動態LOD,從一組預計算的LOD中進行選擇,生成要渲染的網格著色器,沒有CPU干預!

    自適應曲面細分:動態三角形細分格式,使用二進制密鑰進行高效編碼,多幀上的增量細化,網格著色器支持單通道管線,任務著色器更新隱式細分,網格著色器從二進制鍵解碼。

    還可以模擬dx11細分。總之,網格著色–幾何體的新編程模型,結合了計算的靈活性和流水線調度的效率,通過消除串行瓶頸優化管線,在幾何圖形處理中實現更高的效率和控制,支持網格著色的新應用程序的機會,如LOD管理、數據結構遍歷、幾何合成、程序化。

    14.5.4.8 Nanite

    A Deep Dive into Nanite Virtualized Geometry由Epic Games的Brian Karis等人在siggraph2021峰會上呈現,講述了UE5 Nanite的實現細節。

    行業的研發人員一直有個夢想,就是將幾何體虛擬化,就像使用紋理一樣,但無需更多的預算,直接使用電影質量源藝術,無需手動優化,沒有質量損失。但實現起來比虛擬紋理更難,不僅僅是內存管理,還有幾何細節直接的影響。Nanite團隊對比了體素、曲面細分、位移映射、點云、三角形等表達方式,最終發現比三角形更高質量或更快的解決方案,使用三角形是Nanite的核心(但其它表達方式也可用于其它方面的實現)。

    GPU驅動的流水線:渲染器仍處于保留模式,GPU場景表示在多幀之間保持不變,在事情發生變化的地方很少更新,單個大型資源中的所有頂點/索引數據。逐視圖進行GPU實例剔除、三角光柵化,如果僅繪制深度,則整個場景可以使用1個DrawIndirect繪制。三角形分簇剔除的方式先將三角形分成簇,為每個簇構建邊界數據,基于邊界剔除分簇,如視錐剔除、遮擋剔除等。

    其中遮擋剔除是基于層次Z緩沖區(HZB)的遮擋剔除,從邊界計算屏幕矩形,在屏幕矩形小于等于4x4像素的情況下,測試最低mip。怎么建立HZB?本幀還沒有渲染任何內容,將Z緩沖區從上一幀重新投影到當前幀?需要填洞才能有用,不保守。最終使用2通道的裁剪剔除,最后一幀的可見對象可能在此幀中仍然可見,至少是遮擋體的好選擇。2通道解決方案:繪制上一幀中可見的內容,構造HZB,繪制現在可見但不在最后一幀中的內容,幾乎完美的遮擋剔除!保守的,只有在可見性發生極端變化時才會出現視覺瑕疵。

    將可見性與材質分離,以消除在光柵化過程中切換著色器、材質計算過繪制、深度prepass避免過繪制、密集網格導致的像素quad效率低下,可選的方案有REYES、紋理空間著色、延遲材質。

    可見性緩沖區:將幾何數據寫入屏幕(深度、實例ID、三角形ID),每像素材質著色器:加載緩沖區,加載實例轉換,加載3個頂點索引,加載3個位置,將位置轉換為屏幕,導出像素的重心坐標,加載和插值屬性。聽起來很瘋狂?不像看上去那么慢,因為有大量緩存命中,沒有過繪制或像素quad的效率低下。材質通道寫入GBuffer,與其它延遲著色渲染器集成。現在可以用1個繪制調用繪制出所有不透明的幾何體,完全由GPU驅動,不僅僅是深度prepass,每個視圖柵格化一次三角形。

    次線性縮放:可見性緩沖區比以前快得多,但仍然與實例數和三角形數成線性比例。實例中的線性縮放是可以的,至少在通常希望加載的關卡的縮放范圍內。這里可以輕松處理一百萬個實例,三角形中的線性縮放不合適,如果線性擴展,就無法實現“不管你怎么努力都能成功”的目標。光線追蹤是LogN,很好但還不夠。即使渲染速度足夠快,也無法將這些場景的所有數據存儲在內存中。虛擬幾何部分與內存有關。但是光線追蹤對于Nanite的目標來說速度不夠快,即使它適合內存,需要比logN更好的。

    換一種說法,屏幕上只有這么多像素。為什么要畫更多的三角形而不是像素?對于簇,希望在每一幀中繪制相同數量的簇,而不管有多少對象或它們的密度。一般來說,渲染幾何體的成本應該隨著屏幕分辨率而定,而不是場景復雜度。就場景復雜度而言,意味著恒定的時間,而恒定的時間意味著LOD。

    LOD的解決方案是分簇層次結構(Cluster hierarchy),以簇為基礎確定LOD,建立層次結構的LOD,最簡單的是簇樹,父節點是子節點的簡化版(下圖左)。LOD運行時找到所需LOD樹的切割,基于知覺差異的視點依賴(下圖中)。使用流,整棵樹不需要同時存儲在內存中,可以把樹上的任何一塊都標記為葉子,然后把剩下的扔出去,渲染期間按需請求數據,類似虛擬紋理(下圖右)。

    如果每個簇獨立于相鄰簇決定LOD,則會出現裂縫!粗略的解決方案:在簡化過程中鎖定共享邊界邊,獨立的簇總是在邊界上匹配。鎖定的邊界:收集密集的雜亂部分(dense cruft),尤其是在深層的子樹之間。

    可以在構建過程中檢測到這些情況,分組簇:強迫它們做出同樣的LOD決策,現在可以自由解鎖共享邊并折疊它們。

    不同LOD的切換過程示意圖如下:

    LOD裂紋的選項:

    • 直接索引相鄰頂點。
      • *行視圖相關的詳細程度控制。
      • *行視圖相關的核心外漸進網格。
      • 無依賴并行漸進網格。
      • 需要能夠索引任何狀態下的邊界頂點,由于精度原因,不可能出現裂縫,在計算和內存方面復雜且昂貴,三角形的粒度太細了。
    • 裙子(skirt)。
      • 與鄰居的軟關系。
      • 體素沒有裂縫,為什么沒有?實體體積數據,而非邊界表示,將網格視為實體體積,分簇必須閉合,至少在移動范圍內。
      • 分塊LOD。
      • 只有當邊界是筆直的空間分割時。
    • 隱式依賴。空間意味著節點之間的依賴關系。
    • 顯式依賴。節點之間的依賴關系在構建和存儲期間確定。

    合批多重三角剖分是一個很好的理論框架,但Brian Karis發現這篇論文非常難以理解,因為它過于抽象和理論化。直到幾年后,在部分實現了QuickVDR之后,Brian Karis才再次嘗試重新閱讀它,發現它作為一個由多個方案組成的超級集合是多么有洞察力。構建步驟的分解與接下來將介紹的基本步驟相同,并進行一些調整。之前的工作會對三角形本身進行分組,從而使每組的三角形數量可變。但需要128個三角形的倍數,這樣它們就可以被分成正好128個的簇,將簇分組(而不是三角形分組)可以實現這一點。

    構建操作:分簇原始三角形,當NumClusters>1時:將簇分組以清理其共享邊界,將組中的三角形合并到共享列表中,將三角形數量簡化為50%,將簡化的三角形列表拆分為簇(128個三角形)。

    合并和拆分使其成為DAG而不是樹:

    DAG:哪些簇需要分組?將那些具有最多共享邊界邊的對象分組,更少的邊界邊更少的鎖定邊,這個問題稱為圖分區。最小化邊切割代價圖的劃分優化,圖節點=簇,圖邊=用直接連接的三角形連接簇,圖邊權重=共享三角形邊的數量,用于在空間上閉合簇的附加圖邊,為孤島情況添加空間信息、最小圖邊切割最小鎖定邊,使用METIS庫來解決。

    圖劃分:挑兩個,希望剩下的都能解決,簇邊界邊的數量,每簇的三角形數量<=最大值。與簇分組問題完全相同,圖是網格的對偶。需要嚴格的分區大小上限,圖分區算法不能保證這一點,設法用小缺口(small slack)和fallback來強制它。

    網格簡化:邊緣折疊,首先選擇最小誤差邊,使用二次誤差度量(QEM)計算的誤差,優化新頂點的位置,使誤差最小,高度細化,返回引入的錯誤估計,稍后投影到屏幕上的像素數出現錯誤,也是最難的部分。

    誤差度量:基本二次曲面是面積上距離^2誤差的積分,具有屬性的二次曲面將所有錯誤與權重混合在一起,完全啟發式hack。能做得更好嗎?Hausdorff網格距離?渲染結果并使用基于圖像的感知?沒有比率失真優化的概念。導入和構建時間也很重要,Nanite builder的所有代碼都經過了高度優化,希望折疊以優化與返回的像素錯誤相同的度量。縮放獨立性,需要知道屏幕上的尺寸才能知道權重,雞和蛋的問題,假設大多數集群以恒定的屏幕大小繪制,表面積歸一化,邊長度限制,大量的調整,非常注意浮點精度,二次曲面中的許多地方都具有固有的災難性相消。

    下面闡述運行時視圖相關的LOD。首先是LOD的選擇。具有相同邊界但LOD不同的兩個子圖,根據屏幕空間誤差在它們之間進行選擇,由投影到屏幕的simplifier計算的誤差,修正了球體邊界中最壞情況點的距離和角度失真,組中的所有簇必須做出相同的LOD決策,相同的輸入=>相同的輸出。

    LOD并行選擇:LOD選擇對應于剪切DAG,如何并行計算?不想在運行時遍歷DAG。定義切割的是父子節點之間的差異。在以下情況下繪制簇:父節點誤差太高且當前節點的誤差很小,可以并行評估(下圖左)!只有當有一個獨特的切割,強制誤差是單調的(monotonic),父視圖誤差>=子視圖誤差,仔細執行以確保運行時更正也是單調的(下圖右)。

    無縫LOD:二元選擇父或子,這不會產生明顯的跳變嗎?需要*穩過渡嗎?涉及幾何過渡(Geomorphing)和跨簇過渡。如果誤差小于1像素,則它們會有細微差別,TAA將任何差異視為鋸齒。

    基于表面角度的LOD:簡化產生的簇誤差是對象空間的標量,未知方向,位置誤差可能是方向性的,屬性錯誤的混合使得這很困難。投影到屏幕不考慮表面角度,類似于如果mipmap僅僅是距離的函數,也適用于細分因子計算。表示在細分上掃掠角度曲面,求解需要各向異性LOD,不可能通過簇選擇,簇選擇必須是各向同性的,就像mip選擇一樣。其它方案也會產生掃視角成本,如基于點的過繪制、SDF和SVO中的表面讀取。

    層次LOD選擇:可見的簇可能是*的(全部來自單個實例)或遠的(來自不同實例的所有根簇)。需要分層,但是DAG遍歷是復雜的!記住:LOD決策完全是局部的,可以使用任何想要加速的數據結構!

    層次剔除:什么時候可以LOD剔除一個簇?

    ParentError<=閾值,基于ParentError的樹,而不是ClusterError!BVH8:子節點的最大ParentError,內部節點:8個子節點,葉節點:組中的簇列表。

    持久線程:理想的情況是父節點一結束就開始子節點,直接從compute生成子線程。而持久線程模型相反,無法生成新線程,重新使用它們!管理自己的作業隊列,單次調度,有足夠的工作線程來填充GPU,使用簡單的多生產者多消費者(MPMC)作業隊列在線程之間進行通信。層次剔除:當工作隊列不是空的,將節點提取出隊列,測試,讓通過測試的子節點入隊。單次dispatch,沒有遞歸深度或展開(fanout)限制,無需反復排空(drain)GPU,節省10-60%(通常約25%),具體取決于場景復雜度。依賴于調度行為,要求一旦一個組開始執行,它就不會無限期地挨餓,D3D或HLSL未定義調度行為,在控制臺和測試過的所有相關GPU上工作,僅是優化要求,而非Nanite的要求。分簇剔除:葉子是有著共同父親的簇,作為節點進行類似的剔除檢查,輸出可見簇。在同一個持久著色器中進行簇剔除,一次可能沒有足夠的活動BVH節點來填充GPU,執行時間最終可能由最深遍歷的深度決定,盡早開始簇剔除工作,并使用它來填補孔洞。兩個隊列,等待節點出現在節點隊列中時,從簇隊列處理,合并成64的批。

    2通道的遮擋剔除:顯式跟蹤以前可見的狀態變得復雜,LOD選擇可能不同,上一幀中可見的簇可能已經不在內存中了!測試當前選定的簇在最后一幀是否可見,使用以前的變換測試以前的HZB。

    剔除總覽:

    接下來聊光柵化。

    像素級細節:能用大于1個像素的三角形達到像素級細節嗎?取決于如何*滑,一般來說沒有。需要繪制像素大小的三角形。對于小三角形,如果用典型的光柵化器來說太可怕了。典型光柵化器:大型tile用binning,微型tile用4x4,輸出2x2像素quad,像素高度并行而非三角形。現代GPU設置最大4個三角形/clock,輸出SV_PrimitiveID會讓情況變得更糟,能用軟件光柵打敗硬件光柵嗎?實際上,軟件光柵是硬件光柵的3倍速度!!!

    對于小三角形,將三角形binning和只寫最后的像素一樣困難,即使是單個向量戳也會對小三角形進行浪費的測試,基本邊界框更快。在tile級別進行序列化,以處理深度和ROP,輸出2x2像素四邊形,通用的VS+PS調度、輸出格式、排序、混合、clip...針對覆蓋多個像素的較大三角形進行了優化,在像素上廣泛運行,想要很多像素的三角形,在三角形三運行。

    微型軟件光柵化器:128三角形簇=>線程組大小128,每個頂點1個線程,變換位置,存儲在groupshared中,如果超過128個頂點循環(最多2個)。每個三角形1個線程,獲取索引、變換的位置,計算邊緣方程和深度梯度,計算屏幕邊界矩形,對于rect中的所有像素,如果在所有邊內,則寫入像素。

    for( uint y = MinPixel.y; y < MaxPixel.y; y++ )
    {
        float CX0 = CY0;
        float CX1 = CY1;
        float CX2 = CY2;
        float ZX = ZY;
        
        for( uint x = MinPixel.x; x < MaxPixel.x; x++ )
        {
            if( min3( CX0, CX1, CX2 ) >= 0 )
            {
                WritePixel( PixelValue, uint2(x,y), ZX );
            }
            CX0 -= Edge01.y;
            CX1 -= Edge12.y;
            CX2 -= Edge20.y;
            ZX += GradZ.x;
        }
        
        CY0 += Edge01.x;
        CY1 += Edge12.x;
        CY2 += Edge20.x;
        ZY += GradZ.y;
    }
    

    硬件光柵化:大三角形使用硬件光柵化器,逐簇選擇軟件或硬件光柵化,還用于64b原子寫入UAV。

    掃描線軟件光柵化器:多大才算太大?比預期的要大得多,邊緣小于32像素的簇被軟件光柵化,在rect上迭代測試大量像素,最好的情況包括一半,最糟糕的情況是沒有一個(下圖左)。掃描線可以更快嗎?傳統的梯形比較復雜,很多設置和邊緣遍歷(下圖右)。

    for( uint y = MinPixel.y; y < MaxPixel.y; y++ )
    {
        float CX0 = CY0;
        float CX1 = CY1;
        float CX2 = CY2;
        float ZX = ZY;
        
        for( uint x = MinPixel.x; x < MaxPixel.x; x++ )
        {
            // 難道不能知道經過的x區間是多少嗎?
            if( min3( CX0, CX1, CX2 ) >= 0 )
            {
                WritePixel( PixelValue, uint2(x,y), ZX );
            }
            CX0 -= Edge01.y;
            CX1 -= Edge12.y;
            CX2 -= Edge20.y;
            ZX += GradZ.x;
        }
        
        CY0 += Edge01.x;
        CY1 += Edge12.x;
        CY2 += Edge20.x;
        ZY += GradZ.y;
    }
    
    // 改進版本
    float3 Edge012 = { Edge01.y, Edge12.y, Edge20.y };
    bool3 bOpenEdge = Edge012 < 0;
    float3 InvEdge012 = Edge012 == 0 ? 1e8 : rcp( Edge012 );
    
    for( uint y = MinPixel.y; y < MaxPixel.y; y++ )
    {
        // 這不再是固定點,也就是說它不完全相同。
        float3 CrossX = float3( CY0, CY1, CY2 ) * InvEdge012;
        float3 MinX = bOpenEdge ? CrossX : 0;
        float3 MaxX = bOpenEdge ? MaxPixel.x - MinPixel.x : CrossX;
        // 求解經過的x間隔
        float x0 = ceil( max3( MinX.x, MinX.y, MinX.z ) );
        float x1 = min3( MaxX.x, MaxX.y, MaxX.z );
        float ZX = ZY + GradZ.x * x0;
        
        x0 += MinPixel.x;
        x1 += MinPixel.x;
        // 現在只迭代填充像素
        for( float x = x0; x <= x1; x++ )
        {
            WritePixel( PixelValue, uint2(x,y), ZX );
            ZX += GradZ.x;
        }
        ...
    }
    

    光柵化過繪制:沒有逐三角形的剔除,沒有硬件HiZ剔除像素,軟件HZB來自上一幀,剔除簇而不是像素,基于簇屏幕大小的分辨率。過繪制來自大型簇、重疊簇、聚合體、快速運動,過繪制成本:小三角——頂點變換和三角形設置邊界,中等三角形——像素覆蓋測試邊界,大三角形——原子約束。

    小實例:當整個網格只覆蓋幾個像素時會發生什么?DAG以1個根簇結束,128個三角形,停止分辨率縮放。太小的時候剔除?如果結構化成塊則不能。顯然需要在某個時候合并,即使渲染按次線性擴展,內存也不會。實例內存積累得很快,10M * float4X3 = 457MB,未來所需的分層實例化,實例的實例的實例。Nanite沒有合并的特殊解決方案,合并的唯一代理必須在極端距離處替換實例,關鍵的改進是將這段距離推遠。

    可見性緩沖區替代物(imposter):atlas中12 x 12的視圖方向,XY圖集位置八面體映射到視圖方向,抖動方向量化。每個方向12 x 12個像素,正交投影,適用于網格AABB的最小范圍,8:8的深度和三角形ID,每個網格40.5K始終駐留。光線行進以調整方向之間的視差,由于視差小,只需幾步,直接從實例剔除通道中繪制,繞過可見實例列表,想換個更好的方法。

    接下來聊延遲材質評估。

    材質ID:這個像素是什么材質的?VisBuffer解碼:

    • VisibleCluster => InstanceID、ClusterID。
    • ClusterID+TriangleID => MaterialSlotID。
    • InstanceID+MaterialSlotID=>MaterialID。

    材質著色:每種獨特材質的全屏四邊形,跳過與此材質ID不匹配的像素,CPU不知道某些材質是否沒有可見像素,無論如何都會發出材質繪制調用,GPU驅動的不幸副作用。如何有效地做?不要測試每個像素是否匹配每個材質通道的材質ID。

    材質剔除:模板測試?不想為每種材質重置。可以利用深度測試硬件,材質ID->深度值。構建材質深度緩沖區,CS還為兩個深度緩沖區輸出標準深度和HTILE,對于所有材質:繪制全屏四邊形,quad的Z=材質深度,深度測試設置為等于。

    UV導數:仍然是一個連貫的像素著色器,所以有有限差分導數。像素quad跨度(三角形),好!也跨越深度間斷、UV接縫、不同的物體(不好!)。解析導數:計算解析導數,三角形上的屬性梯度,利用鏈式規則在材質節點圖中傳播,如果導數不能用解析的方法計算,回到有限差分,用于使用SampleGrad對紋理進行采樣。額外成本微乎其微,<2%的材質通道成本,僅影響紋理采樣的計算,虛擬紋理代碼已經完成了SampleGrad。

    管線數字:

    性能:上采樣到4k*均約2496x1404,當時使用TAAU,現在使用TSR。約2.5ms以繪制整個VisBuffer,查看+GPU場景=>完成VisBuffer,幾乎零CPU時間。約2ms的延遲材質通道,VisBuffer=>GBuffer,CPU成本低,每種材質1次繪制。

    接下來聊Nanite的陰影。Nanite的陰影用光線追蹤?DXR不夠靈活,復雜的LOD邏輯,自定義三角形編碼,沒有部分BVH更新。想要光柵解決方案,利用所有的其它工作,大多數燈都不移動,應該盡可能多地緩存。

    虛擬陰影圖:Nanite使新技術成為可能,16k x 16k陰影貼圖無處不在,聚光燈用1x投影,點光源用6倍立方體,*行光用Nx clipmap。選擇mip級別,其中1紋素=1像素,僅渲染可見的陰影圖像素,按LOD需求的Nanite剔除和LOD。

    頁面大小=128 x 128,頁表=128 x 128,帶mip。標記需要的頁面,屏幕像素投影到陰影空間,選擇mip級別,其中1 texel=1像素,標記那一頁。為所有需要的頁面分配物理頁面,如果緩存頁面已經存在,請使用該頁面,如果沒有緩存則無效,從需要的頁面掩碼中刪除。

    多視圖渲染:具有顯著同步開銷的深層管線,NumShadowViews = NumLights x NumShadowMaps x NumMips,可以同時剔除和光柵化不同的視圖,以分攤成本,使用視圖id標記元素。

    剔除和尋址頁面:如果不重疊所需頁面,則進行剔除,與HZB試驗類似。軟件光柵逐重疊頁面發出簇,硬件光柵在原子寫入前逐像素的頁表間接尋址。

    Nanite陰影LOD:使用1 texel=1像素渲染頁面,與屏幕像素成比例的NumPages,LOD匹配<1個像素的誤差,與陰影像素成比例的numtriangle。陰影成本與分辨率成正比!而不是場景的復雜性,和每像素的光源數量成正比。

    接下來聊流(streaming)。虛擬幾何,固定內存預算下的無限幾何體。概念上類似于虛擬紋理,GPU請求所需的數據,CPU完成了填充, 獨特的挑戰。在運行時將DAG剪切為僅加載的幾何圖形,必須始終是完整DAG的有效切割,類似于LOD切割,沒有裂縫!

    流單元:簇可以與任何父簇重疊,Rendering cluster=>所有父級都不應渲染,父節點沒有被渲染=>所有兄弟姐妹(或其后代)都需要渲染以填充孔洞,需要完整的組才能渲染組中的簇。應該在組粒度上進行流,幾何體的大小是可變的,使用固定大小的頁面以避免內存碎片 => 每頁可變的幾何體數量。

    分頁:用組填充固定大小的頁面,基于空間局部性以最小化運行時所需的頁面。根頁面的第一頁包含DAG的頂層,總是常駐,所以總是有東西要渲染。頁面內容:索引數據、頂點數據、元數據(邊界、LOD信息、材質表等),駐留頁面存儲在一個大的GPU頁面緩沖區中。

    對于分組部件,松散的:簇很小(約2KB),組可以很大(8-32個集群),如果分配整個組,則會出現明顯的松弛。拆分組:組可以跨越多個連續頁面,根據內存使用情況在簇粒度上進行拆分,在裝載所有部件之前,未激活組,頁面現在以簇粒度(約2KB)填充,約*均每頁1KB的空閑時間!(128KB頁面約有1%的空閑時間)

    決定流的內容:對于虛擬紋理而言容易,直接由UV和漸變/LOD級別給出。對于Nanite,需要層次遍歷,找到本應繪制的簇,需要超越流式切割。完全剔除層次結構始終駐留,關于簇組的元數據(微小),遍歷可以超出加載的級別,立即要求達到目標質量所需的所有級別。

    流請求:持久著色器在剔除遍歷期間輸出頁面請求,根據LOD誤差請求具有優先級的頁面范圍,更新已加載頁面的優先級。請求的異步CPU回讀:添加任何缺少的DAG依賴項,對總優先級最高的頁面發出IO頁面請求,逐出低優先級頁面。處理已完成的IO請求:在GPU上安裝頁面,修復GPU側指針,修復指向新加載/卸載頁面中的組的指針,修復指向已完成或不再完成的拆分組的指針,將簇標記/取消標記為葉子。

    接下來聊壓縮。有兩種表示:內存表示和磁盤表示。內存表示:直接用于渲染,*即時解碼時間,需要支持從可見性緩沖區進行隨機訪問,量化和位壓縮,目標是節省內存和/或帶寬。磁盤表示:當數據流入時,轉碼到內存表示,能夠承受更高的解碼成本,不需要隨機訪問,假設數據將由(硬件)基于字節的LZ壓縮,目標是減少壓縮磁盤大小。

    頂點量化與編碼:全局量化,藝術家控制和啟發的結合,簇以局部坐標存儲值,相對于最小值/最大值范圍。每簇自定義頂點格式,使用每個組件的最小位數:ceil(log2(range)),頂點比特流需要解碼頂點聲明,只是一串位,甚至沒有字節對齊。使用GPU位流讀取器解碼:不同格式=>每次讀取時重新填充?讀取指定讀取大小的編譯時界限,僅當累計編譯時讀取大小溢出時重新填充。

    頂點位置:不一致的量化會導致裂縫!對于單個對象容易避免,如何避免物體之間的裂縫?用模塊構建標高幾何圖形的常用方法。量化到對象空間網格:每物體2N次方的絕對指數(例如1/16cm),以對象原點為中心,不要標準化到邊界。在以下情況下,頂點落在同一網格上:量化級別是相同的,對象之間的*移也是步長的倍數。只有葉子級別是完全對齊的,簡化決策在對象之間不一致,運行時LOD決策不同步。

    隱式切線空間:0位!切線/副切線是視圖空間法線*面中的U/V方向,推導出它們!類似于屏幕導出的切線空間(Mikkelsen),直接在三角形坐標上計算,而不是使用屏幕ddx/ddy,重用已為材質通道中的重心和紋理LOD計算而計算的局部三角形uv/位置三角形。傳統的顯式切線坐標是通過鄰域*均來計算的,對于高多邊形網格不太重要。

    材質表:每個簇存儲材質表,指定材質指定的三角形范圍,兩個編碼別名相同的32位,快速路徑編碼3個范圍,慢路徑指向內存,最多64種材質。

    磁盤表示:硬件LZ解壓,在游戲機里,在使用DirectStorage的PC上,速度驚人,用途廣泛,字符串重復數據消除和熵編碼。為了更好地壓縮:特定于域的轉換,假設數據將被LZ壓縮,關注LZ尚未捕獲的冗余,轉換成更可壓縮的格式。

    GPU轉碼:在GPU上進行代碼轉換,并行轉換的高吞吐量,只要是并行的,每個字節都有大量(異步)計算,目前運行速度約為50GB/s,PS5上的代碼相當未優化,最終將數據直接傳輸到GPU內存。結合硬件LZ功能強大,LZ處理串行熵編碼和字符串匹配工作,可能會成為這一代人的共同模式。Nanite:GPU有上下文,頁面可以引用父頁面中的數據,而無需CPU拷貝。

    Lumen in the Land of Nanite的結果:4.33億個輸入三角形,8.82億個Nanite三角形,原始數據:25.90GB,全浮點、字節索引、隱式切線,內存格式:7.67GB,壓縮:6.77GB,壓縮磁盤格式:4.61GB, 自EA版本以來改善了約20%,每個Nanite三角形5.6字節,每個輸入三角形11.4字節,1百萬個三角形=磁盤上約10.9MB。

    關于Nanite的源碼剖析,可參見:剖析虛幻渲染體系(06)- UE5特輯Part 1(特性和Nanite)

    14.5.4.9 Radiance Caching

    Radiance Caching for real-time Global Illumination由Epic Games的Daniel Wright呈現,講述了UE5 Lumen的實時全局光照實現技術——輻射率緩存(Radiance Caching)。Radiance Caching是Lumen中使用的最終收集技術,針對下一代游戲機上的游戲,在高端PC上提升為質量第一的企業。光線追蹤很慢,存在二級BVH、非相干樹遍歷、實例重疊等問題:

    每像素只能提供1/2光線,但高質量的GI需要數百個!

    以往的實時研究有輻照度場(Irradiance Fields)、屏幕空間降噪器(Screen Space Denoiser)等方式。而Lumen使用了屏幕空間降噪器(Screen Space Radiance Caching)。

    下采樣入射輻射,入射光是相干的,而幾何法線不是,以全分辨率積分BRDF上的輸入照明:

    在輻射緩存空間中過濾,而不是屏幕空間(下圖左)。首先要進行更好的采樣——重要的是對入射光進行采樣(下圖中)。穩定的遠距離照明和世界空間輻射緩存(下圖右)。

    最終收集管線:

    其中屏幕空間的輻照率緩存可以細分成以下階段:

    屏幕探針結構體:帶邊框的八面體圖集,通常每個探針8x8個,均勻分布的世界空間方向,鄰域有相同的方向,二維圖集中的輻射率和交點距離。

    屏幕探針放置:分層細化的自適應布局[K?ivánek等人2007],迭代插值失敗的地方,最終級別的地板填充(Flood fill)。

    自適應采樣:實時性需要上限,不希望在處理自適應探頭時遇到額外障礙,將自適應探頭放在圖集底部。

    屏幕探針抖動:時間抖動放置網格和方向,直接放置在像素上,沒有泄露,屏幕單元格內的遮擋差異必須通過時間過濾來隱藏。

    插值:*面距離加權,防止前臺未命中泄漏到后臺,插值中的抖動偏移,只要還在同一個*面上,在空間上分布探針之間的差異,通過擴展TAA 3x3的鄰域達到時間穩定最終照明。

    重要性采樣:對于入射輻射率\(L_i(l)\),重投射最后一幀的屏幕探針的輻射率!不需要做昂貴的搜索,光線已按位置和方向索引,回退到世界空間探針上。對于BRDF,從將使用此屏幕探針的像素累積,更好的是,希望采樣與入射輻射率\(L_i(l)\)和BRDF的乘積成比例。

    結構重要性采樣(Structured Importance Sampling):將少量樣本分配給概率密度函數(PDF)的層次結構區域[Agarwal等人2003],實現良好的全局分層,樣本放置需要離線算法。

    完美地映射到八面體mip四叉樹!

    集成到管線中:向追蹤線程添加間接路徑,存儲RayCoord、MipLevel,追蹤后,將TraceRadiance組合進均勻的探頭布局,以進行最終集成。

    光線生成算法:計算每個八面體紋理的BRDF的PDF x 光照的PDF,從均勻分布的探針射線方向開始,需要固定的輸出光線計數-保持追蹤線程飽和。按PDF對光線進行排序,對于PDF低于剔除閾值的每3條光線,超級采樣以匹配高PDF光線。

    改進:不允許光照PDF來剔除光線,光照PDF為*似值,BRDF為精確值,借助空間過濾可以更積極地進行剔除,具有較高BRDF閾值的剔除,在空間過濾過程中減少剔除光線的權重,修復角落變暗的問題。

    重要性采樣回顧:使用最后一幀的光照和遠距離光照引導此幀的光線,將射線捆綁到探針中可以提供更智能的采樣。

    接下來聊空間過濾的技術。

    輻射緩存空間中的過濾:廉價的大空間濾波,探針空間為\(3^2\),屏幕空間為\(48^2\),可以忽略空間鄰域之間的發現差異,僅深度加權。從鄰域收集輻射率:從相鄰探針中匹配的八面體單元收集,誤差權重:重投影的相鄰射線擊中的角度誤差,過濾遠處的燈光,保留局部陰影。

    對于*坦表面的效果是良好的,但對于幾何接觸的地方,存在漏光的問題:

    保持接觸陰影:角度誤差偏向遠光=泄漏,遠距離光沒有視差,永遠不會被拒絕。解決方案:在重投影之前,將鄰域的命中距離夾緊到自己的距離。

    接下來聊世界空間的輻射緩存。

    遠距離光存在問題,微亮特征的噪點隨著距離的增加而增加,長而不連貫的追蹤是緩慢的,遠處的燈光正在緩慢變化——緩存的機會,附*屏幕探針的冗余操作。解決方案:對遠距離輻射進行單獨采樣。用于遠距離照明的世界空間輻射緩存(The Tomorrow Children [McLaren 2015]的技術),自世界空間以來的穩定誤差-易于隱藏,就像體積光照圖一樣。

    管線集成:在屏幕探針周圍放置,然后追蹤計算輻射,插值以解決屏幕探測光線的遠距離照明。

    避免自光照:世界探針射線必須跳過插值足跡。

    連結光線:屏幕探針光線必須覆蓋插值足跡+跳過距離。

    還存在漏光的問題。世界探針的輻射應該被遮擋,但不是因為視差不正確。

    解決方案:簡單的球面視差。重投影屏幕探針光線與世界探針球相交。

    稀疏覆蓋:以攝像頭為中心的3d clipmap網格將探針索引存儲到圖集中,Clipmap分布保持有限的屏幕大小。

    圖集:八面體探針圖譜存儲輻射、追蹤距離,通常每個探針為32x32的輻射率。

    放置和緩存:標記將在后面的clipmap間接中插入的任何位置,對于每個標記的世界探針:重用上一幀的追蹤,或分配新的探針索引,重新追蹤緩存命中的子集以傳播光照更改。

    問題:高度可變的成本,快速的攝像機移動和不連續需要追蹤許多未經緩存的探針。解決方案:全分辨率探針的固定預算,緩存未命中的其它探針追蹤的分辨率較低,跳過照明更新的其它探針追蹤。

    重要性采樣。BRDF的重要采樣:從屏幕探針累積BRDF,切塊(Dice )探針追蹤分塊,根據BRDF生成追蹤分塊分辨率。超采樣*的相機,高達64x64的有效分辨率,4096條追蹤!非常穩定的遠距離照明。

    探針之間的空間過濾:再次拒絕鄰域交點,問題是不能假設相互可見性。理想情況下,通過探測深度重新追蹤相鄰射線路徑,單次遮擋試驗效果良好,幾乎免費-重復使用探針深度。


    世界空間輻射緩存還用于引導屏幕探針重要性采樣、頭發、半透明、多反彈。

    回到積分,現在已經在屏幕空間的輻射緩存中以較低的分辨率計算了入射輻射,需要以全分辨率進行積分,以獲得所有的幾何細節。

    重要性采樣BRDF會導致不一致的獲取,8spp*4相鄰探針方向查找,可以使用mips(過濾重要性采樣),但會導致自光照[Colbert等人2007],尤其是在直接照明區域周圍。將探針輻射轉換為三階球諧函數:SH是按屏幕探針計算的,全分辨率像素一致地加載SH,SH低成本高質量積分[Ramamoorthi 2001]。

    粗糙鏡面:高粗糙度下的光線追蹤反射,在漫反射上聚集。重用屏幕探針:從GGX生成方向,采樣探針輻射,自動利用已完成的探針采樣和過濾!下采樣追蹤會丟失接觸陰影。全分辨率彎曲法線:使用快速屏幕追蹤進行計算,與屏幕探針之間的距離耦合的追蹤距離:約16像素。與屏幕空間輻射緩存積分:將屏幕探針GI視為遠場輻照度,全分辨率彎曲法線表示*場的數量,基于水*的間接照明[Mayaux 2018],多重反彈*似給出*場輻照度。


    時間過濾:抖動探針位置需要可靠的時間過濾,使用深度剔除,結果穩定,但對光線變化的反應也很慢。追蹤過程中追蹤命中速度和命中深度,屬于快速移動對象的投影面積。當追蹤擊中快速移動的對象時,切換到快速更新模式,降低時間過濾,提高空間過濾。

    最終收集性能:



    未來的工作是去遮擋質量、高動態場景中的時間穩定性、將屏幕空間輻射緩存應用于Lumen的表面緩存以實現多反彈GI。Radiance Cache只是Lumen的一小部分技術,Lumen還涉及表面緩存、軟件射線追蹤、硬件光線追蹤、反射、透明GI等內容。關于Lumen的源碼剖析可參見:剖析虛幻渲染體系(06)- UE5特輯Part 2(Lumen和其它)

    14.5.4.10 Surfels GI

    Global Illumination Based on Surfels講述了EA內部引擎使用面元來實現實時全局光照的技術。“基于曲面(GIB)的全局照明”是一種實時計算間接漫反射照明的解決方案。該解決方案將硬件光線跟蹤與場景幾何體的離散化相結合,以跨時間和空間緩存和分攤照明計算。它不需要預先計算,不需要特殊的網格,也不需要特殊的UV集。GIBS支持高保真照明,同時可容納任意比例的內容。“

    基于面元(surfel)全局光照的面元可視化。

    該文演講的內容包含Surfel離散化、非線性加速度結構、輻照度積分技術、顏色溢出緩解、多光源采樣、透明度等。首先要弄清楚面元的概念,Surfel=表面元素(Surface Element),一個surfel由位置、半徑和法線定義,并*似了給定位置附*表面的一個小鄰域。

    場景的面元化:從GBuffer中生成面元,當幾何圖形進入視圖時填充屏幕,在世界空間中持久存在,累積和緩存輻照度。迭代屏幕空間填充,將屏幕拆分為16x16塊,找到覆蓋率最低的tile,應用面元覆蓋率和追蹤權重,如果tile超過隨機閾值,則生成surfel。

    除了支持剛體,還支持蒙皮骨骼的面元化。由于所有東西都假設是動態的,所以蒙皮幾何體和移動幾何體都與解決方案的其余部分交互,就像靜態幾何體一樣。

    面元根據屏幕空間投影進行縮放,生成算法確保覆蓋范圍在任何距離,由非線性加速度結構支撐。

    面元管理:所有東西都有固定大小的緩沖區,可預測的預算,固定數量的面元,固定的加速度結構,回收未使用的面元。

    回收啟發式:讓相關的面元保持活躍,最后一次見到時追蹤,如果在間隙檢測期間看到,則重置,位置更新期間增加。啟發式基于激活的面元總數、自從見過的時間、距離、覆蓋率。下圖是距離啟發式:

    光照應用:對每個像素:查找表面網格單元,從單元格里取N個面元,累積表面輻照度,按距離和法線加權,如果輻照度權重<1,則添加加權的*均單元格的輻照度。存在光照溢出的問題,使用徑向高斯深度來解決:

    修復前后對比:

    積分輻照度圖示:

    積分器:修正指數移動*均估值器[BarréBrisebois2019],跟蹤短期均值和方差估計值,使用短期估計器調整混合因子,使我們能夠快速響應變化,同時收斂到低噪點。基于短期方差的偏差光線計數,使用射線計數通知相對置信度的多尺度均值估計器,反饋回路對變化和變化做出快速反應,在穩定的情況下保持光線計數小。

    BRDF的重要性采樣:累積漫反射輻照度,假設是蘭伯特BRDF,通過對余弦葉進行重要采樣來生成光線。

    射線引導:

    每個surfel在其半球上生成一個移動*均6x6亮度圖,存儲在單個4K紋理中(可支持所有surfels的7x7),每個紋理8位+每個紋理單個16位縮放,規范化每幀函數。

    有了重要性采樣變量,函數的每個離散部分都將根據其值按比例選取,還有它的概率密度函數,這是函數在那個位置的值。


    輻照度共享:利用附*的面元數據,允許surfels查找相鄰surfels的輻射,結構加速,使用與surfel VPL相同的權重,Mahalanobis 距離、深度函數。

    輻照度共享前后對比:

    還可以使用BF5方法對光線進行排序,按位置和方向排列的箱射線,12位表示空間,4位表示方向,空間散列的單元定位,射線方向定向,計算箱子總計數和偏移量,根據光線索引和以前計算的面元偏移對光線重新排序。

    多光源采樣使用了重要性采樣(隨機光源分割、儲備采樣)。隨機光源切割是小樣本快速收斂,需要預先構建的數據結構,采樣可能開銷很大。

    蓄水池采樣(Reservoir Sampling)示意圖:

    光線追蹤探針示意圖:

    RT探針體積結構:透明對象需要大屏幕支持,例如不透明對象,Clipmap是滿足需求的最佳選擇:保持*距離的細節,支持大規模場景,具有低內存成本的稀疏探針放置,LOD的變速率更新。

    Clipmap更新算法:計算更新方向和距離,復制移位后有效的探針數據,用更高級別的探針初始化新創建的探針。

    4級clipmap的放置示意圖:

    Clipmap采樣過程如下:

    進一步的采樣優化:藍色噪聲梯度抖動采樣。

    一幀概覽:

    • 持續的。位置更新,回收利用,網格分配,射線排序,光線追蹤,Clipmap更新,探針追蹤。
    • 創建。幾何法線重建,空隙填充,射線排序,光線追蹤,寫入持久存儲,寫入探針體積。
    • 過濾。空間降噪,時間降噪。
    • 應用。注入新的創建,應用照明(以四分之一區域分辨率運行),照明上采樣,Clipmap采樣。

    14.5.5 結果期總結

    CPU和GPU的總計算能力和并行度都有了顯著的提升,各類省電、高性能的架構、部件和管線誕生,例如多核、TBDR、HSR、FPK、RT Core等等。

    游戲引擎向著更加逼真、可信度、影視級效果發展,各類實時的全局光照計算層出不窮,最為突出的是基于硬件的光線追蹤和UE5的Nanite和Lumen。現代圖形API的誕生和發展,給游戲引擎帶來了新的機會,使得游戲引擎有了更大的發揮空間,從而也催生了基于渲染圖的中間層渲染計算,也使得渲染畫質邁向新的高度。

    各類渲染技術(如AA、地形、物理、景觀模擬、特殊材質等)的出現,為游戲引擎和游戲開發團隊提升了扎實的動力。

    移動端、XR、云渲染、Web端等分支也得到了充分的發展,成為行業豐富生態的重要組成部分。

     

    14.6 本篇總結

    14.6.1 游戲引擎的未來

    未來將實現和普及一個場景一次繪制調用。GPU管線命令序列幾乎每幀不變,命令不會根據場景結構進行更改,繪制調用的數量可預測且合理較低,可管理的最大索引緩沖區大小,最糟糕的內存開銷約為6%,在嵌套命令列表中記錄所有可能損害的GPU管線命令。

    設備生成的命令。有條件地編寫GPU管線命令的可能性,GPU上的PSO選擇更具靈活性,基于運行時條件,通過編譯時優化切換PSO的可能性,避免空牽引鏈的可能性,減少了保留內存大小。

    此外,硬件和引擎的架構更加完善、全面,工具鏈更加人性化、自動化、智能化,光線追蹤逐漸普及,視覺效果更加逼真、絢麗。PBR更加徹底,并引入納米級的光照模型,如干涉、色散、衍射、疊加、焦散等等。AI的融合更廣泛、普遍,并行化、多GPU、多設備逐漸普及。GPU-Driven、離線技術實時化、其它分支充分發展,渲染調試工具更加完善、智能、可視化。

    免責聲明:以上純憑經驗預測,不具備權威性。

    14.6.2 結語

    本篇是筆者撰寫的所有文章中,參考文獻最多的一篇,達到540多篇。在整理參考文獻之前,參閱了1000多篇各類文獻,即便如此,筆者尚以為這只是圖形渲染技術和游戲引擎技術的冰山一角。

    很多技術深深刻著時代的烙印,但也很多技術突破時代的枷鎖,即便過去數十年,依然歷久彌新,流傳于當今主流的引擎之中。如同歷史一樣,技術演進的輪回一直在上演著,從未間斷。

    本篇從規劃到完成,耗費*半年,總字數達到43萬多,參考文獻達到540多,配圖達到3400多,蘊含了*幾十年來實時渲染領域的主要研究和應用成果,信息密度高,總量也大。所以,童鞋們學fei了嗎O_O?發量可還健在?(反正博主先掉為敬,童鞋們隨意~)

     

     

    特別說明

    • 感謝所有參考文獻的作者,部分圖片來自參考文獻和網絡,侵刪。
    • 本系列文章為筆者原創,只發表在博客園上,歡迎分享本文鏈接,但未經同意,不允許轉載
    • 系列文章,未完待續,完整目錄請戳內容綱目
    • 系列文章,未完待續,完整目錄請戳內容綱目
    • 系列文章,未完待續,完整目錄請戳內容綱目

     

    參考文獻

    posted @ 2022-05-14 02:41  0向往0  閱讀(1466)  評論(2編輯  收藏  舉報
    国产美女a做受大片观看