2.创建G-Buffer

首先讨论一些理论。选择G-Buffer格式是deferred shading中非常关键的步骤。你必须在速度和画面质量间权衡利弊。更多的参数和更高的精度会让图像质量更高,而精度和参数决定了G-Buffer的大小,而大小会影响性能。

Multiple Render Targets

通过上一章的讨论我们知道G-Buffer需要存储大量的信息。通常绘制的结果是一张图像,但现在我们需要几张:每张对应G-Buffer的一个分量。要获得几张图像的一个方法是在多个pass中绘制场景,每次处理一个shader,然后将结果存储在一个渲染目标中。这会导致要对场景中的几何体计算多次,调用很多次Draw。

Multiple Render Targets (MRTs) 允许pixel shader同时输出几个颜色,这正是我们需要的,绘制一次场景就能获得所有的信息。

要使用MRTs,我们需要在Pixel Shader使用从COLOR0到COLOR3的语义。将不同颜色输出到不同渲染目标的pixel shader示例代码如下:

struct PixelShaderOutput
{
    float4 Color : COLOR0;
    float4 Color1 : COLOR1; 
    float4 Color2 : COLOR2;
};

PixelShaderOutput PixelShaderFunction(VertexShaderOutput input) 
{
    PixelShaderOutput output; 
    output.Color = float4(1,0,0,1);
    output.Color1 = float4(0,1,0,1);
    output.Color2 = float4(0,0,1,1);
    
    return output; 
}

使用MRT有一些限制。首先所有的RenderTarget必须拥有相同的位深度(bit-depth)。你不能让一个RenderTarget的位深度为SurfaceFormat.Color (32位RGBA),另一个是SurfaceFormat.HalfVector4(64位R16G16B16A16F)。这给创建G-Buffer带来了新的限制。如果我们使用很大的格式(64 bits),则精度较高(法线、位置、深度),但在其他地方就会存在空间浪费(颜色,高光强度等)。相反,当我们使用一个小的格式时(32位),性能较好,但精度较低。

接下来我们将分析每个G-Buffer分量。主要分量为:漫反射颜色,法线和位置,然后关注材质属性,例如镜面高光颜色(specular intensity)和镜面高光强度(specular power)。

选择漫反射颜色的RenderTarget 格式

在这个例子中,我们使用32位的SurfaceFormat.Color。但是,如果其他属性更适用于64位渲染目标,我们也会选择SurfaceFormat.HalfVector4,第二个选择浪费了一些空间。

选择法线的RenderTarget格式

我们有很多选择。首先看一下SurfaceFormat.Vector4 (128位)或SurfaceFormat.HalfVector4(64位)。128位不予考虑,我们无需如此高的精度,而且如果选择128位,那么一个4-MRT的G-buffer在分辨率1024*768时就有48 MB,太大了。而且Xbox360上不支持128位。第二个选择,HalfVector4 提供了很好的精度,但如果64位也嫌太大,我们可以使用32位,此时仍有两个选择:

选择位置的RenderTarget格式

存储位置信息的选择如下所述:

选择其它数据的RenderTarget格式

如果进行到这步,你应该有了额外的通道储存这些额外的信息。如果空间不够,我们还可以创建一个新RenderTarget用于储存这些信息,使用32位时可能的选择是SurfaceFormat.Color,64位用SurfaceFormat.HalfVector4

本文的G-Buffer设置

在本文的余下部分,我将使用以下G-Buffer设置,包含三个32位的Render Targets。

RenderTarget 0(SurfaceFormat.Color) Color.R Color.G Color.B Specular Intensity
RenderTarget 1(SurfaceFormat.Color) Normal.X Normal.Y Normal.Z Specular Power
RenderTarget2 (SurfaceFormat.Single) Depth 32bit

