Deferred Shading介紹

在本文我將展示如何在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.tganull_specular.tga是兩張以後要用到的紋理。
  • Models文件夾包含本教程用到的模型文件。

本文的代碼會用在後面的章節中,如果你想略過此步,可以下載DeferredShadingTutorial01.zip,然後進入第二章。

本文會創建一個deferred renderer,它可以很容易地集成到已有遊戲項目中。首先,在XNA中創建一個新項目,名爲DeferredShadingTutorial,在項目中添加Camera.csQuadRenderer.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系列以上。

發佈了25 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章