14.3 成長期(2000~2009)
14.3.1 圖形API
14.3.1.1 DirectX
在2000時代,DirectX釋出了8、9、10、11四個大版本,每個大版本又包含數個小版本。
它們的具體描述如下表:
版本 | 時間 | 著色模型 | 特性 |
---|---|---|---|
DirectX 8 | 2000 | Shader Model 1.0 -1.4 | 可程式設計著色器、曲面細分 |
DirectX 9 | 2002 | Shader Model 2.0 - 3.0 | 4K紋理、3D紋理、事件查詢、BC1-3、遮擋查詢、浮點格式(無混合)、擴充套件功能、MRT(4個)、浮點混合(有限)等。 |
DirectX 10 | 2006 | Shader Model 4.0 - 4.1 | 統一著色器模型、幾何著色器、流輸出、alpha-to-coverage、8K紋理、MSAA紋理、雙面模板、通用渲染目標檢視、紋理陣列、BC4/BC5、全浮點格式支援、立方體貼圖陣列,擴充套件的MSAA |
DirectX 11 | 2009 | Shader Model 5.0 | hull&domain著色器、DirectCompute (CS 5.0)、16K紋理、BC6H/BC7、擴充套件的畫素格式、邏輯混合操作、獨立於目標的光柵化、每個管道階段的UAV增加槽數、UAV僅渲染強制樣本計數、恆定緩衝區偏移和部分更新 |
傳統可程式設計架構(左),新的曲面細分架構(右)。新架構為GPU流水線增加了兩個階段,以灰色顯示。
DX1.0到10.0的演變圖。
DirectX10的程式設計師視角(上)、系統架構(中)和可配置管線(下)。
DirectX10的輸出合併階段。
DirectX10的固定緩衝區說明及建議,建議按更新頻率拆分引數到不同的緩衝區,並啟用快取(上)。固定緩衝區被不同的Shader共享並訪問(下)。
DirectX10的邏輯管線演變。
DirectX10新的跨階段的聯動方式。放棄了“按名稱繫結”模型,轉而採用可歸類為“按位置繫結”的方案。該模型將其視為每個階段之間的一組暫存器,每個階段按一定順序輸出資料,下一個階段按該順序使用它。並且在暫存器庫中按位置繫結 - 連結由物理位置標識,意味著不是在繪製時進行對映,而是由應用程式在著色器創作時維護順序。換句話說,已經將這項工作推到了外迴圈,這些連結通過叫簽名的構造被維護並繫結到著色器(上圖)。簽名的使用案例(下圖)。
DX9和DX10的畫面對比。
2009年釋出的DirectX 11專注於提高可伸縮性、改善開發經驗,擴充套件GPU覆蓋面,提高效能。Direct3D 11是D3D 10和10.1的嚴格超集,新增對新特性的支援。DirectX 11的新增的特性有曲面細分、計算著色器、多執行緒、動態著色器連結、改進的紋理壓縮及其它。
DX11的渲染管線,新增了曲面細分、計算著色器。
DX11的曲面細分執行示意圖。
DX11下新的資產創作管線。
DX11的計算著色器可用於圖形處理、後處理、A-Buffer、OIT、光線追蹤、輻射度量、物理、AI等等。
DX11還允許多執行緒處理,非同步資源載入(上傳資源、建立著色器,建立狀態物件),並行併發地渲染,多執行緒繪製和狀態提交,在許多執行緒中展開渲染工作,對每個物件顯示列表的有限支援。
D3D裝置的功能拆分成3個:裝置(Device)、立即上下文(Immediate Context)、延遲上下文(Deferred Context)。裝置有空閒的執行緒資源建立,即時上下文是狀態、繪製和查詢的單一主裝置,延遲上下文是狀態和繪製的逐執行緒裝置。
DirectX11的多執行緒模型。
對於非同步資源,使用Device的介面建立資源,所有介面都是執行緒無關的,使用良好的粒度同步圖元。資源的上傳和著色器編譯可以同時發生。對狀態和繪製提交,存在兩個優先順序,高優先順序的是多執行緒的提交,專用的顯示列表(display list),低優先順序的是逐物體的顯示列表,可多次重複使用。此外,DX11的顯示列表是不可更改的。
DirectX11的多執行緒資源建立和圖元繪製。
DX11可以建立多個延遲上下文,每個都可繫結到一個執行緒(執行緒不安全的),延遲上下文上傳顯示列表,顯示列表被立即上下文或延遲上下文使用。延遲上下文不可從GPU下載或回讀資料(如查詢、資源鎖定),也不支援帶DISCARD的鎖定方式。
DX11時代的著色器已經變得越來越大且複雜,需要相容寬泛的硬體平臺,需要優化不同著色器配置驅動的特例化(Specialization)。解決的方案有兩種:
-
全能著色器(Uber Shader)。所有組合情況的邏輯都在同一個著色器中。
foo(...) { if(m == 1) { // do material 1 } else if (m == 2) { // do material 2 } (...) if(l == 1) { // do light model 1 } else if (l == 2) { // do light model 2 } (...) }
- 優點:
- 一個著色器控制了所有的著色程式碼。
- 所有函式在一個檔案。
- 減少執行時的狀態改變。
- 只要一個編譯步驟。
- 更流行的編碼方式。
- 缺點:
- 複雜。
- 缺乏組織。
- 暫存器使用總是在最壞情況的路徑上。
- 優點:
-
特例化(Specialization)。每個組合情況生成一個專門的shader。
-
優點:
- 總是最好的暫存器使用情況。
- 更易針對性地優化。
-
缺點:
- 海量的生成著色器,導致爆炸式的組合。(下圖)
- 執行時管理是個痛點。
-
解決方案是動態著色器連結和麵向物件程式設計(OOP)。選擇你想要的特定類例項,執行時將內聯類的方法,等效註冊使用到一個專門的著色器,內聯是在本機程式集快速操作中完成的,適用於所有後續的Draw()呼叫。
全能著色器和動態連結的對比。
在紋理壓縮方面,由於之前的塊狀調色盤插值過於簡單,導致過於明顯的塊狀瑕疵,不支援HDR,於是DX11新增了BC6和BC7。BC6支援HDR,達到1/6的壓縮比(16 bpc RGB),針對高質量(非無損)的視覺效果。BC7支援Alpha的LDR,1/3的壓縮比,高的視覺效果。新的格式依舊採用塊狀壓縮,每個塊是獨立的,具有固定的壓縮比,但新增了多種塊型別,為不同型別的內容量身定製,如平滑的梯度和帶噪點的法線貼圖、變化的Alpha和不變的Alpha等。
BC紋理格式的塊狀分割槽表圖例。
BC紋理格式的新舊對比。
DX11還支援以下特性:
- Addressable Stream Out
- Draw Indirect
- Pull-mode attribute eval
- Improved Gather4
- Min LOD Texture clamps
- 16K texture limits
- Required 8-bits subtexel, submip filtering precision
- Conservative oDepth
- 2 GB Resources
- Geometry shader instance programming model
- Optional double support
- Read-Only depth or stencil views
14.3.1.2 OpenGL
OpenGL在2000時代釋出了以下幾個版本:
版本 | 時間 | 特性 |
---|---|---|
OpenGL 1.3 | 2001年8月 | 多重紋理、多重取樣、紋理壓縮 |
OpenGL 1.4 | 2002年7月 | 深度紋理、GLSlang |
OpenGL 1.5 | 2003年7月 | 頂點緩衝區物件 (VBO)、遮擋查詢 |
OpenGL 2.0 | 2004年9月 | GLSL 1.1、MRT、NPOT紋理、點精靈、雙面模板 |
OpenGL 2.1 | 2006年7月 | GLSL 1.2、畫素緩衝物件 (PBO)、sRGB紋理 |
OpenGL 3.0 | 2008年8月 | GLSL 1.3、紋理陣列、條件渲染、幀緩衝物件 (FBO) |
OpenGL 3.1 | 2009年3月 | GLSL 1.4、例項化、紋理緩衝區物件、統一緩衝區物件、圖元重啟 |
OpenGL 3.2 | 2009年8月 | GLSL 1.5、幾何著色器、多采樣紋理 |
OpenGL在GPU上執行的圖形管道的擴充套件版本,部分固定功能流水線已被可程式設計級取代。
同時,作為嵌入式和移動裝置的輕量級圖形API Open ES也在發展。OpenGL和OpenGL ES之間的顯著區別是OpenGL ES不再需要用glBegin和glEnd括起OpenGL庫呼叫,原始渲染函式的呼叫語義被更改為有利於頂點陣列,並且為頂點座標引入了定點資料型別,以及新增了屬性以更好地支援嵌入式處理器的計算能力,這些處理器通常缺少浮點單元 (FPU)。下表是OpenES在2000時代釋出的版本及說明:
版本 | 時間 | 特性 |
---|---|---|
OpenGL ES 1.0 | 2003年7月 | 四邊形和多邊形渲染基元,texgen、線條和多邊形網格,刪除部分技術性更強的繪圖模式、顯示列表和反饋、狀態屬性的推送和彈出操作、部分材料引數(如背面引數和使用者定義的剪下平面) |
OpenGL ES 1.1 | 未知 | 多紋理(包括組合器和點積紋理操作)、自動mipmap生成、頂點緩衝區物件、狀態查詢、使用者裁剪平面、更好控制的點渲染。 |
OpenGL ES 2.0 | 2007年3月 | 可程式設計管線、著色器控制流 |
14.3.2 硬體架構
在新世紀的初期,索尼釋出了風靡一時的PS2遊戲主機。
PS2主機外觀(上)和內部硬體部件圖(下)。
PS2主機的硬體架構互動和關係圖如下,其硬體架構和內部結構包含了EE Core、VIF0、VIF1、GIF、DMAC和路徑1、2 和 3,每條資料匯流排都標有其寬度和速度,實際上這個架構經歷了多次修改:
PS2硬體架構簡化圖例。包含Emotion引擎、圖形合成器、RAM、IO處理器、音訊處理器等等部件。每個部件之間的頻寬已線上條中標明。
上圖的Emotion引擎包含了CPU、DMA、記憶體介面和兩條16M的記憶體:
Emotion引擎使用了兩個向量處理單元(VPU),被成為VPU0和VPU1,它們的硬體結構和互動有所差異。在VPU0和VPU1的支援下,PS2有了少許的並行處理能力。下面是串聯和並行對比圖:
PS2的影像合成器擁有較完整的渲染管線(預處理、光柵化、紋理對映、畫素測試、後處理)、暫存器和4M的視訊記憶體,它們的互動和流程圖如下:
PS2架構還支援多緩衝機制(雙緩衝、三緩衝、四緩衝),使用每個例項的不同變換矩陣渲染單個圖元的多個例項——並顯示出顯著的加速,降低延時:
從上到下:PS2的單緩衝、雙緩衝、三緩衝、四緩衝的互動和時序圖。
結合多快取機制和兩個VPU等部件,可以實現GS、DMA、VU的並行處理,下面是它們的資料流和時序圖:
有了全新的影像合成器、平行計算架構以及多緩衝技術的支撐,PS2平臺上游戲的畫質也有了較大提升。下面分別是2001年發行的Crash Bandicoot: The Wrath of Cortex和Final Fantasy X的遊戲截圖:
2000年,Nvidia收購了研製出Voodoo晶片的3dfx,從而開啟了GPU發展的迅猛紀年。GPU包含大量算術邏輯單元(ALU),具有數量級計算速度更快的能力,但每個處理單元都應該執行相同的命令,只是資料不同(即資料並行)。GPU和CPU之間存在物理和互動上的距離,它們通過系統匯流排連線,這會導致將資料從主存傳輸到GPU記憶體消耗大量的時間,是渲染瓶頸的主要誘因之一。
CPU和GPU通過PCI-E匯流排連線,是引發渲染瓶頸的主因之一。
早期開發了多種匯流排型別(原標準:ISA、MCA、VLB、PCI,1997年的AGP(加速圖形埠)標準制定),當時主要的解決方案是PCI Express標準,提供高速序列計算機擴充套件匯流排標準。下表是不同PCI Express版本的速度表:
PCIe 1.0 | PCIe 2.0 | PCIe 3.0 | PCIe 4.0 | PCIe 5.0 | PCIe 6.0 |
---|---|---|---|---|---|
2.5 GT/s | 5.0 GT/s | 8.0 GT/s | 16.0 GT/s | 32.0 GT/s | 64.0 GT/s |
更詳細的PCIe的屬性和描述見下表:
PCIe的發展,為渲染效能提升了不少,另外,GPU硬體架構也在飛速發展。以ATI Radeon HD 3800 GPU為例,擁有320個流處理器、6億多個電晶體、計算速度超過1 terraFLOPS。
Geforce 8800硬體架構圖。
Geforce GTX 280硬體架構圖。
GPU硬體的改進,使得計算速度以大於摩爾定律的曲線發展:
Intel CPU從2003到2013的發展趨勢圖。
2006年的單元處理器模型如下圖:
GPU晶片設計重點和CPU不同,主要區別在於GPU使用多執行緒來容忍延遲,每次等待讀取時,只需啟動另一個執行緒,如果有很多執行緒,就可以保持核心的工作負載(詳見下表)。
CPU | GPU |
---|---|
指令多,資料少 亂序執行 分支預測 |
指令少,資料多 SIMD 硬體執行緒 |
重用和區域性性 | 很少重複使用 |
任務並行 | 資料並行 |
需要作業系統 | 無作業系統 |
複雜同步 | 簡單同步 |
延遲機器 | 吞吐量機器 |
指令保持不變 | 指令不斷變化 |
GPU的指令架構集(ISA)的指令變化很快的原因有:
- 一個新的遊戲變得流行。
- API設計師 (Ms) 新增新功能,讓遊戲編寫更簡單。
- 硬體廠商關注遊戲,並新增新硬體以加快遊戲執行速度。
- 新的硬體。
- 遊戲開發者著眼於新硬體並通過超越任何人認為指令可以做的事情來思考有趣的新效果(更逼真)。
對類似的程式碼,CPU和GPU的效能的區別是怎麼樣的?舉個具體的例子,假設CPU和GPU都要線上程中執行以下程式碼:
// load
r1 = load (index)
// series of adds
r1 = r1 + r1
r1 = r1 + r1
......
// Run lots of threads
典型的CPU操作是單個CPU單元一次迭代,無法達到100%核心利用率。難以預取資料,多核無濟於事,叢集沒有幫助,未完成的提取數量有限:
GPU執行緒在於吞吐量(較低的時鐘,不同的規模),ALU單元達到100%利用率,最終輸出的硬體同步,海量執行緒,Fetch單元 + ALU單元,快速執行緒切換,有序完成:
注意上圖的Fetch和ALU是可重疊的,存在許多未完成的抓取。頂部的大條顯示ALU何時執行,如果有足夠的執行緒,它是100%處於活動狀態。
Wavefront是64個執行緒的單元,它們也被稱Warp,所有資源都是在啟動時分配的,因此不會出現死鎖。
執行佇列中的執行緒數量計算如下:
-
每個SIMD有256個暫存器集。
-
每個暫存器集有64個暫存器(每個暫存器128位)。
- 256 * 64 * 10 個128位暫存器 = 163840個128位暫存器。
- 256 * 64 * 10 * 4 個32位暫存器 = 665360個32位暫存器。
-
如果每個執行緒需要5個128位暫存器,那麼:
- 有256 / 5 = 51個Wavefront可以進入執行佇列。
- 51個Wavefront = 每個SIMD有3264個執行緒或32640個執行或等待執行緒。
因此,CPU的負載決定效能,編譯器儘量最小化ALU程式碼,減少記憶體開銷,嘗試使用預取和其它技巧來減少等待記憶體的時間。而GPU的執行緒決定效能,編譯器儘量最小化ALU程式碼,最大化執行緒,嘗試重新排序指令以減少同步和其它技巧以減少等待執行緒的時間。
2008年主流的GPU程式設計模型如下:
圓框是可程式設計,方框是固定功能,所有其它東西都在庫中浮動。
上圖中的所有並行操作都通過特定領域的API呼叫隱藏,開發人員編寫順序程式碼 + 核心,核心對一個頂點或畫素進行操作,開發人員從不直接處理並行性,不需要自動並行編譯器。這樣的機制下,開發者只需寫一小部分程式,其餘程式碼來自庫,一般是200-300個小核心,每個少於100行程式碼,可以沒有競爭條件和沒有錯誤報告,可以將程式視為序列(每個頂點/形狀/畫素)。沒有開發人員知道處理器的數量(不像 pthreads)。這樣的結果非常成功,程式設計足夠簡單。
ATI Radeon HD 4870(上)和4800系列(下)的架構和引數。
部分ATI GPU的可程式設計區域百分比對比。
2000年代GPU的渲染管線變化如下圖所示:
2009年的Nvidia提供了完整的開發者套件,包含各類內容建立、軟體開發SDK、效能除錯和技術書籍等:
14.3.3 引擎演變
14.3.3.1 綜合演變
2000時代,渲染技術的進步和硬體效能的提升,為遊戲引擎提供了強力的支撐,使得遊戲引擎能夠提供更加強大的功能,使得遊戲開發者可以研製出多種多樣的遊戲型別。
2000時代的多種遊戲類。從上到下依次是實時策略、格鬥、賽車、多人線上遊戲。
2000年早期,遊戲引擎初具規模,核心模組引入了諸多通用功能,例如音訊、AI、流、執行緒、物理、渲染、碰撞、動畫網路等等,具體見下圖:
隨著場景的複雜度越來越高,場景管理的技術也在發生改變,例如下面兩圖分別是BVH(層次包圍盒)加速結構和共享資料的一種場景節點結構:
基於共享資料的結構還有其它變種,比如用連結節點解耦資源(下圖上)和場景節點及基於元件的場景節點結構(下圖下):
其場景圖節點的UML圖如下:
用於引擎的設計模式常用的有抽象工廠模式(Abstract Factory Pattern)、原型模式(Prototype Pattern)、單例模式(Singleton Pattern)、介面卡模式(Adapter Pattern)、橋接模式(Bridge Pattern)、代理模式(Proxy Pattern)、命令模式(Command Pattern)、觀察者模式(Observer Pattern)、模板方法模式(Template Method Pattern)、訪問者模式(Visitor Pattern)等。
利用以上設計模式,可以很好地將引擎分層,解決迴圈依賴的問題。下面分別是Designing a Modern Rendering Engine中的YARE引擎的分層架構和圖形、核心的子模組圖:
YARE引擎在應用層的渲染管線如下所示:
YARE引擎在Shader上,採取了當時比較流行的Effect、Technique框架 + CG著色器語言:
其Effect框架UML圖如下:
)
YARE在繪製管線上,會對場景節點執行如下處理:
- 首先,為網格物件中的每個幾何圖形建立一個Renderable類的例項。
- 其次,對每個Renderable的instance執行以下步驟:
- 可渲染物件獲得分配的相應幾何圖形。
- 從網格物件中檢索可渲染的包圍體。
- 可渲染的結構上下文從場景資料庫中檢索(包括結構變數和效果)並分配給可渲染。
- 從場景資料庫中獲取所有全域性變數和效果並分配給可渲染物件。
- 從場景資料庫中獲取與可渲染物件相交的所有體積變數和效果,並將其分配給可渲染物件。
- 如果在此更新過程中建立了可渲染例項,則將其新增到繪圖中。
YARE引擎的可渲染物件由場景圖的幾何形狀、變數和效果構成,並被插入到繪圖中。
YARE引擎的渲染效果。
2003年,A Framework for an R to OpenGL Interface for Interactive 3D graphics描述了一個在R中提供互動式3D圖形的框架。其中RGL1(簡稱R)是一個擴充套件庫,使用了具有互動式視點導航的3D視覺化裝置系統。R使用OpenGL作為實時渲染後端,關鍵元件是R和OpenGL之間的介面,提供了一組命令,用於指定啟用3D圖形視覺化的物件和操作。
設計中的一個重要目標是促進對不同作業系統的可移植性,使用物件導向的方法,通過在圍繞相關空間的球體表面移動檢視器,提供了一個簡單直觀的使用者介面,用於使用指標裝置進行3D導航,檢視集中在球體的中心。
該框架實現的重點是操作3D “圖元”,構成更復雜的3D物件。可以直接從R訪問通過控制形狀和外觀的函式,以實現許多有吸引力的OpenGL功能,例如多重光照、霧、紋理對映、alpha混合和依賴於側面的渲染,以及控制裝置和場景管理、環境設定(設定燈光、邊界框、視點)和匯出(製作和匯出快照)等。
為了能夠無縫整合到R系統中,軟體設計需要高度抽象,從而進一步實現跨平臺可移植的總體設計目標。由於R缺少視窗系統和OpenGL的可移植介面,而該架構提供了這些功能。(下圖)
簡要概述了架構中涉及的軟體模組,基礎層代表平臺抽象,包括五項核心服務。
場景描述儲存在複合物件模型中,渲染引擎以非常頻繁的速率對其進行計算。下圖的邏輯資料模型使用堆疊語義同時管理多個形狀和燈光,管理三個額外的物件槽,一次儲存一個物件,插槽物件被替換,而堆疊物件可以立即彈出或可選地清除。
對於GUI層,採用了抽象層和實現層相分離,使用工廠模式建立具體的裝置例項,以達到封裝不同繪製庫的目的:
渲染引擎和資料模型已使用下圖中描繪的類層次結構實現,渲染是使用多型性執行,外觀資訊在由Shape和BBoxDeco類聚合的Material類中實現,名為Scene的中心類管理資料模型並實現整體渲染策略。
2004年,Beyond Finite State Machines Managing Complex, Intermixing Behavior Hierarchies提出了基於行為的代理的架構。
基於行為的代理的架構圖。
對比FSM,基於行為的編碼支援混合(可以同時處於多個“狀態”),層次結構比平面FSM更具表現力,目標和行為之間的動態耦合,可以看作是行為樹的初代雛形。
Game Mobility Requires Code Portability則闡述了針對版本眾多的移動平臺編寫更易於跨平臺的程式碼及使用的技術。該文從專案目錄組織、資料夾結構、框架設計、編碼風格、巨集定義、重視編譯器警告等方面提供建議或示範。其框架設計使用鉤子(hook)及有限狀態機建立事件驅動的應用程式設計,並使用這些掛鉤來驅動應用程式的核心。實現的虛擬碼如下:
static boolean EventHandler( ... Parameters ... )
{
switch ( eventCode )
{
case EVT_APP_START:
ClearScreen( curApp )
MemInit( curApp ); // Initialize memory manager
GameStart( curApp );
GamePostfix( curApp );
break;
case EVT_APP_STOP:
GameEnd( curApp );
MemExit( curApp ); // Exit the memory manager
break;
case EVT_APP_EVENT: // Special treatment may be
GameEvent( curApp ); // required here depending on
break; // the actual platform
case EVT_APP_SUSPEND:
SuspendGame( curApp );
break;
case EVT_APP_RESUME:
ResumeGame(curApp );
break;
case EVT_KEY_PRESS:
keyPressed(curApp, parameter );
break;
case EVT_KEY_RELEASE:
keyReleased( curApp, parameter );
break;
}
}
以上是驅動遊戲引擎的平臺無關程式碼的掛鉤,通過這個介面,抽象了核心平臺依賴,可以為當時幾乎所有環境建立此核心,並且可以輕鬆擴充套件以新增功能,例如手寫筆。
對於記憶體管理,主張確保過載new、delete和new[]、delete[]和新增新增除錯工具,例如邊界檢查、洩漏檢測和其它統計資訊。
void *operator new( size_t size );
void operator delete( void *ptr );
void *operator new[]( size_t size );
void operator delete[]( void *ptr );
在GUI上,通過拼接來動態建立各種尺寸的背景:
此外,為了更好的跨平臺可移植性,該文建議注意以下事項:
- 記錄程式碼並使用合理且不言自明的變數和函式名稱。
- 儘量使用斷言。不僅可以檢測程式邏輯中的錯誤,還可以揭示手機和平臺之間的移植問題,例如不正確的位元組順序、對齊問題、損壞的資料等。
- 有意識地尋找和隔離平臺和裝置依賴關係。
- 如果資料結構發生變化,確保在所有配置中進行正確的調整,永遠不要讓程式碼開放或容易出現錯誤。
- 儘可能針對多裝置編寫程式碼。
- 完成應用程式後,鎖定遊戲引擎程式碼,以免將來被破壞。可以使用版本控制系統來做到這一點,或者將檔案設為只讀。
- 正確記錄構建應用程式所需的步驟。
- 記錄實現新手機版本所需的步驟。
- 永遠不要在程式碼中使用#ifdef。最不便攜的解決方案,本質上是一種hack,通常會在以前執行良好的版本和構建中引入錯誤。
- 移植時,儘量少修改程式碼。更改的程式碼越少,引入新錯誤的機會就越小!
Adding Spherical Harmonic Lighting to the Sushi Engine分享了Sushi引擎新增球面諧波(Spherical Harmonic)作為光照的技術和經驗。已知渲染方程如下所示:
假設模型是剛體、不移動且光源是遠處的球體,約束V和H項後,積分變成了恆定量。在此情況下,可以預計算這些項(Pre-Computed Radiance Transfer,PRT)並儲存到所有的P點。其中傳輸函式(transfer function)的公式和圖例如下:
傳輸函式編碼了在P點有多少光可見和有多少光被反射,儲存時使用球面諧波(Spherical Harmonic,SH),積分入射光時只剩下兩個向量的點積。具體做法是:
- 離線階段,預計算漫反射輻射率傳輸,按逐頂點或逐紋素儲存結構。
- 執行階段,引擎投射光源到SH。
- 畫素和頂點著色器用漫反射傳輸函式積分入射光,以計算全域性漫反射。
實現的工作流如下圖:
文中給出了詳細的實現細節和步驟,這裡就不詳述,有興趣的同學可以看原論文。
下圖是Sushi引擎的PRT效果圖:
Speed up the 3D Application Production Pipeline則闡述了RenderWare引擎的架構、渲染管線和支援的特性。RenderWare包含3個元件:遊戲框架、工作區、管理器。它們的關係如下圖:
RenderWare的圖形架構如下圖,通過外掛、套件和部分引擎API和應用程式互動,達到可擴充套件、解耦的目的。
RenderWare的渲染管線普通(General)、特殊案例(Special-cased)、平臺優化(Platform-optimized)、自定義管線(Custom-built pipelines)4中型別。普通模式支援靜態網格、蒙皮網格和多紋理,特殊案例支援複製、粒子、優化的光照設定、簡化的權重蒙皮、貝塞爾批處理圖元、光照貼圖等,平臺優化只要為DX9和XBOX優化頂點/畫素著色器,超過150個用於PS2的手 VU流水線,手動優化的 GameCube組裝器管道,可用時進行硬體蒙皮。
在圖形方面,RenderWare支援帶有API的DMA管理器、紋理快取管理、管線構建套件、PDS管線傳輸系統、動態頂點緩衝區管理、渲染狀態快取、原生幾何體/紋理例項化等。另外還支援場景管理、平臺優化的檔案系統、流式載入、非同步載入、粒子系統、檔案打包、智慧管線選擇等功能。
RenderWare渲染效果圖。(2004)
An explanation of Doom 3’s repository-style architecture提到Doom 3的架構圖,可知Doom 3包含了場景圖、遊戲邏輯框架、物理和碰撞、骨骼蒙皮、低階渲染、資源管理、第三方庫、核心系統等模組。其中核心繫統包含斷言、數學庫、記憶體管理(Zone Memory)、自定義資料結構。(下圖)
Doom 3的引擎模組架構和依賴圖。(2004)
Doom 3的低階渲染(LLR)是所有遊戲引擎中最大、最複雜的元件之一,應用於3D圖形時極為重要,低階渲染包含引擎的所有原始渲染工具,處理遊戲中存在的許多預處理任務。LLR負責底層資料結構,負責遊戲操作,以及預處理關卡設計。關卡是二維設計的,因此不能有多層的關卡,遊戲將樓層劃分為 2 個單獨的級別。
在資源管理方面,Doom 3使用了一個名為DeePsea的資源管理器,包含支援3D物件建模、物件碰撞解決等的包,遊戲資訊/資料包儲存在稱為WAD的特殊檔案中,IWAD(內部 WAD)或 PWAD(補丁 WAD),包含相關資料的WAD被組裝成塊,例如:名為“BLOCKMAP”的IWAD塊儲存指示地圖中的任何兩個物件是否相互接觸的資訊。
在渲染特效方面,Doom 3已經支援光照貼圖、HDR照明、PRT照明、粒子和貼花系統、後期效果、環境對映等。
上:Doom 3的光照對比圖;下:Doom 3的特效系統。
Doom 3的場景圖(Scene Graph)包含場景/區域中的所有渲染模型和紋理,與前臺元件互動確保後臺所需的一切都上傳到GPU記憶體,處理剔除並將命令傳送到後臺。
Doom 3場景圖中的光源裁剪示意圖。
有了以上技術的支援,使得同期的場景效果出現明顯的改善:
2004年的遊戲室內場景截圖。
Development of a 3D Game Engine - OpenCommons提到了2000中後期的引擎設計思路,此引擎被稱為Spark Engine。Spark Engine的遊戲引擎主要邏輯的虛擬碼如下:
OnDraw()
Frustum cull check
If inside or intersects, call Draw()
Draw()
If Node, call OnDraw() for children
If Mesh, add to render queue to be processed
If set to skip, call Render() immediately
Render() (Mesh only)
Apply render states (checked against renderer’s enforced states)
Apply material
Update material definitions (sets shader constants)
Evoke device draw call
對於場景渲染器,它是引擎的渲染工具和橋樑,是在繪製過程中傳遞給每個空間的物件,並提供將物件實際渲染到螢幕的功能。場景渲染器被設計成完全獨立的,每個渲染器都擁有一個攝像頭,因此也擁有一個視口——它可以是整個螢幕,也可以是其中的一小部分。允許多個渲染器在不同的上下文中同時處於活動狀態,例如,一個分屏遊戲,兩個玩家使用自己的相機渲染相同的場景圖,場景圖資料完全獨立於渲染器。除了包含與圖形裝置互動和啟動繪圖呼叫的功能,渲染器提供了一些額外的功能來提高渲染效能。渲染器通過兩種方式完成此操作:
-
渲染狀態快取。渲染器跟蹤應用到圖形裝置的最後渲染狀態,以減少狀態切換。切換狀態可能很昂貴,因此減少冗餘是渲染器採用的有用策略。
-
渲染佇列。渲染器擁有一個渲染佇列來管理一組桶,使得開發人員可以控制渲染物件的順序。預設情況下,引擎支援四個桶:Pre-Bucket、Opaque、Transparent 和Post-Bucket,它們按此順序呈現。控制渲染幾何圖形的順序很有用,可以允許正確繪製透明物體。下圖用一個透明和不透明的立方體展示了此技術特點,左邊的圖片有正確排列的立方體,而右邊的圖片沒有:
渲染狀態會影響在頂點和畫素流經渲染管道時幾何圖形的處理方式。在微軟遊戲開發套件XNA中,狀態被定義為引擎使用的列舉。與XNA不同,引擎將這些列舉分組為單獨的類,這些類可以直接附加到諸如Spatial的類,場景圖允許渲染狀態被繼承和結合。引擎在定義渲染狀態時也採用了稍微不同的方法,引擎渲染狀態是指與幾何資料相關的所有資訊,包括材質顏色資訊、紋理和光照。渲染狀態分為兩大類:
- 全域性渲染狀態。包含來自XNA的列舉,類似於渲染狀態的典型定義,例如alpha混合、三角形剔除、深度緩衝等。這些狀態被定義為全域性狀態,因為它們的資訊獨立於Spatial類的任何屬性。因此,這些狀態本質上只不過是包裝器提供與XNA中的渲染狀態列舉介面的元件化形式。
- 資料渲染狀態。是為引擎的材質系統提供著色、紋理和照明資訊的特殊用途類。將資料與材質和著色器分離允許靈活性和重用——資料渲染狀態所持有的紋理和燈光物件不對應於任何一個著色器。引擎的TextureState可以儲存任意數量的紋理,這些紋理可以被不同的材質解釋。例如,一些著色器可能只使用第一個紋理作為漫反射顏色,其它著色器可能將第二個和第三個紋理用於法線和高光貼圖。
所有Spark Engine渲染狀態都繼承自一個名為AbstractRenderState的抽象類。以下是引擎當前支援的渲染狀態:
- BlendState:控制透明度選項的 alpha 和顏色混合。
- CullState:控制三角形剔除,例如逆時針、順時針或無剔除。
- FillState:控制應如何填充物件,無論是線框、實體還是點。
- FogState:控制固定功能的DirectX9.0c的霧功能。
- ZBufferState:控制深度緩衝。
- MaterialState:用於控制物件統一材質顏色的資料渲染狀態,例如作為漫反射、環境反射、發射和鏡面反射顏色。
- TextureState:應用於物件的紋理的資料渲染狀態,還管理紋理過濾和包裝模式的取樣器狀態。
- LightState:燈光物件的資料渲染狀態,跟蹤依附於空間的光源列表。
對於材質系統,材質定義了物件應該如何著色和著色的屬性,每個被渲染的物件都需要有一個關聯的材質,與XNA的Effect API直接相關。 材質系統是一個簡單但非常靈活的系統,旨在解決直接使用Effect API的幾個缺點, 繪製呼叫中效果的典型呼叫過程虛擬碼如下:
Load the shader effect file
Set shader constants
Call Effect.Begin()
For each EffectPass do
Call EffectPass.Begin()
Draw geometry
Call EffectPass.End()
Call Effect.End()
Spark Engine還支援點光源、聚光燈、定向光等光源型別,抽象各種型別的光源的結構體如下:
//Light struct that represents a Point, Spot, or Directional
//light. Point lights are with a 180 degree inner/outer angle,
//and directional lights have their position's w = 1.0.
struct Light {
//Color properties
float3 Ambient;
float3 Diffuse;
float3 Specular;
//Attenuation properties
bool Attenuate;
float Constant;
float Linear;
float Quadratic;
//Positional (in world space) properties
float4 Position; //Note: w = 1 means direction is used
float3 Direction;
float InnerAngle;
float OuterAngle;
};
它們的渲染效果如下:
Spark Engine光源渲染效果。從上到下依次是點光源、聚光燈、平行光。
此外,Spark Engine還支援不同著色階段的光照、法線貼圖、高光貼圖、輪廓光、立方體環境圖、地形編輯、光照圖等效果。
Spark Engine的立方體環境圖。
QUAKE III ARENA在網路同步方面,採用了CS架構,通過網路連結伺服器,以便伺服器同步各個客戶端之間的操作:
QUAKE III ARENA的遊戲子系統包含遊戲的伺服器實現,還包含一些遊戲伺服器之間共享的庫和客戶。PMOVE模組是更新播放器的主要模組整個遊戲的狀態資訊,採取的輸入是當前播放器狀態 (player_state_t),以及從客戶端接收到的使用者操作(usercmd_t)。 一個新的生成的player_state_t代表當前玩家在遊戲中的狀態。(下圖)
上圖是QUAKE III ARENA的客戶端同步到伺服器的通訊模型,下圖則是伺服器通訊到客戶端的同步模型:
同期的ORGE引入了場景管理器、資源管理器、渲染及外掛等模組,其架構圖如下:
OGRE的根物件是入口點,必須是第一個建立的物件,必須是最後一個刪除的物件,啟用系統配置,有一個連續的渲染迴圈。場景管理器包含出現在螢幕上的所有內容和地形(高度圖)、外部和內部場景的不同管理器。Entity是可以在場景中渲染的物件型別,包含任何由網格表示的東西(玩家、地面……),但燈光、廣告牌、粒子、相機等物件不是實體。SceneNode(場景節點)跟蹤連線到它的所有物件的位置和方向,實體僅在附加到 SceneNode 物件時才會在螢幕上呈現,場景節點的位置總是相對於它的父節點,場景管理器包含一個根節點,所有其他場景節點都連線到該根節點。最後的結構是場景圖。ORGE的渲染主迴圈如下:
void Root::startRendering(void)
{
// ... Initialization ...
mQueuedEnd = false;
while( !mQueuedEnd )
{
//Pump messages in all registered RenderWindow windows
WindowEventUtilities::messagePump();
if (!renderOneFrame())
break;
}
}
bool Root::renderOneFrame(void)
{
if(!_fireFrameStarted())
return false;
if (!_updateAllRenderTargets()) // includes _fireFrameRenderingQueued()
return false;
return _fireFrameEnded();
}
ORE的資源管理策略如下:
-
每個資源有4種狀態:
- Unknown:Ogre不知道該資源,它的檔名被儲存了,但 Ogre 不知道如何處理它。
- Declared:標記為建立。Ogre知道它是什麼型別的資源,以及在建立它時如何處理它。
- Created:Ogre建立了資源的一個空例項,並將其新增到相關的管理器中。
- Loaded:建立的例項已完全載入,訪問資原始檔的階段。
-
Ogre的原生ResourceManagers是在Root::Root中建立的
-
通過呼叫指定資源位置
-
手動宣告資源。
-
指令碼解析自動宣告資源。
-
滿足某些條件的資源才會被設定為載入的。
2006年的UE3支援了64位HDR色彩、逐畫素光照、高階動態光影(動態模板緩衝陰影體積、軟陰影、預計算陰影、大數量的預計算光)、材質系統、體積霧、支援物理和環境互動的粒子系統,以及其它諸多渲染特性(如法線貼圖、引數化的 Phong 光照; 自定義藝術家控制每個材料照明模型,包括各向異性效果; 虛擬位移對映; 光衰減功能; 預先計算的陰影掩模; 定向光照貼圖; 以及使用球諧圖預先計算的凹凸粒度自陰影等等)。這些特性的加持,使得UE3的渲染畫質有了顯著的提升。(下圖)
UE3的部分渲染特性。從左上到右下依次是帶有自陰影的角色動態軟陰影、動態軟陰影、逐畫素光影、法線貼圖的半透明物件扭曲和衰減幀緩衝區、雲體動態陰影、體積霧、64位HDR色彩、法線貼圖漫反射和鏡面反射照明與模糊陰影的相互作用。
UE3的角色和場景物體渲染效果。
到了CryEngine3,因為引入了更多新興的技術,使得渲染畫面又上了一個臺階(下圖)。
CryEngine 3的遊戲畫面截圖。
2006年,A realtime immersive application with realistic lighting: The Parthenon和The Parthenon Demo: Preprocessing and Rea-Time Rendering Techniques for Large Datasets描述了一個互動式系統的設計和實現,該系統能夠實時再現Siggraph 2004上展示的短片“帕臺農神廟”中的關鍵序列之一,該演示程式旨在在特定的沉浸式現實系統上執行,使使用者能夠以接近電影級的視覺質量感知虛擬環境。
實時演示的螢幕截圖。時間從黎明流逝到黃昏,陰影在建築物上移動,場景的整體色調根據天空照明而變化。這些照片是從不同的位置拍攝的,每次都更靠近建築物。
該文討論了與資料集大小和所需照明計算的複雜性相關的一些技術問題。為了解決這些問題,提出了使掃描的3D模型更適合實時渲染應用程式的方法,並且描述了基於直接和間接光之間的分離和廣泛的預計算的照明演算法。其中直接光和非直接光的公式如下:
該文還展示瞭如何使用現有渲染引擎預先計算光照不變數,以及如何使用現代GPU實現這些著色演算法。由此產生的技術已被證明是準確的(在渲染結果方面)並且對於實時應用程式來說是負擔得起的(在時間方面)。此外,由於演算法執行僅限於硬體著色器,如何將這種計算整合到現有的應用程式框架中。由於演示中使用的技術非常通用,因此可以將相同型別的計算整合到其它現有的視覺化系統中。
實時著色示例:注意陰影如何在幾何體上精確移動(上圖和左下圖),以及HDR照明計算如何允許調整曝光以更好地感知陰影下的細節(右圖)。
漫射照明預計算。左側是用於照明的球面諧波基,正負值以紅色和綠色編碼。在右側,通過使用諧波作為天穹光源照亮場景,可以看到物件的每個部分受該諧波影響的程度。
在現代GPU的硬體著色器上完成所有計算後,擴充套件現有渲染引擎以適應額外的資料和著色器管理是可行的。通過這種方式,甚至可以為大型3D資料集視覺化工具新增逼真的照明。
該文還提及了漸進式緩衝區(Progressive Buffer)的技術,需要對模型進行預處理,將模型拆分為Cluster,引數化Cluster和樣本紋理。詳細見小節14.3.4.4 Progressive Buffer。
論文還探討了陰影圖、PCF軟陰影、SH預計算、例項化、天空渲染(天空環境光、直接光)、遮擋剔除等技術。
SH估算。使用三階球諧函式 (SH) 為每一幀匯出和記錄HDR照明資訊,用於提供漫反射照明資訊以渲染場景。
天空環境光渲染。每頂點彎曲法線用於查詢SH表示,使用笛卡爾SH評估,12條指令用於3階,用於衰減環境光的環境光遮蔽紋理(半解析度)。
天空直接光渲染。從天空盒中提取每幀太陽的顏色、強度和位置,凹凸貼圖只需要作為細節紋理。
PCF在4樣本下優化前後的效果對比。
遮擋查詢幾何剔除。繪製的每個Cluster都經過遮擋查詢測試,以檢視為當前幀繪製了多少畫素。如果繪製了任何畫素,則將體素標記為下一幀繪製,如果沒有可見畫素,下一幀啟用廉價的“探測”,用顏色和禁用Z寫入來渲染四邊形,以代替體素。
2006年,Practical Parallax Occlusion Mapping For Highly Detailed Surface Rendering講到了視差遮擋對映的技術,以提升物體表面的細節和可信度。
平行視差遮擋對映(左)和法線對映(右)對比圖。
視差遮擋對映依賴法線貼圖、高度(位移)圖兩種資源,它們的計算都在切線空間中完成,因此可以應用於任意曲面。(下圖)
)
在計算視差效果時,可以通過應用高度圖並使用幾何法線和檢視向量偏移高度圖中的每個畫素來計算表面的運動視差效果,通過高度場追蹤光線以找到表面上最近的可見點。演算法的核心思想是跟蹤當前在高度圖中反向渲染的畫素,以確定高度圖中的哪個紋素將產生渲染的畫素位置,如果實際上一直在使用實際的位移幾何體。輸入網格提供了用於向下移動曲面的參考平面,高度場被歸一化以進行正確的射線高度場交叉計算(0表示參考多邊形表面值,1表示凹陷)。
在實現的過程中,有逐頂點和逐畫素兩種方式,可以採用高度圖輪廓追蹤(Height Field Profile Tracing),選擇合適的射線相交檢測、動態的採用率,可以實現自陰影、軟陰影等效果。在計算光照時,使用計算的紋理座標偏移量來取樣所需的貼圖(反照率、法線、細節等),給定這些引數和可見性資訊,可以根據需要應用任何照明模型(例如Phong),計算反射/折射,非常靈活。
此外,還可以採用自適應LOD系統,計算當前mip map級別。對於最遠的LOD級別,使用法線貼圖(閾值級別)進行渲染,隨著表面接近觀察者,提高取樣率,作為當前Mip貼圖級別的函式。在閾值LOD級別之間的過渡區域,在法線貼圖和全視差遮擋貼圖之間進行混合。
Rendering Gooey Materials with Multiple Layers涉及了多層材質的渲染。多層材質主要用於渲染半透明、體積材質、參合介質、多種介質的渲染,涉及分層組合(層間遮擋、以Alpha格式儲存不透明度)、深度視差(層深或厚度引起的視差)、光源擴散(光在層之間散射)等技術。該文拋棄了傳統的多紋理混合的技術,採用了全新的組合技術,包含法線貼圖、半透明遮罩、平行視差、影像過濾等。
多層材質渲染案例:心臟。
總之,該文開創了多層材質渲染的先例,具有計算效率高,視覺效果佳(體積的深度視差、次表面散射的紋理模糊)等特點。
Real-time Atmospheric Effects in Games講述了天空光渲染、全域性體積霧及它們的組合效果:
此文還探討了軟粒子和雲體的實現:
禁用(左)和啟用(右)軟粒子的對比圖。
雲體渲染使用了逐畫素深度,且實現了雲體對地形的遮擋陰影效果:
雲體陰影效果。雲陰影在單個全屏通道中投射,使用深度恢復世界空間位置,變換為陰影圖空間。
此外,該文還介紹到逐畫素深度可用於河流等水體渲染,以呈現水下深度不同而具體不同顏色的效果:
Shading in Valve’s Source Engine講述了2006年的Source Engine使用的著色技術,包含用於世界光照的輻射法線貼圖(Radiosity Normal Mapping)和高光計算,用於模型光照的輻照度體積(Irradiance Volume)、半蘭伯特(Half-Lambert)、馮氏光照(Phong),用於HDR渲染的色調對映、自動曝光以及色彩校正。
使用輻射度量的光照更加真實,比直接光更高的寬容度,避免惡劣的照明情況,減少對內容製作光源的微觀管理,因為不能像電影一樣逐個調整燈光。
僅直接光(上)和輻射度光(下)的對比圖。
Source Engine著色的關鍵在於建立了Radiosity Normal Mapping來有效地解決輻射度和法線對映,在新的基(novel basis)上表達了完整的光照環境,以便有效地對任意數量的燈光執行漫反射凹凸對映。
輻射度法線對映的基。
計算光照圖的值時,傳統的光照傳輸預處理計算光照貼圖值只會計算單個顏色值,在輻射法線對映中,計算基礎中每個向量的光值,使得光照貼圖儲存量增加了三倍,但Source引擎研發人員認為它提升了質量和靈活性,值得承擔額外的開銷。
對三種光照貼圖顏色進行取樣,並根據變換後的向量在它們之間進行混合:
float3 dp;
dp.x = saturate( dot( normal, bumpBasis[0] ) );
dp.y = saturate( dot( normal, bumpBasis[1] ) );
dp.z = saturate( dot( normal, bumpBasis[2] ) );
dp *= dp;
diffuseLighting = dp.x * lightmapColor1 + dp.y * lightmapColor2 + dp.z * lightmapColor3;
可變照度密度視覺化。
Source Engine在模型光照使用的半蘭伯特時,通常在端接器處將N·L擷取為零,半蘭伯特將-1到1餘弦項(紅色曲線)縮放1/2,偏差1/2和正方形以將光一直拉到周圍(藍色曲線)。
對於非直接光照,Source引擎採用環境立方體基(Ambient Cube Basis),六個RGB波瓣儲存在著色器常量中,比前兩個球諧函式更簡潔的基礎(九種 RGB 顏色):
環境立方體基實現細節。
環境立方體與球諧函式的比較。
其它遊戲(上)和採用半蘭伯特、環境立方體(下)的對比。
Source引擎的光照計算樹如下:
Ambient Aperture Lighting闡述了用可視孔徑(Visibility aperture)、區域光源和軟硬陰影,並應用於地形渲染。所謂環境孔徑光照,是指使用孔徑來近似可見度函式的著色模型,預先計算的可見性,動態球面光源和點光源,支援硬和軟陰影,類似於地平線對映,但允許區域光源,“環境”來自這樣一個事實,即使用修改後的環境遮擋計算來找到平均可見度的孔徑。
環境孔徑照明分兩個階段工作:
- 預計算階段。逐頂點或逐畫素在網格上的每個點計算可見性函式,使用球冠儲存可見性函式,球形帽儲存一個平均的、連續的可見區域,球冠是被平面截斷的球體的一部分(半球本身就是球冠)。
- 渲染階段。球形帽用作孔徑,孔徑用於限制入射光,使其僅從可見(未遮擋)方向進入,面光源投射到半球上並夾在光圈上,決定了有多少光通過孔徑。
孔徑照明示意圖。
使用光圈進行渲染的過程如下:
- 將球面光源投射到半球上。
- 投影面光源覆蓋半球的某些區域。投影球體形成一個球冠,就像孔徑一樣。
- 找到投射光的球冠和孔徑的球冠的交點。
- 一旦找到相交區域,就知道通過孔徑的光源部分。
精確光影(上)和孔徑近似結果(下)對比圖。
Fast Approximations for lighting of Dynamic Scenes主要闡述了遊戲Small world採用了體積進行光照近似的計算,並例舉了幾個具體的應用案例,如輻照度切片( Irradiance slices)、有符號距離函式(Signed Distance Functions)及檢視對齊的輻照度體積。
輻照度切片的目標是混合來自動態平面光源的柔和和銳利陰影,而無需預先計算。平行於光源將世界切片,在每個切片儲存一個紋理:
從最靠近光線的地方開始,依次追蹤來自每個平面的光線。如果光線到達前一個平面,則停止跟蹤,並在前一個平面返回結果:
對於SDF,使用它來測量表面曲率,以便獲得近似的AO,如摺痕和凹陷內的區域接收到的天光較少,表面曲率是一個很好的開始。
SDF計算AO示意圖。
輻照度體積(Irradiance Volume)是當時很多遊戲用來儲存(和取樣)流過場景中任何點的光,通常它們是世界對齊的,並以粗解析度預先計算,儲存在任何方向流動的輻照度,通常使用球諧函式壓縮。
檢視對齊的輻照度體積不同於世界對齊的輻照度體積,它是檢視對齊的。在動態場景中,無法預先計算輻照體積,因此,基於潛在的大量光源發射器,使用GPU以低解析度每幀動態重新計算它。在螢幕空間中計是有意義的,在文中示例,針對約束的小世界,因此使用少量切片 (16),與螢幕平行,它們在後投影空間中均勻分佈,即在“w”中均勻分佈(1/z)。
檢視對齊的輻照度體積的渲染效果圖。
總之,該文使用小場景的體積表示呈現了3種新技術以獲得漂亮的外觀。第一個是在場景中重複縮放和模糊切片可以產生令人信服的半影效果;第二個是GPU更新的體積紋理用於快速計算來自“天窗”的遮擋資訊,以提供帶有一些反射光效果的漂亮AO外觀;第三個是螢幕對齊的“輻照度體積”梯度用於快速計算來自大量移動光源的照明。
An Analysis of Game Loop Architectures談到了遊戲迴圈架構的設計、實現等內容,目的是隱藏複雜性、覆蓋面小、最高階別的遊戲架構。該文給出定義遊戲迴圈的虛擬碼:
GameLoop()
{
Startup();
while (!done)
{
GetInput();
Sim();
Render();
}
Shutdown();
}
遊戲迴圈至少有一個執行執行緒,包括啟動階段、處理輸入/輸出的迴圈階段、關閉階段等。該文還定義了遊戲迴圈架構,陳述了從1940年代到2000年代的演變節點:
遊戲迴圈的複雜性可以由下圖描述。設計遊戲迴圈時,最初也是最基礎的首先考慮時間問題。然後開始使用多個遊戲迴圈並增加系統複雜性,此時必須處理迴圈耦合。最後需要考慮轉移到具有多個CPU的平臺,即併發,這時的複雜度將大大提升。有趣的是,還可以將“複雜性”視為時間箭頭,或“處理器數量”箭頭,隨著遊戲引擎的發展,可以將其視為單執行緒單CPU,然後是多執行緒單CPU,然後是多執行緒多CPU。也可以視為歷史時間線或經過多次迭代演變的遊戲引擎,沿著邊緣移動,遊戲變得更加複雜,並且使用了更多的處理器。此圖是累積的,不解決耦合和時間就無法解決併發問題。
對於最內圈的時間,為了實時流暢執行,可以使用頻率驅動的遊戲迴圈,將時間劃分為離散的迭代,嘗試執行足夠快和足夠流暢。這樣的結果是每個Loop迭代都是一個時間片。面臨的挑戰是如何保持具有可變執行時間的迴圈頻率,影響的因素效能、寬容度、決策和簡單性。而實現的架構決策有排程(Scheduling)和時間步長(Time Step)兩種方式。排程控制迴圈迭代何時開始。
排程模型:即時、最佳匹配、對齊。
對於即時的排程模式,儘可能塊地執行,利於是效能和簡潔性,不利於寬容度,缺少決策性。即時的排程模式又可細分為精確、快速、緩慢、可變等方式:
即時排程模式常見於早期的冒險遊戲,其用例和虛擬碼如下:
對於最佳匹配的排程模式,描述時間的平均性,嘗試維持幀率、在準確的時間開始以及跟上幀率。利於效能和寬容度,不利於簡潔性,不具備決策性。
對齊的排程模型描述了垂直同步,利於簡單,不利於效能和寬容度,缺少決策性。
對於時間步長(Time Step)的時間架構,分為無(None)、固定值(Fixed-Value)、實時(Real-Time)三種模式,它們的特點分別如下所示:
上面分析完最內圈的時間,接著分析中間圈的耦合性。耦合性的問題是支援不同頻率的系統,可以通過多個遊戲迴圈解決,將獲得迴圈耦合,即每對迴圈之間的依賴關係。耦合性面臨的挑戰如何將程式碼和資料拆分為多個迴圈,影響的因素有效能、寬容、簡單、視訊模式、記憶體、可擴充套件性等。架構決策分為頻率耦合和資料耦合。
其中頻率描述一個迴圈在多大程度上依賴於另一個迴圈的頻率,分為相等(Equal)、多次(Multi)、解耦(Decoupled)三種方式,它們的特點如下所示:
資料解耦描述了共享資料的數量和方法,分為緊密(Tight)、鬆散(Loose)、無(None),它們的特點如下所示:
遊戲迴圈的最外圈是併發,存在的問題是硬體製造商需要極端的效能,可通過硬體包含多個CPU來解決,結果是遊戲迴圈和CPU之間的對映而形成併發。面試的挑戰是如何管理同時執行,影響的因素有效能、簡單、可擴充套件性。架構決策有低階併發(Low-Level Concurrency)和高階併發(High-Level Concurrency)。
低階併發是在遊戲迴圈內併發,包含無(None)、指令(Instruction)、函式(Function)三種模式。低階併發廣泛覆蓋,最容易過渡到下一代架構,從小處著手併成長,開放式MP,自下而上的方法,可以延時執行。
高階併發是跨越一對遊戲迴圈的並行,包含序列(Sequential)、交叉(Interleaved)、平行(Parallel)三種模式,它們的描述如下:
下圖是遊戲Madden在資料解耦方面採用的具體策略:
Ritual™ Entertainment: Next-Gen Effects on Direct3D® 10詳細地講述了DirectX10的新特點及使用它來實現新效果的案例。文中提到DirectX10的特點是:
- 一致性(如控制檯)。有保證的基本功能集供您定位,跨所有晶片組的嚴格定義的行為。
- 更高的效能上。通過設計顯著提高小批量效能,使用更強大的幾何引擎解除安裝CPU。
- 更好的視覺效果。提高靈活性和可程式設計性,新的硬體功能。
DirectX10改進了硬體渲染管線,包含紋理陣列、幾何著色器、流輸出、資源檢視、輸入彙編器、通用著色器核心 (SM 4.0)、整數/位指令、比較過濾、常量緩衝區、狀態物件、用於HDR、法線/凹凸貼圖內容的新壓縮格式、更多紋理、RT、指令、暫存器、級間通訊、預測渲染、阿爾法覆蓋率、多樣本回讀等等。
在軟體棧上,特性有流線型和分層執行時、乾淨一致的API、強大的除錯層、精益的核心、具有新語言功能的新HLSL編譯器、新效果系統,以及基於新的 Windows Vista™ 顯示驅動程式模型構建。
無處不在的資源訪問資源檢視示例:Cubemap,檢視可以描述不同繫結位置的資源。
檢視可以重新解釋資源資料的格式。
不同於DirectX9,DirectX10可以通過幾何著色器訪問所有的圖元資訊(點、線、三角形):
通過幾何著色器(GS),可以實現全GPU材質系統,按逐圖元材質選擇和設定,可以計算邊長和皺紋模型、平面方程、輪廓邊,將重心設定為超過插值器的數量。可以構建指定輸出型別的圖元(點、線帶、三角形帶),有限的幾何放大/去放大:每次呼叫輸出 0-1024 個值,不再有 1進1出的限制,可以實現陰影體積/毛皮/翅膀、程式幾何/細節、全GPU粒子系統、點精靈等。
GS還可以觸發系統解釋值(System-Interpreted Value),例如圖元的RenderTargetArrayIndex,為體積渲染選擇切片,為渲染到立方體貼圖選擇一個面,但MRT仍然在PS中指定。
另外,VS或GS支援流輸出(Stream Output),將VS/GS結果流式傳輸到記憶體中的一個或多個緩衝區,DrawAuto() 無需App/CPU干預即可繪製動態數量的GS資料。可用於迭代、程式幾何處理、全GPU粒子系統等。(下圖)
Feeding the Monster: Advanced Data Packaging for Consoles針對主機平臺闡述了資源載入問題、LIP解決方案、C++物件打包等內容。文中提到,為了滿足下一代資料需求,載入將需要更頻繁,光碟機效能不會隨著記憶體/CPU功率的增加而擴充套件,因此載入效能必須是最佳狀態,且必須消除除原始磁碟傳輸之外的任何處理。
常規的載入資料策略是使用載入畫面(Loading Screen),但它具有破壞性的、技術無趣、不可跳過的過場動畫也不是更好,因此它不適合當時的需求。還有一種策略是後臺載入(Background Loading),線上程或其它處理器中使用阻塞I/O,遊戲資產在遊戲過程中載入,玩家沉浸感得以保留。但當時的要求是不能比載入螢幕慢很多、必須低於CPU開銷、不得阻止其它IO,因此後臺載入的載入效能必須是最佳的,必須消除除原始磁碟傳輸之外的任何處理。
對下一代載入技術的要求是:必須以接近硬體傳輸限制的速度載入大量資產,必須以很少的CPU成本實現後臺載入,資料資產必須在不造成記憶體碎片的情況下流入和淘汰。
載入時間包含釋放記憶體空間(卸貨、碎片整理)、搜尋時間、讀取時間、分配、解析、重定位(指標、雜湊 ID 查詢)、註冊(例如物理系統)等。減少載入時間的策略有:
-
始終載入壓縮檔案。使用N:1壓縮將載入N倍快,雙緩衝隱藏解壓時間,大量處理能力可用於在下一代遊戲機上進行解壓。
-
利用光碟功能。將經常訪問的資料儲存在光碟的外部,將音樂流儲存在中間(防止完全搜尋),在中心附近儲存一次性資料(視訊、過場動畫、引擎可執行檔案),小心層切換(0.1秒消耗)。
-
使用輕量級設計模式。如幾何例項化、動畫分享。
-
優先選取程式化技術。如引數化曲面、紋理(火、煙、水)。
-
始終離線準備資料。消除引擎中的文字或中間格式解析,浪費在轉換或解釋資料上的引擎時間,載入本機硬體和中介軟體格式,直接載入C++物件。
文中還提到載入C++物件的原因,有更自然的資料處理方式、無需解析或解釋資產、建立快、指標重定位、雜湊ID轉換、物件註冊等。載入C++物件需要非常智慧的封裝系統:成員指標、虛擬表、基類、對齊問題、位元組順序(Endianness)。而載入非C++物件時,必須是讀入記憶體後可以使用的格式(如紋理/法線貼圖、Havok結構體、音訊、指令碼位元組碼),實現和使用都很簡單。
文中提到了一種新的載入技術叫就地載入(Load-In-Place,LIP),是遊戲資產打包和載入解決方案,用於定義、儲存和載入本機C++物件的框架,具備動態儲存,即一個自我碎片整理的遊戲資產容器。
LIP載入技術。
LIP技術涉及到了LIP條目(item),1個LIP條目對應1個遊戲資產,1個LIP條目對應唯一雜湊ID(64 位),其中的32位用於型別ID和屬性,另外32位用於雜湊資產名稱(CRC-32)。LIP條目是最小的資料單位,用於查詢、碎片整理移動、解除安裝,支援C++物件和二進位制塊。LIP條目樣例包括聯合動畫、人物模型、環境模型部分、碰撞地板部分、遊戲物件(英雄、敵人、觸發器等)、指令碼、粒子發射器、紋理等等。
基於C++的LIP條目可以由任意數量的C++物件和陣列組成,在光碟上,所有內部指標都保持相對於LIP條目塊,指標重定位從重定位建構函式上的新位置開始,內部指標通過建構函式連結自動重定位。
為了順利從磁碟載入C++的LIP條目,需要過載new
操作符(語法:new(<address>) <type>;
),呼叫建構函式但不分配記憶體,初始化虛擬表,為主類重定位建構函式上的每個LIP項呼叫一次。然後重定位建構函式,所有類和結構都需要:可以由LIP框架載入、包含需要重定位的成員,支援3種建構函式(載入重定位建構函式、移動重定位建構函式(碎片整理)、動態建構函式(可選,可以是虛擬的),但不支援預設建構函式。對於物件成員重定位,內部指標必須指向LIP專案塊內並轉換成絕對指標,外部引用(僅限LIP條目)儲存為LIP條目雜湊ID並轉換為全域性資產表條目中指向所引用LIP條目的指標,LIP框架為所有指標型別提供了帶有適當建構函式的封裝類。重定位示例程式碼:
// --- GameObject定義 ----
class GameObject {
public:
GameObject(const LoadContext& ctx);
GameObject(const MoveContext& ctx);
GameObject(HASHID id, Script* pScript);
protected:
lip::RelocPtr<Transfo> mpLocation;
lip::LipItemPtr<Script> mpScript;
};
// --- GameObject實現 ----
GameObject::GameObject(const LoadContext& ctx) :
mpLocation(ctx),
mpScript(ctx) {}
GameObject::GameObject(const MoveContext& ctx) :
mpLocation(ctx),
mpScript(ctx) {}
GameObject::GameObject(HASHID id, Script* pScript) :
mpLocation(new Transfo),
mpScript(pScript) { SetHashId(id); }
// --- 重灌new操作符 ----
template<typename LipItemT>
void PlacementNew(lip::LoadContext& loadCtx)
{
new(loadCtx.pvBaseAddr) LipItemT(loadCtx);
}
// 載入示例
loadCtx.pvBaseAddr = pvLoadMemory;
PlacementNew<GameObject>(loadCtx);
基於C++的LIP條目構建的步驟和流程圖例如下:
LIP還存在載入單元(Load Unit),它是LIP條目組,可以載入的最小資料單位,1個載入單元對應1個載入命令,檔案數量最小化,1 個獨立於語言的檔案(如模型、動畫、指令碼、環境……)及N個語言相關的檔案(字型、遊戲內文字、紋理、音訊……),載入單元檔案通常被壓縮。載入單元還涉及載入單元表,每個LIP條目在表中都有一個條目,包含雜湊ID、LIP條目的偏移量。
文中採用了動態載入,它的載入過程如下:
- 讀取載入單元檔案並解壓縮到可用儲存記憶體。
- 載入單元表偏移被重新定位。
- 載入單元表條目合併到全域性資產表中。
- 為每個LIP條目呼叫一個新的展示位置。
- 某些LIP條目型別可能需要第二次初始化通過(例如註冊)。
動態載入的卸貨過程如下:
- 每個LIP條目都可以單獨移除。
- 一個載入單元的所有LIP條目可以一起移除。
- 在C++ LIP條目上呼叫解構函式。
- 動態儲存演算法稍後會對新的碎片進行整理。
LIP條目可以被鎖定,鎖定的條目無法被移動或解除安裝。
此外,LIP還可以用於基於網路的資產編輯,LIP條目可以在遊戲過程中從關卡中遷移出來或遷移進去,資產規模的變化無關緊要。另外,LIP也可以用於Maya匯出用於儲存中間藝術資產,比解析XML效率高得多。
該文還探討了編輯器所需的資料以及實現方式、虛擬函式表資料對齊、引擎所需的資訊及用於重定位的各類智慧指標等內容。
引擎所需的資訊之一:型別雜湊表。
用於重定位的各類智慧指標:弱引用、強引用智慧指標。
Best Practices in Game Development講述了遊戲架構的演變、軟體的限制、遊戲架構的趨勢等內容。
軟體限制包含物理定律、軟體法則、演算法的挑戰、釋出的難度、設計的問題、組織的重要性、經濟學的影響、政治的影響、人類想象力的極限等。大多數超高效組織通過可執行檔案的增量和迭代釋出來發展他們的架構。
遊戲架構中的力量,當時處於一個由以下因素驅動的拐點:
- 新控制檯的誕生。戲劇性的技術轉變。
- 新的遊戲型別。開發工作室必須學習新的程式設計模型,購買新的開發工具,從單執行緒、線性、執行模型轉變為多執行緒、並行、執行模型。
文中提到影響軟體的因素很多,如下所示:
並指出架構軟體是不同的,沒有等效的物理定律,不同的透明度,具體複雜性(狀態空間的組合爆炸、非連續行為、系統性問題),需求和技術流失,複製和分發成本低。軟體工程的整個歷史是不斷上升的抽象層次之一,在計算機語言、平臺、處理、架構、工具、賦能等方面都在不斷演化:
架構化的理由有在多產專案中圍繞發展可執行架構的流程中心,結構良好的系統充滿模式,可以抗風險,簡單有彈性。下圖是文中提出的幾種軟體架構元模型:
跨功能機制涉及一些結構和行為橫切元件(如安全、併發、快取、永續性),這些元素通常表現為散佈在整個系統中的小程式碼片段,很難使用傳統方法進行本地化。文中也提到了4+1檢視的軟體架構模型,下面是ABIO的部署檢視和邏輯檢視:
文中還談及了提高軟體經濟學的效率問題,給出瞭如下的公式:
其中:
- 複雜度代表人工生成的程式碼量。
- 過程代表方法、符號、成熟度。
- 團隊代表技能、經驗、動機。
- 工具代表處理自動化。
可以用下圖的二維平面來衡量之,其中橫座標是從鬆散到嚴律,縱座標是從瀑布型到迭代型,它們各有不同的特點:
通用技術棧如下圖:
2007年是DirectX 10釋出之後的一年,已經有不少文獻闡述利用它的新渲染管線和特性以優化渲染效能和實現一些新的視覺效果,Introduction to Direct3D 10 Course便是其中之一。該文談及在應用程式中使用DX10以最大限度地提高效能的最佳實踐。每當需要建立或更新資料時,管道都會以某種方式停止,通過控制需要將資料傳送到管道的頻率,可以最大限度地減少每次狀態更新、資源建立或不斷修改的開銷。將工作移出到最外層迴圈,也可以顯著提升效率:
下圖是當時盛行的FX架構:
該文還建議使用依賴圖來追蹤資源的依賴性及資源的更新,利用資源的依賴關係,可以消除重複的記憶體例項和資料:
對於固定常量,建議首先按CPU頻率組織,可以最小化API呼叫,減少CPU到GPU的頻寬。其次按所需的著色器標記組織,最小化材質依賴,塞入4096大小的float4緩衝區:
打包常量時,也有很多細節需要注意:
利用DX10,可以實現更好的草、沙石等效果:
與此同時,大量的實時全域性光照計算也慢慢被髮掘,並引入到各種渲染引擎中,Practical Global Illumination with Irradiance Caching便是最好的證明。該文獻實則是實時光線追蹤的一系列文章,涉及隨機光線追蹤、輻照度快取演算法、輻射率中的輻照度快取、光子對映、光澤反射、時間一致性、輻照度分解以及相關的軟體和硬體實現等內容。其中輻射率中的輻照度快取演算法步驟如下:
-
環境常量的計算。
- 所謂的“環境項”近似於無窮級數的餘數。
- 頂級間接輻照度的平均值是一個很好的近似值。
- 可以使用移動平均線,因為輻照度快取會隨著時間的推移而被填充。
- 高估環境項比低估更糟糕。
-
半球自適應超級取樣。
- 為了最大限度地提高間接輻照度積分的準確性。
- 基於鄰域檢測方差對高方差區域進行超取樣,取樣直到誤差在投影半球上一致或達到取樣限制。
-
最大和最小記錄間距。
-
如果沒有最小記錄間距,內角會被解析到畫素級別。
-
應用最小間距,準確度在一定的場景比例下逐漸下降。
-
最大值間距是最小間距的64倍,似乎是正確的。
-
-
記錄間距的梯度限制。
-
漸變不控制間距,除非
||gradient||*spacing > 1
。 -
然後,為了避免負值並提高準確性,可以減少間距。
-
如果已經達到最小間距,反而減少梯度。
-
-
使用旋轉梯度的凹凸貼圖。
-
凹凸不平的表面減少了記錄共享。
-
忽略凹凸貼圖,我們可以對剛剛計算的輻照度應用旋轉梯度。
-
促進了最佳的重複使用和間距,也避免了樣品洩漏的問題。
-
-
排除表面/材質的選項。
- 使用者選擇的材質(以及他們修改的表面)可能會被間接排除在外。
- 這可以節省數小時在草地等領域中毫無意義的相互反射計算。
- 如果只包含少數材質,則可以指定包含列表。
- 最好有第二種型別的相互反射計算可用。
-
為多處理器記錄共享資料。
- 除了為後續檢視重用記錄外,輻照度快取檔案還可用於在多個程式之間共享記錄。
- 同步:鎖定→讀取→寫入→解鎖。
- 其它程式的記錄被讀入,然後這個程式的新記錄被寫出。
- NFS鎖管理器並不總是可靠的。
如果光照變化率高且記錄不足會導致插值瑕疵,而自適應輻射率快取可以解決之。
特別是,將間接照明的空間取樣密度調整為實際的區域性照明條件,與輻照度快取形成對比,其中取樣密度僅基於場景幾何進行調整。對比舊的方法,新的方法可以提升渲染效果:
文中還談及了GPU的實現細節,例如八叉樹儲存和遍歷過程:
為了更好地在GPU上實現IC(Irradiance Cache),文章重新定製了演算法,步驟如下:
該系列文獻還給出了其它的技術分析、實現細節以及和其它方法的對比,非常值得點選原文檢視。
Advanced Real-Time Rendering in 3D Graphics and Games也講解了大量的渲染技術,包含地形渲染、曲面細分、CryEngine 2的架構設計和照明技術、GPU粒子、自陰影等等。文中提到了Valve的Source引擎利用距離場改進Alpha-Tested的材質效果:
64x64紋理編碼的向量效果。(a)是簡單的雙線性過濾 ;(b)是 alpha測試;(c) 是距離場技術 。
距離場的生成需要依賴高解析度的輸入紋理:
(a) 高解析度 (4096×4096) 二進位制輸入用於計算 (b) 低解析度 (64×64) 距離場。
利用生成的距離場資訊,可以渲染出高質量的抗鋸齒的鏤空材質,甚至支援軟硬邊、發光、描邊、軟陰影、銳角等效果。在Valve發行的遊戲《軍團要塞2》中,採用了風格化的著色模型,其具體步驟如下圖所示:
子文獻Animated Wrinkle Maps講述了利用多張法線圖對映到不同的表情,以便通過插值獲得表情間的法線,更加精確、自然地匹配人臉面部表情的效果。
)
Ruby的面部紋理(從左到右):反照率貼圖、切線空間法線貼圖、臉部拉伸後的切線空間皺紋貼圖 1、臉部壓縮後的切線空間皺紋貼圖 2。
八個皺紋蒙版分佈在兩個紋理的顏色和 Alpha 通道(白色代表 Alpha 通道的內容)。 [左] 左眉(紅色)、右眉(綠色)、中眉(藍色)和嘴脣(Alpha)的面具。 [右] 左臉頰(紅色)、右臉頰(綠色)、左上臉頰(藍色)和右上臉頰(alpha)的蒙版。還使用了此處未顯示的下巴面罩。
值得一提的是,這種技術在許多年後,被Unreal Engine用在了數字人的表情渲染上,詳情參考筆者的另一篇文章:剖析Unreal Engine超真實人類的渲染技術Part 1 - 概述和皮膚渲染。
子章節Terrain Rendering in Frostbite Using Procedural Shader Splatting講述了Frosbite引擎利用過程化著色器濺射改進引擎的地形渲染。引擎團隊提出了一種靈活的稱為Procedural Shader Splatting的地形渲染框架和技術,其中基於圖形的表面著色器控制地形紋理合成和分佈,以允許單獨專門化地形材質以平衡效能、記憶體、視覺質量和工作流程。該技術使引擎能夠支援針對地面破壞的動態高度場修改,同時保持遠距離和近距離的高視覺質量以及低記憶體使用率。灌木叢的程式例項已整合到系統中,使用地形材質分佈和著色器是一種非常強大的工具和簡單的方法,可以在記憶體和內容建立中以低成本新增視覺細節。
Frosbite引擎的地形渲染效果。
Frostbite Rendering Architecture and Real-time Procedural Shading & Texturing Techniques講述了2007年的Frostbite引擎渲染架構、渲染技術及相關應用。用Frostbite研製的主機遊戲Battlefield: Bad Company支援的特性有大型可破壞景觀、可破壞的建築物和物體、可破壞樹葉的大森林、機動車(吉普車、坦克、船隻和直升機)、動態天空、動態照明和陰影等。此階段的Frostbite的渲染架構圖如下:
上圖的藍色是主要系統,綠色是渲染子系統。著色系統的特點是與平臺無關的高階渲染API,簡化和概括渲染、著色和照明,輕鬆快速地進行高質量著色,處理與GPU和平臺API的大部分通訊。
著色系統還支援多種影像API和高階著色狀態,高階著色狀態與影像API無關,方便上層的使用者和系統使用,減少重複程式碼。高階著色狀態的用例有光源(數量、顏色、型別、陰影)、幾何處理(蒙皮、例項化)、效果(霧、光照散射)、表面著色(VS、PS)等。總之,高階著色狀態更易於使用,對使用者來說更高效,在系統之間共享和重用功能,隱藏和管理著色器排列的地獄模式,通用並集中到著色器管道,平臺可能以不同的方式實現狀態(取決於功能),多通道照明而不是單通道。
早期Frostbite的著色視覺化編輯器。
Frostbite著色系統管線如下:
- 大型複雜離線預處理系統。系統報告想要的狀態組合。
- 為執行時生成著色解決方案。每種著色狀態組合的解決方案,示例:具有流例項化、表面著色器、光散射並受室外光源和陰影影響的網格以及用於Xbox 360的2個點光源。
- 生成HLSL頂點和畫素著色器。
- 解決方案包含完整的狀態設定。如通道、著色器、常量、引數、紋理等。
Frostbite著色系統執行時步驟如下:
- 使用者壓入渲染塊到佇列。幾何和高階狀態組合。
- 查詢狀態組合的解決方案。在管道離線階段建立。
- 渲染塊由後端傳送給D3D/GCM。
- 渲染塊已排序(類別和深度)。
- 後端設定特定於平臺的狀態和著色器。由該解決方案的管道確定,輕量且靜默。
- 繪製。
Frostbite的世界渲染器(World renderer)負責渲染3d世界,管理世界檢視和渲染子系統(例如地形、網格和後處理),可以根據啟用的功能將檢視拆分為多個子檢視(用於陰影貼圖渲染的僅深度陰影檢視、動態環境貼圖的簡化檢視)。世界渲染器分為3個階段:
- 剔除(cull)。為每個檢視收集可見實體和光/影/實體互動,從所有可見實體複製渲染所需的實體資料,多執行緒需要,因為資料在渲染時可能會更改(不好)。
- 構建(build)。可見實體被傳遞給它們各自的實體渲染器,實體渲染器與子系統通訊(比如網格渲染器),子系統構建渲染塊和狀態(在著色系統中按檢視入隊)。
- 渲染(render)。為每個檢視重新整理著色系統中的佇列渲染塊以進行實際渲染,對檢視應用後處理:Bloom、色調對映、DOF、色彩校正等。
它們都在不同的執行緒上執行(下圖),需要多核的控制檯和PC,以雙緩衝和級聯方式執行(簡化同步和流程)。
室外光源採用基於混合影像的現象學模型,漫射太陽和天空光具有分析性(3 個方向:太陽、天空、地面)並且非常易於控制,來自天空的鏡面反射是基於影像的(動態立方體貼圖),mipmap用作可變粗糙度,來自太陽的鏡面反射可用於精確分析。統一鏡面粗糙度,單個 [0,1] 粗糙度值控制立方體貼圖mipmap偏差(天空)和分析鏡面反射指數(太陽),可能因畫素而異。
同年的CryEngine 2利用DirectX 10支援了以下特性:
- 支援不同的場景環境,各有特點。
CryEngine 2渲染的叢林、外星人室內、冰雪等不同型別的場景。
- 電影級質量渲染,不影響恐怖谷。
- 動態光影。預計算光照對於許多提高效能和質量的演算法至關重要,擁有動態光照和陰影使我們無法使用這些演算法中的大多數,因為它們通常依賴於靜態屬性。
- 支援多GPU和多CPU (MGPU & MCPU)。多執行緒和多顯示卡的開發要複雜得多,而且通常很難不破壞其它配置。
- 支援4km × 4km的大場景。
- 針對從著色器模型2.0到4.0 (DirectX10)的GPU。
- 高動態範圍。在《孤島驚魂》中使用HDR取得了不錯的效果,而對於逼真的外觀,可以在沒有LDR限制的情況下開發遊戲。
- 動態環境(易碎)。最酷的功能之一,但實現並不容易。
在光影方面,CryEngine 2放棄了模板陰影,選擇具有高質量的軟陰影的陰影圖,並且陰影圖可以調整以獲得更好的效能或質量。在直接光照方面,採用了動態遮擋圖、具有螢幕空間隨機查詢的陰影貼圖、具有光源空間隨機查詢的陰影貼圖、陰影遮蔽紋理、延遲陰影遮蔽生成、點光源的展開陰影貼圖、方差陰影貼圖(VSM)等技術。
CryEngine 2不同結果質量的陰影貼圖示例。從左到右:無 PCF、PCF、8個樣本、8個樣本+模糊、PCF+8個樣本、PCF+8個樣本+模糊。
CryEngine 2具有隨機查詢的陰影貼圖示例。左上:無抖動1個樣本,右上:螢幕空間噪聲8個樣本,左下:世界空間噪聲8個樣本,右下:調整設定的世界空間噪聲8個樣本。
CryEngine 2給定場景的陰影遮罩紋理示例:左圖:使用太陽(作為陰影投射器)和兩個陰影投射燈的最終渲染,右圖:RGB 通道中具有三個燈光的光罩紋理。
CryEngine 2給定場景的陰影遮罩紋理示例 - 紅色、綠色和藍色通道儲存 3個單獨燈光的陰影遮蔽。
CryEngine 2將方差陰影貼圖應用於場景的示例。上圖:未使用方差陰影貼圖(注意硬法線陰影),下圖:使用方差陰影貼圖(注意兩種陰影型別如何組合)。
在非直接光方面,CryEngine 2採用了3D傳輸取樣(3D Transport Sampler)、實時環境圖、SSAO等技術,在當時,這些都是新興的具有開創性的渲染技術。
其中3D傳輸取樣可以計算分佈在多臺機器上的全域性光照資料(出於效能原因),計算全域性光照使用的是光子對映(Photon Mapping),它可以輕鬆整合並快速提供良好的結果。
CryEngine2單個光源的實時環境圖。
CryEngine 2的SSAO視覺化和應用到場景的對比圖。
此外,CryEngine 2根據不同的情形對水體、地形、網格等物體執行了細緻的LOD技術,採用了溶解、FFT、方形水面扇、螢幕空間曲面細分(下圖)等技術。
CryEngine 2的螢幕空間曲面細分線框圖。
左:沒有邊緣衰減的螢幕空間曲面細分(注意左邊沒有被水覆蓋的區域),右:有邊緣衰減的螢幕空間曲面細分。
CryEngine 2作為下一代引擎,選擇以上技術主要是因為質量、生產時間、效能和可擴充套件性,並且在遊戲《孤島危機》獲得成功的驗證,成為當時令人矚目的主流引擎之一。
Collaborative Soft Object Manipulation for Game Engine-Based Virtual Reality Surgery Simulators提及的遊戲引擎一種抽象架構:
該文分析了UE、id Tech、Source Engine等引擎的特點,然後給與了一種引擎選擇的評估策略,以模擬虛擬手術。
操作使用者(上)和觀察使用者看到的可變形心臟模型(下)。
The Delta3D Gaming and Simulation Engine: An Open Source Approach to Serious Games闡述了開源3D引擎Delta3D在專案開發的經驗和教訓,通過構建一個基於遊戲的開源模擬引擎,提高產品的成功概率。其中Delta3D的架構如同下圖所示:
Delta3D支援的特性見下圖:
它的渲染效果如下圖所示:
Software Requirement Specification Common Infrastructure Team提到了一種可互動的遊戲引擎的架構圖:
應用程式、平臺框架和遊戲引擎所需的最常見元素,並促進了訊息傳遞、狀態維護和實體註冊等操作。
框架常見的功能,以促進應用程式開發。該框架還提供平臺的一些介面,以加快應用程式級別的開發。
遊戲世界由許多處理狀態變化、遊戲邏輯和渲染的子模組組成。來自GUI的事件被髮送到遊戲控制器,它會提醒其餘的子模組根據需要改變行為。
在光照渲染方面,Interactive Relighting with Dynamic BRDFs在PRT和動態BRDF基礎上提出了新的BRDF積分方式。具體做法是將傳輸的入射輻射預先計算為場景中照明和BRDF的函式,以便考慮動態BRDF的全域性照明效果。為了克服輻射傳輸和BRDF之間的非線性關係問題,採用了一種基於預計算傳輸張量的技術。另外,還通過張量近似表示表面點的BRDF空間,能夠在改變三個場景條件中的任何一個時獲得快速著色。使用傳輸的入射輻射和BRDF張量基礎,可以使用動態BRDF及其相應的全域性照明效果有效地執行場景的執行時渲染。
上圖中:
- \(I_x(l, \omega_i)\)是傳輸的入射輻射,被預先計算為照明和BRDF(預計算傳輸張量)的函式。
- \(f(\omega_o, \omega_i)\)是BRDF空間,由用於快速著色的張量近似表示。
- \(B_x(\omega_o)\)是執行時渲染,根據傳輸的入射輻射和BRDF張量基計算的。
其中\(I_x(l, \omega_i)\)轉變為預計算傳輸張量(PTT)的過程如下面系列圖所示:
不同光路的傳輸輻射在PTT中單獨處理,PTT是光照和BRDF的線性函式,PTT可以在執行時快速組合以獲得整體轉移的入射輻射。\(f(\omega_o, \omega_i)\)由張量近似的過程如下系列圖所示:
最終合成的執行時渲染方程如下:
演算法的總體流程如下:
渲染效果圖如下:
文中還給出了渲染時的具體實現細節和效能分析,限於篇幅,此處不再闡述,有興趣的同學可以閱讀原文。
時間來到了2008年,Advances in Real-Time Rendering in 3D Graphics and Games (SIGGRAPH 2008 Course)大量闡述了當年實時渲染領域最新的研究成果、渲染技術和實際應用,包含Halo 3的光照和材質、StarCraft II渲染、虛擬紋理、GPU並行模擬、硬體級wavelet等。
Halo 3支援Cook Torrance BRDF、球諧光照圖和漫反射、多種鏡面高光(解析高光、環境圖高光、區域高光)等特性。
Halo 3使用二次SH的諧波光照圖紋理。
Halo 3用於區域高光的預積分紋理。從左到右:C(0,2,3,6)、D(0,2,3,6)和CD(7,8)。橫軸表示檢視變化,縱軸表示粗糙度變化。
Halo 3的渲染畫面截圖。
Advanced Virtual Texture Topics表明虛擬紋理是一個mipmap紋理,用作快取,以允許模擬更高解析度的紋理以進行實時渲染,同時僅部分駐留在紋理記憶體中。通過最近幾代商品GPU上可用的高效畫素著色器功能已經可以實現此功能。文中討論了由於虛擬紋理的使用、內容建立問題、結果、效能和影像質量而對引擎設計的技術影響,還介紹了幾個實際應用案例,以突出挑戰並提供解決方案。虛擬紋理涉及紋理過濾、塊壓縮、浮點精度、磁碟流、UV邊界、mipmap生成、LOD選擇等技術細節。
虛擬紋理法的典型使用場景.
使用虛擬紋理受益的場景示例 - 遊戲Crysis中的貼花(道路、輪胎痕跡、泥土)在地形材料混合之上使用。
March of the Froblins: Simulation and rendering massive crowds of intelligent and detailed creatures on GPU由AMD呈現,文中專門開發了演示程式Froblin,用展示大規模蒙皮角色的模擬、渲染及AI。
Froblin演示畫面。
Froblin動態尋路視覺化。
Froblin的動畫紋理佈局。變換儲存為3x4矩陣,使用了一個紋理陣列,其中水平和垂直維度對應關鍵幀和骨骼索引,切片編號用於索引動畫序列。沿著紋理的一個軸改變時間可以使用紋理過濾硬體在關鍵幀之間進行插值。注意,根據權重對每個頂點的骨骼影響進行排序,並使用動態分支來避免獲取零權重的骨骼,可以顯著提升效能,因為大多數頂點不具有超過兩個骨骼影響。
// 用於獲取、插入和混合骨骼動畫的著色器程式碼
float fTexWidth;
float fTexHeight;
float fCycleLengths[MAX_SLICE_COUNT];
Texture2DArray<float4> tBones;
sampler sBones; // should use CLAMP addressing and linear filtering
void SampleBone( uint nIndex, float fU, uint nSlice, out float4 vRow1, out float4 vRow2, out float4 vRow3 )
{
// compute vertical texture coordinate based on bone index
float fV = (nIndices[0]) * (3.0f / fTexHeight);
// compute offsets to texel centers in each row
float fV0 = fV + ( 0.5f / fTexHeight );
float fV1 = fV + ( 1.5f / fTexHeight );
float fV2 = fV + ( 2.5f / fTexHeight );
// fetch an interpolated value for each matrix row, and scale by bone weight
vRow1 = fWeight * tBones.SampleLevel( sBones, float3( fU, fV0, nSlice ), 0 );
vRow2 = fWeight * tBones.SampleLevel( sBones, float3( fU, fV1, nSlice ), 0 );
vRow3 = fWeight * tBones.SampleLevel( sBones, float3( fU, fV1, nSlice ), 0 );
}
float3x4 GetSkinningMatrix( float4 vWeights, uint4 nIndices, float fTime, uint nSlice )
{
// derive length of longest packed animation
float fKeyCount = fTexWidth;
float fMaxCycleLength = fKeyCount / SAMPLE_FREQUENCY;
// compute normalized time value within this cycle
// if out of range, this will automatically wrap
float fCycleLength = fCycleLengths[ nSlice ];
float fU = frac( fTime / fCycleLength );
// convert normalized time for this cycle into a texture coordinate for sampling.
// We need to scale by the ratio of this cycle's length to the longest,
// because the texture size is defined by the length of the longest cycle
fU *= (fCycleLength / fMaxCycleLength);
float4 vSum1, vSum2, vSum3;
float4 vRow1, vRow2, vRow3;
// first bone
SampleBone( nIndices[0], fU, nSlice, vSum1, vSum2, vSum3 );
vSum1 *= vWeights[0];
vSum2 *= vWeights[0];
vSum3 *= vWeights[0];
// second bone
SampleBone( nIndices[1], fU, nSlice, vRow1, vRow2, vRow3 );
vSum1 += vWeights[1] * vRow1;
vSum2 += vWeights[1] * vRow2;
vSum3 += vWeights[1] * vRow3;
// third bone
if( vWeights[2] != 0 )
{
SampleBone( nIndices[2], fU, nSlice, vRow1, vRow2, vRow3 );
vSum1 += vWeights[2] * vRow1;
vSum2 += vWeights[2] * vRow2;
vSum3 += vWeights[2] * vRow3;
}
// fourth bone
if( vWeights[3] != 0 )
{
SampleBone( nIndices[3], fU, nSlice, vRow1, vRow2, vRow3 );
vSum1 += vWeights[3] * vRow1;
vSum2 += vWeights[3] * vRow2;
vSum3 += vWeights[3] * vRow3;
}
return float3x4( vSum1, vSum2, vSum3);
}
對角色模型採用了曲面細分,以便近景特寫時新增角色細節。
左:啟用了曲面細分,模型細節更豐富;右:未啟用曲面細分,輪廓更粗糙。
下圖是GPU的曲面細分管線:
壓縮動畫頂點的位佈局如下圖所示,位置的每個分量使用16位,切線使用兩個8位球座標,法線佔用32位,每個UV座標16位。 由於切線座標系是正交的,剔除儲存副法線(可由解壓縮的法線和切線重新計算)。由於可以使用完整的32位欄位,對法線使用類似DEC3N的壓縮,比球座標需要更少的ALU操作。如果需要額外的資料欄位,8位球座標可用於法線,其質量水平與DEC3N相當,在ATI Radeon™ HD 4870 GPU上對所有替代方案進行了試驗,發現它們之間的效能或質量幾乎沒有實際差異。
壓縮動畫頂點的位佈局。
因為角色是動態的,所以他們的陰影不能作為預處理被寫入SHLM(球諧光照圖),取而代之的是一種更傳統的實時陰影方法,即並行陰影對映(parallel‐spit shadow mapping),用來渲染它們的陰影。理想情況下,希望陰影貼圖只減弱太陽對光線貼圖的影響,而不想在角色投射陰影的地方簡單地暗化地形。可以通過在地形的畫素著色器中將一個主導方向光從光照環境中分離出來。(下圖)
人物在地形上的陰影。左半邊在山的陰影下,右半邊在陽光直射下。上:人物不正確地在地形的遮擋區域投下了雙重陰影;下:使用陰影校正因子來防止雙重陰影的影響。
動態角色(底部)和其他靜態場景道具(頂部)通過從地形的SHLM取樣來建立一個近似的光照環境進行著色。
Using wavelets with current and future hardware介紹了小波(wavelet)的概念、性質和用途。小波是由單個波形(稱為母小波)的縮放和平移副本形成的數學函式。它們允許將函式分解為不同頻率分量的疊加,可以單獨對其進行操作(稱為多解析度分析)。一個函式可以通過使用小波變換變成小波形式,並且可以通過逆變換(類似於傅立葉變換)轉換回原始函式。作為基函式的小波比標準傅立葉表示(及其在球面上的類似物,球諧函式)具有幾個顯著的優勢,它們更擅長表示具有不連續性或急劇變化的函式、非週期性函式,並且在許多情況下具有區域性支援,這允許對資料集進行有效的視窗修改。它們是分層細化的系統,因此可以稀疏地表示資料中低對比度的區域性區域,同時,它們可以是正交的。小波變換可以是連續的或離散的,並且可以表示任何維度的資料。一般來說,我們會對離散的二維小波感興趣,特別是二維非標準Haar。
非標準2D Haar的垂直、水平和對角小波。
小波在實時渲染方面有許多潛在的應用,以下是潛在的應用列表:
-
實時著色器紋理解壓縮。表示任意標量資料(例如影像或球諧係數)的紋理可以有損地壓縮成小波樹,然後使用線性紋理緊湊地表示。給定畫素或頂點著色器中的 (u, v),可以使用著色器內的展開遍歷實時恢復該位置的原始紋理值。此外,可以在影像和濾波器核心之間分層執行任意濾波器操作(包括雙線性濾波器核心)。
實時GPU紋理解壓。子塊紋理中的每個紋理元素都包含小波樹的偏移量。
-
照明的實時雙重和三重積(triple product)積分。小波可用於對照明積分的元素進行編碼和壓縮,選擇正交的小波集(例如非標準的2D Haar)可以將雙重積積分分解為點積運算的稀疏列表。可以通過描述如何通過一組簡單(和小)的規則推匯出三倍係數來將其擴充套件到三重積積分,小波還可用作BRDF解析表示的近似值。
(左)紅色區域光和(右)格雷斯大教堂照明環境之間的實時GPU雙重積積分。
同上的格雷斯大教堂連續三幀,但誇張化對比。
使用點取樣進行BRDF積分近似的連續三幀的三重積積分,以及用於漫射照明的高頻法線貼圖。
-
靜態陰影貼圖。完全靜態或主要靜態的陰影貼圖可以用小波表示,可能具有高度壓縮,深度值的查詢方式與上述紋理壓縮相同。可以通過僅考慮其覆蓋與變化區域相交的那些小波來合併陰影貼圖的區域性變化,證明了區域性支援下建立基組的價值。
-
位移貼圖壓縮。類似地,位移貼圖也可以進行小波壓縮,小波壓縮將明顯壓縮低變化區域,並將高變化區域表示為任意精度水平。此外,小波壓縮是一種用於壓縮非常大的位移貼圖的有用方法,例如地形表示——通常會在周圍散佈一些高頻變化的小區域,中間有平滑的低頻資料。使用合理的壓縮比,允許單個紋理(這裡儲存小波樹而不是精確的位移值)跨越比當前圖形硬體允許的最大紋理解析度大得多的區域,並且可能允許更輕鬆的流式傳輸和LOD方法。
-
更容易的靜態和動態紋理打包。 在任意網格上自動生成UV對映函式是一個棘手的問題,當需要額外的對映特性時更是如此,例如失真最小化,並嘗試在圖集邊界上匹配紋素。除了這些要求之外,還希望最大化總紋理使用率,以便有效地使用可用記憶體。有一些程式可以生成良好的UV對映,但通常仍然存在生成紋理使用不佳的對映的情況。小波影像壓縮將大量壓縮多邊形之間未使用的間隙,即使對於近乎無損的壓縮也可以產生良好的結果。對於動態打包,可以採用在大塊中分配新的紋理空間,而不必費心思有效地與現有圖集執行分塊,填充後,該塊將被小波壓縮。
-
幾何表示。具有關聯UV對映的可變形物件可以具有由表面上的小波表示的變形。一種可能的方法是允許對要使用的小波數量有一個固定的上限(也許是為了控制記憶體使用)。通過移除最古老的現有高頻小波,可以為新的變形騰出空間——因此舊變形的寬廣形狀可以保持更長的時間,但會犧牲更精細的細節。此外,多解析度表示可用於直接更新物體的質心和慣性矩,可能具有不同的精度水平。 這表明以單一多解析度形式表示的資料更易於在具有不同精度要求的不同系統中使用。
StarCraft II: Effects & Techniques由暴雪呈現,講述了2008年的星際爭霸2的圖形引擎的特性和技術。星際爭霸2的圖形引擎的設計目標有:
-
可擴充套件性優先。讓遊戲最大程度流暢地執行在不同的系統、圖形API、硬體裝置之中。
-
讓GPU壓力大於CPU。在提升遊戲質量水平時,選擇了更多地強調GPU而不是CPU。其中一個主要原因是,在星際爭霸 II中,可以生成和管理潛在的數百個較小的基本單元。最多有8名玩家同時遊戲,亦即一次在螢幕上最多顯示大約500 個角色。因為建造的單位數量很大程度上在玩家的控制之下(以及由於所選種族的選擇),所以平衡引擎負載使得CPU潛力在高單位數量和低單位單位中都得到充分利用計數情況變得繁瑣。
玩家可以控制非常多的角色,因此平衡批次計數和頂點吞吐量是可靠效能的關鍵。
-
引擎的雙重性質。存在兩種模式:遊戲模式和故事模式。在正常的遊戲過程中(即遊戲模式),會從相對較遠的距離渲染場景,批量數量較多,並且關注動作而不是細節。在故事模式下,玩家通常會坐下來欣賞遊戲豐富的故事、傳說和視覺效果,通過對話與其他角色互動並觀看動作展開,與遊戲模式有完全不同且經常相反的限制; 故事模式通常擁有較少的批次數量、特寫鏡頭和更沉思的感覺。
上:遊戲模式;下:故事模式。
對於星際爭霸 II,任何渲染的不透明模型都會將以下內容儲存到繫結在其主渲染通道中的多個渲染目標中:
- 不受區域性光照影響的顏色分量,例如自發光、環境貼圖和前向光照顏色分量;
- 深度;
- 每畫素法線;
- 環境光遮蔽術語,如果使用靜態環境光遮蔽。 如果啟用了螢幕空間環境光遮蔽,烘焙的環境光遮蔽紋理將被忽略;
- 無照明的漫反射材質顏色;
- 無照明的鏡面材質顏色。
MRT提供了可用於各種效果的每畫素值,例如:
- 照明、霧量、動態環境遮擋和智慧位移、景深、投影、邊緣檢測和厚度測量的深度值。
- 動態環境光遮擋的法線。
- 用於照明的漫反射和鏡面反射。
星際爭霸 II支援以下特性:
-
延遲光照。畫素位置重建、模板、Early-Z 和 Early-Stencil等。
上:Early-ZS示意圖;下:延遲光照效果。
-
螢幕空間環境光遮蔽。SSAO的主要思想是通過對螢幕空間中相鄰畫素的深度進行取樣來近似可見表面上的點的遮擋函式,得到的解決方案將缺少來自當前隱藏在螢幕上的物件的遮擋線索,但由於環境遮擋往往是一種低頻現象,因此近似值通常相當令人信服。
星際爭霸2的SSAO效果。
-
景深。利用彌散圓(Circle of Confusion)來模擬攝像機的焦距內清晰焦距外模擬的景深效果。
DOF的處理流程和步驟。
-
半透明陰影。使用額外的資訊通道(第二張陰影貼圖儲存半透明陰影資訊,額外的顏色緩衝區儲存半透明陰影的顏色)來擴充套件陰影貼圖的每畫素資訊,從而輕鬆地增強具有半透明陰影支援的陰影貼圖。光線照射到前面的透明物上,並在連續照射到每個透明層時被過濾。
光源過濾過程示意圖。
Lighting and Material of HALO 3詳細地分享了Halo 3所使用的光照、材質模型、HDR渲染等方面的技術內容。該文先是對比了之前的一些光照表達方式:
利用DX SDK改進了UV的打包方式,提升利用率:
對光照圖進行了二度優化:訊號處理、紋理壓縮。
場景渲染和物體渲染的步驟如下所示:
還可以對SH的儲存和計算進行優化:
在材質方面,Halo 3不同當時的大多數遊戲,已經支援更加PBR的Cook-Torrance的BRDF、複雜度更高的區域光,並且做到了更高的實時性和更小的儲存量。
由此,Halo 3實現了漫反射+多種頻率(低頻、中頻、高頻)高光的光照模型,其中漫反射用SH輻照度,低頻高光用新的區域高光模型,中頻高光用預過濾的環境圖,高頻高光用直接解析評估的點光源。
預積分SH光照資訊的推導過程和實現過程如下:
下圖是組合了SH輻照度的漫反射 + 預過濾環境圖中頻高光 + 點光源解析的高頻高光的效果圖:
下圖是Halo 3的HDR渲染管線:
Halo 3的渲染目標需要考慮記憶體大小、渲染速度、硬體混合支援、動態範圍、階數(banding)等因素,動態範圍和階數可用於曝光範圍。下圖是XBox 360的渲染目標詳情:
The Intersection of Game Engines & GPUs: Current & Future分享和討論DICE在Frostbite引擎及遊戲中當前和未來的圖形用例以及對圖形硬體的影響,具體包含引擎現狀、著色器、並行、紋理、光線追蹤、計算著色器等內容。Frostbite在很早的版本就開始使用基於圖形節點的著色編輯器(下圖),它的好處在於:
- 豐富的高階著色框架。被所有內容和系統使用。
- 藝術家友好。易於建立、調整和管理。
- 靈活。程式設計師和藝術家可以擴充套件和公開功能。
- 以資料為中心。封裝資源,可變換到不同的著色平臺。
基於圖形節點的著色編輯器會生成很多著色器排列(Shader permutation),著色器排列是每個使用的特徵/資料組合,包含HLSL頂點和畫素著色器,如果有許多特性會引起排列爆炸(著色器圖、照明、幾何),需要平衡排列和特徵的效能,如動態分支,存在在許多排列中。
在並行方面,Frostbite已經支援多執行緒命令錄製,以便重複利用當時日漸增多的CPU核心。
Frostbite引擎考量了軟體遮擋剔除和硬體遮擋剔除。軟體遮擋剔除的方案是在 SPU/CPU上光柵化粗粒度的z-buffer,光柵化時使用低多邊形遮擋網格、100m視距、最大10000個頂點/幀、手動保守方式,z-buffer是256x114浮點格式,專為PS3打造,已應用於上線的遊戲中。然後在傳遞給所有其它系統之前根據z-buffer使用螢幕空間bbox測試剔除所有物件,可以節省大量工作。需要GPU光柵化和測試,但是遮擋查詢引入開銷和延遲,可以管理,不理想,條件渲染只對GPU有幫助,而不是CPU、幀記憶體或繪圖呼叫。也期望低延遲額外GPU執行上下文,在GPU上完成光柵化和測試,與CPU同步;將整個剔除和渲染移至GPU,如場景圖、剔除、系統、排程、最終目標。
Frostbite還探討了實時光線追蹤,更加關注效能,光柵化主光線,輕鬆整合到引擎中,只是切換某些效果和物件的另一種方法而不更換整個管道,高效的動態幾何、程式和手動動畫(樹葉、角色)、破壞(樹葉、建築物、物體)。光追發射想要玻璃、金屬度,對重要物體的正確反射,用於測試的簡化版世界幾何體和著色。
Software Instrumentation of Computer and Video Games闡述了軟體分析工具對遊戲和引擎的作用,並基於虛幻引擎提出並驗證了由初級到高階逐漸完善的幾種方案,可用來監控、控制、改善玩家的體驗等。
感測器(Sensor)用於收集對內容分析有用的各種資料:角色死亡、角色使用武器、車輛犯罪、使用攻擊性語言、角色的性別和種族多樣性以及各種其他遊戲統計資料,資料可以在整個遊戲過程中報告,也可以僅在遊戲結束時作為摘要報告,感測器可以在執行時進行配置,以根據正在進行的內容分析的需要定製收集的資料。
下圖是分析器分析的部分資料和資訊:
John Carmack Archive - Interviews詳細記錄了id公司創始人Carmack在id公司、Domm/Quake引擎、渲染技術及行業趨勢等方面的探討,其中包含了光線追蹤、GPU、引擎架構、跨平臺等方面的細節。
UnrealScript: A Domain-Specific Language講述了UnrealScript的由來、目標、特點、案例等技術細節。UnrealScript是Unreal Engine早期版本的指令碼語言,類Java風格,允許使用引擎快速開發遊戲,允許輕鬆開發修改。
Operation: Na Pali的截圖,Unreal Tournament的修改版(Unreal Engine 1 – 1999 年釋出)。
UnrealScript的設計目標是直接支援遊戲概念(Actor、事件、持續時間、網路),高度抽象(物件和互動,而不是位和畫素),程式設計簡單(OO、錯誤檢查、GC、沙盒)。
UnrealScript看起來像Java,類Java語法(類、方法、繼承),遊戲特定功能(狀態、網路),在框架中執行,遊戲引擎向物件傳送事件,物件為服務呼叫遊戲引擎(庫)。
// UnrealScript示例程式碼
function TranslatorHistoryList Add(string newmessage)
{
prev=Spawn (class,owner);
prev.next=self;
prev.message=newmessage;
return prev;
}
Unrealscript被編譯成在執行時執行的位元組碼,沒有JIT!
UnrealScript的Actor狀態就是語言的一部分,支援網路、變數修改、錯誤檢測等,編譯為VM位元組碼(如Java),比C++慢20倍。但即使有100多個物件,CPU也只花費5%的時間來執行UnrealScript,圖形、物理引擎完成大部分工作,因此UnrealScript不需要很快。
UnrealScript和C++的分工可由下圖明確,主要是UI、遊戲邏輯事件處理、動作和攻擊、狀態機、武器邏輯、碰撞回撥等。
GC上採用了分代垃圾收集器,增加了世界中的actor有一個destroy()函式的複雜性,垃圾收集器還負責將指向已銷燬actor的指標設定為NULL。
語言的靈活度如下圖所示,從上往下靈活性提升,但維護工作也隨之增加。
隨著UE4的釋出,UnrealScript被藍圖取代,消失在UE快速迭代發展的歷史程式中。不過它的設計理念和使用的技術依然值得我們學習和探究。
Emergent Game Technologies: Gamebryo Element Engine涉及了跨平臺、流處理等技術,並在Gamebryo Element Engine做了實踐和驗證。其中流處理提到了頂點動畫+骨骼動畫的應用案例:
由流定義的任務依賴關係,將任務分類為執行階段,使用其它任務結果的任務在後期執行,階段N+1任務依賴於階段N任務的輸出,給定階段的任務可以併發執行,一個階段完成後,可以執行下一個階段。
多執行緒化之後的cpu使用率對比,注意後者的cpu佔用更均勻:
Creating believable crowds in ASSASSIN'S CREED描述了刺客信條的群體模擬、渲染、優化等技術。刺客信條採用了分層動畫機制:
還擁有複雜的運動系統,更逼真的動畫等於控制元件無響應,非常複雜意味著對其它系統的影響太大,於是進行了簡化,保持最大的流動性。下圖是簡化後的移動系統圖例:
在執行模擬上,採用了併發,利用很多執行緒,在PC和360上執行良好,在PS3上儘可能多的SPU。
2000的中後期,由於遊戲針對多平臺發行成為主流,這也要求引擎具備強力的跨平臺支援。How To Go From PC to Cross Platform Development Without Killing Your Studio闡述了Source引擎如何將遊戲多平臺化,加速生成過程,解決各種問題。文中提到,跨平臺開發存在開發人員效率、人員分配、迭代、認證、使用者體驗、程式設計等方面的問題。為了解決這些問題,文章提出了混合處理資產的方案,即每晚將資產樹編譯成包,藝術家指定要在本地覆蓋的單個資產,以獲得兩全其美的效果。Source引擎管道中的工具自動處理平臺差異,而不是針對每個資產做不同平臺的版本,從而加速資源的跨平臺化。為了解決資產的增長快過記憶體容量的問題,Source引擎對資產執行引用跟蹤、壓縮、裁剪、維護等操作,以大幅減少資產的佔用。對資產進行細緻的定位可以獲得更好的效果,反之差異明顯(下圖)。
上:未壓縮和裁剪資產的渲染效果;下:由於資產處理不當,引發畫面顯著模糊。
資產壓縮對紋理效果最顯著,並且80%的問題由20%的紋理引起。Source引擎的工具可以方便地檢視20%的這部分紋理:
針對資產載入時間長的問題,Source引擎對不同的載入操作執行了不同的優化:
問題 | 解決方案 |
---|---|
查詢 | 連續檔案,精心佈局 |
未對齊的讀取 | 扇區對齊 |
緩衝訪問 | 無緩衝的DMA I/O |
同步卡頓 | 非同步載入 |
按需載入的小檔案 | 大容量的單個檔案 |
由於當時的主機遊戲包體都存於DVD中,Source引擎採取了Zip檔案格式、平衡了CPU或IO頻寬的壓縮、專用的執行緒非同步載入等措施來提速DVD的資料載入。下圖是Source引擎的資產載入架構圖:
另外的一篇文獻也給出了不同的資產架構圖:
下面兩圖分別是Source引擎的同步載入和非同步載入對比圖:
關鍵的載入技術有:
- I/O 執行緒執行無緩衝的DMA傳輸。
- 保持磁碟連續旋轉。
- 無鎖實現。
- 用CPU/SPU換取I/O頻寬
- 返回虛擬值以同步負載。
對於大檔案,採用流式載入:
- 始終儲存每個動畫和音訊的前1/2秒。
- 在後臺非同步載入其餘部分。
- 需要一個資源抽象層,它可以告知幾個狀態:有資料、正在獲取資料、永遠不會得到資料。
對於小檔案,將所有小的臨時(Ad Hoc)檔案預編譯成一個大blob,在單次操作中讀取它,建立假檔案系統,不必更改遊戲程式碼。(下圖)
提前預處理每個關卡的資源引用,如果要建立一個pak,需要知道pak裡面有什麼,包括每一項資產,分析載入依賴關係,載入包體外的資源時觸發崩潰。
在多執行緒處理上,Source引擎已經支援作業佇列系統,可以由主執行緒生成作業,插入作業佇列,然後計算執行緒去作業佇列獲取作業執行,併產生結果。
Source引擎的作業佇列架構圖。其中作業是程式碼和區域性資料包,作業被放入作業佇列,然後由其它執行緒從作業佇列獲取作業並消費。
圖形也是跨平臺經常出現問題的模組,例如電視和電腦顯示器的畫素和顏色空間不同導致色差:
著色器也造成平臺差異的主要因素之一。Source引擎在PC和控制檯都使用HLSL,但著色器編譯器可能有點不同,最複雜的著色器會出現一些問題,並且GPU/CPU 功率平衡略有不同,Source團隊在每晚離線編譯所有內容以進行迴歸測試,以便減少和避免這些平臺的差異。
考慮到當時的360、PS3都是採用有序的PowerPC CPU,在執行雜亂的程式碼時,效率上比x86慢許多。直接使用交叉編譯程式碼的速度提高了25%-50%,仔細優化則可以接近x86的速度,若使用SIMD,則在PPC上比x86更勝一籌。
Source還注重渲染管線的特點和問題,比如PPC具有高延遲、高吞吐量,瞭解所有潛在的風險:暫存器依賴、載入命中儲存、快取未命中、微碼、ERAT、TLB……注重分析器的內容,80%的效能來自於接觸20%的程式碼。在編碼中,儘量使用SIMD技術。使用適用於所有平臺的抽象介面,推送原生向量類,用浮點數替換雙精度數。以向量相加為例,程式碼如下:
FORCEINLINE Vector Add ( const Vector & a, const Vector & b )
{
#ifdef _X360
return __vaddfp( a, b );
#elif defined(_SSE)
return _mm_add_ps( a, b );
#else
return Vector( a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w );
#endif
}
Modern Graphics Engine Design提到場景管理包含了許多加速結構,諸如KD樹、四叉樹等,以便快速查詢和剔除不可見的物體,減少Draw Call。但在當時,最常用的技術還是對物體進行排序,按具有最有利於連貫性的屬性排序。另外,使用逐頂點資料對著色引數進行編碼,減少設定頂點著色器常量的需要,減少切換頂點著色器的需要(例如索引調色盤蒙皮),也可將每個頂點索引應用於其它事物(如照明、遮擋等)。
使用紋理編碼著色引數,減少設定畫素著色器常量的需要,減少切換畫素著色器的需要,例如將光澤度放入法線貼圖的alpha中,而不是通過SetPixelShaderConstant() 設定它。將4個光照遮擋項編碼為光照貼圖,並一次繪製所有4個帶陰影的光源。
在光照計算上,存在三種技術:完全靜態(預先計算每個頂點或光照貼圖)、部分動態(燈光可以改變顏色和強度,但不能移動,將逐光遮擋項構建到頂點或紋理中)、完全動態(為陰影執行大量 CPU 光線投射,使用GPU輔助的陰影,如陰影貼圖或陰影體積)。光照相關的特點和消耗情況如下表:
技術 | CPU消耗 | VS消耗 | PS消耗 | 備註 |
---|---|---|---|---|
靜態光照圖 | 低,如果使用紋理頁 | 低 | 低 | 任意數量的光源和陰影都無關 |
動態光照圖 | 高,至少光源改變時 | 低 | 低 | 越多光源更新消耗越大 |
動態光照圖(帶陰影) | 限制級 | 低 | 低 | 對於CPU太多光線投射 |
遮擋對映 | 低,如果使用紋理頁 | 低 | 中 | 限制燈光數量為4個左右 |
逐頂點遮擋 | 低 | 中 | 低 | 光源只能更改顏色 |
模板陰影(CPU) | 高,僅用於批次計數和輪廓 | 低 | 高 | 每個表面限制為3個光源 |
模板陰影(GPU) | 對批次尺寸中等 | 非常高 | 高 | 每個表面限制為3個光源 |
深度陰影圖 | 低 | 中 | 中 | 鋸齒瑕疵 |
基於SH的PRT | 低 | 中 | 低 | 僅無限的光源,沒有動畫 |
在著色器管理上,該文提出處理著色器有兩種主要方法,取決於遊戲型別。對於開放式 - 在關卡編輯器中由藝術家驅動,高度靈活,使用 HLSL / .FX 檔案來管理複雜性,支援許多著色器型別的有點複雜,使用註釋識別著色器引數,但如果不小心,可能會造成著色器爆炸,經常切換著色器也不利於減少繪製呼叫。另外一種是統一著色器模型——由引擎驅動能力或遊戲需求,更少、更具體、優化的著色器,實用 C++ 編碼來設定著色器,仍然可以使用 .fx 檔案,但不需要那麼多,著色器來自更有限的選擇集,通過限制著色器更改導致的最大繪製呼叫數,有利於更高的幀速率,必須將著色器引數構建到幾何和紋理中以獲得速度優勢。
在測試引擎中,將世界劃分為一個個16x16x16米的3d格子(Cell),場景的角形被剪裁到格子,每個單元格都有一個頂點和一個索引緩衝區,碰撞三角形的AABBTree匹配渲染三角形的鑲嵌,也是材質記錄的向量(包含三角形的索引緩衝區範圍和用於帶材質的三角的AAB),還有移動實體列表(包含 AABox和對僅用於渲染的網格資料的引用)。
將世界分成大量格子的優勢:
- 高效剔除。
- 可以共享相同的VB和IB而不超過65K的頂點或三角形限制。可以為IB使用16位索引,可以為AABB樹使用16位索引,可以將樹中的 AABB框壓縮到16或8位元組。
- 每個軸並且仍然具有良好的精度。
- 可以更快地拒絕其他單元格中的移動實體。
- 可以將照明限制為每個Tile僅7個光源。
用這種3d網格劃分法,可以實現以下特性:
- 在一次繪製呼叫中繪製整個世界單元格。
- 多達7個光源。
- 漫反射和高光凹凸貼圖。
- 柔和的陰影。
- 光澤對映,顏色偏移鏡面反射。
- Masked的自發光。
- 儲存在Dest Alpha中的水或霧深度。
- 霧、霧和水是部分alpha通道。基於dest alpha混合霧層顏色。
此外,測試引擎還提到了平均L凹凸對映(Averaged L Bump Mapping)的技術,以解決普通的凹凸對映在光源數量較多的情況下效能低下的問題。
“A bit more Deferred” - CryEngine 3中提到CryEngine 3在2009年引入的新技術,比如改進流式載入、多執行緒、改善光照、效能檢測工具、追蹤shader編譯問題等。由於引入了Uber Sahder(全能著色器),編譯所有可能的排列會產生記憶體、生產和效能等諸多問題,CryEngine3的解決方案有動態分支/分離成多個通道/減少組合並接受更少的功能和更少的效能、非同步著色器編譯、分散式作業系統編譯著色器快取等。
在延遲渲染上,CryEngine 3做了改進,不再使用CryEngine 2的延遲渲染,而是採用了Light Pre-Pass渲染,將通道分成3個:
1、前向渲染生成GBuffer資料(即幾何通道)。
2、延遲光照(Phong)並累加到紋理。
3、用光照累加紋理前向著色。(不是延遲著色)
這種方式的優勢是更少的頻寬和記憶體佔用,更加靈活的著色。對於光照累加紋理,存在6通道和4通道佈局,後者效果有少許差異但更快:
CryEngine 3還將光照累加紋理用於IBL,以加速渲染。(下圖)
左:漫反射RGB+高光RGB的高質量畫面;右:漫反射RGB+高光強度的快速渲染畫面。差異通常可以忽略不計(取決於環境)。
傳統儲存GBuffer的法線的方式和特點如下:
-
XYZ世界空間。如果採用8位,極端反射/鏡面反射有問題;如果是10位,效果良好,但是鏡面強度和PS3無法滿足。
-
解決量化瑕疵的方法有細節法線貼圖、噪聲、抖動。
-
XY 檢視空間(Z 重構)8/10/16 位,取反Z位(透視和法線對映)。但是
z = z_sign * sqrt(1-x*x-y*y)
當z接近0時,精度變得很差。(下圖)XY檢視空間的法線儲存精度問題。圖中紅色表示z接近0時存在問題。
針對以上的法線儲存精度問題,CryEngine 3做了修正,對法線的z進行調整,並縮放xy分量:
這樣做的好處是在重要的地方更精確(明亮的部分)、友好的幀緩衝混合、沒有z重建問題,缺點是浪費面積、檢視空間的法線佔用的ALU比世界空間多。
CryEngine 3改進了SSAO的效果,利用法線計算出更加精確的AO:
左:改進前的SSAO;右:利用法線改進後的SSAO。
在光照方面,支援2D(矩形)和3D(凸面體)進行光源光柵化,其中3D方式可以利用Z緩衝區和更緊緻的包圍盒(更少的處理畫素)。除了常規的光源型別,還支援交叉陰影圖查詢,它的優點是無需額外的記憶體、更少的頻寬、在陰影遮蔽通道數量上沒有限制。
在IBL光照上,遠距離光采用光照探頭,結合立方體貼圖獲得實時高效的HDR照明,從鏡面立方體圖獲得漫反射立方體圖,不同的Mip代表不同強度的鏡面反射的效果,通過新增法線依賴和鏡面光照改善環境光照條件下的陰影,光照探頭可以在指定的水平位置生成,延遲光照允許混合區域性光照探針,結合SSAO獲得更好的效果。
CryEngine 3的幾種光照效果。從左上到右下依次是:亮環境+SSAO,暗環境+陰影投射光源+SSAO,灰環境(SH)+影投射光源+SSAO,IBL環境光(鏡面+漫反射)+影投射光源+SSAO。
此外,CryEngine 3還嘗試了實時動態全域性光照的效果,在Xbox360、PS3和PC上快速實現,全動態(幾何、材質和燈光),無預計算,達到靜態和動態物件統一。
上:沒有GI;下:開啟了動態GI。
Hitting 60Hz with the Unreal Engine: Inside the Tech of Mortal Kombat vs DC Universe提到了使用Unreal Engine 3遇到的效能問題以及解決方案。效能開銷主要有CPU和GPU方面,細分如下:
-
GPU開銷
-
GPU固定開銷
- 後期處理
- 通常是最大的固定成本。
- 將盡可能多的操作組合在一起以隱藏工作(即Bloom+DOF+Gamma+解析度重定向)
- 儘可能多地切角和必要的特殊情況——例如, 我們根據情況使用 3 種不同 DOF 方法中的 1 種:普通玩法用經典模糊交叉淡入淡出,主選單/電影用擴大泊松圓盤,Klose-Kombaty用一系列模糊平面。
- 後期處理
-
一般渲染開銷
- 8bpc渲染目標,0..2的線性色階。
- 我們以γ=1.0和γ=2.2的組合進行照明,取決於照明的內容,以節省成本。
- 不透明:使用MSAA。
- 半透明:MSAA後解析。
- 遊戲的3D解析度為1040x624,然後按比例放大以允許HUD以1280x720渲染。
-
多通道開銷
- 通道的逐光源開銷太高。
- 大多是預光照,所以選擇了前向渲染。
- Z-Prepass典型的深度複雜度 < 1.5。
- 通過“細節環”從前到後對不透明物件進行鬆散分類,移除Z-prepass可節省約0.75毫秒。
- 如果可能,每個畫素只處理一次。
-
照明開銷
-
世界光照(靜態)。使用Illuminate Labs的Beast進行預計算照明,並使用Turtle構建了一些動態的RNM,動態RNM在材質中或通過MITV進行動畫處理。預計算光照是紋理和頂點RNM光照的混合,新增了一條快速路徑以支援對遠處物體的僅逐頂點漫反射RNM評估。
-
世界光照(動態)。高效點光源是通過混合按畫素照明(地面)和按頂點(環境的其餘部分)來完成的;考慮到最大負載,著色器使用三個僅漫反射點光源啟用並注入材質;沒有分支,始終評估所有三個光源,這些光源在3-deep FIFO中全域性分配和管理。
-
角色光照。
-
自定義照明模型:SH係數集的輻照度。評估梯度以確定每個物件的SH集,僅使用前4 個係數(環境和方向項)對模型進行漫射,3 個高效點光源按頂點進行評估,並組合成最終的漫反射光照結果,通過 (E•N) 的功率縮放並乘以漫射照明來偽造規格。下圖是角色高光效果:
-
通過使用 (E•N) 作為漫射照明和SH環境項之間的lerp因子來偽造皮膚透射。
-
邊緣照明:衰減功率縮放 (1-E•N),然後通過硬閾值 (1-E•N) 進行mul,如果閾值提高到足夠高(~0.7),最終看起來像chrome對映。角色網格採用批量渲染,下圖是皮膚和金屬的效果:
-
-
-
粒子開銷。
-
-
CPU開銷
- 粒子開銷
- 布料和水體
- 渲染執行緒虛擬開銷、狀態快取
- 通用渲染執行緒優化:大量工作以減少不必要的操作;渲染執行緒虛擬意味著大量的空閒;快取儘可能多的狀態以減少冗餘的虛擬呼叫, 例如,將FMaterialRenderProxy的GetMaterial虛擬呼叫替換為快取呼叫;從著色器處理內部移除大量不必要的對 GetXXX()(即GetPixelShader)狀態的重複呼叫。
- 垃圾回收
- 從遊戲中移除了所有實時呼叫GC,僅在退出模式時呼叫。
- 記憶體管理切換到UObjects/AActors的延遲(按幀)清理。
- 通過Rootset捕獲的所有載入資料。
- 引入UResource類,一個引用計數UObject。
- 所有USurface派生類(即UMaterial、UTexture等)都通過UResource進行引用計數,以防止不必要的刪除。
- 其它建議
- 預先預算效能!
- 鑑於Edge和360的統一著色器,幾何問題比填充率更小。
- 預先確定有效的PostFx並硬連線大多數排列。
- 儘可能減少動態臨界區記憶體分配,會大量卡住所有效能。
- 儘可能使用池分配器,並注意重新分配。
- 強制設計師和藝術家使用效能指標執行!
針對這些開銷,除了以上建議,該文還提出了其它很多具有建設性的優化建議和改進技術,即便過去了許多年,也具有一定參考價值,值得一看。
在資產方面的跨平臺,也有論文分享技術和經驗,例如Practical techniques for managing Code and Assets in multiple cross platform titles。該論文從資產管線入手,目的是將資料匯入遊戲、版本控制/工作流程、將資料轉換為優化的目標格式、針對特定平臺進行優化、驗證/錯誤檢查、檢查資產的錯誤/問題(骨骼數量、紋理縱橫比、多邊形數量、檔案大小……)及管理同一專案的多個版本。他們的資產管道概述如下:
- 每個專案都有一組作業(每個源資產一個)。
- 所有作業設定和來源都儲存在一箇中心位置(對資產使用perforce)。
- 自定義使用者介面。
- 與工作流程工具相關聯(不同的權利/角色)。
- 藝術家/資產建立者在本地工作並在完成後提交他們的更改。
- 有引用這些轉換器的轉換器可執行檔案和作業型別。
- 示例:紋理的不同作業型別(UI、環境、字元……)
- JobTypes是專案特定的。
- 作業型別可用於定義限制/約束。
- 作業型別定義強制/預設轉換器的一些引數為常用設定。
- 每個專案在其目錄結構中都有自己的轉換器可執行檔案副本。
- 自定義專案特定轉換器。
- 控制用於專案的轉換器的版本,最終能夠歸檔專案所必需的。
- 大多數專案都有特定於該專案的特殊轉換器。
- 地圖/關卡資料轉換器。
- 資料庫包含待定(待構建)作業列表。
- 每個客戶端都拉出待處理的作業,在本地構建它們並將結果推送到中央伺服器上。
- 轉換器可執行檔案+作業設定+源資產必須始終在所有機器上建立相同的結果(與上下文無關)。
- 每個客戶端都從伺服器複製最新版本。
- 轉換後的資源放入遊戲構建資料夾(包含所有檔案的平面資料夾)。
- 專案的每個版本都有一個遊戲構建資料夾。
- 對於光碟版本,重新排序並打包這些檔案。
- 專案的每個版本都有一個遊戲構建資料夾。
- 大多數平臺直接從這個目錄載入。
- 其他人首先需要光碟模擬/ROM構建。
- 快速重新載入某些資產以支援快速迭代時間。
Next-Gen Asset Streaming Using Runtime Statistics探討了面向下一代引擎特性的資產流式載入技術。它的核心思想在於利用執行時收集的統計資訊,在構建管線中生成資源依賴圖,然後用資源依賴圖可以找到哪些需要載入哪些不需要載入。
場景物體和資源的依賴圖。虛線表示場景物體引用了執行時新增的依賴資源。
構建資源依賴圖機制架構圖。包含執行時非同步收集資訊傳送給統計伺服器,右統計伺服器開啟統計構建器構建的資源依賴資訊。
該文還探討了構建延遲、記憶體管理等問題。
The Next Mainstream Programming Language: A Game Developer’s Perspective是Tim Sweeney(Epic Games的CEO)在2009年分享的演講,從遊戲開發者的視角講述了下一代主流程式語言。該文提及遊戲開發的典型流程和技術(遊戲模擬、數值計算、著色),以及當時的語言併發性和可靠性上存在哪些缺陷。用UE3研發的Gears of War(戰爭機器)的遊戲邏輯達到25萬行C++程式碼,而當時的UE3的引擎原始碼也是如此。(下圖)
UE3研發的Gears of War的遊戲畫面。
Gears of War、UE3、圖形API及第三方庫的架構圖及程式碼量。
由於遊戲的邏輯和渲染技術越來越複雜,當時的UE遊戲存在三種型別的程式碼:遊戲邏輯模擬、數值計算、著色。
對於遊戲邏輯模擬,隨著互動物件隨著時間的推移對遊戲世界的狀態進行建模,物件導向的高階程式碼,用C++或指令碼語言編寫,指令式程式設計風格通常被垃圾收集。在規模上,每秒30-60次更新(幀),約1000個不同的遊戲類別(包含命令式狀態、成員函式,高動態),約10000個活躍的遊戲物件,每次更新遊戲物件時,通常會接觸其它5-10個物件。
對於數值計算,包含演算法(場景圖遍歷、物理模擬、碰撞檢測、尋找路徑、聲音傳播),低階高效能程式碼,用C++編寫,帶有SIMD內在函式,本質上是功能性的,使用大型常量資料結構將小型輸入資料集轉換為小型輸出資料集。
對於著色,生成畫素和頂點屬性,用HLSL/CG著色語言編寫,在GPU上執行,本質上是資料並行,控制流是編譯期已知資訊,令人尷尬的並行,當時的GPU是16-寬到48-寬。在規模上,遊戲以30 FPS@1280x720p執行,約5000個可見物體,每幀渲染約1000萬畫素,每畫素光照和陰影需要每個物件和每個光照多渲染通道,典型的畫素著色器大約100條指令長,著色器FPU是4寬的SIMD,約500GFLOPS的計算能力。
這三種型別的程式碼資訊對比表如下:
當時的UE3常面臨的難題有:
- 效能。當以60FPS更新10000個物件時,一切都是效能敏感的。
- 模組化。每個遊戲大約10-20箇中介軟體庫非常重要。
- 可靠性。容易出錯的語言/型別系統導致浪費精力去尋找瑣碎的錯誤,顯著影響生產力。
- 併發。硬體支援6-8個執行緒,但C++不具備併發性。
對於效能,UE認為生產力同樣重要,樂意犧牲10%的效能來提高10%的生產力,並且從不使用匯編語言,沒有一組簡單的“熱點”可以優化!在模組化上,基本目標是在開放世界系統中並行擴充套件整個軟體框架的類層次結構。下圖是基礎框架和擴充套件架構的程式碼示意圖:
在可靠性方面,對非法地址、越界訪問等問題進行修正,以獲得較為健壯的程式碼。(下圖)
上:改進前的不可靠的程式碼;下:修正後的程式碼。
在併發方面,理想的情況是:任何執行緒都可以隨時修改任何狀態,所有同步都是顯式的、手動的,沒有正確性屬性的編譯時驗證:無死鎖、無競爭。要完全符合理想的情況實踐上很難!UE3的措施是:
- 1 個主執行緒負責完成不能希望安全地進行多執行緒的所有工作。
- 1個重量級渲染執行緒。
- 4-6個輔助執行緒池。動態地分配給簡單的任務給它們。
- 必須非常小心地程式設計!
但是以上方式會導致巨大的生產力負擔,無法很好地適應執行緒數。
在著色併發上,UE3的新程式語言針對“令人尷尬的並行”著色器程式設計,它的構造自然對映到資料並行實現,使用靜態控制流(通過掩碼支援的條件)。
在數值計算併發上,本質上是純函式演算法,但支援在可變狀態下本地執行,Haskell ST、STRef解決方案支援在引用透明程式碼中封裝本地堆和可變性,這些是隱式並行程式的構建塊,UE3中約80%的CPU工作可以通過這種方式並行化。
三種型別的程式碼的特點和並行方式如下表所示:
並行性和純度的關係如下圖所示:
Tim Sweeney還給出了遊戲技術的簡史:
Building a Dynamic Lighting Engine for Velvet Assassin描述了在遊戲Velvet Assassin中構建了不使用光照圖的完全動態光照。該遊戲使用的引擎支援鬆散八叉樹場景管理、物體三角形OBB樹、帶有陰影貼圖的混合單通道/多通道照明、可見性入口、通過反射燈間接照明及針對Xbox 360特定優化。
OBB和ABB對比。
混合光照是多通道和單通道前向渲染器的混合體,每個主光一個通道,所有次級光源組合成一個通道。主光源採用了經典多通道(Doom 3風格),可以投射陰影,周圍幾何體的燈光查詢。次級光源採用經典的單通道(Half Life 2風格),光源收集到一個通道中(基於計數的著色器變化),不能投射陰影,周圍燈光的幾何查詢(最大數量)。
對於反彈光,給出從表面首次反射的間接光的外觀,不得照亮其放置的表面,具有由軸確定的半球影響半徑。
對每一幀:
- 讓所有主光源都在視野中。
- 分發陰影貼圖池。
- 為每個渲染陰影貼圖:
- 渲染光截頭體中包含的所有物件。
- 獲取所有在檢視中的物件。
- 渲染基礎通道:
- 對於每個物件,為著色器收集最近的N次級光源(按重要性排序)。
- 為每個主光渲染附加通道:
- 對在檢視中且在光截頭體中的每個物件。
這就是為什麼需要一個高效的空間:索引資料結構。
Valvet Assassin採納了多執行緒渲染,第一個執行緒執行所有空間查詢並編譯一個“drawlist”,第二個執行緒設定著色器暫存器、渲染狀態和提交批次,大多數場景有300到1200批次/幀。
Techniques for Improving Large World and Terrain Streaming談到了MMO的大世界地圖的需求、面臨的挑戰和新引入的技術。
文中提到載入所有遊戲資源的粗略方法無法擴充套件到大型世界:載入時間過長,記憶體佔用過大,需要合作編輯。需要引入一種新的技術,滿足不要載入所有資料,根據需要動態載入和解除安裝資料,在執行時保持最低要求的資料集。
基礎流式載入將所有物件例項儲存在記憶體中,例項不需要大量記憶體(位置、旋轉、比例、狀態……),流入/流出所需資源,資源資料(網格、紋理等)需要更多。載入角色周圍區域的資源,解除安裝不再可見的資源。基礎流式載入存在的問題:需要將所有物件儲存在記憶體中,對流媒體行為的控制很少,不規則和不可預測的流式傳輸,沒有資源依賴就很難排程。
該文改進了流式載入的管線流程:
流式載入對工具的要求是:
-
合作編輯
- 多人應該能夠一起編輯世界。
- 需要解決/防止編輯衝突。
- 將場景儲存為多個圖層檔案。
- 圖層可以由不同的人獨立鎖定和編輯。
- 與修訂控制同步。
-
可流式傳輸的世界資料
- 沒有單一的世界檔案。
- 將大檔案分解為單獨的檔案以進行編輯和流式傳輸。
- 藝術工具必須促進和支援網格例項的重用。
-
資源依賴
-
計算並儲存世界上所有的資源依賴。
-
-
分組
- 更好地控制資料的方式:已編輯、已烘焙、流式傳輸。
-
地形分割槽
- 自動分組為二維網格。
- 分割槽提供了可管理的資料塊來編輯、烘焙和流式傳輸。
- 地形資料可以是100MB。
- 僅將相關扇區保留在記憶體中。
- 流入,根據需要換出。
- 遙遠扇區的較低細節近似值。
- 實時編輯和繪畫。
- 支援合作編輯。
- 逐扇區鎖定。
- 編輯器自動鎖定並處理連續性。
-
區域
-
物件例項可以由關卡設計師在空間上分組到區域中。
-
每個區域都會生成一個單獨的檔案,當玩家接近時,可以動態流式傳輸。
-
允許生成/銷燬物件組。
-
-
資料處理
-
編輯時的程式/原始資料。
-
執行時的烘焙資料。
-
地形分割槽
- 每個分割槽一個檔案(One File Per Sector),包含:高度圖、紋理混合貼圖、烘焙n個最重要的紋理、植被概率圖、預計算物件例項。
- LOD:預計算錯誤指標、世界空間法線貼圖。
- 自定義索引列表。
-
區域
- 建立每個區域在匯出時使用的資源列表。
- 編輯器知道使用了哪些資源(和依賴項)。
- 在執行時,資源列表將按順序處理和載入。
- 根據依賴關係排序,因此載入不需要停止(例如,著色器庫是載入網格的先決條件)。
-
流式載入對執行時的要求是:
- 資源管理系統
- 動態載入/解除安裝。
- 引用計數。
- 記憶體使用資訊。
- 資源依賴資訊。
- 用於評估上次使用資源的時間戳。
- 可以解除安裝x秒未使用的資源。
- 硬記憶體和軟記憶體限制。
- 用於紋理、網格等的不同資源池。
- 流式區域
- 流媒體區域。
- 所有資源的預快取。
- 單獨執行緒:從檔案載入資源資料,可選資料轉換/生成。
- 主執行緒:建立資源。
- 載入資源後,在區域中建立所有物件例項。速度很快,因為資源已經載入,可選擇隨時間分佈(即從最大/最重要的物件開始)。
- 如果玩家直接傳送到有待處理資源的區域怎麼辦?
- 選項 A:顯示替換資源(例如非常低解析度的紋理)。
- 選項 B:一旦相關資源可用,物件就會彈出。
- 選項 C:暫停遊戲並從主執行緒載入剩餘資源。
- 流式地形分割槽
- 使用較低細節的幾何圖形和紋理載入和渲染遠處扇區。
- 流式傳輸高細節版本。
- 使用世界空間法線貼圖來避免照明偽影/跳變。
Insomniac Physics描述了IG物理系統的演變、著色器、庫著色器、自定義事件著色器等內容。該物體系統經歷的數次迭代的示意圖如下:
演變的依據和變化是多執行緒化、流程精細化、細粒度化、核心利用率提升。
State-Based Scripting in Uncharted 2: Among Thieves講述了神祕海域2的指令碼系統,包含擴充套件遊戲物件模型、狀態指令碼語法、例研究、實現討論、總結和建議。
使用指令碼的主要好處:減輕工程團隊的壓力,程式碼變成資料——快速迭代,賦予內容創作者權力,Mod社群的關鍵推動者。
有兩種遊戲指令碼語言:資料定義語言和執行時語言。執行時指令碼語言通常:由虛擬機器 (VM) 解釋,簡單小——低開銷,可供設計師和其他“非程式設計師”使用,強大——一行程式碼就可以產生大的影響。頑皮狗大量使用資料定義和執行時指令碼,兩者都基於PLT Scheme(Lisp 變體)。類Lisp語言的主要優點:易於解析,資料定義和執行時程式碼可以自由混合,強大的巨集系統——易於定義自定義語法,頑皮狗有著豐富的Lisp傳統。資料定義語言有:自定義文字格式、Excel逗號分隔值 (.csv)、XML等等,執行時語言有Python、Lua、Pawn(小 C)、OCaml、F#等等,許多流行的引擎已經提供了指令碼語言:Quake C, UnrealScript, C# (XNA)等等
每個遊戲引擎都有某種遊戲物件模型,定義遊戲世界中的所有物件型別,通常(但不總是)用物件導向的語言編寫,常用於擴充套件本機物件模型的指令碼語言,有很多方法可以做到這一點。
UnrealScript與C++物件模型緊密整合,帶有一些附加元件的單根類層次結構,UnrealScript (.uc) 中定義的類,C++標頭檔案 (.h) 自動生成,用C++或完全用UnrealScript實現。
以屬性為中心的設計用於Thief、Dungeon Siege、Age of Mythology、Deus Ex 2等,遊戲物件只是一個唯一的id (UID),“裝飾”有各種屬性(健康、盔甲、武器等),屬性封裝資料+行為。
Uncharted Engine的物件模型,類層次結構較淺、單根,含大量附加元件。
Uncharted 2的狀態指令碼在許多方面類似於以屬性為中心的模型,新增有限狀態機 (FSM) 支援,與“屬性”無關,粗粒度(每個物件一個指令碼),更像是現有實體型別的指令碼擴充套件或協調其它實體行動的“導演”。狀態指令碼包括屬性和狀態,狀態通過執行時指令碼程式碼定義物件的行為:對事件的反應,隨時間推移的自然行為(更新事件),狀態之間的過渡動作(開始/結束事件)。
例項化狀態指令碼時,附加到原生 (C++) 遊戲物件:設計師擴充套件或修改原生C++物件型別定義全新的物件型別;附加到觸發區域:凸體積、檢測進入、退出和佔用;作為獨立物件放置:“director”協調其它物件的動作(例如 IGC);關聯任務:任務即檢查點,指令碼管理關聯的任務,安排AI更新,控制玩家目標。
下圖是自定義物件型別(易碎標誌)的案例:
虛擬機器實現方面,由簡單VM實現的類方案執行時語言,每個軌道編譯成稱為lambda的位元組碼塊:
VM的內部狀態包括指向當前lambda的指標(位元組碼程式)、當前指令的索引、臨時和即時資料的暫存器庫,其中暫存器是變體型別。
語言支援巢狀函式呼叫,因此需要呼叫堆疊,堆疊幀 = 暫存器組 + 程式計數器。
狀態指令碼程式碼可以等待(睡眠),通過稱為延續(Continuation)的東西實現。
成功的指令碼系統的主要特徵:虛擬機器整合到遊戲引擎中,能夠每幀執行程式碼(更新),能夠響應事件和傳送事件,能夠引用遊戲物件(通過控制程式碼、唯一ID等),操縱遊戲物件的能力,設計人員可以在指令碼中定義新的物件型別。
許多不同的引擎指令碼架構:指令碼驅動引擎(引擎只是一個由指令碼呼叫的庫),引擎驅動指令碼(簡單的指令碼事件處理程式、指令碼化的屬性或元件、指令碼遊戲物件類)。
Star Ocean 4 - Flexible Shader Managment and Post-processing分享了Aska遊擎在RGP遊戲STAR OCEAN:The Last Hope使用的著色器管理和後處理技術。其中完全靈活的著色器是指管理藝術家可以在Maya中建立材質,使用Hypershade介面,根據藝術家設定自動生成著色器二進位制檔案。
設計方針是藝術家在Maya中建立著色器,他們可以在沒有程式設計師的情況下建立著色器,不受程式設計師限制,可以立即嘗試新想法,需要培訓藝術家,如何設定引數和構造著色器,物理知識(一點點)和模板。
執行時(開發期間)生成的著色器,其優點:不必在資原始檔中包含著色器二進位制檔案,藝術家可以自由建立著色器,在執行時輕鬆支援著色器的變化,易於管理著色器二進位制檔案。其缺點:著色器變化的數量爆炸式增長,大型著色器二進位制檔案,必須建立可能的著色器變化,必須通關遊戲中所有可能的內容。
可以細分著色器,實現具有特定功能的小型著色器節點,對應Maya中的Shader Node,藝術家可以自由連線每個輸入和輸出:UV、顏色、正常、Alpha…
使用照明結果對錶面進行著色:Phong、各向異性Phong、Blinn-Phong、歸一化Phong、Ashikhmin、Kajiya-Kay、Marschner(反照率貼圖、高光貼圖、光澤度(光澤度)貼圖、菲涅耳圖、偏移貼圖、半透明、環境光遮蔽)。
著色編輯器還支援法線、UV、陰影、投射、計算及其它眾多功能節點。
著色節點編輯器。
在後處理方面,Aska遊擎支援色調對映(標準色調對映、膠片模擬(再現薄膜或 C-MOS 感測器的規格、再現膠片顆粒或數字噪聲)、抖動)、鏡頭模擬(DOF、強光、基於物理的鏡頭結構、運動模糊(相機、物件)、彩色濾光片(對比、亮度、單調、色調曲線、色溫)、其他效果(戶外光散射、光軸模擬、螢幕空間環境光遮蔽)等。
著色檔案採用了快取機制,將編譯好的著色器儲存在devkit上的著色器快取檔案中,快取元件(著色鍵、常數表、著色器二進位制),假設此檔案包含遊戲中使用的所有可能的著色器組合。
著色器快取是在QA期間建立,隨著專案接近尾聲,快取檔案的大小增加,一開始估計10M,但實際上超出了10M。解決尺寸問題的方案是在執行時解壓縮每個著色器二進位制檔案,將著色器快取分離到L1和L2,支援多個著色器快取檔案,建立了一個工具來管理Windows中的著色器檔案,平衡實現的效能與尺寸控制引數。
但是,儘管付出了很多努力,尺寸還是超過50M,超過30000種快取組合,即便拆分了快取檔案,但它們仍然超出了可接受的檔案大小。開始分析著色器快取檔案的細節,發現是著色器介面卡佔用了大多數著色器組合。
什麼是著色器介面卡?
在執行時新增的著色器,如陰影、投影儀等等……這幾種shader佔據了80%,尤其是陰影,有一個著色器甚至支援一個物件的5個陰影。
增加對著色器介面卡(陰影)數量的限制,在生成期間或在工具中限制數量,尺寸顯著減小,但導致外觀問題,需要手動調整。支援非生成著色器的實現功能,在著色器介面卡的情況下,使用的基礎著色器,總比消失好。
快取檔案由QA團隊建立,在修復與著色器相關的規範和資源時建立快取檔案,使用除錯功能,通過玩遊戲,合併由多個測試人員建立的檔案,但由於對此係統不熟悉,這個過程比預期的要艱難得多,數週與數十名測試人員,一次又一次地完成。
以上著色系統的優點是:高靈活性,藝術家可以在沒有程式設計師的情況下建立各種著色器,可以建立不合理的著色器組合,優化效能(著色器立即常數…),可以使用著色器編譯器自動生成最佳著色器程式碼。
以上著色系統的缺點是:著色器快取建立的成本,自動建立的限制,檔案大小問題,快取檔案太大。因此,在建立資源的情況下應該意識到這一點,嘗試減少著色器數量,建立著色器的困難,藝術家必須知道著色器的機制。
此外,Aska遊擎還實現了基於物理的Bokeh DOF、鏡頭模擬、HDR渲染等效果。
Bokeh景深渲染流程圖。
對於色調對映,不使用特定演算法,而是使用膠片或C-MOS感測器的規格來建立曲線表,曲線是基於對數基礎壓縮的:
float u = saturate(log2(vInCol.r+1)/2.32);
vOutCol.r = tex1D(s, u).r;
色調對映渲染流程圖。
常見的色調曲面效果對比。
不同質量等級下的後處理效果對比。
Light Propagation Volumes in CryEngine 3分享了CryEngine 3的光照管線、核心思想、應用、改進、組合技術、主機優化等。
2009年的CryEngine 3已經支援眾多主流平臺,適合室外和室內的光照渲染,採用統一陰影貼圖、SSAO、延遲光照等。
CryEngine 3採用了照明累積管線:應用全域性/區域性半球環境,將其替換為本地延遲光探測器(可選),全域性照明,將間接項乘以SSAO以應用環境光遮蔽,在間接照明之上應用直接照明。
上圖中的全域性光照,CryEngine 3採用了光照傳播體積(Light Propagation Volumes,LPV)。LPV的目標是將照明覆雜性與螢幕覆蓋率解耦(解析度×過度繪製),Radiance快取和儲存技術,點光源的大規模照明,全域性照明,參與媒體渲染(仍在進行中……),主機(Xbox 360、PlayStation 3)友好。
LPV最重要的步驟是在輻射體積中的傳播光,從發射器的給定初始輻射分佈開始,輻射傳播的迭代過程,用於相鄰單元的6點軸向模板(收集,GPU更高效,能量守恆),每次迭代都會增加結果,然後進一步傳播。
6點軸向模板(6-points axial stencil)示意圖。*
輻射傳播過程示意圖(以2D單個單元格的單次迭代為例)。
在上圖中,不妨將初始輻射分佈僅放置在發射器所在的單元格中(一種非常方便的情況,因為只需要為一個光源渲染一個畫素),想要在網格上獲得最終的輻射分佈,建議的解決方案是迭代地傳播輻射,每次迭代都會對每個單元應用一個6點軸向模板,意味著對於每個單元,通過收集方案傳輸來自相鄰軸向單元的輻射。收集是GPU友好的,每次迭代的結果被收集到最終的輻射體積中,下一次迭代應用於前一次的結果。
LPV多次迭代示意圖。
上圖是幾個輻射傳播迭代的結果。第一行是初始輻射分佈,可以看到有很多光源,左上角的四邊形是一片輻射紋理,所以3D輻射紋理在這張圖片中展開。該過程是高度衰減的,意味著可以通過幾次迭代來限制它(根據光源的初始強度,對於32x32x32輻射體積,8到16次),由此產生的輻射分佈是所有這些輻射迭代的累積。
用LPV渲染時,按常規著色,類似於SH Irradiance Volumes,使用世界空間位置的簡單3D紋理查詢,與法線的餘弦葉積分以獲得輻照度,著色器中二階SH的簡單計算,透明物體和參與媒體的照明,延遲著色/照明,將體積的形狀繪製到累積緩衝區中,支援幾乎所有的延遲優化。
LPV還支援海量光源的照明,結合反射陰影圖(Reflective Shadow Maps,RSM)後,可以獲得全域性光照的效果。其中反射陰影貼圖帶有MRT佈局的陰影貼圖:深度、法線和顏色,是高效的虛擬點光源(Virtual Point Light,VPL)生成器。
RSM攜帶的資料:法線(右上)、深度(左下)、顏色(右下)。
結合LPV和RSM的全域性光照的渲染步驟如下:
- 將來自VPL的初始輻射注入輻射體積。
- 點渲染。
- 將每個點放入適當的單元格,使用頂點紋理獲取/R2VB。
- 每個帶有SH的VPL的近似初始輻射,著色器中的簡單解析表示式。
- 傳播輻射。
- 渲染具有傳播輻射的場景。
LPV效果圖。
VPL存在的問題是:VPL的注入涉及位置偏移,注入VLP的位置變為網格對齊,空間輻射近似的結果,非預期的輻射溢位,例如雙面薄幾何照明。
解決方案有:
- 將VPL朝法線或光源方向偏移半個格子。
- 通過各向異性雙邊濾波耦合,在最終渲染過程中。表面法線偏移的樣本輻射,計算輻射梯度,將輻射度與輻射度梯度進行比較。
全域性光照的級聯光照傳播體積描述如下:
- 一個網格尺寸有限,解析度低。
- 輻射體積的多解析度方法。類似於Cascaded Shadow Maps技術,在視野之外保留周圍的輻射。
- 每個級聯都是獨立的。每個級聯都有單獨的RSM,通過相鄰邊緣傳輸輻射,按特定RSM的大小過濾物件。
- 輻射發射器的有效分層表示。
GI組合SSGI的步驟如下:
- 螢幕空間全域性照明。
- SSGI的侷限性:只有螢幕空間資訊,近距離物體的巨大核心半徑。
- LPV的侷限性:區域性解決方案,低解析度空間近似。
- SSGI和LPV互相補充。
- 自定義混合。
[Lighting Research at Bungie](https://advances.realtimerendering.com/s2009/SIGGRAPH 2009 - Lighting Research at Bungie.pdf)介紹了Bungie公司研製的遊戲Halo 3的實時光照和預計算全域性光照。實時光照方面涉及天空和大氣、天空光、概率陰影測試(Probabilistic Shadow Test)、方差陰影圖(VSM)、指數陰影圖(ESM)、CSM、EVSM等。預計算方面,在光子對映的基礎上,改進了原先比較慢的部分,提出了新的渲染流程,獲得很大的速度提升。
在大氣渲染上,採用了Precomputed Atmospheric Scattering的方法,考慮單次和多次散射,在GPU上的預計算,從太空可見,支援光束。散射模型採用了Raleigh和Mie散射,預計算部分預包含透射比、內散射、輻照度,將預先計算的查詢表儲存為紋理,使用GPU生成紋理。(下圖)
天空光方面,對於遠處的山脈和物體採用了Precomputed Atmospheric Scattering的方法,使用單一顏色作為天空輻照度。近處的物體為了更好地逼近特寫幾何,使用CIE天空亮度分佈,預先計算的輻照度進行縮放,每個方位角投影到SH,用多項式擬合係數,使用PRT渲染GI外觀。
天空光的對比,從上到下:僅直接光、用SH近似、用PRT近似。
概率陰影測試(Probabilistic Shadow Test)在給定樣本在陰影中的概率、當前接收器、遮擋器深度下,其公式如下:
其中:
- \(d_o\)是隨機變數,表示遮擋體的深度分佈函式。
- \(d_r\)是當前陰影接收者的深度。
對於基於方差的陰影測試(Variance-Based Shadow Test),二進位制測試(Binary test)成為概率分佈函式,即當前片段處於陰影中的概率。\(P_r(d_o \ge d_r)\)源自兩個矩(moment):
使用切比雪夫(Chebyshev)不等式作為檢驗的上限:
在VSM基礎上了,為了達到更好的效果,Bungie團隊還嘗試了ESM、EVSM以獲得更精確的陰影效果。
對於預計算光照方面,傳統的CPU光子對映管線存在兩大瓶頸:
Bungie團隊針對瓶頸部分做了優化,直接照明階段使用GPU KD樹的快速光線投射,最終收集階段使用GPU KD樹的快速光線投射,光子照明切割(Cut),間接照明的分簇樣本點。
光子照明裁剪(Cut)類似於光源切割,估算光子樹每個節點的輻照度,通過樹計算“切割”,使用RBF基進行插值。
Theory and Practice of Game Object Component Architecture闡述了面向元件和麵向物件的特點和區別及實現方法。
遊戲物件(GameObject)是任何在遊戲世界中具有代表性的事物(如角色、道具、車輛、導彈、相機、觸發體積、燈光等),需要標準本體,要求明晰、均勻,功能、事物和工具移動性,程式碼重用和維護(例如使用模組化/繼承減少重複)。
早期的引擎常以整合來實現不同型別的遊戲物件,隨著物體種類增加,多重繼承是解決問題的一種方法, 但它不能很好地擴充套件,也沒有解決最終的挑戰:設計/要求的變化。(下圖)
這種繼承主導的方法不是每組關係都可以用有向無環圖來描述,類層次結構很難改變,功能向父類遷移,在兄弟型別特化需要消耗額外的記憶體。對複雜的應用程式,將引起很多錯綜複雜的類繼承樹:
基於元件的方法與面向方面的程式設計相關但不相同,一個類是一個容器:屬性(資料)和行為(邏輯),屬性即鍵值對列表,行為即帶有 OnUpdate() 和 OnMessage() 等響應函式的物件。
物件導向和麵向元件的對比。
該文還提到了資料驅動的建立,文字或二進位制,從管道載入,併發載入,延遲例項化,專用工具,資料驅動的繼承等。
TOD_BeginObject GameObject 1 "hotdog_concession"
{
behaviours
{
PhysicsBehaviour 1
{
physicsObject "hotdog_concession"
} ,
RenderBehaviour 1
{
drawableSource "hotdog_concession"
} ,
HealthBehaviour 1
{
health 2.000000
} ,
GrabbableBehaviour 1
{
grabbableClass "2hnd"
}
}
}
TOD_EndObject
資料驅動的建立優勢:賦予新屬性很容易,建立新型別的實體很容易,行為是可移植和可重用的,與遊戲物件對話的程式碼與型別無關,一切都經過包裝和設計,可以相互互動。簡而言之:可以編寫通用程式碼。
資料驅動的建立劣勢:必須編寫通用程式碼,遊戲物件是無型別且不透明的,如果物件具有可附加行為,則附加到它,程式碼必須同等對待所有物件,不能查詢資料和屬性,例如:
if object has AttachableBehaviour // 無法訪問屬性
then attach to it
Rendering Technology at Black Rock Studio分享了Alpha組合基礎、地面植被、樹木渲染、螢幕空間透明度遮蔽等技術。
文中提到了Alpha Test由一位值確定片元是否可見,可與z緩衝區一起使用,可能導致鋸齒。而Alpha To Coverage將alpha轉換為畫素的覆蓋掩碼,覆蓋掩碼與MSAA覆蓋掩碼進行“與”運算,與alpha測試結合使用時提供更柔和的邊緣,可與z緩衝區一起使用,但是由此產生的alpha漸變並不總是看起來很好。(下圖)
Alpha Test(上)和Alpha To Coverage(下)導致的瑕疵。
該文針對以上問題提出了全新的處理流程,以獲得更柔順自然的地表覆蓋物混合效果。新的渲染流程分為3個階段:
- 密度圖。離線生成,用來放置地表覆蓋物的2D對映圖。
- 分塊快取。相機移動時,計算地表覆蓋物的型別、位置、尺寸等。
- 頂點緩衝。逐幀生成,編碼了型別、位置、尺寸的精靈頂點。
在渲染時,在相機周圍的固定區域渲染草,區域劃分為8平方米的400個分塊,每平方米包含4個螢幕對齊的精靈,所以每個圖塊包含256 個精靈,每個精靈資訊都被編碼為一個4維向量,分塊快取在記憶體中。根據相機的位置確定需要渲染的分塊,然後查詢密度圖獲取地板覆蓋物的資料,每幀生成頂點緩衝區,分塊資訊從分塊快取中複製,CPU需要為每個可見的分塊複製16KB。
渲染相機附近分塊的地板覆蓋物的圖例。
幾種混合效果對比圖。
Alpha混合需要幾何排序,但是規則的幾何放置使排序更容易,包含兩級粒度:從後到前渲染分塊和精靈在每個分塊中排序。每個分塊內的排序是預先計算的,預先計算了16個攝像機方向的渲染順序(下圖),並選擇與當前最接近的相機方向的順序。為了提高效能,將精靈分組為 32個“單元”,共8個,並且僅在單元級別進行排序。
預計算的分塊排序,總共16個方向,圖中只選取了其中的4個方向。
這種混合的好處是高品質阿爾法混合地面覆蓋,低成本CPU排序,藝術家友好的工作流程。缺點是較高的過繪製,對GPU來說意味著較高的開銷。
該文還提出了螢幕空間Alpha蒙版流程以更好地繪製樹木等物體:
繪製樹的Alpha模板時,使用解析的深度,禁止Z寫入,渲染出樹的alpha值。
sampler alphaTexture : register(s0);
struct PSInput
{
float2 vTex : TEXCOORD0;
};
float4 main( PSInput In ) : COLOR
{
return tex2D( alphaTexture , In.vTex ).aaaa;
}
輸出Alpha蒙版時,存在兩種方式:source + destination
和max(source, destination)
。
ADD給出不透明的結果,MAX提供更多細節和更柔和的輪廓,幸運的是,可以一次建立兩者!用於寫入渲染目標的顏色和alpha分量的不同混合模式,在最終組合過程中平均兩個值。組合的PS程式碼如下:
sampler maskImage: register(s0);
sampler treeImage: register(s1);
sampler worldImage: register(s2);
float4 main( float2 vTexCoord : TEXCOORD ) : COLOR
{
float4 vTreeTexel = tex2D( treeImage, vTexCoord.xy );
float4 vWorldTexel = tex2D( worldImage, vTexCoord.xy );
float4 vMaskTexel = tex2D( maskImage, vTexCoord.xy );
float lerpValue = (vMaskTexel.r +vMmaskTexel.a) * 0.5f;
return lerp( vWorldTexel, vTreeTexel, lerpValue );
}
最終效果對比如下:
該文還分享了延遲著色的實踐經驗。Split/Second通道使用延遲著色渲染器,將光照與幾何圖形解耦,在渲染主場景時,照明所需的資訊被寫入幾何緩衝區 (G-Buffer),場景的照明被推遲到在後處理階段發生的照明通道。MRT的佈局如下:
針對延遲著色的MSAA進行了優化。全屏抗鋸齒的變化,FSAA將場景渲染到比要求更高的解析度,平均值下降到所需的解析度,嚴重影響效能。MSAA的每個畫素只執行一次畫素著色器,為多邊形覆蓋的每個片元設定片元顏色,但不能使用硬體對片元進行平均,因為G-Buffer不適合插值,意味著必須手動混合每個片元。
通過觀察,發現85%的畫素位於多邊形內部,意味著它們的所有片元都是相同的,能否快速識別出其它15%的不同之處?需要識別的片元如下圖紅色所示:
可以使用一個試圖識別多邊形邊緣的硬體特性:質心取樣(Centroid sampling),質心取樣避免了在多邊形邊界之外取樣的頂點屬性。
質心取樣將用於確定顏色的位置調整為多邊形覆蓋的所有采樣點的中心,因此,如果質心移動,那麼就在多邊形邊緣。
幸運的是,在畫素著色器中可以獲取質心樣本的值,如果它不為零,就可以知道三角形不會覆蓋畫素的所有樣本。
struct PSInput
{
float4 vPos : TEXCOORD0;
// 質心取樣的座標!TEXCOORD1_CENTROID是系統屬性值
float4 vPosCentroid : TEXCOORD1_CENTROID;
};
float4 main( PSInput In ) : COLOR
{
// 對比質心和畫素位置的差異,如果完全一樣,說明質心沒有移動,畫素完全在三角形內。
float2 vEdge = In.vPosCentroid.xy - In.vPos.xy;
// 筆者注,為了防止誤差,應改成:float fEdge = (abs(vEdge.x) + abs(vEdge.y) <= 0.001f) ? 0.0f : 1.0f;
float fEdge = (vEdge.x + vEdge.y == 0.0f) ? 0.0f : 1.0f;
// 對於延遲著色,通常會將這個值打包到G-Buffer中的一位。
return float4( fEdge );
}
為了避免陰影瑕疵,使用百分比漸進過濾,PCF從陰影貼圖中獲取多個樣本,對每個影子接收器執行深度測試,然後平均結果。可以將螢幕分成3個區域:絕對處於陰影中的區域、絕對不在陰影中的區域、可能在陰影中或不在陰影中的區域。實際上,只需要將PCF應用於這些區域中的最後一個(可能在陰影中或不在陰影中的區域)。雖然不可以準確地計算出來,但可以近似,生成一個掩碼來顯示必須執行PCF的位置。
第1個Pass輸出1/4螢幕尺寸的陰影圖,第2個Pass是螢幕尺寸的1/16,使用保守光柵化擴充套件邊緣。
1/4尺寸的陰影圖和使用保守演算法擴充套件邊緣的1/16陰影圖。
該文還分享了輻照度體積的實踐、效果及效能的優化。
14.3.3.2 並行處理
在2000年初,已經有文獻(Designing the Framework of a Parallel Game Engine)闡述如何將引擎並行化處理的技術和實現。其中,該文獻提及了無鎖步(Free Step)和鎖步(Lock Step)兩種執行模式。(下圖)
上:Free Step模式;下:Lock Step模式。
Free Step執行模式允許系統在完成計算所需的時間內執行。 Free Step可能會產生誤導,因為系統並非隨時可以自由完成,而是可以自由選擇需要執行的時鐘數。使用這種方法,向狀態管理器簡單地通知狀態更改不足以完成任務,還需要將資料與狀態更改通知一起傳遞,因為當需要資料的系統準備好進行更新時,已修改共享資料的系統可能仍在執行。此模式需要使用更多記憶體和更多副本,因此可能不是所有情況下最理想的模式。
Lock Step執行模式要求所有系統在一個時鐘內完成它們的執行。實現起來更簡單,並且不需要通過通知傳遞資料,因為對另一個系統所做的更改感興趣的系統可以簡單地向另一個系統查詢該值(當然在執行結束時)。Lock Step還可以通過在多個步中交錯計算來實現偽Free Step操作模式。 這樣做的一個用途是讓AI在第一個時鐘內計算其初始“大檢視”目標,而不是僅僅為下一個時鐘重複目標計算,它現在可以根據初始目標提出一個更集中的目標。
該文獻還提到了執行緒池的技術,將任務分拆成若干個粒度較小的子任務,由工作管理員排程,分派到執行緒池的執行緒中執行。(下圖)
並行化引擎涉及到系統的核心概念,系統和場景、任務、物體的關係如下圖所示:
遊戲引擎主迴圈的步驟如下:
- 呼叫平臺管理器來處理在當前平臺上操作所需的所有視窗訊息和/或其它平臺特定專案。
- 將執行轉移到排程程式,排程程式在繼續之前等待時鐘時間到期。
- 對於Free Step模式,排程程式檢查哪些系統任務在前一個時鐘完成了執行。 所有已完成(即準備執行)的任務都會發給工作管理員。
- 排程程式現在將確定哪些任務將在當前時鐘完成並等待這些任務完成。
- 對於Lock Step模式,排程程式發出所有任務並等待它們完成每個時鐘步長。
在執行任務時,排程器會從任務佇列獲取任務,分發到執行緒池中執行。其中執行的任務涉及到了工作管理員、服務管理器、狀態管理器、環境管理器等諸多概念(詳見下圖,當今的多執行緒並行系統已經簡化了它們)。
該文獻抽象出的引擎架構分成了引擎層和系統層,它們通過介面層通訊和互動。各個層又涉及到了諸多概念和子系統,見下圖:
引擎和系統的引用和互動關係如下圖,其中全域性場景(Universal Scene)擁有幾何、圖形、物理系統場景代表,每個場景代表有著和全域性場景對應的物體代表,這些場景代表各自會產生許多工,以便工作管理員排程與執行。
2004年的Challenges in programming multiprocessor platforms闡述了多處理的各種技術,其中該文涉及以下的概念:
- 硬體處理器佈置
- 異構:多個不同的處理器
- 同構:同一處理器的倍數
- 軟體安排
- 非對稱:執行不同的程式碼庫
- 對稱:執行相同的程式碼
- 工作單元
- 應用程式:要解決的問題,由產品要求定義。
- 任務:程式設計師在應用程式中工作的有界表示,在設計時定義。
- 執行緒:在應用程式中實現任務的機制,在軟體實現期間使用。
在軟體設計時,可程式設計性至關重要,執行多個動態應用程式時增加複雜性,軟體的工具/可見性越來越難,關注驗證及可重複性,設計和測試開發環境變得越來越複雜。另外,可重用性是不爭的事實,需要解決方案的可移植性,還需要功能的分層抽象。
經典的單指令上下文(單處理器)無法使用當前方法進行擴充套件,無法從指令級並行性中提取更多資訊,處理器引擎需要從應用程式設計師那裡獲得幫助,讓開發人員使用多指令上下文來表示他們的應用程式。
來自高MHz的高效能正在達到桌上型電腦和嵌入式的熱量或能量的極限,英特爾取消P4支援多核,ARM釋出多處理器核心,IBM表示無法從流程縮減中擴充套件。因此,微架構必須進化。
CPU核數和能量消耗關係圖。
經典的異構不對稱CPU架構如下:
T.I. OMAP雙核處理器架構。
同構不對稱CPU架構。
非對稱多處理 (AMP)使程式設計師能夠同時執行多個應用程式的軟體模型,在異構和同構處理器之間使用基於訊息的互連,以各種形式提供。當應用程式可以跨處理器靜態分割槽時,提供高效的解決方案,允許將任務的影響與其他任務隔離開來,提供一種將現有程式碼擴充套件到MPSoC的簡單機制。
AM使用案例。左邊是主CPU,右邊是從屬CPU。
AMP的挑戰包括:程式設計師需要拆分應用程式並將子應用程式靜態分配給處理器,可能跨越不同的微架構,如果不瞭解應用程式,將非常困難;管理開放平臺的動態工作負載的複雜性打破了這種模式,難以確保處理器的有效利用,動態特性會使特定處理器過載,難以提供單任務可擴充套件性;所有供應商的解決方案都不同,導致工具支援的碎片化,如果需要更改,則需要重寫/重新架構。
對稱多處理 (SMP)使程式設計師能夠利用多種指令上下文架構的軟體模型(假設由各種硬體架構提供的公共記憶體和外設),具有一致互連的非對稱MP、一致快取的對稱MP、公共快取的多執行緒單處理器。SMP還提供通用模型以提高標準,程式設計師使用執行緒來表示他們的任務,作業系統在處理器上排程執行緒,被視為下一代主要的程式設計模型,在單處理器設計之間仍可移植。
多工應用程式示例。
當時有幾種實現選項:
- 單處理器。事件驅動,合作時間切片,非同步工作分派,搶佔式時間切片多執行緒。
- 多處理器。與單處理器相同,作業系統還能夠通過CPU共享執行緒,降低上下文切換的成本,提高系統級響應。
- 在這兩種情況下,最簡單的方法是簡單地將應用程式任務對映到執行緒,允許使用現有的程式碼實現。
多執行緒涉及的機制有:
- Fork-Exec:按需建立執行緒。任務有明確的開始和結束條件,任務是長期存在的,足以隱藏建立/殺死執行緒的成本,有助於將現有程式碼遷移到多工應用程式,每個任務可能有多個同步點,不正確的分割槽會破壞效能。
- 工作池:將工作移交給工作池。應用程式有明確定義的“工作單元”,等待工作的任務池,任務同步最好限於工作單元的拆分/合併,需要確保工作項不是序列相關的。
// Fork-Exec虛擬碼
main()
{
while( !Shutdown )
{
work = WaitForWork();
CreateThread(WorkerTask, work);
}
}
WorkerTask(work)
{
DoWork(work);
}
// Worker Pooling虛擬碼
main()
{
For(i=0; i< numCPU * 2; i++)
{
CreatThread(WorkerTask, workQueue);
}
While( !Shutdown )
{
work = WaitForWork();
PostWork(workQueue, work);
}
}
WorkerTask(workQueue)
{
while( !Shutdown )
{
work = WaitforWork(workQueue);
DoWork(work);
}
}
多工處理效果很好,直到單個任務需要比單個處理器更高的效能。如果任務很容易由多個子任務表示,則不是一個重要問題。子任務可能會很複雜的情況包含:由單一線性演算法表示,特別是如果已經在程式碼中設定;演算法是一系列相互依賴的操作。幸運的是,在軟體程式碼塊或迴圈級別尋找並行性可以簡化這些問題,跨處理器拆分迴圈的迭代,在處理器上放置單獨的程式碼段。
對稱MP的挑戰有:
- 難以將一項任務的影響與其他任務隔離開來,所有任務共享相同的處理器,OS API通常提供親和力和任務級別優先順序。
- 程式設計師需要注意不要濫用通用記憶體系統,通過需要太多同步點,通過導致資料需要在處理器之間不斷遷移。
- 硬體必須解決處理子系統的瓶頸,圍繞確保記憶體一致性,圍繞處理器之間的同步。
ARM MPCore混合多處理器。
總之,嵌入式開放平臺MPSoC不能簡單地複製桌面微架構,無法承受傳統一致性的成本(慢速系統匯流排和通訊的SoC元件),需要將低功耗置於峰值效能之上。解決方案必須由開放平臺開發團隊程式設計,允許遷移現有程式碼庫,使用“大眾市場”模型,也可以提供高水平的處理效率。
Real World Multithreading in PC Games Case Studies也提及了多執行緒的並行技術。文中提到了超執行緒和普通執行緒的對比:
超執行緒擁有兩個邏輯執行緒,可以併發地處理資源,從而提升資源利用率,提高吞吐量。
多處理器(左)和超執行緒(右)硬體結構對比圖。
為什麼遊戲很難執行緒化?可從技術和商業兩方面解答。技術方面,在階段之間共享單個資料集的順序管道模型,高度優化的密集程式碼最大限度地減少了HT的好處,執行緒經常涉及重大的高階設計更改。商業方面,多執行緒程式設計經驗少,支援HT(如SSE)的系統的市場份額有限,以及消費者不知道HT。
但是,遊戲應該執行緒化,依然可以從技術和商業兩方面解答。技術方面,並行是CPU架構的未來->易於擴充套件(HT、多核等),在等待顯示卡/驅動程式時做其它事情,良好的MT設計可擴充套件,並防止重複重寫。商業方面,在競爭環境中脫穎而出,所有PC平臺都將支援多執行緒,並行程式設計教育將在多個平臺(PC、控制檯、伺服器等)上獲得回報,MT可擴充套件更多 -> 延長產品壽命。
遊戲管線模型,遊戲更新和渲染存在時序依賴和資料依賴。
多執行緒策略有兩種:
-
任務並行。同時處理不相交的任務。對整個3D圖形管線進行多執行緒化,例如執行緒1處理N幀,執行緒2處理N+1幀。有利於GPU密集型 遊戲,由於存在依賴,較難實現,但不是不可能。
-
資料並行。同時處理不相交的資料。可以在輔助執行緒上執行音訊處理、網路(包括 VoIP)、粒子系統和其它圖形效果、物理學、人工智慧、內容(推測)載入和拆包等任務。多執行緒程式內容建立,如幾何、紋理、環境等。適合CPU密集型遊戲,易於實現。
下圖是論文提出的執行緒管理器,以便管理所有真實執行緒的生命週期:
下圖是文中給出的程式化天空盒的多執行緒實現案例:
該文還探討了檔案非同步載入、天氣系統渲染、粒子系統渲染的多執行緒化。
Exploiting Scalable Multi-Core Performance in Large Systems闡述了Intel CPU的發展趨勢以及多執行緒的架構分析、執行緒使用、除錯和效能分析等。
Intel執行緒技術的四個方面。
文中談及了執行緒之間的資料競爭和死鎖,給出了勢力及詳細的解決步驟,包含彙編級分析。
Intel CPU在資料競爭的彙編級分析。
死鎖示例。
當時的Intel執行緒分析工具的技術模型如下:
文中還給出了多執行緒鎖的優化、單執行緒和多執行緒安全的讀寫鎖,以及在序列區域禁用鎖的程式碼示例(下圖)。
High-Performance Physics Solver Design for Next Generation Consoles闡述了當前時單元處理器模型,以及如何並行地處理物理模擬。下面倆圖分別是文中提及的流式積分過程以及利用併發DMA和處理來隱藏傳輸時間:
下面兩圖是剛體管道並行化的對比圖:
為了減少頻寬,提升執行效率,可以批處理作業:
下圖是多SPU的並行示意圖和效能對比:
另外,文中還給出了高階和低階並行化,下圖是低階並行化:
2007年,CPU和GPU的速度提升越來越快,CPU以1.4倍年增長率,而GPU以1.7倍(畫素)到2.3倍(頂點)的年增長率。例如,3.0GHz Intel Core2 Duo(Woodcrest Xeon 5160)的計算峰值達到48GFLOPS,記憶體頻寬峰值達到21GB/s,而英偉達GeForce 8800 GTX的計算峰值達到330GFLOPS,記憶體頻寬峰值達到55.2GB/s。
上:CPU和GPU從2001到2007年的速度提升;下:GeForce 8800 GTX的統一著色器架構圖。
速度的提升使得應用層有更多的機會執行更多通用的計算,文獻General-Purpose Computation on Graphics Hardware便是其中的典型代表。GPU的通用計算包含大型矩陣/向量運算 (BLAS)、蛋白質摺疊(分子動力學)、財務建模、FFT(SETI,訊號處理)、光線追蹤、物理模擬(布料、流體、碰撞……)、序列匹配(隱馬爾可夫模型)、語音/影像識別(隱馬爾可夫模型、神經網路)、資料庫、排序/搜尋、醫學成像(影像分割、處理),還有很多很多……
文中涉及的內容涵蓋GPU架構、資料並行演算法、搜尋和排序、GPGPU語言、CUDA、CTM、效能分析、GPGPU光柵化、光線追蹤、幾何計算、物理模擬等內容,可謂內容全面,將GPU的技術內幕和應用細節剖析得鞭辟入裡。
當時的GPGPU計算量大,適合大規模並行,為獨立操作設計的圖形管道,可容忍較長的延遲,深度的前向反饋管道,可以容忍缺乏準確性,擅長並行、算術密集、流記憶體問題。新功能很好地對映到GPGPU,可以直接訪問新 API 中的計算單元,新增的統一著色器可以簡化3D渲染管線,提升計算單元的利用率。
上:任務並行GPU架構;下:統一著色器GPU架構。
上:Geforce 6800的3D渲染管線,非統一著色器架構;下:Geforce 8800的統一著色器架構。
GPU是各項的混合體,包含歷史的、固定功能的能力和新的、靈活的、可程式設計的功能。固定功能是已知的訪問模式、行為,由專用硬體加速。可程式設計的是未知的訪問模式,通用性好。而GPU記憶體模型必須兼顧兩者,結果是充足的專用功能和限制靈活性可能會提高效能。
2007年CPU和GPU的記憶體架構模型。
GPU的資料結構分類:
- 密集陣列。地址轉換:n維到m維。
- 稀疏陣列。靜態稀疏陣列(如稀疏矩陣)、動態稀疏陣列(如頁表)。
- 自適應資料結構。靜態自適應資料結構(如k-d樹)、動態自適應資料結構(如自適應多解析度網格)。
- 不可索引的結構(如堆疊)。
GPU比CPU在記憶體訪問上更受限制,僅在計算之前分配/釋放記憶體,與CPU之間是顯式的傳輸。GPU由CPU控制,無法發起傳輸、訪問磁碟等,因此,GPU更擅長訪問資料結構,CPU更擅長構建資料結構,隨著CPU-GPU頻寬的提高,考慮在其“自然”處理器上執行資料結構任務。
GPU在計算(核心)期間只能有限地記憶體訪問,包括暫存器(每個片段/執行緒)的讀寫、本地記憶體(執行緒間共享)、全域性記憶體(計算期間只讀,計算結束時只寫,即預計算地址)以及全域性記憶體(允許一般分散/收集,即讀/寫,見下圖)。
通用計算的典型用例很多,比如合併元素、流資料壓縮、掃描、排序等等。
GPGPU通用計算用例。從上到下依次是2D資料合併、流資料壓縮、平衡樹掃描、雙調歸併排序。
另外,GPGPU還可用於分組計算:
在GPU並行排序的管線和效能對比如下所示:
GPU快取模型的特點是小型資料快取,以更好地隱藏記憶體延遲,供應商不披露快取資訊——這對於GPU上的科學計算至關重要。可以設計簡單的模型,確定快取引數(塊和快取大小),提高排序、FFT和SGEMM的效能。快取高效的演算法效能如下圖所示:
GPU快取效率演算法效能圖例。觀測的時間比理論峰值要稍高一些。
下圖是CPU、GPGPU、CUDA的快取模型對比圖:
由史丹佛大學研發的開源工具GPUBench可以GPU記憶體訪問的延時細節,如延時是否可隱藏以及訪問模式是否會影響延時。具體測量方法是嘗試不同數量的紋理提取和不同的訪問模式(快取命中:每次提取到相同的紋素,順序:每次取指都會將地址加1,隨機:隨機紋理的相關查詢),增加著色器的ALU操作,ALU操作必須依賴以避免優化。下面是不同的訪問模式在ATI、NVidia幾種GPU的資料獲取消耗:
對於Early-Z的測試,通過設定Z緩衝區和比較函式以遮蔽計算,改變塊的連貫性,更改塊的大小,設定要繪製的不同數量的畫素。在NVIDIA 7900 GTX的測試結果如下:
以上顯示隨機訪問的效率最低,4x4塊的一致性訪問效率最佳。
對於著色器分支,採用測試語句if{ do a little }; else { LOTS of math}
,並且改變塊的連貫性,更改塊的大小,有不同數量的畫素執行繁重的數學分支。測試結果顯示如下:
由此可知完全一致的訪問擁有最好的效能,隨機訪問最差。
通用計算還適用於動態總面積表(Dynamic Summed Area Table)。動態總面積表的演算法概述:生成動態紋理、反射貼圖等,使用資料並行GPGPU計算生成SAT,在傳統渲染過程中使用 SAT 資料結構。
SAT可用於光澤反射(模糊度取決於反射物體的距離)和景深(模糊度取決於與眼睛的距離)。
此外,還可用於解析度匹配的陰影圖計算:
Saints Row Scheduler探討了多執行緒排程器的概念、架構和效能等主題。文種定義了排程器(Scheduler),認為它是控制流程的機制,類似於函式呼叫或執行緒排程,用於管理跨多個執行緒的獨立可排程實體或“作業”的排程,存在很多很多有效的設計,但通常特定於平臺/硬體。而作業(Job)是獨立可排程的實體,沒有順序或資料,依賴於其它“準備好的”工作,通常是非阻塞的,無需等待I/O、D3D裝置、其他非同步事件,相當於一個函式指標/資料塊對。將應用程式的處理分解為作業是使應用程式多處理器就緒的大部分工作。
排程器設計的準則是儘可能簡單儘可能直觀,可按應用程式配置,儘可能靈活,高效能/低開銷,允許精細的作業粒度,優雅地處理搶先事件的機制。通用哲學是保持電線(wire)懸空,建立和使用構建塊,避免高階語言結構。
Saints Row源自單執行緒應用程式,後有六個硬體執行緒。初始執行緒佈局:標準模擬/渲染拆分、幀時間變化,約20到50ms,通常約33ms、音訊驅動程式每5毫秒使用約2.3毫秒、流式傳輸活動時,使用整個執行緒、音訊每約30毫秒執行一次。
但這樣的設計架構存在諸多問題,例如:
- 從其中一個主執行緒排程大量作業可以獨佔作業執行緒, “先到先得”順序的後果。
- 部分執行緒減少了頻寬:帶有DSP/音訊驅動程式的執行緒4僅約65%、執行緒1可以專用於流式傳輸、音訊和流媒體都是搶先的。
- 組合和預通渲染兩個冗長且持續的操作。
- Havok問題,使用自己的執行緒實用程式,必須為每個執行緒分配執行緒記憶體,經常序列。
Saints Row改進了排程器設計,採用先到先得的順序,使用FIFO作業Q會產生“自然”的作業處理順序。存在的問題是從一個主執行緒排程一大塊作業可能會阻塞作業執行緒,更精細的作業粒度和為工作新增優先順序都無濟於事。解決方案是新增額外的FIFO作業Q和“作業偏差”,作業執行緒偏向於提供模擬型別或渲染型別的作業,動態可配置——可以動態更改偏差演算法和執行緒/作業型別,確保每個主執行緒都有一些工作執行緒時間。
處理執行緒的特殊情況:
-
在Sim(模擬)和Render作業之間拆分作業執行緒。
- Sim在不流式傳輸時得到0、2 和1。
- Havok僅在0、1和2上執行。
- Render得到3、5和音訊處理後剩下的4。
- 在幀的渲染密集部分也得到1和2。
-
組合和PrePass:
- 在主渲染執行緒上執行prepass,釘住組合通道作業到執行緒3。
- 最優先的工作。
- 執行緒3可自由供其它處理執行緒使用。
-
Havok:
- 帶有自己的執行緒實用程式。無動態控制,每個執行Havok處理的執行緒都需要Havok執行緒記憶體,序列執行約一半的處理。
- 解決方案:
- 將三個執行緒專用於Havok,僅為這些執行緒分配執行緒記憶體。
- 從排程程式呼叫Havok時間步長。允許執行緒控制,在每幀的基礎上新增或刪除執行緒,效能與Havok執行緒實用程式相同。
- 自己分解並分解序列部分。
最終的執行緒佈局是Havok移至前3個執行緒,固定到執行緒3的組合通道,在大部分幀期間允許渲染作業,在密集的模擬處理期間允許模擬工作,實際的模擬視窗更復雜,作業可以在主執行緒空閒時間進行排程。
對應用程式而言,當拆分作業時,最佳作業大小是排程程式開銷的功能,設定一些“可接受”的標準,例如“5%或更少的開銷”,然後測量每個作業的排程時間,對於Saints Row,最佳大小約為250-500微秒,如果作業需要更長的時間是不可取的。
Saints Row在PIX的時間線顯示CPU使用率在90%左右。
Saints Row排程器內部的技術細節補充:
- 由臨界區或自旋鎖提供所有保護。
- 六個作業執行緒位於每個硬體執行緒上。
- 將作業插入作業Q會啟用任何空閒的匹配作業執行緒。事件,而不是訊號量,以實現靈活性。
- 作業生成執行緒可能會掛起並等待排程事件。掛起執行緒指定可以在他的硬體執行緒上執行的作業型別。
- 完成後,作業會觸發事件(事件觸發器)或安排更多作業(排程觸發器)。
在效能方面,如果是單作業佇列,將作業塊移動到作業佇列更有效,更靈活地逐個移動。由於執行緒爭用,作業佇列是一個重要的開銷來源。如果使用臨界區保護,可能應該阻塞佇列。可以嘗試無鎖,無鎖結構(堆疊、佇列)的效能明顯優於臨界區保護結構,後進先出—— SLists、GPGems 6 堆疊,非常平坦的,先進先出——Michael的浮動節點、Fober的重新插入,謹慎使用。
Dragged Kicking and Screaming: Source Multicore介紹了Source引擎在多核面臨的決策、如何利用多核,並提供了演算法和正規化。
Source引擎多核化的目標是將在Valve的業務中整合多核,無需重新編譯即可擴充套件到核心,提升遊戲幀率,將核心應用於新遊戲邏輯。當時面臨的挑戰是遊戲需要最大的CPU利用率,遊戲本質上是串聯的,之前存在著數十年單執行緒優化經驗,已為單執行緒編寫的數百萬行程式碼。多核的策略包含執行緒模型和執行緒框架。執行緒模型又有細粒度執行緒、粗粒度執行緒和混合執行緒三種型別。
線上程安全方面,Source引擎採用高效的執行緒安全,無同步(“無需等待”),每個執行緒都有執行操作所需的所有資料的私有副本:處理獨立問題的執行緒、用執行緒私有資料替換全域性變數、重新定向到管道。
Source引擎的無鎖的資料執行緒安全模型:空間分割槽。其中上圖是客戶端和伺服器共享同一份資料,下圖是空間分割槽,達到無鎖的執行緒安全的目標。
此外,Source引擎用更好的同步工具、技術分析資料訪問,例如使用讀/寫鎖的符號表,使用排隊函式呼叫來解耦。Source引擎還採用了混合執行緒技術,為作業使用適當的方法,例如一些系統在核心執行(例如聲音),一些系統以粗粒度的方式在內部拆分。跨核心細粒度拆分昂貴的迭代,當核心空閒時入隊一些作業以讓它處於忙碌狀態。需要強大的技術,最大化核心利用率。
Source的渲染架構和流程如下圖:
存在的問題是按檢視構建的場景限制了機會,任意物件型別的順序和任意的程式碼執行,可以採用模擬和渲染交錯,亦即惰性計算優化。迭代變換(如骨骼動畫),並行化惰性計算觸發器,將骨骼設定重構為每個檢視單遍,將所有檢視重構為單遍,其他CPU密集型階段的模式相同。修改後的管道如下:
- 為多個場景並行構建場景渲染列表(例如世界及其在水中的倒影)。
- 重疊圖形模擬。
- 平行計算所有場景中所有角色的角色骨骼變換。
- 允許多個執行緒並行繪製。
- 在另一個核心上序列化繪圖操作。
實現混合執行緒,讓程式設計師解決遊戲開發問題,而不是執行緒問題,使所有程式設計師能夠利用核心。對多數程式設計師而言,作業系統太過低階別,編譯器擴充套件 (OpenMP)又太不透明,只能使用量身定製的工具:正確的抽象。量身定製的工具包括遊戲執行緒基礎設施,定製工作管理系統,使得程式設計師可以專注於保持核心忙碌。執行緒池:N-1個執行緒用於N個核心,支援混合執行緒:函式執行緒、陣列並行、入隊和立即執行。量身定製的工具的目標是使系統易於使用且不易混亂,例如編譯器生成的函子(functor),使用模板打包函式和資料,呼叫點看起來很相似,像序列一樣地呼叫佇列化函式,節省時間,減少錯誤,鼓勵實驗。下面是Source引擎的幾種多執行緒呼叫方式:
// 一次性推送到另一個核心
if ( !IsEngineThreaded() )
_Host_RunFrame_Server( numticks );
else
ThreadExecute( _Host_RunFrame_Server, numticks );
// 並行迴圈
void ProcessPSystem( CParticleEffect *pEffect );
ParallelProcess( particlesToSimulate.Base(), particlesToSimulate.Count(), ProcessPSystem );
// 入隊一堆工作項,等待它們完成
BeginExecuteParallel();
ExecuteParallel( g_pParticleSystem, &CParticleSystem::Update, time );
ExecuteParallel( &UpdateRopes, time );
EndExecuteParallel();
對於爭用,如果無法消除對共享資源的爭用怎麼辦?例如,分配器被大量使用,多個固定大小的塊池,每個池具有自定義自旋鎖互斥鎖,互斥限制規模,不想採用逐執行緒的分配器。可以嘗試無鎖的演算法:無論排程或狀態如何,都沒有執行緒可以阻塞系統,在所有服務和資料結構的底層,依賴於原子寫入指令:比較和交換。
// c++
bool CompareAndSwap(int *pDest, int newValue, int oldValue)
{
Lock( pDest );
bool success = false;
if ( *pDest == oldValue )
{
*pDest = newValue;
success = true;
}
Unlock( pDest );
return success;
}
// asm
bool CompareAndSwap(int *pDest, int newValue, int oldValue)
{
__asm
{
mov eax,oldValue
mov ecx,pDest
mov edx,newValue
lock cmpxchg [ecx],edx
mov eax,0
setz al
}
}
在分配器中使用無鎖演算法,用每個池的無鎖列表替換互斥鎖和傳統的每個池的空閒列表,需要依賴Windows API或XDK SList等系統API。下面是無鎖列表的插入示例程式碼:
void Push( SListNode_t *pNode )
{
SListHead_t oldHead, newHead;
for (;;)
{
oldHead.value64 = m_Head.value64;
newHead.value.iDepth = oldHead.value.iDepth + 1;
newHead.value.iSequence = oldHead.value.iSequence + 1;
newHead.value.Next = pNode;
pNode->pNext = oldHead.value.pNext;
if ( ThreadInterlockedAssignIf64( &m_Head.value64, newHead.value64, oldHead.value64 ) )
{
return;
}
}
}
無鎖列表特別有用在為每個執行緒提供上下文不切實際時保留上下文結構池,有效地收集並行過程的結果以供以後處理,使用Push() 構建要操作的資料列表,然後使用Detach()(也稱為“Flush”)在單個操作中獲取另一個執行緒中的資料。Source的無鎖演算法細節如下:
- 執行緒池工作分配佇列。源自Half Life 2非同步I/O佇列,專為一個生產者一個消費者而設計,帶有互斥鎖的簡單優先佇列,任意優先順序,所有執行緒的一個佇列。
- 解決方案:使用無鎖佇列,對固定優先順序的介面進行返工,每個優先順序一個佇列,除了共享佇列之外,每個核心一個佇列,使用原子操作獲取“門票”,實際完成的工作可能會有所不同。
- 鎖允許一個穩定的現實。
- 無鎖允許現實改變指令。
- 利用推理而不是鎖定來了解系統的一部分是穩定的。
- 避免等待總是更好。
Source研發人員還呼籲行業建立或獲得強大的工具和新技術,採用無鎖機制將工作和資料移入和移出無等待程式碼,準備在多個核心上分解特徵,使用可訪問的解決方案來授權所有程式設計師,而不僅僅是系統程式設計師,根據遊戲問題支援更高階別的執行緒。
Snakes! On a seamless living world闡述了名為Stackless的Python庫模擬多執行緒的一種架構和技術。Stackless具有最少執行緒管理的 1000 個小任務,超快速上下文切換,強大的異常處理,管理小任務等。它的技術架構如下:
它擁有獨立的排程器和事件過濾器:
Threading Successes of Popular PC Games and Engines談到了遊戲引擎多執行緒化的實際案例,提出了一些多執行緒化的模型:
操作佇列使資料保持本地化,並降低頻寬。將資料儲存在一個地方,但在其上排隊操作,在完美世界中,讀取是即時、免費且一致的:
當時的遊戲多核化時,需要關注:
- 執行緒效能。幀率,載入時間,與時間切片相比,更容易平滑幀速率,很常見
- 執行緒功能。許多可能的CPU密集型效果。
- 使用執行緒中介軟體。做任何事情,並擴大規模,很常見,但是它們一起工作的效果未知。
THQ/Gas Powered Games Supreme Commander and Supreme Commander: Forged Alliance也闡述了類似的多執行緒模型:
以上架構可以很好地適應負載,渲染負載通常會佔主導地位,重新渲染以保持幀速率,模擬重的地圖將嘗試以模擬為主。下圖是模擬執行緒和渲染執行緒的執行過程圖:
記憶體管理器可以提供額外的提升,如果線上程遊戲中不小心管理記憶體,記憶體使用可能會破壞快取 ,記憶體分配/釋放可能很慢。疑似記憶體管理有問題,例如執行很多小分配,構建程式碼可以輕鬆切換記憶體管理器。自定義記憶體管理器優於預設的malloc/free,可能會導致一些除錯問題。
對遊戲引擎進行多執行緒化時,儘量從一開始就進行執行緒架構設計,多執行緒化以前的單執行緒程式碼,儘可能解耦執行緒。不要害怕多執行緒化單執行緒程式碼。
Namco Bandai, EA Partner For Hellgate: London提及了幾種並行化作業的技術和案例。他們第一次嘗試是採用fork-and-join方法,主執行緒將在雙核上完成一半的工作,專用執行緒將完成另一半,沒有資料複製, 記憶體是一致的。
第二次嘗試:非同步更新、同步渲染。非同步更新不會阻塞主執行緒,記憶體不再一致,當有新位置可用時(可能每一幀),天氣粒子會重新繪製。更棘手,但看起來像一個贏家。
所做的程式碼更改是定義了一個回撥來更新天氣粒子,使用了現有的作業任務池,建立頂點緩衝區來儲存天氣粒子,粒子系統被標記為常規或非同步。非同步系統分為“善良雙胞胎”和“邪惡雙胞胎”:在繪製過程中,善良雙胞胎從最後一幀繪製邪惡雙胞胎的粒子,然後為邪惡雙胞胎捕獲繪製狀態;在更新期間,邪惡雙胞胎在回撥中處理並填充頂點緩衝區。
他們總結到,執行緒化特性有很大的潛力,每個額外的核心都會在池中提供一個額外的執行緒,每個額外的執行緒相當於一個額外的功能。
Project “Smoke” N-core engine experiment分享了專案Smoke的多執行緒化的經驗。該文提及的遊戲引擎的一種多執行緒架構如下:
也認同了2007年關於遊戲會多執行緒化的趨勢和走向:
系統訂閱更改訊息機制如下:
任務的劃分、步驟、執行和訊息傳統如下系列圖:
New Dog, Old Tricks: Running Halo 3 Without a Hard Drive談到了高階IO設計、載入內容、載入過程等技術。該文談及到了Halo 3為了解決載入衝突,加入了快取的資源訪問狀態:
資源訪問狀態如下所示:
該文還詳細探討了區域集過渡的策略和方法:
資源載入由資源排程器控制,每個優先順序生成一個所需的集合,按優先順序順序處理,發出I/O或無法成功時停止處理:
地圖則通過優化後的地圖佈局和共享地圖佈局重新連結地圖檔案,從而獲得優化後的地圖:
2009年,Parallel Graphics in Frostbite – Current & Future闡述了Frostbite在CPU和GPU方面的並行技術,以及如何引入到渲染系統中。在當時,主流的PC已經達到2到8個硬體執行緒,PS3有2個硬體執行緒和6個SPU,而Xbox 360也有6個硬體執行緒。
2009年前後的CELL處理器外觀圖。
為了充分利用這些硬體執行緒,Frostbite引入作業系統,將系統劃分為作業,帶有顯式輸入和輸出的非同步函式呼叫,通常完全獨立的無狀態函式作業依賴建立作業圖,所有核心都消耗作業。
構建大型的CPU作業圖時,採用了合批,混合CPU和SPU作業,以降低GPU作業的延時。其中作業依賴性決定執行順序、同步點、負載均衡、例如有效並行度。
對於渲染,Frostbite大量地將渲染系統劃分為作業,渲染作業主要包含地形幾何處理、灌木生成、貼花投影、粒子模擬、視錐裁剪、遮擋裁剪、遮擋光柵化、命令緩衝生成、PS3上的三角形裁剪等。大多數渲染作業將轉移到GPU,主要是單向資料流的作業。
並行記錄命令緩衝區時,將繪圖呼叫和狀態並行排程到多個命令緩衝區,使用核心相同的線性擴充套件,每幀1500-4000次繪製呼叫,減少延遲並提高效能。在DX11上實現並行命令記錄,是減少CPU開銷和延遲的殺手級功能,大約90%的渲染排程工作時間在D3D/驅動程式中。實現步驟大致如下:
1、為每個核心建立一個DX11的延遲裝置上下文,與動態資源(cbuffer/vbuffer)一起用於延遲上下文。
2、渲染器有想要為幀的每個渲染“層”執行的所有繪製呼叫的列表。
3、將每一層的繪製呼叫拆分為大約256 個塊(chunk),並且與延遲上下文並行排程,每個塊生成一個命令列表。
4、渲染到即時上下文並執行命令列表。
當時的目標是當獲得完整的DX11驅動程式支援時,接近線性擴充套件到八核(現在到 IHV)。
對於遮擋剔除,不可見的物體仍然必須更新邏輯和動畫、生成命令緩衝區、需要在CPU和GPU上兩端處理,部分物體難以實現全剔除,例如可破壞的建築物、動態遮擋、難以預先計算、GPU遮擋查詢的渲染可能很繁重。
面對以上的遮擋剔除問題,Frostbite的解決方案是使用軟體遮擋光柵化。分為兩大步驟:
- 在SPU/CPU上光柵化粗糙的zbuffer。256x114浮點,非常適合SPU LS(區域性快取),但可能是16位,低多邊形遮擋網格,手動設定保守,100米的視距,最大10000個頂點/幀,並行SPU頂點和光柵作業,消耗在幾毫秒。
- 然後根據zbuffer剔除所有物件。在傳遞給所有其它系統之前,以獲得較大的效能提升,螢幕空間邊界框測試。
來自《Battlefield: Bad Company》PS3 的圖片和粗糙z-buffer。
除了軟體遮擋剔除,Frostbite還支援GPU遮擋剔除。理想情況下需要GPU光柵化和測試,但是遮擋查詢引入開銷和延遲(可以管理,但遠非理想),條件渲染只對 GPU 有幫助(不是 CPU、幀記憶體或繪圖呼叫)。當時待探索的目標有兩個:
-
低延遲額外GPU執行上下文。在它所屬的 GPU 上完成光柵化和測試,與CPU鎖步,需要在幾毫秒內讀回資料,在所有硬體上需要LRB上是可能的。
-
將整個剔除和渲染移至GPU。世界代表,剔除、系統、派遣,也是最終目標。
在延遲光照方面,Frostbite還採用了螢幕空間的分塊分類(Screen-space tile classification),具體步驟如下:
-
將螢幕分成小塊並確定有多少和哪些光源與每個小塊相交。
-
僅對每個小塊中的畫素應用可見光源。在單個著色器中使用多個光源降低頻寬和設定成本。
對螢幕進行分塊之後,可以方便地和計算著色器協調工作,從而一次性完成工作。此方法已用於頑皮狗的Uncharted(下圖)和SCEE PhyreEngine。
來自“The Technology of Uncharted”的螢幕空間分塊技術,GDC'08。
在螢幕空間分塊的基礎上,便可以實現基於CS的延遲著色。使用 DX11 CS 進行延遲著色,在Frostbite 2中的實驗性地實現,未經生產測試或優化,需要計算著色器5.0,假設沒有陰影。
新的混合圖形/計算著色管道:
- 圖形管道光柵化GBuffer,用於不透明表面。
- 計算管道使用GBuffer,剔除光源,計算光照,並組合著色結果。
計算著色器的步驟有:
- 載入GBuffer和深度。
- 計算執行緒組/分塊中的最小和最大Z。
- 確定每個分塊的可見光源。
- 對於每個畫素,累積來自可見光的光照。
- 結合光照和陰影反照率/引數。
每個步驟又有較多的細節,詳情可以參看論文或4.2.3.2 Tiled-Based Deferred Rendering(TBDR)。利用此技術可以支援場景的海量光源照明:
基於CS延遲著色的優缺點如下:
-
優點:
- 恆定和絕對最小頻寬。
- 僅讀取GBuffer和深度一次!
- 不需要中間光緩衝器。
- 使用HDR、MSAA和顏色鏡面反射會佔用大量記憶體。
- 擴充套件到大量的大重疊光源!
- 細粒度剔除 (16x16)。
- 僅ALU成本,良好的未來擴充套件性。
- 可能對積累VPL(虛擬點光源)有用。
- 恆定和絕對最小頻寬。
-
缺點:
-
需要支援DX11以上的硬體。
- CS 4.0/4.1由於原子性和分散的組共享寫入而變得困難。
-
剔除小光源的開銷。
- 可以使用標準光體渲染來累積它們。
- 或單獨的CS用於tile-classific。
-
潛在效能。
- MSAA紋理載入/UAV 寫入可能比標準PS慢。
-
無法輸出到MSAA紋理。
- DX11 CS UAV限制。
-
總之,良好的並行化模型是良好遊戲引擎效能的關鍵,混合任務和資料並行CPU和SPU作業的作業圖非常適合Frostbite,其中SPU-jobs 做繁重的工作。通過充分利用DX11進行高效的互操作性,Frostbite開啟了混合計算/圖形管道的全新嘗試和技術里程。此外,Frostbite還期望一個使用者定義的流式傳輸管道模型,富有表現力和可擴充套件的帶有佇列的混合管道,專注於資料流和模式,而不是進行順序記憶體傳遞。
Parallelizing the Physics Pipeline : Physics Simulations on the GPU則講述瞭如何利用GPU進行並行化模擬物理的技術,包含剛體、流體、粒子等的物理引數、行為及碰撞。步驟概覽如下:
- 粒子值的計算。對於每個粒子:讀取剛體的值並寫入粒子值。
- 網格生成。
- 碰撞檢測和反應。對於每個粒子:從網格中讀取鄰居,寫入計算的力(彈簧和阻尼器)。
- 更新動量。對於每個剛體:總結粒子的力並更新動量。
- 更新位置和四元數。對於每個剛體:讀取動量,更新之。
上述步驟中涉及了格子生成、GPU樹形結構遍歷及動態生成等技術。下圖描述瞭如何在GPU中用歷史標記遍歷樹狀結構的優化技術及和堆疊的效能對比圖:
該文還提到了並行更新問題:如果一個剛體碰撞到另一個剛體,沒問題;如果一個剛體與多個剛體發生碰撞,則無法並行更新。解決方案是合批,不同時更新所有內容,將它們分成幾批,按順序更新批次,以便並行更新批量碰撞。在GPU建立合批相比CPU並非易事,需要結合當時GPU的特點按照特定的策略並行地建立批次:
此外,該文還探討了多GPU協調執行物理模擬的策略和實踐:
上:設計多GPU並行模擬物理效果;下:不同GPU數量的效能對比。
id Tech 5 Challenges - From Texture Virtualization to Massive Parallelization闡述了id Tech 5的GPU虛擬紋理、用虛擬紋理並行化作業系統、將作業遷移到 (GP) GPU等。虛擬紋理技術概覽如下圖:
虛擬紋理視覺化:
回饋資訊分析告知需要哪些頁面,由於是實時應用程式,所以不允許阻止。快取處理命中、排程未命中以在後臺載入,獨立於磁碟快取管理的常駐頁面,物理頁面組織為每個虛擬紋理的四叉樹,免費、LRU 和鎖定頁面的連結串列。虛擬紋理的回饋分析時,生成相當於帶優先順序的廣度優先四叉樹順序:
具有依賴關係的計算密集型複雜系統,但id希望在所有不同平臺上並行執行。虛擬紋理的管線如下所示:
id Tech 5的作業處理系統強調簡單性是可擴充套件性的關鍵,作業有明確的輸入和輸出,獨立無狀態、無停頓、始終完成,新增到作業列表的作業,具有多個作業列表,工作作業完全獨立,通過“訊號”和“同步”令牌簡單同步列表中的作業。
但是,同步意味著等待,等待破壞了並行性。架構決策:作業處理需要1幀延遲才能完成,作業結果遲到一幀,需要一些演算法操作(例如 葉子),排除一些演算法(例如透明度排序的螢幕空間分箱),但總的來說,不是一個糟糕的妥協。
id Tech 5作業化的子系統包含碰撞檢測、動畫混合、避障、虛擬紋理、透明度處理(樹葉、顆粒)、布料模擬、水面模擬、細節模型生成(岩石、鵝卵石等)等。
對於(GP) GPU 上的作業,沒有足夠的工作來填補SIMD / SIMT通道,不同工作的程式碼路徑分歧太大,作業作為工作單元很有用(延遲容忍和小記憶體佔用),需要利用工作中的資料並行性。將作業拆分為許多細粒度的執行緒,輸入中的資料依賴性,輸出資料的收斂,細粒度執行緒的記憶體訪問很重要。
A Novel Multithreaded Rendering System based on a Deferred Approach介紹了為多執行緒渲染而設計的渲染系統的架構,遵循延遲渲染方法的架構實現在雙核機器上顯示了65%的提升。
遊戲通常有25%到40%的幀時間用在D3D執行時和驅動程式,如果在渲染場景時引擎可以處理其它事情,那麼開銷就不會成為問題。但是,由於圖形系統需要共享資料在其工作時保持不變,其它引擎系統會被阻塞,因此,如果圖形系統是單執行緒的,則應用程式會浪費大量的CPU功率。該文討論的系統架構旨在利用所有CPU核心建立命令緩衝區,以儘快將共享資料的所有權歸還給其它系統,一旦建立了緩衝區,update系統就可以再次執行,而只有一個圖形執行緒仍將命令緩衝區提交給GPU。下圖說明了所描述的流程:
該文還對渲染狀態進行了封裝,以在不同平臺進行差異化實現:
圖形管理器是抽象層的中心類,負責初始化執行緒池,並隨後為它們提供來自應用程式的工作。對於建立的每個執行緒,都會例項化並分配一個上下文,這種所有權貫穿執行緒的整個生命週期。更改上下文的所有權是不可能的,因為不同的執行緒可能永遠不會呼叫相同的上下文。(下圖)
下圖顯示了渲染引擎使用多執行緒圖形管理器與常見的單執行緒解決方案獲得的每秒幀數。單執行緒結果由標題為ST的綠色條顯示,多執行緒的由標題為MT的紅色條表示。橫座標表示物體數量,豎座標表示幀數。
上圖顯示,在物體數量少的情況下,多執行緒的幀數反而有輕微的下降,但隨著物體數量增加,多執行緒的優勢凸顯,當物體達到2006個時,多執行緒的幀數是單執行緒的1.7倍。
Regressions in RT Rendering: LittleBigPlanet Post Mortem分享了遊戲LittleBigPlanet使用的技術,包含頂點動畫、配置檢測、Cluster、陰影、接觸AO、精靈光源、耶穌光、雙層透明、DOF、運動模糊、水體、流體等技術。
Cluster是無網格(mesh-less)演算法:最小二乘法將新的剛性矩陣擬合到變形的頂點雲:
- 移除質心 - 給出平移分量。
- \((\text{P} - \text{COM}) \ \cross \ (\text{P}_{\text{rest}} - \text{COM}_{\text{rest}})\) 的二元矩陣和。
- 乘以靜止姿勢中預先計算的矩陣的逆。
- 正交化得到的最佳擬合矩陣以提供仿射變換,並可選擇通過歸一化來保留體積。
- 與原始剛體矩陣混合,根據需要製作出柔軟/剛硬的形狀。
更多細節參見:Meshless Deformations Based on Shape Matching。
LittleBigPlanet為角色新增了一個AO光,可以分析計算建模為5個球體(2個腳、2 條腿、1個腹股溝)的角色的遮擋,具體推導見:sphere ambient occlusion - 2006。
接觸AO的原理(上)及開啟前後的效果對比(下)。
精靈光源效果。
雙層半透明效果。
水體網格。
Zen of Multicore Rendering分享了當時多核控制檯硬體的有效渲染技術的編譯和實踐。
該文提到,多核時代的處理能力相比上一代:70倍三角形吞吐量、450倍畫素填充率、390倍紋理速率、110倍頻寬、16倍視訊記憶體。
填充率是通過完全非同步的亂序VPU(向量處理單元)計算來實現的,在Larrabee上,每個核心有4個硬體執行緒,每個執行緒都亂序,但是對於一個執行緒的執行,頂點和畫素是同步的。所以基本上有256個亂序程式,每個都由一組大約16個同步的畫素或頂點組成,在任何時候都在執行中。預期著色器觸發器將增長最多,速度不是來自更高的時脈頻率,來自大量低功耗核心的速度,記憶體預計不會趕上著色器flops。ALU或VPU增加300倍,未來受紋理獲取限制而不是ALU,同構計算讓ALU或VPU忙於快取一致的本地資料。
多核的影響或應用有動態幾何的遮擋、動態可見性計算、空間變化的BRDF、高頻照明、高品質解析度、刪除剩餘的妥協等。實用技巧包含定向光照貼圖基礎、區域諧波、螢幕空間環境遮擋、陰影貼圖等。其中動態輻照亮度的計算流程如下:
上圖的小波輻射快取(Wavelet radiance cache)包含3個步驟:Haar wavelet基、可見性、輻射分解。對於Haar wavelet基,球面諧波不是可用於輻射傳輸的唯一基,輻射度和區域光的總和也可以用Haar小波表示。Haar小波的輻射可見的三重積分足夠快,可以在GPU上實時執行。
Haar Wavelet。
對於2D Haar小波和可見性,可見性函式 V(x, theta) 也是一個二元函式,將可見性乘以小波輻射是在空間和物理上開啟和關閉小波方程的一部分。小波輻射度和可見度乘積的積分也簡化了執行時間方程,在某些方面,球諧函式是定向光照圖中基的頻率校正分佈,帶狀諧波正確地取樣和儲存輻射貢獻,而不偏向於一個方向。
可以利用多核的不僅僅是陰影,還有完整的輻射照明模型,每個通道不是一盞燈,在tfetchCube中有效地取樣稀疏小波資料。在計算輻射度的過程中,需要使用主成分分析(Principal Components Analysis,PCA)來降維簡化計算,下圖是PCA之後的複雜輸入資料,PCA所做的是提取正交變數及其結果分佈:
14.3.3.3 移動生態
遊戲開發者雜誌Game Developer - October 2005中出現了基於手機的遊戲開發教程(下圖)。該雜誌提到,在手機遊戲中建立炫酷的3D效果比想象中容易。在索尼愛立信開發者世界,可以找到加快移 JavaTM 3D開發速度所需的一切,從技術文件到響應式技術支援,還有Mascot Capsule v3 和 JSR-184 (m3g) 的移動外掛,可以讓開發者使用最喜歡的工具(如3D Studio Max、Maya 和 Lightwave)。
隨著OpenGL ES的發展及移動裝置的完善,移動生態初見苗頭,而The Mobile 3D Ecosystem正是詳細全面地闡述了2007年移動平臺的技術和生態。該系列文中緊緊圍繞著OpenGL ES 1.x和2.0展開討論,描述了它的特點和應用。該文說到2007年將達到30億移動使用者,
到2009年,無線寬頻使用者將超過10億,到2010年,60億人口中的90%將擁有行動網路。
移動裝置的特點是功率是最終瓶頸,通常不插在牆上,只有電池,電池不遵循摩爾定律,每年僅5-10%的容量提升。Gene定律(Gene’s law)表明隨著時間的推移,積體電路的功耗呈指數級下降,意味著電池將持續更長時間。自1994年以來,執行IC所需的功率每2年下降10倍,但是2年前的效能還不夠,需要提高速度,並儘可能省電。另外的制約因素則是散熱和螢幕。
2005年之後的常見移動裝置(上)和畫面演變(下)。
當年的移動端圖形API的架構圖如下:
OpenGL ES 1.0的特點是保留OpenGL結構,消除不需要(冗餘/昂貴/未使用)的功能,保持緊湊和高效,小於50KB的佔用空間,無需硬體 FPU。另外,它推動創新,允許擴充套件,協調它們,與其它移動3D API (M3G / JSR-184) 保持一致,被Symbian OS、S60、Brew、PS3 / Cell architecture等系統支援。此時的渲染管線如下:
OpenGL ES 1.1新增了緩衝區物件、更好的紋理(大於2個紋理單位、組合 (+,-,interp)、dot3 凹凸、自動mipmap生成)、使用者裁剪平面、點精靈(粒子作為點而不是四邊形,隨著距離減小尺寸)、狀態查詢(啟用狀態儲存/恢復,適用於中介軟體)。下面是部分移動GPU的引數和特性:
當時的移動裝置型別和平臺繁多,表現在:
- CPU的速度和可用記憶體各不相同。電流範圍30Mhz到600MHz,ARM7至ARM11,無FPU。
- 不同的解析度。QCIF (176x144) 到VGA (640x480),在高階裝置上抗鋸齒,每通道顏色深度4-8 位 (12-32 bpp)。
- 可移植性問題。不同的CPU、作業系統、Java VM、C編譯器……
GPU的圖形功能包含:
- 通用多媒體硬體。純軟體渲染器(全部使用 CPU 和整數 ALU 完成),軟體 + DSP / WMMX / FPU / VFPU,多媒體加速器。
- 專用3D硬體。軟體T&L+硬體三角形設定/光柵化、全硬體加速。
- 效能:50K – 2M tris、1M – 100M畫素/秒。
OpenGL ES可以充當硬體抽象層,提供程式設計介面(API)和不同裝置的相同功能集以及統一的渲染模型(但無法保證效能)。
OpenGL ES 1.x渲染效果。
OpenGL ES使用Shader步驟。
移動遊戲Playman Winter Games – Mr. Goodliving的2d和3d畫面截圖。
下面兩個顯示了當時的普通遊戲開發過程和使用M3G框架的移動遊戲開發過程:
下圖是M3G框架在高中低端裝置的架構圖:
OpenGL ES 2.0釋出初期,遊戲開發者面臨的難題是需要在新硬體之前開發他們的遊戲引擎,OpenGL ES 2.0可能需要手持開發者顯著改變他們的引擎,基於著色器的API將更多負擔轉移到應用程式,通過可程式設計性實現更大的靈活性。但可以通過OpenGL ES 2.0 Emulator和Render Monkey(下圖)模擬渲染效果。
當時AMD的Sushi引擎率先支援了Open ES 2.0的整合,面臨的主要挑戰是設計一個引擎以針對具有不同功能集的多個API,設計基於著色器的引擎,平臺相容性,手持平臺功能差異很大,各類限制使便攜性成為挑戰。
上:2005年,Sushi對DX9功能集進行了抽象,使用擴充套件來支援OpenGL中缺少的功能;下:2007年,影像API多了幾種,選擇不再那麼容易,特別是如果將遊戲機新增到組合中……
當時的Sushi引擎抽象API由需求驅動:必須使用所有API的最新功能,公開最小公分母不是一種選擇,在每個API上執行相同的演示不是必需的,讓內容驅動功能集而不是API抽象。API抽象之後看起來很像DX10:資源、檢視、幾何著色器、流輸出、所有最新和最強大的功能......每個API實現都支援這些特徵的一個子集。API抽象還存在回退(fallback)路徑:引擎基於使用Lua的指令碼系統,Lua指令碼提供後備渲染路徑。對Sushi來說,權衡高階功能與內容可移植性是良好的折衷。
手持平臺有很多限制:沒有標準模板庫、沒有C++異常、手動清理堆疊、不完整的標準庫、有限的記憶體佔用、無浮點單元等。Sushi為了保障平臺可移植性,採用了標準抽象層(數學、I/O、記憶體、視窗等)、自定義模板類(列表、向量、對映表等)、限制使用C++(沒有異常和STL)。
14.3.4 渲染技術
本節將闡述20000時代誕生的部分重要渲染技術。
14.3.4.1 Spherical Harmonic
- SH基礎
Spherical Harmonic(SH)譯為球面諧波、球諧、球諧函式,定義了球體\(S\)上的正交基,類似於一維圓上的傅立葉變換。
球體\(S\)如果使用卡迪爾座標系和球面座標系的引數化公式分別如下所示:
球面基函式定義為:
其中\(l\)是階數(band index,另稱波段索引),\(m\)是階數內的索引(\(-l \le m \le l\)),\(P_{\ell }^{m}\)是相關的勒讓德(Legendre)多項式。假設\(K_l^m\)是如下形式的歸一化常數:
上述定義構成復基,通過簡單的變換得到一個實數基:
將\(K_l^m\)代入\(Y_{\ell }^{m}(\theta ,\varphi )\)之後,可得:
下圖是\(l=6\)時(前7階)的SH正交基的視覺化:
還存在半球諧函式:
- SH投影和重建
因為SH基是標準正交的,定義在球體\(S\)上的標量函式\(f\)可以通過積分投影到它的係數中:
有些論文用其它類似的形式和起點不一樣的\(i\)的投影公式(但本質是一樣的,只是表現形式不同):
這些係數提供了\(n\)階重建函式:
隨著階數\(n\)的增加,它越來越接近\(f\),低頻訊號只需幾個SH頻段即可準確表示,較高頻率的訊號通過低階投影進行階限(bandlimited,即平滑無走樣)。投影到\(n\)階涉及\(n^2\)個係數,根據投影係數和基函式的單索引向量重寫$\overset{\frown} {f} $通常很方便,通過:
其中\(i=l\ (l+1)+m+1\)。這個公式很明顯,在\(s\)處對重構函式的評估表示\(n^2\)分量係數向量\(f_i\)與評估基函式\(y_i(s)\)的向量的簡單點積。
SH投影例子。取一個由兩個面光源組成的函式,SH將它們投影到4個band=16個係數中。
對於低頻訊號(圖中是低頻光源),可以重建訊號,僅使用這些係數來找到原始訊號(光源)的低頻近似值。
SH訊號重建就是簡單地線性組合各個SH基函式和對應係數的乘積。
- SH性質
SH投影的一個關鍵特性是它的旋轉不變性, 也就是說,給定\(g(s)=f(Q(s))\),其中\(Q\)是\(S\)上的任意旋轉,然後滿足:
類似於一維傅立葉變換的平移不變性。實際上,此屬性意味著當來自\(f\)的樣本在一組旋轉的樣本點處收集時,SH投影不會導致走樣失真。
SH投影具有旋轉不變性。
SH基的正交性提供了有用的性質,即給定\(S\)上的任意兩個函式\(a\)和\(b\),它們的投影滿足:
換句話說,階限(bandlimited)函式乘積的積分簡化為它們的投影係數的點積。
將圓對稱核函式\(h(z)\)與函式\(f\)的卷積表示為\(h*f\)。注意,\(h\)必須是圓對稱的(因此可以定義為\(z\)而不是\(s\)的簡單函式),以便將結果定義在\(S\)而不是高維旋轉組\(SO(3)\)上。卷積的投影滿足:
換句話說,投影卷積的係數是單獨投影函式的簡單縮放乘積。注意,因為\(h\)是關於\(z\)的圓對稱,所以它的投影係數僅在\(m=0\)時是非零的。卷積特性提供了一種使用半球餘弦核對環境貼圖進行卷積的快速方法,定義為\(h(z)=max(z, 0)\),以獲得輻照度貼圖,其中\(h_l^0\)由解析給出公式。卷積特性還可用於生成具有較窄核心的預過濾環境圖。
一對球面函式\(c(s)=a(s)b(s)\)其中\(a\)已知和\(b\)未知的乘積的投影可以看作是投影係數\(b_j\)通過矩陣\(\widehat{a}\)的線性變換:
其中對重複的\(j\)和\(k\)索引隱含求和。注意\(\widehat{a}\)是一個對稱矩陣,\(\widehat{a}\)的分量可以通過使用從著名的Clebsch-Gordan系列推匯出的遞迴對基函式的三重積進行積分來計算。它也可以使用數值積分來計算,而無需事先對函式\(a\)進行SH投影。請注意,乘積的\(n\)階投影涉及兩個因子函式的係數,最高可達\(2n-1\)階。
球諧函式是球面上的一個帶符號的正交函式系統,由函式表示在球座標系中,可由球面座標或隱式的卡迪爾座標表示。
SH函式是基函式,基函式是可用於產生函式近似值的訊號片段:
我們可以使用這些係數來重建原始訊號的近似值:
對於SH光照,主要使用直接作用於係數本身的操作。
SH函式是正交基函式,是具有特殊性質的函式族,就像不重疊彼此足跡的函式,有點像傅立葉變換將函式分解為分量正弦波的方式。
- SH光照
SH是一種有效的方法來捕獲和顯示一個物件的表面上的全域性照明解決方案,常用於帶動態光照的靜態模型,渲染速度非常快,與光源的數量和大小無關,免費的高動態範圍光照,是漫反射照明的臨時替代品。
我們已知光照渲染方程如下:
假設有以下帶有光源、遮擋物的場景:
作為球面訊號來照明,則是如下情形:
H(s)和V(s)可以合併成傳輸函式T(s):
如果將光源和傳輸函式都表示為SH係數的向量,則光照積分如下:
可以通過係數之間的點積計算:
意味著:照明計算與光源的數量或大小無關,軟陰影比硬邊緣陰影開銷更小,傳輸函式可以離線計算。對於複雜的照明函式,可以用HDR光照探頭代替之,而不需要額外的成本:
對於次級光照(間接光),我們還可以利用被照亮的點來捕捉漫反射到漫反射的顏色溢位:
漫反射GI的優點:SH係數是在全域性照明解決方案中傳輸能量的完美方式,計算直接光照後,自傳輸不需要額外的光線追蹤。漫反射GI的缺點:假設所有遠處的點都有相同的照明函式(例如,物體的一半以上沒有陰影)。
由於SH假設光源在無限遠處,所以光源不能進入p點和阻擋器之間。
為了SH投影一個光照函式,先計算球面上隨機點的SH函式,再求和照明函式和SH值的乘積:
如果能保證樣本均勻分佈,就可以將權重移到總和之外:
渲染方程的簡化漫反射版本:
餘弦項對於物理校正渲染必不可少,可來自能量傳輸公式:
將渲染方程轉換成兩部分,就得到了要進行SH投影的傳輸函式,傳輸函式將反射率、表面法線和陰影編碼為一個函式,無需為每個頂點儲存表面法線:
下面的程式碼是一個SH前處理器,一個簡單的光線追蹤器,用於計算模型中每個頂點的SH係數:
for(int i=0; i<n_samples; ++i)
{
double H = DotProduct(sample[i].vec, normal);
if(H > 0.0)
{
if(!self_shadow(pos,sample[i].vec))
{
for(int j=0; j<n_coeff; ++j)
{
value = H * sample[i].coeff[j];
result[j] += albedo * value;
}
}
}
}
const double factor = 4.0*PI / n_samples;
for(i=0; i<n_coeff; ++i)
coeff[i] = result[i] * factor;
上面的光照追蹤存在一個問題,陰影測試通常會從模型內部發射光線,注意單邊射線-三角形相交,首選不帶孔洞的歧管模型(manifold model):
渲染畫面結果如下:
在此基礎上可以做得更好,即新增自傳輸。完整的渲染方程描述了照明表面如何相互照亮,產生顏色滲色:
自傳輸圖例如下,A點從B點接收與餘弦項成正比的光照:
可以用圖形表示為:
A點現在從上方接收光線,即使該方向在技術上是不可見的,使用SH照明,光照傳輸只是一系列乘法相加。渲染結果:
在執行時使用SH係數,根據實現的目標,有多種使用SH係數來重建影像的方法:單色燈或彩色燈,可重新著色的表面、固定顏色或自傳輸。
for(int j=0; j<n_coeff; ++j)
{
vertex[i].red += light[j] * vertex[i].sh_red[j];
vertex[i].green += light[j] * vertex[i].sh_green[j];
vertex[i].blue += light[j] * vertex[i].sh_blue[j];
}
照明函式的精度受階數影響,更多係數可以編碼更高頻率的訊號。
另外,可以使用SH照明作為照明計算的一部分,對天空球體使用SH光照並新增點光源和硬陰影來模擬太陽,對錶面反射的漫反射部分使用SH光照。建立SH光源的方法有:
-
來自極座標函式的數值。
-
光線追蹤多邊形模型或場景。
-
來自HDR光照探頭或環境貼圖。
-
直接來自解析解。例如圓盤光源的解決方案,角度t:
分析圓盤光源:在Maple或Mathematica中對球體上的此圓盤光函式進行符號積分,以找到解析表示式:
5階圓盤燈的25個係數中只有4個非零,最後使用SH旋轉來定位光源。
代理陰影是一種偽造物體間陰影的方法,如果場景中的每個物體都有自己的照明函式,可以使用分析“阻擋器”從另一個物體的方向減去光線。定義$b_t(s) $作為\(1-d_t(s)\)允許我們構建一個遮蔽SH係數的傳輸矩陣:
SH照明中的未解決問題:
- 更快的SH旋轉方法。借鑑計算化學研究。
- SH照明非靜態物體。當物體相對移動時,可見度函式V(s)會發生根本性的變化,如何編碼?
- 利用SH向量的稀疏性。SH向量通常包含很少的非零係數。
- 高光鏡面SH照明。一種編碼和使用任意BRDF的優雅方式,對於一般情況還是太慢。
總之,SH光照是一種用於照明3D模型的新技術,為實時遊戲帶來面光源和全域性照明,適用於任何可以進行Gouraud著色的平臺,可用作靜態場景中漫反射照明的替代品,2階陰影僅使用4個係數。
另外,基於SH的PRT(Precomputed Radiance Transfer,預計算輻射傳輸)支援漫反射自傳輸、光澤反射自傳輸,計算過程見下圖。
自傳輸執行時概覽。紅色表示SH係數的正值,藍色表示SH係數的負值。對於漫反射表面(頂行),SH照明係數(左側)乘以表面(中間)上的傳輸向量場以產生最終的結果(右)。表面上特定點的傳輸向量表示該表面如何響應該點的入射光,包括全域性傳輸效應,如自陰影和自反射。對於光滑表面(底行),在模型上的每個點(而不是向量)都有一個矩陣,該矩陣將照明係數轉換為表示傳輸輻射的球函式係數,結果與模型的BRDF核進行卷積,並在與檢視相關的反射方向R處進行評估,以在模型上的某一點產生光照結果。
附完整的PRT實現程式碼:
// 3D向量
struct Vector3
{
float x;
float y;
float z;
};
// 球體
struct Spherical
{
float theta;
float phi;
}
// 樣本
struct Sample
{
Spherical spherical_coord;
Vector3 cartesian_coord;
float* sh_functions;
};
// 取樣器
struct Sampler
{
Sample* samples;
int number_of_samples;
};
// 獲取樣本
void GenerateSamples(Sampler* sampler, int N)
{
Sample* samples = new Sample [N*N];
sampler->samples = samples;
sampler->number_of_samples = N*N;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
float a = ((float) i) + Random()) / (float) N;
float b = ((float) j) + Random()) / (float) N;
float theta = 2*acos(sqrt(1-a));
float phi = 2*PI*b;
float x = sin(theta)*cos(phi);
float y = sin(theta)*sin(phi);
float z = cos(theta);
int k = i*N + j;
sampler->samples[k].spherical_coord.theta = theta;
sampler->samples[k].spherical_coord.phi = phi;
sampler->samples[k].cartesian_coord.x = x;
sampler->samples[k].cartesian_coord.y = y;
sampler->samples[k].cartesian_coord.z = z;
sampler->samples[k].sh_functions = NULL;
}
}
};
// 獲取隨機數
float Random()
{
float random = (float) (rand() % 1000) / 1000.0f;
return(random);
}
// 勒讓德多項式
float Legendre(int l, int m, float x)
{
float result;
if (l == m+1)
result = x*(2*m + 1)*Legendre(m, m);
else if (l == m)
result = pow(-1, m)*DoubleFactorial(2*m–1)*pow((1–x*x), m/2);
else
result = (x*(2*l–1)*Legendre(l-1, m) - (l+m–1)*Legendre(l-2, m))/(l-m);
return(result);
}
float DoubleFactorial(int n)
{
if (n <= 1)
return(1);
else
return(n * DoubleFactorial(n-2));
}
// 計算球諧函式
float SphericalHarmonic(int l, int m, float theta, float phi)
{
float result;
if (m > 0)
result = sqrt(2) * K(l, m) * cos(m*phi) * Legendre(l, m, cos(theta));
else if (m < 0)
result = sqrt(2) * K(l, m) * sin(-m*phi) * Legendre(l, -m, cos(theta));
else
result = K(l, m) * Legendre(l, 0, cos(theta));
return(result);
}
// 計算歸一化係數K
float K(int l, int m)
{
float num = (2*l+1) * factorial(l-abs(m));
float denom = 4*PI * factorial(l+abs(m));
float result = sqrt(num/denom);
return(result);
}
// 預計算SH函式。
void PrecomputeSHFunctions(Sampler* sampler, int bands)
{
for (int i = 0; i < sampler->number_of_samples; i++)
{
float* sh_functions = new float [bands*bands];
sampler->samples[i].sh_functions = sh_functions;
float theta = sampler->samples[i].spherical_coord.theta;
float phi = sampler->samples[i].spherical_coord.phi;
for (int l = 0; l < bands; l++)
for (int m = -l; m <= l; m++)
{
int j = l*(l+1) + m;
sh_functions[j] = SphericalHarmonic(l, m, theta, phi);
}
}
}
}
// 顏色
struct Color
{
float r;
float g;
float b;
};
// 光照探針訪問
void LightProbeAccess(Color* color, Image* image, Vector3 direction)
{
float d = sqrt(direction.x*direction.x + direction.y*direction.y);
float r = (d == 0) ? 0.0f : (1.0f/PI/2.0f) * acos(direction.z) / d;
float tex_coord [2];
tex_coord[0] = 0.5f + direction.x * r;
tex_coord[1] = 0.5f + direction.y * r;
int pixel_coord [2];
pixel_coord[0] = tex_coord[0] * image.width;
pixel_coord[1] = tex_coord[1] * image.height;
int pixel_index = pixel_coord[1]*image.width + pixel_coord[0];
color->r = image.pixel[pixel_index][0];
color->g = image.pixel[pixel_index][1];
color->b = image.pixel[pixel_index][2];
}
// 投影光照函式
void ProjectLightFunction(Color* coeffs, Sampler* sampler, Image* light, int bands)
{
for (int i = 0; i < bands*bands; i++)
{
coeffs[i].r = 0.0f;
coeffs[i].g = 0.0f;
coeffs[i].b = 0.0f;
}
for (int i = 0; i < sampler->number_of_samples; i++)
{
Vector3& direction = sampler->samples[i].cartesian_coord;
for (int j = 0; j < bands*bands; i++)
{
Color color;
LightProbeAccess(&color, light, &direction);
float sh_function = sampler->samples[i].sh_functions[j];
coeffs[j].r += (color.r * sh_function);
coeffs[j].g += (color.g * sh_function);
coeffs[j].b += (color.b * sh_function);
}
}
float weight = 4.0f*PI;
float scale = weight / sampler->number_of_samples;
for (int i = 0; i < bands*bands; i++)
{
coeffs[i].r *= scale;
coeffs[i].g *= scale;
coeffs[i].b *= scale;
}
}
// 三角形
struct Triangle
{
int a;
int b;
int c;
};
// 場景
struct Scene
{
Vector3* vertices;
Vector3* normals;
int* material;
Triangle* triangles;
Color* albedo;
int number_of_vertices;
};
// 投影無陰影的場景
void ProjectUnshadowed(Color** coeffs, Sampler* sampler, Scene* scene, int bands)
{
for (int i = 0; i < scene->number_of_vertices; i++)
{
for (int j = 0; j < bands*bands; j++)
{
coeffs[i][j].r = 0.0f;
coeffs[i][j].g = 0.0f;
coeffs[i][j].b = 0.0f;
}
}
for (int i = 0; i < scene->number_of_vertices; i++)
{
for (int j = 0; j < sampler->number_of_samples; j++)
{
Sample& sample = sampler->samples[j];
float cosine_term = dot(&scene->normals[i], &sample.cartesian_coord);
for (int k = 0; k < bands*bands; k++)
{
float sh_function = sample.sh_functions[k];
int materia_idx = scene->material[i];
Color& albedo = scene->albedo[materia_idx];
coeffs[i][k].r += (albedo.r * sh_function * cosine_term);
coeffs[i][k].g += (albedo.g * sh_function * cosine_term);
coeffs[i][k].b += (albedo.b * sh_function * cosine_term);
}
}
}
float weight = 4.0f*PI;
float scale = weight / sampler->number_of_samples;
for (int i = 0; i < scene->number_of_vertices; i++)
{
for (int j = 0; j < bands*bands; j++)
{
coeffs[i][j].r *= scale;
coeffs[i][j].g *= scale;
coeffs[i][j].b *= scale;
}
}
}
// 射線和三角形相交測試
bool RayIntersectsTriangle(Vector3* p, Vector3* d, Vector3* v0, Vector3* v1, Vector3* v2)
{
float e1 [3] = { v1->x – v0->x, v1->y – v0->y, v1->z – v0->z };
float e2 [3] = { v2->x – v0->x, v2->y – v0->y, v2->z – v0->z };
float h [3];
cross(h, d, e2);
float a = dot(e1, h);
if (a > -0.00001f && a < 0.00001f)
return(false);
float f = 1.0f / a;
float s [3] = { p->x – v0->x, p->y – v0->y, p->z – v0->z };
float u = f * dot(s, h);
if (u < 0.0f || u > 1.0f)
return(false);
float q [3];
cross(q, s, e1);
float v = f * dot(d, q);
if (v < 0.0f || u + v > 1.0f)
return(false);
float t = dot(e2, q)*f;
if (t < 0.0f)
return(false);
return(true);
}
// 可見性函式
bool Visibility(Scene* scene, int vertexidx, Vector3* direction)
{
bool visible (true);
Vector3& p = scene->vertices[vertexidx];
for (int i = 0; i < scene->number_of_triangles; i++)
{
Triangle& t = scene->triangles[i];
if ((vertexidx != t.a) && (vertexidx != t.b) && (vertexidx != t.c))
{
Vector3& v0 = scene->vertices[t.a];
Vector3& v1 = scene->vertices[t.b];
Vector3& v2 = scene->vertices[t.c];
visible = !RayIntersectsTriangle(&p, direction, &v0, &v1, &v2);
if (!visible)
break;
}
}
return(visible);
}
// 投影有陰影的場景
void ProjectShadowed(Color** coeffs, Sampler* sampler, Scene* scene, int bands)
{
...
for (int i = 0; i < scene->number_of_vertices; i++)
{
for (int j = 0; j < sampler->number_of_samples; j++)
{
Sample& sample = sampler->samples[j];
if (Visibility(scene, i, &sample.cartesian_coord))
{
float cosine_term = dot(&scene->normals[i], &sample.cartesian_coord);
for (int k = 0; k < bands*bands; k++)
{
float sh_function = sample.sh_functions[k];
int materia_idx = scene->material[i];
Color& albedo = scene->albedo[materia_idx];
coeffs[i][k].r += (albedo.r * sh_function * cosine_term);
coeffs[i][k].g += (albedo.g * sh_function * cosine_term);
coeffs[i][k].b += (albedo.b * sh_function * cosine_term);
}
}
}
}
...
}
// 渲染入口
void Render(Color* light, Color** coeffs, Scene* scene, int bands)
{
glBegin(GL_TRIANGLES);
for (int i = 0; i < scene->number_of_triangles; i++)
{
Triangle& t = &scene->triangles[i];
Vector3& v0 = scene->vertices[t.a];
Vector3& v1 = scene->vertices[t.b];
Vector3& v2 = scene->vertices[t.c];
Color c0 = { 0.0f, 0.0f, 0.0f };
Color c1 = { 0.0f, 0.0f, 0.0f };
Color c2 = { 0.0f, 0.0f, 0.0f };
for (int k = 0; k < bands*bands; k++)
{
c0.r += (light[k].r * coeffs[t->a][k].r);
c0.g += (light[k].g * coeffs[t->a][k].g);
c0.b += (light[k].b * coeffs[t->a][k].b);
c1.r += (light[k].r * coeffs[t->b][k].r);
c1.g += (light[k].g * coeffs[t->b][k].g);
c1.b += (light[k].b * coeffs[t->b][k].b);
c2.r += (light[k].r * coeffs[t->c][k].r);
c2.g += (light[k].g * coeffs[t->c][k].g);
c2.b += (light[k].b * coeffs[t->c][k].b);
}
glColor3f(c0.r, c0.g, c0.b);
glVertex3f(v0.x, v0.y, v0.z);
glColor3f(c1.r, c1.g, c1.b);
glVertex3f(v1.x, v1.y, v1.z);
glColor3f(c2.r, c2.g, c2.b);
glVertex3f(v2.x, v2.y, v2.z);
}
glEnd();
}
附PRT的shader程式碼:
// vertex shader
struct app2vertex
{
float4 f4Position : POSITION;
float4 f4Color : COLOR;
float3 vf3Transfer [N*N];
};
struct vertex2fragment
{
float4 f4ProjPos : POSITION;
float4 f4Color : COLOR;
};
vertex2fragment VertexShader
(
app2vertex IN,
uniform float3 vf3Light [N*N],
uniform float4x4 mxModelViewProj
)
{
vertex2fragment OUT;
OUT.f4ProjPos = mul(mxModelViewProj, IN.f4Position);
OUT.f4Color = float4(0.0f, 0.0f, 0.0f, 1.0f);
for (int i = 0; i < N*N; i++)
{
OUT.f4Color.r += (IN.vf3Transfer[i].r * vf3Light[i].r);
OUT.f4Color.g += (IN.vf3Transfer[i].g * vf3Light[i].g);
OUT.f4Color.b += (IN.vf3Transfer[i].b * vf3Light[i].b);
}
return(OUT);
}
// fragment shader
struct fragment2screen
{
float4 f4Color : COLOR;
};
vertex2fragment PixelShader( vertex2fragment IN )
{
fragment2screen OUT;
OUT.f4Color = IN.f4Color;
return(OUT);
}
14.3.4.2 Virtual Texture
Virtual Texture(VT,虛擬紋理)是一個mipmap紋理,用作快取,模擬更高解析度的紋理以進行實時渲染,同時僅部分駐留在紋理記憶體中。快取是一種常見的技術,允許快速訪問較大的資料集以駐留在較慢的記憶體中,此處描述的虛擬紋理使用傳統的紋理對映快取來自較慢內容裝置的資料。下圖顯示了紋理儲存的不同硬體裝置如何以不同的速度/數量比進行分類。
硬體可以根據速度/數量比進行分類,容量從高到低依次是外圍裝置、記憶體、視訊記憶體,但速度依次提升。GPU紋理查詢操作僅限於一個紋理,且無法隨機訪問整個視訊記憶體,因為受限於size3或高延遲。因此,圖中的圖表將“紋理”和“視訊記憶體”列為單獨的單元。
由於2000時代中後期的遊戲場景、包體和紋理尺寸越來越大(8K),32位的記憶體地址無法完全容納遊戲的所有資料,並且載入完整的資料到記憶體或視訊記憶體不切實際,因為IO頻寬是最大的瓶頸。有限的實體記憶體可以通過使用硬碟驅動器的虛擬記憶體來補償,不幸的是,這個選項對於實時渲染並不可行,因為傳統的虛擬記憶體(作為作業系統和硬體功能)會阻塞,直到請求得到解決。如果沒有可用的硬碟驅動器,這種情況會加劇,遊戲機可能就是這種情況。為了解決這個問題並獲得快速的關卡載入時間,現代引擎需要進行紋理流式傳輸。
在虛擬紋理中,只將紋理的相關部分保留在快速記憶體中,並從較慢的記憶體中非同步請求丟失的部分(同時使用較低mip-map的內容作為後備),意味著需要將較低的mip-map保留在記憶體中,並且需要先載入這些部分。為了可以高效地查詢紋理,將mip-map的紋理分割成合理大小的小塊來實現連貫的記憶體訪問。所有塊(tile)使用固定大小,可以更輕鬆地管理快取,並且可以更輕鬆地管理所有tile操作(如讀取或複製),具有恆定的時間和記憶體特性。CryTek的實現不處理小於tile大小的mip-map,對地形渲染之類的應用程式,它們的紋理永遠不會很遠並且少量鋸齒是可以接受的。可以通過簡單地將多個mip-map打包到一個tile中來解決此問題,對於遠處的物體,甚至可以回退到正常的mip-map紋理對映,但需要一個單獨的系統來管理。
如下圖所示,典型的虛擬紋理是在預處理時從某種源影像格式建立的,而不會對紋理大小施加API和圖形硬體限制。資料儲存在任何訪問速度較低的裝置上(如硬碟驅動器),可以刪除虛擬紋理的未使用區域以節省記憶體。視訊記憶體中的紋理(tile cache)由渲染3D檢視所需的tile組成,還需要間接資訊以有效地重建虛擬紋理佈局,tile紋理快取和間接紋理(indirection texture)都是動態的並且適應一個或多個檢視。
虛擬紋理的執行機制,包含了儲存於慢速裝置(如硬碟)的分成固定大小的紋理、位於視訊記憶體的分塊紋理快取,以及關聯了檢視和分塊紋理快取的間接紋理。
CryTek使用四叉樹來管理虛擬紋理,保證所有必需的操作都可以在恆定時間內實現。樹的狀態代表虛擬紋理當前使用的紋理tile,所有節點和葉子都與紋理tile相關聯,在基本實現中,僅使用四叉樹中的最高可用解析度。當丟棄一些葉子時,較低解析度的資料被儲存為fallback,也可以用於逐漸過渡到更高解析度的紋理tile,僅在葉子級別細化或粗化虛擬紋理。除了四叉樹之外,還需要額外實現快取策略(例如最近最少使用)。
畫素著色器中的四叉樹遍歷被更高效的單個未過濾的紋理查詢所取代,間接紋理在記憶體中可能非常小,並且由於連貫的紋理查詢,對頻寬也非常友好。單個紋理查詢允許在恆定時間內使用簡單的數學計算tile快取中的紋理座標。可以在畫素著色器中使用以下HLSL程式碼為給定的虛擬紋理座標計算關聯的tile快取紋理座標:
float4 g_vIndir; // 非直接紋理邊界:w, h, 1/w, 1/h
float4 g_Cache; // tile紋理快取邊界:w, h, 1/w, 1/h
float4 g_CacheMulTilesize; // tile紋理快取邊界*tilesize:w, h, 1/w, 1/h
// 取樣器(最近點)
sampler IndirMap = sampler_state
{
Texture = <IndirTexture>;
MipFilter = POINT;
MinFilter = POINT;
MagFilter = POINT;
// MIPMAPLODBIAS = 7; // using mip-mapped indirection texture, 7 for 128x128
};
// 為給定的虛擬紋理座標計算關聯的tile快取紋理座標
float2 AdjustTexCoordforAT( float2 vTexIn )
{
float fHalf = 0.5f; // half texel for DX9, 0 for DX10
float2 TileIntFrac = vTexIn*g_vIndir.xy;
float2 TileFrac = frac(TileIntFrac)*g_vIndir.zw;
float2 TileInt = vTexIn - TileFrac;
float4 vTiledTextureData = tex2D(IndirMap,TileInt+fHalf*g_vIndir.zw);
float2 vScale = vTiledTextureData.bb;
float2 vOffset = vTiledTextureData.rg;
float2 vWithinTile = frac( TileIntFrac * vScale );
return vOffset + vWithinTile*g_CacheMulTilesize.zw + fHalf*g_Cache.zw;
}
如前所述,假設只有單個間接紋理並且tile快取中的所有tile具有相同的大小,則可以計算虛擬紋理解析度:
下面舉幾個例子:
注意,為了避免使用DXT等塊狀壓縮紋理的雙線性過濾瑕疵,需要額外的4畫素邊框。由於有損的DXT壓縮,在壓縮相鄰tile時需要完全相同的塊內容,否則可能會重建出錯誤的顏色值,從而導致可見的接縫。對於tile快取紋理的更新,要求如下:
- 低延遲和高吞吐量。
- 頻寬高效(僅複製所需部分)。
- 記憶體開銷小。
- 更新不能有卡頓,且正確同步紋理狀態。
- 當通過CPU更新內容時,不應從GPU記憶體複製到CPU記憶體(應使用丟棄)。
- 對於從tile快取紋理進行快速紋理化,應該在適當的記憶體佈局 (swizzled12) 和記憶體型別(視訊記憶體)中。 注意,在某些硬體上壓縮紋理以線性形式儲存(沒有swizzle)。
- 多個tile更新應該具有線性或更好的效能。
當時有3種方法可實現tile快取紋理更新:
- 方法1:直接更新CPU。目標紋理需要在D3DPOOL_MANGED中,並通過LockRect() 函式更新紋理的一部分。這種方法會浪費主記憶體,並且很可能會延遲傳輸,直到繪製呼叫正在使用紋理。很簡單,但不理想。
- 方法2:使用(小)中間tile紋理。此方法需要一個可鎖定的中間紋理帽子以容納一個tile(包括邊框),使用LockRect()或StretchRect() 函式以複製內容。此法不適用於壓縮紋理 (DXT),因為它們不能用作渲染目標格式,並且受圖形API和硬體特性影響。
- 方法 3:使用(大)中間tile快取紋理。此方法需要在D3DPOOL_SYSTEM中具有完整紋理快取擴充套件的可鎖定中間紋理。使用 LockRect(),中間紋理僅在需要時更新,隨後的UpdateTexture()函式呼叫將資料傳輸到目標紋理,UpdateTexture()要求目的地位於D3DPOOL_DEFAULT中。
一旦新的tile更新到紋理快取中,就可以更新間接紋理。間接紋理需要很少的記憶體,因此頻寬不是問題,但使用多個間接紋理並對其進行更新通常會成為效能瓶頸,可以通過鎖定或上傳新紋理從CPU更新紋理。如果間接紋理使用渲染目標紋理格式,還可以考慮由CPU觸發的GPU更新,通過繪製呼叫來完成更新,並且可以將簡單的四邊形渲染到紋理以有效地更新大區域。不應該有太多更新,因為tile快取更新且兩者相互依賴的情況應該很少見。
在CryTek的實現中,間接紋理仍然有一個通道,並且可以儲存一個tile混合值。使用額外的著色器成本,允許通過緩慢混合tile來隱藏紋理tile替換。帶過濾的混合值更佳,因為可以隱藏tile之間的接縫。
Tile資源的來源有:從磁碟流式傳輸、通過網路流式傳輸、程式內容生成。其中程式內容生成可以發生在CPU或GPU,典型代表是已在許多遊戲的地形中使用的巨大紋理生成,地形細節通常由一些tile紋理組成,這些紋理以低得多的解析度與插值資訊混合,可以產生良好的效果,尤其是在與低解析度紋理結合以增加變化並突破tile外觀時。
Crysis使用地形材質混合(下圖)來獲得巨大地形的詳細地面紋理,此法需要在畫素級別混合多種材質。由於每個地形頂點都分配給一種材質(最多3個,因此在三角形內混合最多需要三個通道)。在離線過程中烘焙地表紋理允許更復雜的混合並保持渲染效能不變,也允許烘焙諸如道路或輪胎痕跡之類的細節。
使用虛擬紋理受益的場景示例:遊戲Crysis中的貼花(道路、輪胎痕跡、泥土)在地形材料混合之上使用。
和CryTek略有不同,id Tech 5使用了稀疏紋理金字塔的四叉樹(下圖)來儲存、管理、優化虛擬紋理。
id Tech 5使用稀疏紋理金字塔的四叉樹管理虛擬紋理。
虛擬紋理視覺化。
id Tech 5在實現時關注了以下虛擬紋理的問題:
-
紋理過濾。不嘗試過濾,嘗試了無邊界雙線性過濾,帶邊框的雙線性過濾效果很好,三線性濾波合理但仍然昂貴,可通過TXD (texgrad) 進行各向異性過濾,需要4-texel邊框(最大aniso=4),帶有隱式梯度的TEX也可以(在某些硬體上)。
-
由於實體記憶體超出而導致崩潰。有時需要的物理頁面要多於擁有的,如果使用傳統的虛擬記憶體,無法達成。使用虛擬紋理,可以全域性調整回饋的LOD偏差,直到工作集適合。
-
高延遲下的LOD過渡。首次需要和可用性之間的延遲可能很高,特別是如果需要讀取光碟,大於100毫秒的查詢。放大的紋理更改LOD時會發生可見的跳變,如果使用三線性過濾,細節混合會很容易,但可以使用混合資料持續更新物理頁面。
立即對粗糙頁面進行上取樣,然後在可用時融合更精細的資料。
對於虛擬紋理管理,回饋資訊分析告知需要哪些頁面,由於是實時應用程式,所以不允許阻止。快取處理命中、排程未命中以在後臺載入,獨立於磁碟快取管理的常駐頁面,物理頁面組織為每個虛擬紋理的四叉樹,免費、LRU 和鎖定頁面的連結串列。虛擬紋理的回饋分析時,生成相當於帶優先順序的廣度優先四叉樹順序:
虛擬記憶體的轉碼包含漫反射、鏡面反射、凹凸和覆蓋/alpha等資料,儲存在凹凸紋理中的高光塊縮放,通常達到2-6k的輸入和40k的輸出,Map、Unmap和Transcode都在可以直接寫入紋理記憶體的平臺上並行發生。下圖是轉碼流水線到塊或行級別以減少記憶體配置檔案:
具有依賴關係的計算密集型複雜系統,但id希望在所有不同平臺上並行執行。虛擬紋理的管線如下所示:
id Tech 5在id Tech 5中,作業化的子系統包含了虛擬紋理,以便重複利用多核優勢,提升虛擬紋理的吞吐量。
14.3.4.3 Tone Reproduction
Tone Reproduction and Physically Based Spectral Rendering文獻解決了渲染管道兩端的兩個相關關鍵問題領域,即在實際渲染過程中用於描述光的資料結構,以及以有價值的方式顯示此類輻射強度的問題。
第一個子問題的興趣來源於使用RGB顏色值來描述光強度和表面反射率是常見的行業慣例。雖然在不努力實現真正現實主義的方法的背景下是可行的,但如果要預測自然,則必須用更精確的物理技術代替這種方法。
第二個子問題是,雖然對渲染影像方法的研究為我們提供了更好更快的方法,但由於顯示硬體的限制,我們不一定能看到它們的全部效果。標準計算機顯示器的低動態範圍需要某種形式的對映來生成感知準確的影像,色調再現操作試圖複製真實世界亮度強度的效果。
該文獻還回顧了關於光譜渲染和色調再現技術的工作,包括對光譜影像合成方法和準確色調再現的需求的調查,以及對物理正確渲染和關鍵色調對映演算法的主要方法的討論,未來將考慮光譜渲染和色調再現技術,以及顯示硬體進步的影響。
雖然對建立影像方法的研究為我們提供了更好更快的方法,但由於顯示限制,我們通常看不到這些技術的全部效果。為了準確的影像分析和與現實的比較,顯示影像必須與原始影像儘可能接近。在需要預測成像的情況下,色調再現對於確保從模擬中得出的結論是正確的非常重要(下圖)。
理想的色調再現處理過程。
該文還總結出了以往的色調再現方法的分類圖:
色調再現方法圖譜。橫座標是時間無關和時間相關,縱座標是空間均勻和空間可變。
由此可知,目前廣泛流行的色調對映技術早在幾十年前就已有很多前人開始了相關的研究,為後續色調再現和色調對映的應用和普及奠定了基礎。
14.3.4.4 Progressive Buffer
漸進式緩衝(Progressive Buffer,PB)用於渲染大型多邊形模型的資料結構和系統,支援紋理、法線貼圖,支援LOD之間的平滑過渡(無跳變)。Progressive Buffer需要對模型進行預處理,將模型拆分為Cluster,引數化Cluster和樣本紋理,為不同的LOD建立多個(例如5個)靜態頂點/索引緩衝區,每個緩衝區都有其父節點的1/4,通過從一個LOD一次簡化每個圖表(chart)來實現這一點,一直到下一個,簡化了邊界頂點到它的鄰居,簡化遵循邊界約束並防止紋理翻轉,還可以對每個緩衝區執行頂點快取優化。
Progressive Buffer示例:從紅色(最高解析度)到綠色(最低解析度)的五級細節顏色編碼。
為了解決取樣不足,對網格的Cluster需要進行紋理引數化,生成紋理座標的分層演算法。(下圖)
在處理粗糙Cluster時,需要對上一級的精細曲線邊界進行直線化,以解決Cluster失真:
PB涉及的紋理打包計算,可參考Tetris packing [Levy 02]和Multi-Chart Geometry Images,為了儘量減少浪費的空間(下圖黑色),一次放置一個圖表(從大到小),選擇最佳位置和旋轉(最大限度地減少浪費的空間),對多個正方形尺寸重複上述操作,挑選最佳佈局。
A:Tetris packing打包演算法將圖表一張一張插入,並在此過程中保持“水平”(藍色),每個圖表(綠色)都插入到最小化其底部水平線(粉紅色)和當前水平線之間的“浪費空間”(黑色)的位置,然後使用當前圖表的頂部水平線(紅色)來更新水平線。B:樣例模型(恐龍)資料集的結果。
PB的每個靜態緩衝區將包含一個索引緩衝區和兩個頂點緩衝區:
-
精細頂點緩衝區。表示當前LOD中的頂點。
-
粗糙頂點緩衝區。頂點與精細緩衝區對齊,這樣每個頂點對應於下一個粗糙LOD中精細緩衝區的“父”頂點(注:需要頂點複製)。
PB的各級LOD層次圖例如下:
執行時,靜態緩衝區流式傳輸到頂點著色器(LOD根據Cluster到相機的中心距離確定),頂點著色器平滑地混合位置、法線和UV(混合權重基於到相機的頂點距離)。
結合下圖加以說明緩衝區過渡。若降低LOD,橙色的\(PB_i\)過渡到對應的黃色,然後交換\(PB_i\)和\(PB_{i-1}\),黃色的\(PB_{i-1}\)過渡到對應的綠色。若增加LOD,則反向操作之。
下圖顯示了LOD的選擇機制和過程,以及涉及的各個概念和符號:
上圖詳細展示了PB的LOD選擇的機制、原理和過程。其中橫向座標從左到右表示距攝像機的距離逐漸增加,縱座標從下往上表示LOD的級別依次增加。\(S\)表示使用物體最高LOD的距離,\(r\)表示物體包圍盒的半徑,\(e\)表示幾何過渡的距離,\(K\)表示每個LOD對應的距離範圍(隨著LOD的降低而翻倍,如K、2K、4K等等),中間的梯級往下的曲面表示了LOD之間的平滑過渡,隱藏了跳變。這種幾何過渡方式和CSM比較類似。LOD的級數和權重計算如下圖所示:
上圖中,\(d\)表示物體和相機的距離,\(i\)表示LOD級數,\(d_e\)表示當前降低LOD的幾何過渡的遠端,\(d_s\)表示當前降低LOD的幾何過渡的近端。
紋理的LOD類似於頂點LOD,每個細節層次也有紋理,每個較粗的LOD有前一個LOD的1/4的頂點數和1/4的紋素數。本質上,在粗化時刪除了最高mip級別,並在細化時新增了一個mip級別。紋理像頂點一樣混合:頂點變形權重傳遞給畫素著色器,畫素著色器執行兩次提取(每個LOD一次),畫素著色器根據插值權重混合生成的顏色。
粗糙緩衝區層次結構(Coarse buffer hierarchy,CBH)將所有Cluster的粗糙LOD儲存在視訊記憶體中的單個頂點/索引/紋理緩衝區中,當相鄰Cluster遠離相機時進行分組繪製呼叫。
處理CBH紋理時,最粗糙LOD的體素紋理被分組:
CBH紋理始終儲存在視訊記憶體中,調整了CBH緩衝區中的紋理座標,從粗糙靜態緩衝區切換到CBH緩衝區時沒有可見的跳變。
資料結構的限制:頂點緩衝區大小加倍(但只有一小部分資料駐留在視訊記憶體中),Cluster大小應大致相同(大型Cluster會限制最小LOD級數大小),比純粹的分層演算法有更多的繪圖呼叫(不能在同一個繪圖呼叫中切換紋理;粗略層次結構部分解決了這個問題),直線邊界導致紋理拉伸。
PB受系統記憶體、視訊記憶體、幀率(不太穩定)、最大級數大小等限制,\(k\)和\(s\)的值會相應地慢慢調整以保持在上述限制內(即自動LOD控制):
對於記憶體管理,使用單獨的執行緒載入資料,並根據到相機的距離設定優先順序如下:
然後計算每個緩衝區的連續LOD,取整數部分,可以得到靜態緩衝區,併為其分配優先順序3:
如果連續LOD在另一個靜態緩衝區LOD的指定閾值內,則相應地設定該緩衝區的優先順序:
通過預取並保留大約20%的額外資料,而不是正在渲染的資料,可以確保擁有渲染所需的適當Cluster的LOD,如果沒有預取,幾個緩衝區可能會變得不可用。根據硬碟尋道時間、後臺任務、其它CPU使用情況,可能會有很大差異。
下圖的統計資訊顯示,可變LOD比固定LOD在FPS和記憶體方面具有更穩定的表現。
14.3.4.5 其它
2000時代湧現的技術枚不勝數,上面只是取其中的一小部分比較重要的加以說明。此外,諸如延遲著色及變種、SSAO、LPV等光影技術,以及各種後處理、多層材質、各類紋理對映等特殊渲染技術都已經出現並應用到了遊戲引擎和發行的遊戲中。例如,YARE引擎提到了很多基礎渲染技術的實現,包含點光源、聚光燈、定向光,以及紋理對映、發現對映、平行對映(Parallax Mapping)、浮雕對映(Relief Mapping)、位移對映(Displacement Mapping)、渲染到立方體圖(Render to Cubemap)、動態立方體圖等,還有部分Bloom等後處理效果。
YARE引擎實現Bloom效果的通道。從左到右:原始影像、下采樣影像、水平模糊影像、完全模糊影像、最終影像。
Half Life使用了紋理根據場景變化而變化的技術。圖中以眼睛為例,可以看到五種不同的眼睛效果。
14.3.5 成長期總結
2000時代渲染引擎的發展總結起來如下:
- 視覺效果的提升。
- 圖形API和硬體的發展。
- 多執行緒化,併發技術。
- 跨平臺。
- 引擎功能愈來愈多、複雜,模組快速增長。
- 本篇未完待續。
特別說明
- 感謝所有參考文獻的作者,部分圖片來自參考文獻和網路,侵刪。
- 本系列文章為筆者原創,只發表在部落格園上,歡迎分享本文連結,但未經同意,不允許轉載!
- 系列文章,未完待續,完整目錄請戳內容綱目。
- 系列文章,未完待續,完整目錄請戳內容綱目。
- 系列文章,未完待續,完整目錄請戳內容綱目。
參考文獻
- Unreal Engine Source
- Rendering and Graphics
- Materials
- Graphics Programming
- Unreal Engine 2 Documentation
- Unreal Engine 3 Documentation
- Game engine
- List of game engines
- Game Engine Architecture
- Game Engine Architecture Third Edition
- Game Engine Architecture Course
- Welcome to CS1950u!
- Game Engine Fundamentals
- Designing the Framework of a Parallel Game Engine
- 深入GPU硬體架構及執行機制
- 由淺入深學習PBR的原理和實現
- Playstation 2 Architecture
- Procedural Rendering on Playstation® 2
- Independent Game Development
- CryEngine Sandbox Far Cry Edition
- Information Visualisation utilising 3D Computer Game Engines
- Using Commonsense Reasoning in Video Games
- GeForce 6 series
- List of game engines
- 3D Game Engine Architecture
- 《黑神話:悟空》12分鐘UE5實機測試集錦
- Unreal Engine
- 虛幻引擎
- FROM ABSTRACT DATA MAPPING TO 3D PHOTOREALISM: UNDERSTANDING EMERGING INTERSECTIONS IN VISUALISATION PRACTICES AND TECHNIQUES
- Game Developer - October 2005
- Torque Tutorial #2
- Game Engine Tutorials:Chapter 1 - Introduction
- Unreal Engine 3 Technology
- Using the Source Engine for Serious Games
- Designing a Modern Rendering Engine
- Finding Next Gen – CryEngine 2
- CryENGINE 3 Game Development Beginner's Guide
- GPU overview Graphics and Game Engines
- 3dfx Interactive
- RIVA TNT
- DirectX
- Direct3D feature levels
- Feature levels in Direct3D
- High-Level Shader Language
- DirectX 3.0
- Microsoft Ships DirectX Version 3.0
- Microsoft Ships DirectX 5.0
- DirectX 5.0
- Microsoft's Official DirectX 6.0
- Multitexturing in DirectX 6
- Microsoft® DirectX® 7: What's New for Graphics
- GLFW
- OpenGL Related toolkits and APIs
- Nurien Mstar Online - (4minute - Huh) Bubble Item Mode
- GDM_April_2008(Game Developer)
- How To Go From PC to Cross Platform Development Without Killing Your Studio
- “A bit more Deferred” - CryEngine 3
- CryEngine
- Far Cry®
- CryENGINE 2
- Crysis 1 XBOX/PS3
- CryENGINE 3
- CryEngine Features
- The Matrix Awakens: An Unreal Engine 5 Experience
- Hitting 60Hz with the Unreal Engine: Inside the Tech of Mortal Kombat vs DC Universe
- Parallel Graphics in Frostbite – Current & Future
- Frostbite (game engine)
- EA Tech Blog
- irrlicht
- Practical techniques for managing Code and Assets in multiple cross platform titles
- Next-Gen Asset Streaming Using Runtime Statistics
- Light Pre-Pass Deferred Lighting: Latest Development
- The Next Mainstream Programming Language: A Game Developer’s Perspective
- An explanation of Doom 3’s repository-style architecture
- TerraForm3D Plasma Works 3D Engine & USGS Terrain Modeler
- Entity component system
- Entity Component Systems in Unity
- Entity Component System VS Entity Component
- Entities, components and systems
- Redefining Game Engine Architecture through Concurrency
- Tim Sweeney on the first version of the Unreal Editor
- Integrating Architecture Soar Workshop
- Ray Tracing for Games
- Modern Graphics Engine Design
- Ogre Golf
- Selecting the Best Open Source 3D Games Engines
- Theory, Design, and Preparation
- The Benefits of Multiple CPU Cores in Mobile Devices
- GAME ENGINES
- Development of a 3D Game Engine - OpenCommons
- Physically Based Shading Models for Film and Game Production
- Physically Based Shading at Disney
- Real Shading in Unreal Engine 4
- The Game Engine Space for Virtual Cultural Training
- Game Engines
- Quake III Arena Game Structures
- iOS and Android development with Unity3D
- The use cases that drive XNAT development
- Exploiting Game Engines For Fun & Profit
- GAME ENGINES: A 0-DAY’S TALE
- Comparison and evaluation of 3D mobile game engines
- Barotrauma
- Microsoft XNA
- Game Engines
- Game Engines-The Overview
- Multithreading for Gamedev Students
- A History of the Unity Game Engine
- A brief insight into BRTECH1
- Redefining Game Engine Architecture through Concurrency
- CRYENGINE 5.3 - Getting Started Guide
- The architecture and evolution of computer game engines
- Designing a Modern GPU Interface
- Game Engine Programming
- DX12 & Vulkan Dawn of a New Generation of Graphics APIs
- GPU-Driven Rendering Pipelines
- Limitation to Innovation in the North American Console Video Game Industry 2001-2013: A Critical Analysis
- The 4+1 View Model of Architecture
- Architectural Blueprints—The “4+1” View Model of Software Architecture
- Serious Games Architectures and Engines
- Game Engine Solutions
- Remote exploitation of the Valve Source game engine
- Game Engines - Computer Science
- High quality mobile VR with Unreal Engine and Oculus
- The Report by the written by Amanda Wong funded by the July 2017
- Entity Component Systems & Data Oriented Design
- A Taxonomy of Game Engines and the Tools that Drive the Industry
- Torque 3D Documentation
- Comparative Study on Game Engines
- MOBILE GAME DEVELOPMENT
- Evaluation of CPU and Memory performance between Object-oriented Design and Data-oriented Design in Mobile games
- Efficient generation and rendering of tube geometry in Unreal Engine
- Global Illumination Based on Surfels
- Radiance Caching for real-time Global Illumination
- BigWorld
- Ubisoft Anvil
- Destiny Game Engine: Tiger
- Multithreading the Entire Destiny Engine
- Lessons from the Core Engine Architecture of Destiny
- Destiny 2: Tiger Engine Changes
- Destiny
- Destiny 2
- RE Engine
- Rockstar Advanced Game Engine
- NVIDIA DLSS
- OpenGL
- OpenGL ES
- Tone Reproduction and Physically Based Spectral Rendering
- A Framework for an R to OpenGL Interface for Interactive 3D graphics
- Beyond Finite State Machines Managing Complex, Intermixing Behavior Hierarchies
- Game Mobility Requires Code Portability
- Challenges in programming multiprocessor platforms
- Adding Spherical Harmonic Lighting to the Sushi Engine
- Real World Multithreading in PC Games Case Studies
- Speed up the 3D Application Production Pipeline
- New 3D Graphics Rendering Engine Architecture for Direct Tessellation of Spline Surfaces
- A realtime immersive application with realistic lighting: The Parthenon
- The Parthenon Demo: Preprocessing and Rea-Time Rendering Techniques for Large Datasets
- Rendering Gooey Materials with Multiple Layers
- Practical Parallax Occlusion Mapping For Highly Detailed Surface Rendering
- Real-time Atmospheric Effects in Games
- Shading in Valve’s Source Engine slide
- Shading in Valve’s Source Engine
- Ambient Aperture Lighting
- Fast Approximations for lighting of Dynamic Scenes
- An Analysis of Game Loop Architectures
- Exploiting Scalable Multi-Core Performance in Large Systems
- Ritual™ Entertainment: Next-Gen Effects on Direct3D® 10
- Feeding the Monster: Advanced Data Packaging for Consoles
- Best Practices in Game Development
- High-Performance Physics Solver Design for Next Generation Consoles
- Collaborative Soft Object Manipulation for Game Engine-Based Virtual Reality Surgery Simulators
- Introduction to Direct3D 10 Course
- Practical Global Illumination with Irradiance Caching
- General-Purpose Computation on Graphics Hardware
- GPUBench
- The Mobile 3D Ecosystem
- Advanced Real-Time Rendering in 3D Graphics and Games
- Frostbite Rendering Architecture and Real-time Procedural Shading & Texturing Techniques
- Saints Row Scheduler
- PCI Express
- The Delta3D Gaming and Simulation Engine: An Open Source Approach to Serious Games
- Software Requirement Specification Common Infrastructure Team
- Dragged Kicking and Screaming: Source Multicore
- OpenGL ES 2.0 : Start Developing Now
- Interactive Relighting with Dynamic BRDFs
- Advances in Real-Time Rendering in 3D Graphics and Games (SIGGRAPH 2008 Course)
- Advanced Virtual Texture Topics
- March of the Froblins: Simulation and rendering massive crowds of intelligent and detailed creatures on GPU
- Using wavelets with current and future hardware
- StarCraft II: Effects & Techniques
- The Intersection of Game Engines & GPUs: Current & Future
- Software Instrumentation of Computer and Video Games
- John Carmack Archive - Interviews
- UnrealScript: A Domain-Specific Language
- GPU evolution: Will Graphics Morph Into Compute?
- Introduction to the Direct3D 11 Graphics Pipeline
- Lighting and Material of HALO 3
- Snakes! On a seamless living world
- Threading Successes of Popular PC Games and Engines
- THQ/Gas Powered Games Supreme Commander and Supreme Commander: Forged Alliance
- Emergent Game Technologies: Gamebryo Element Engine
- Namco Bandai, EA Partner For Hellgate: London
- Project “Smoke” N-core engine experiment
- New Dog, Old Tricks: Running Halo 3 Without a Hard Drive
- Creating believable crowds in ASSASSIN'S CREED
- id Tech 5 Challenges - From Texture Virtualization to Massive Parallelization
- Building a Dynamic Lighting Engine for Velvet Assassin
- Techniques for Improving Large World and Terrain Streaming
- A Novel Multithreaded Rendering System based on a Deferred Approach
- Regressions in RT Rendering: LittleBigPlanet Post Mortem
- Meshless Deformations Based on Shape Matching
- sphere ambient occlusion - 2006
- Insomniac Physics
- State-Based Scripting in Uncharted 2: Among Thieves
- Zen of Multicore Rendering
- Star Ocean 4 - Flexible Shader Managment and Post-processing
- Light Propagation Volumes in CryEngine 3
- [Lighting Research at Bungie](https://advances.realtimerendering.com/s2009/SIGGRAPH 2009 - Lighting Research at Bungie.pdf)
- Precomputed Atmospheric Scattering
- Theory and Practice of Game Object Component Architecture
- Rendering Technology at Black Rock Studio
- NVIDIA Developer Tools for Graphics and PhysX
- Animated Wrinkle Maps
- Terrain Rendering in Frostbite Using Procedural Shader Splatting
- Spherical Harmonic Lighting: The Gritty Details
- Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments
- A Gentle Introduction to Precomputed Radiance Transfer
- Spherical harmonics
- Tetris packing [Levy 02]
- Multi-Chart Geometry Images