目前为止,我们使用了一个简单的光照模型(Phong),这个模型中光照是由漫反射光照、高光颜色组成的。因此我们只需用到与高光有关的参数,它们是高光颜色和强度。32位深度值用来计算像素在世界空间中的位置。而法线的精度不够,某些表面可能会出现错误。优点是设置比较简单,性能也很好。

创建G-Buffer

我们需要在DeferredRenderer类中添加一些变量,它们是三个RenderTargets:colorRT, normalRT和depthRT,并在LoadContent方法中进行初始化。

private RenderTarget2D colorRT; //保存漫反射颜色和高光颜色的Render Target 
private RenderTarget2D normalRT; //保存法线和Specular Power 的Render Target 
private RenderTarget2D depthRT; //保存深度的Render Target

[...]
protected override void LoadContent()
{
    scene.InitializeScene(); 
    
    //获取后备缓存的大小,用于设置渲染目标
    int backbufferWidth = GraphicsDevice.PresentationParameters.BackBufferWidth; 
    int backbufferHeight = GraphicsDevice.PresentationParameters.BackBufferHeight; 
    colorRT = new RenderTarget2D(GraphicsDevice, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth24); 
    normalRT = new RenderTarget2D(GraphicsDevice, backbufferWidth, backbufferHeight, false, SurfaceFormat.Color, DepthFormat.None); 
    depthRT = new RenderTarget2D(GraphicsDevice, backbufferWidth, backbufferHeight, false, SurfaceFormat.Single, DepthFormat.None); 
    base.LoadContent();
}

然后创建两个叫做SetGBuffer和ResolveGBuffer的方法,将渲染目标设置到设备上。我们将colorRT作为第一个渲染目标,normalRT作为第二个,depthRT为第三个。

private void SetGBuffer() 
{
    GraphicsDevice.SetRenderTargets(colorRT, normalRT, depthRT);
}
private void ResolveGBuffer()
{
    GraphicsDevice.SetRenderTargets(null); 
}

清除G-Buffer

在绘制前我们需要将G-Buffer清除为默认值。这里会遇到的问题是我们无法简单地通过使用GraphicsDevice.Clear()达到目的,这样做会导致将所有渲染目标设置成相同的颜色,而这并不是我们想要的结果。我们需要将colorRT 设置为黑色,depthRT设置为白色(表示最大深度),normalRT设置为灰色,这样当它从[0,1]映射到[-1,1]后会变为(0,0,0),这个值对于默认法线来说是一个非常合适的,这样可以避免在背景中出现光照错误。要清除G-Buffer,我们需要创建一个叫做ClearGBuffer.fx的新effect文件。在这个effect中,我们将渲染目标设置成想要的值。在vertex shader中,只需简单地变换顶点位置即可。我们将使用quadRenderer绘制整个屏幕。

Shader的代码很简单:

struct VertexShaderInput
{
    float3 Position : POSITION0;
};
struct VertexShaderOutput
{
    float4 Position : POSITION0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output; 
    output.Position = float4(input.Position,1);
    return output;
}
struct PixelShaderOutput
{
    float4 Color : COLOR0;
    float4 Normal : COLOR1;
    float4 Depth : COLOR2;
};
PixelShaderOutput PixelShaderFunction(VertexShaderOutput input)
{
    PixelShaderOutput output;
    //设置为黑色
    output.Color = 0.0f;
    output.Color.a = 0.0f; 
    //将0.5f映射到[-1,1]区间,结果为0.0f
    output.Normal.rgb = 0.5f;
    //镜面高光强度设为0
    output.Normal.a = 0.0f;
    //最大深度 output.Depth = 1.0f;
    return output;
}
technique Technique1 
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction(); 
        PixelShader = compile ps_2_0 PixelShaderFunction(); 
    }
}

我们将这个effect加载到一个变量中,然后在DeferredRenderer类中创建一个叫做ClearGBuffer()的方法,并使用这个effect绘制整个屏幕平面。

private Effect clearBufferEffect; 
protected override void LoadContent() 
{
    [...]
    clearBufferEffect = Game.Content.Load<Effect>("ClearGBuffer");
}

