在本文我將展示如何在XNA中使用deferred rendering。首先讓我們理解什麼是deferred shading,然後學習這個技術的幾個步驟,從創建Geometry Buffer一直到管理材質。最後,我們介紹如何創建一個內容管道處理器使用這個技術。在每個步驟中,我會詳細解釋原理,在後面的章節中,有時我也會回到前面並重寫某些代碼。你最好理解不同的座標系,例如世界空間、視空間和屏幕空間,這可以參考creators.xna.com上的Shader Series。
實時光照
現今的遊戲中一個物體往往要被許多光源照亮,今天這仍是一個代價昂貴的操作,也沒有一個完美的解決方案。讓我們首先看一下解決這個問題的幾種常用方法。
在Single-Pass光照方法中,每個對象對需要被繪製,所有光照運算都在一個shader中進行。可參加creators.xna.com上的示例。但一個shader有指令數量的限制,所以這個技術只適用於光源數量較少的情況(例如,Creator’s Club上的例子Shader Series 4: Materials and Multiple Light Sources(或本站的譯文,注意:這個例子沒有升級到XNA4.0)在SM 2.0中支持2個光源,SM 3.0支持8個。在某些遊戲中,只需要少量光源,例如室外白天場景,這就是個較好的選擇。這個技術的缺點是光源數量較少,而且shader計算會浪費在不可見的物體上。
另一個方法是Multi-Pass光照。對每個光源,物體光照的計算只在當前光源shader中進行。這會導致非常高的batch數量(調用Draw的次數),最壞的情況會達到光源數量乘以物體數量。繪製不可見對象的缺點仍然存在,某些操作會重複多次,例如頂點的轉換。Creator’s Club上的例子爲Shader Series 5: Multipass Lighting。
Deferred Shading使用另一種不同的方法。首先,所有物體在不進行光照運算的情況下被繪製,然後對每個像素生成一組數據,這些數據包括位置、法線、高光顏色等。之後,將每個光源以一個2D後期處理的方式施加到最終圖像上,這個過程使用的數據是在上一個pass中寫入的。因爲所有對象使用相同的shader ,導致引擎管理變得非常簡單。我們無需基於對象使用的材質進行排序,調用繪製的數量減少到物體數量+光源數量。此外,光源計算只針對可見像素(這些像素生成最終的圖像)。
Deferred Shading
讓我們現在看一下deferred shading的細節。如前所述,我們首先需要繪製所有物體獲取在後面的光照處理中需要的信息,這些信息存儲在一個叫做Geometry Buffer (G-Buffer)的緩存中,存在在這個緩存中的數據通常是:
- Position – 這個數據對於區域光源(local lights,即不影響所有物體的光源)是必須的。全局光源(global light,例如環境光和單向光)均等地影響所有物體,而區域光源(點光源和聚光燈)隻影響距離足夠近的物體。所以我們需要每個像素的位置信息。
- Normal –除了環境光,法線對於任何一種光照計算都是必須的。它被用來確定一個表面是否被照亮,方法是計算光線方向和法線方向的點積。當生成法線時,我們還可以使用法線映射添加物體表面的細節。
- Color – 也被稱爲漫反射顏色(diffuse color)或反射率(albedo)。通常是來自於紋理的顏色。
- 其他數據 – 基於我們使用的光照模型,我們可能還會使用其他數據,例如:鏡面高光強度(specular power),鏡面高光顏色(specular intensity)等其他係數。
可見一個像素所需的數據是非常多的,因此 導致了deferred rendering的第一個缺點,稱作memory usage,這是因爲某些數據(法線,位置) 需要以一個很高的精度存儲(floating point textures);這也是這些年來deferred rendering只是作爲一個可行性選擇的主要原因。要加速這個處理,我們還需使用Multiple Render Targets。
完成上述步驟後,我們就獲取了施加光照所需的所有數據。對場景中的所有光源,我們將對圖像進行2D後期處理並生成shading信息。在這個步驟中,我們還可以計算陰影,Shadow Map技術可以很好地整合到deferred shading中。
在施加光照時,我們首先確定場景中的哪些區域會被光照亮,對這個區域中的每個像素,我們將從G-buffer獲取對應的信息,然後基於光照公式計算當前像素的光照情況。每個光源的光照被混合,最後和顏色數據組合在一起獲取最終圖像。根據工作原理,我們可知只有可見的像素纔會被處理。我們還能發現計算光照所需的時間與光照的影響範圍緊密相關,這意味着許多小光源可能比少量大光源運行得更快。
分析工作流程你會發現deferred shading的兩個缺點。因爲相同的光照shader施加在所有像素上,而且我們只能將這麼多數據存儲在G-Buffer中,導致物體上的材質數量會有所限制。在實際生活中,一個shader作用在所有物體上,而在遊戲中,我們通常使用指定的shader作用在指定的物體上,我們會在後面的章節中處理這個問題。第二個缺點是deferred shading無法處理透明物體,這是因爲deferred shading只會處理最近的表面,解決方法也會在最後一章進行討論。
最後,當繪製最終的圖像時,我們還可以在這張圖像上施加其他效果,例如體積霧(Volumetric Fog),發光(Glow),HDR,Bloom,Edge Smoothing,Screen-Space Ambient Occlusion等。
開始代碼
在開始編碼前,請下載Resources.zip(16MB)。它包含以下文件:
- Camera.cs 是一個處理相機的GameComponent,它來自於官網的Skinned Model示例。使用手柄的扳機鍵或鍵盤Z和X鍵進行縮放控制,使用右搖桿或WASD移動相機。
- QuadRendered.cs 是一個來自於Ziggyware的GameComponent,它幫助我們在屏幕上繪製一個用於後期處理的長方形。我不使用SpriteBatch而使用這個類替代是因爲SpriteBatch無法處理某些shader變量,例如紋理和採樣器。
- null_normal.tga和null_specular.tga是兩張以後要用到的紋理。
- Models文件夾包含本教程用到的模型文件。
本文的代碼會用在後面的章節中,如果你想略過此步,可以下載DeferredShadingTutorial01.zip,然後進入第二章。
本文會創建一個deferred renderer,它可以很容易地集成到已有遊戲項目中。首先,在XNA中創建一個新項目,名爲DeferredShadingTutorial,在項目中添加Camera.cs和QuadRenderer.cs。
然後,創建一個新GameComponent(右擊項目,選擇添加->新建項,然後選擇GameComponent),命名爲DeferredRenderer並設置從DrawableGameComponent繼承。
然後添加兩個變量,一個用於Camera,另一個用於QuadRenderer,並在Initialize方法中進行初始化。現在的DeferredRenderer.cs代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
using
System; using
System.Collections.Generic; using
System.Linq; using
Microsoft.Xna.Framework; using
Microsoft.Xna.Framework.Audio; using
Microsoft.Xna.Framework.Content; using
Microsoft.Xna.Framework.GamerServices; using
Microsoft.Xna.Framework.Graphics; using
Microsoft.Xna.Framework.Input; using
Microsoft.Xna.Framework.Media; namespace
DeferredShadingTutorial { public
class
DeferredRenderer : Microsoft.Xna.Framework.DrawableGameComponent { private
Camera camera; private
QuadRenderComponent quadRenderer; public
DeferredRenderer(Game game) : base (game) { } public
override
void
Initialize() { camera
= new
Camera(Game); Game.Components.Add(camera); quadRenderer
= new
QuadRenderComponent(Game); Game.Components.Add(quadRenderer); base .Initialize(); } protected
override
void
LoadContent() { base .LoadContent(); } public
override
void
Update(GameTime gameTime) { base .Update(gameTime); } public
override
void
Draw(GameTime gameTime) { base .Draw(gameTime); } } } |
爲了管理場景我們還想創建一個叫做Scene的類,並添加方法進行初始化和繪製。代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
using
System; using
System.Collections.Generic; using
System.Text; using
Microsoft.Xna.Framework; namespace
DeferredShadingTutorial { class
Scene { private
Game game; public
Scene(Game game) { this .game
= game; } public
void
InitializeScene() { } public
void
DrawScene(Camera camera, GameTime gameTime) { } } } |
現在,將Scene類的對象插入DeferredRenderer.cs中,並在LoadContent中進行初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
DeferredRenderer : Microsoft.Xna.Framework.DrawableGameComponent { [...] private
Scene scene; public
DeferredRenderer(Game game) : base (game) { scene
= new
Scene(game); } protected
override
void
LoadContent() { scene.InitializeScene(); [...] } } |
現在這個scene類並不複雜,但足夠用於這個教程了。
最後在Game1.cs的構造函數中添加以下代碼:
1
2
3
4
5
6
|
public
Game1() { [...] DeferredRenderer
renderer = new
DeferredRenderer( this ); Components.Add(renderer); } |
現在我們完成了準備工作,可以進行後繼步驟了。
顯卡要求
爲了實現本文中的技術,你的顯卡需要支持Multiple Render Targets和floating point textures。對於ATI顯卡來說,需要Radeon 9500以上,對於NVIDIA,需要6000系列以上。