Deferred Shading介绍

来源:互联网 发布:第一款聊天软件 编辑:程序博客网 时间:2024/04/28 16:31

在本文我将展示如何在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
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingMicrosoft.Xna.Framework;
usingMicrosoft.Xna.Framework.Audio;
usingMicrosoft.Xna.Framework.Content;
usingMicrosoft.Xna.Framework.GamerServices;
usingMicrosoft.Xna.Framework.Graphics;
usingMicrosoft.Xna.Framework.Input;
usingMicrosoft.Xna.Framework.Media;
 
 
namespaceDeferredShadingTutorial
{
    publicclass DeferredRenderer : Microsoft.Xna.Framework.DrawableGameComponent
    {
        privateCamera camera;
        privateQuadRenderComponent quadRenderer;
         
        publicDeferredRenderer(Game game)
            base(game)
        {
             
        }
 
        publicoverride void Initialize()
        {
            camera = newCamera(Game);
            Game.Components.Add(camera);
            quadRenderer = newQuadRenderComponent(Game);           
            Game.Components.Add(quadRenderer);
            base.Initialize();
        }
 
        protectedoverride void LoadContent()
        {
            base.LoadContent();
        }       
 
        publicoverride void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }
 
        publicoverride 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
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;
usingMicrosoft.Xna.Framework;
namespaceDeferredShadingTutorial
{
    classScene 
    {
        privateGame game;
        publicScene(Game game)
        {
            this.game = game;
        }
        publicvoid InitializeScene()
        {
        }       
        publicvoid DrawScene(Camera camera, GameTime gameTime)
        {
        }
    }
}

现在,将Scene类的对象插入DeferredRenderer.cs中,并在LoadContent中进行初始化。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclass DeferredRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    [...]
    privateScene scene;
    publicDeferredRenderer(Game game) : base(game)
    {
        scene = newScene(game);
    }
    protectedoverride void LoadContent()
    {
        scene.InitializeScene();
        [...]
    }
}

现在这个scene类并不复杂,但足够用于这个教程了。

最后在Game1.cs的构造函数中添加以下代码:

?
1
2
3
4
5
6
publicGame1()
{
    [...]
    DeferredRenderer renderer = newDeferredRenderer(this);
    Components.Add(renderer);
}

现在我们完成了准备工作,可以进行后继步骤了。

显卡要求

为了实现本文中的技术,你的显卡需要支持Multiple Render Targets和floating point textures。对于ATI显卡来说,需要Radeon 9500以上,对于NVIDIA,需要6000系列以上。

0 0
原创粉丝点击