private void ClearGBuffer()
{
    clearBufferEffect.Techniques[0].Passes[0].Apply(); 
    quadRenderer.Render(Vector2.One * -1, Vector2.One); 
}

最后在Draw方法中,我们设置G-Buffer,然后清除G-Buffer,绘制场景,最后将渲染目标重置为后备缓存。

public override void Draw(GameTime gameTime)
{
    SetGBuffer(); 
    ClearGBuffer();
    scene.DrawScene(camera, gameTime); 
    ResolveGBuffer();
    base.Draw(gameTime); 
}

如果现在运行程序,你看不到任何东西。(实际上,你会看到一个紫色的屏幕)。这很正常,因为我们还没有绘制任何东西。接下来,我们将要在Scene类中添加一些对象,然后使用一个shader绘制它们。

绘制场景

现在我们将要创建一个effect用来绘制游戏中的所有几何体,这个effect会向所有渲染目标输出数据,它也负责填充G-Buffer,所以也是deferred renderer的主要代码片段。要创建一个新shader,右击Content项目,添加新项目,选择Effect文件,命名为RenderGBuffer.fx。这样我们就有了一个effect文件模板,然后对它进行修改。我们需要添加一个纹理和一个采样器,用来绘制模型的颜色。对于高光颜色和强度,我们需要使用两个参数,在后面的章节中,我们会学习如何从纹理中读取这个数据。specularPower范围在[0,1]之间,它会乘以255获取位于0和255之间的强度系数。

float4x4 World; float4x4 View; 
float4x4 Projection;
float specularIntensity = 0.8f;
float specularPower = 0.5f;
texture Texture; 
sampler diffuseSampler = sampler_state
{
    Texture = (Texture);
    MAGFILTER = LINEAR;
    MINFILTER = LINEAR; 
    MIPFILTER = LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};

因为我们需要输出法线和深度,所以需要在VertexInput结构中添加法线,在VertexOutput 结构中添加法线、深度和纹理坐标。因为只需在pixel shader中将深度除以w分量,所以它是一个两个分量的矢量,否则当三角形的顶点在视锥体之外时会出现一些奇怪的数值。

struct VertexShaderInput
{
    float4 Position : POSITION0; 
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput 
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
    float3 Normal : TEXCOORD1; 
    float2 Depth : TEXCOORD2;
};

现在VertexShaderFunction基本保持不变,目前我们只是在输出中新添加了三个指令。法线被转换到世界空间,而深度是由output.Position.z和output.Position.w组合而成的。

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output; 
    float4 worldPosition = mul(input.Position, World); 
    float4 viewPosition = mul(worldPosition, View); 
    output.Position = mul(viewPosition, Projection);
    output.TexCoord = input.TexCoord; //直接输出纹理坐标 
    output.Normal =mul(input.Normal,World); // 计算世界空间中的法线
    output.Depth.x = output.Position.z;
    output.Depth.y = output.Position.w; 
    return output;
}

然后是Pixel Shader。因为我们输出不止一个渲染目标,所以需要一个pixel shader输出结构,这个结构体包含3个float4,分别对应三个渲染目标。

struct PixelShaderOutput
{
    half4 Color : COLOR0;
    half4 Normal : COLOR1; 
    half4 Depth : COLOR2;
};

在PixelShaderFunction中,我们需要输出颜色,法线和深度,分别对应三个渲染目标。我们将法线值从[-1,1]映射到[0,1]。通过除以两个深度分量获取深度值。代码如下:

PixelShaderOutput PixelShaderFunction(VertexShaderOutput input)
{ 
    PixelShaderOutput output;
    output.Color = tex2D(diffuseSampler, input.TexCoord); // 输出Color
    output.Color.a = specularIntensity; //输出高光颜色
    output.Normal.rgb = 0.5f * (normalize(input.Normal) + 1.0f); //对法线数据进行映射
    output.Normal.a = specularPower; //输出SpecularPower
    output.Depth = input.Depth.x / input.Depth.y; //输出Depth
    return  output;
}

