剖析虛幻渲染體系(14)- 延展篇:現代渲染引擎演變史Part 2(成長期)
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模型更適合實時渲染應用程序的方法,並且描述了基於直接和間接光之間的分離和廣泛的預計算的照明算法。其中直接光和非直接光的公式如下:
L_{direct} &=& \ \text{Lambertian} \\
L_{indirect} &=& \sum_{i=1}^{4} \text{Coeff}_i \cdot \text{SkySH}_i
\end{eqnarray}
\]
該文還展示了如何使用現有渲染引擎預先計算光照不變量,以及如何使用現代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的材質效果:
64×64紋理編碼的矢量效果。(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的動畫紋理布局。變換存儲為3×4矩陣,使用了一個紋理數組,其中水平和垂直維度對應關鍵幀和骨骼索引,切片編號用於索引動畫序列。沿着紋理的一個軸改變時間可以使用紋理過濾硬件在關鍵幀之間進行插值。注意,根據權重對每個頂點的骨骼影響進行排序,並使用動態分支來避免獲取零權重的骨骼,可以顯著提升性能,因為大多數頂點不具有超過兩個骨骼影響。
// 用於獲取、插入和混合骨骼動畫的着色器代碼
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是256×114浮點格式,專為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分辨率為1040×624,然後按比例放大以允許HUD以1280×720渲染。
-
多通道開銷
- 通道的逐光源開銷太高。
- 大多是預光照,所以選擇了前向渲染。
- 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](//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):
\mu &=& E(d_o) \\
\sigma^2 &=& E(d_o^2) – E(d_o)^2
\end{eqnarray}
\]
使用切比雪夫(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的測試結果如下:
以上顯示隨機訪問的效率最低,4×4塊的一致性訪問效率最佳。
對於着色器分支,採用測試語句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。256×114浮點,非常適合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和顏色鏡面反射會佔用大量內存。
- 擴展到大量的大重疊光源!
- 細粒度剔除 (16×16)。
- 僅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 (176×144) 到VGA (640×480),在高端設備上抗鋸齒,每通道顏色深度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\)如果使用卡迪爾坐標系和球面坐標系的參數化公式分別如下所示:
s &=& (x,\ y,\ z) \\
&=& (\sin\theta \cos\varphi,\ \sin\theta \sin\varphi,\ \cos\theta)
\end{eqnarray}
\]
球面基函數定義為:
\]
其中\(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}\)的線性變換:
c_i &=& \int a(s)\Big(b_j y_j(s) \Big)y_i(s)ds \\
&=& \Big(\int a(s)y_i(s)y_j(s)ds\Big)b_j \\
&=& \Big(a_k\int y_i(s)y_j(s)y_k(s)ds\Big)b_j \\
&=& \widehat{a}_{ij}b_j
\end{eqnarray}
\]
其中對重複的\(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具有相同的大小,則可以計算虛擬紋理分辨率:
\]
下面舉幾個例子:
16k &=& 128 \cross 128 \\
65k &=& 256 \cross 256 \\
256k &=& 256 \cross 1024
\end{eqnarray}
\]
注意,為了避免使用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](//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