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位,此时仍有两个选择:
- SurfaceFormat.Color – 这个格式给法线的每个分量X,Y和Z分配8位,还空闲8位(来自于alpha通道),这个8位我们可以用于其它参数。如果我们选择这个格式,必须处理如何储存和读取法线数据。SurfaceFormat.Color只能存储介于0.0f和1.0f之间的值,但法线的范围位于-1.0f到1.0f之间。这个问题可以通过将[-1.0,1.0]映射到[0.0,1.0]加以解决,在读取数据时需要将它们转换回来。
- SurfaceFormat.HalfVector2 – 这个格式提供两个16位分量。你可能会说,法线是3D空间中的矢量,所以需要三个分量。你说的没错,但是法线总是会被归一化,而X,Y和Z轴是互相垂直的,所以我们只需存储X和Y分量,然后通过公式Z = sqrt( 1 – X*X – Y*Y )计算Z。使用这个方法,我们既可以有较高的精度,又可以使用32位的渲染目标。
- SurfaceFormat.Rgba1010102 – Shawn Hargreaves推荐用这个格式,这个格式在32位上精度很高,并还空闲有2位通道。但是,我在三台电脑上尝试创建RenderTarget都没有成功。
选择位置的RenderTarget格式
存储位置信息的选择如下所述:
- SurfaceFormat.Vector4 – 这是128位格式。它强制所有RenderTarget都是128位,这会导致内存的浪费,而且在Xbox上不可用。
- SurfaceFormat.HalfVector4 – 64位格式。精度很高,而且空闲一个通道可以存储额外的数据。
- SurfaceFormat.Single – 这是第三个选择,也是选择32位渲染目标时的唯一选择。要使用这个格式,你只需储存像素的深度信息而不是整个位置信息。当需要使用位置信息时,你需要获取像素的屏幕空间的X、Y位置和深度,然后进行计算获取对应的世界空间的位置。
选择其它数据的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.fbx和Models\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