最后,我们将PixelShader和VertexShader版本设置为2_0:

technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction(); 
    }
}

下面在Scene类中添加一些代码。首先需要在游戏中添加一些模型。在Content项目中添加一个Models目录。然后将Models\ship1.fbxModels\ship1_c.tga (来自于Resources.zip)添加到Content项目中的Models目录。然后添加一个变量保存这个模型,在InitializeScene方法中进行初始化。

class Scene
{
    private Game game;
    Model shipModel;
    Texture2D shipColor; 
    Effect gbufferEffect;
    [...]
    public void InitializeScene()
    {
        shipModel = game.Content.Load<Model>("Models\\ship1");
        shipColor = game.Content.Loadd<Texture2D>("Models\\ship1_c"); 
        gbufferEffect = game.Content.Loadd<Effect>("RenderGBuffer"); 
    }
}

最后,在DrawScene方法中,设置正确的渲染状态和effect参数,然后绘制模型:

public void DrawScene(Camera camera, GameTime gameTime)
{
    game.GraphicsDevice.DepthStencilState = DepthStencilState.Default; 
    game.GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise; 
    game.GraphicsDevice.BlendState = BlendState.Opaque; 
    gbufferEffect.Parameters["World"].SetValue(Matrix.Identity); 
    gbufferEffect.Parameters["View"].SetValue(camera.View); 
    gbufferEffect.Parameters["Projection"].SetValue(camera.Projection); 
    gbufferEffect.Parameters["Texture"].SetValue(shipColor); 
    gbufferEffect.CurrentTechnique.Passes[0].Apply();
    foreach (ModelMesh mesh in shipModel.Meshes)
    {
        foreach (ModelMeshPart meshPart in mesh.MeshParts)
        { 
            game.GraphicsDevice.Indices = meshPart.IndexBuffer; 
            game.GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer); 
            game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshPart.VertexOffset, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
        }
    }
}

现在让我们添加方法观察G-Buffer中的内容。要做到这点,我们需要在DeferredRenderer类中添加一个SpriteBatch,在Draw代码的最后绘制三个渲染目标。

public class DeferredRenderer : Microsoft.Xna.Framework.DrawableGameComponent
{
    [...]
    private SpriteBatch spriteBatch;
    [...]
    protected override void LoadContent()
    {
        [...]
        spriteBatch = new SpriteBatch(Game.GraphicsDevice);
    }
    [...]
    public override void Draw(GameTime gameTime)
    {
        SetGBuffer();
        ClearGBuffer();
        scene.DrawScene(camera); 
        ResolveGBuffer();
        
        int halfWidth = GraphicsDevice.Viewport.Width / 2; 
        int halfHeight = GraphicsDevice.Viewport.Height / 2; 
        spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.PointClamp, null, null);
        spriteBatch.Draw(colorRT, new Rectangle(0, 0, halfWidth, halfHeight), Color.White);
        spriteBatch.Draw(normalRT, new Rectangle(0, halfHeight, halfWidth, halfHeight), Color.White);
        spriteBatch.Draw(depthRT, new Rectangle(halfWidth, 0, halfWidth, halfHeight), Color.White);
        spriteBatch.End();
        base.Draw(gameTime); 
    }
}

现在我们就可以看到G-Buffer的内容了,深度贴图看起来全是白色的,但实际上不是:它接近于白色。

程序截图

本章我们学习了G-Buffer的用途以及如何创建它,我们使用MRT技术编写了一个特殊的effect文件将数据输出到三个渲染目标中,但这只是开始。在下一章中,我们会在场景中添加一个单向光源。

源代码 DeferredShadingTutorial02.zip下载。

文件下载(已下载 1571 次)

发布时间:2011/4/2 下午1:58:44  阅读次数:10590

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号