§8.2实现Post-Screen Shaders

VBScreenHelper和PreScreenSkyCubeMapping类前面已经介绍过了,但实现Post-Screen Shader还需要渲染目标,它由xna中的RenderTarget类处理。这个类的主要问题是你仍要调用很多方法并自己处理很多事情。特别是如果你还想为渲染目标使用深度缓冲并在设置后储存,这对阴影映射shader是很有用的。

RenderToTexture类

一个新的辅助类RenderToTexture提供了重要的方法,使Post-Screen Shader更容易处理(见图8-10)。最重要的方法是构造函数(使用SizeType作为参数),Resolve和SetRenderTarget,通过属性获得XnaTexture和RenderTarget也很有用。请注意,这个类从Texture类继承了所有功能。

图8-10

例如,如果您想为PostScreenDarkenBorder.fx shader创建一个全屏的场景贴图,可以使用以下代码:

sceneMapTexture = new RenderToTexture(RenderToTexture.SizeType.FullScreen);

构造函数使用如下代码:

/// <summary>

/// Creates an offscreen texture with the specified size which

/// can be used for render to texture.

/// </summary> public RenderToTexture(SizeType setSizeType)

{

sizeType = setSizeType; CalcSize();

texFilename = "RenderToTexture instance " + RenderToTextureGlobalInstanceId++;

[...]

SurfaceFormat format = SurfaceFormat.Color;

// Create render target of specified size.

renderTarget = new RenderTarget2D( BaseGame.Device, texWidth, texHeight, 1, format);

} // RenderToTexture(setSizeType)

使用渲染目标,并使渲染所有物体只需调用SetRenderTarget方法,它使用了BaseGame类中的一些新的辅助方法去处理多个堆叠(multiple stacked?)的渲染目标(一些更复杂的Post-Screen Shader会用到):

sceneMapTexture.SetRenderTarget();

所用渲染完成后你调用Resolve方法,把渲染目标复制到贴图中(通过XnaTexture属性)。在XNA中这一步是新的,在DirectX中不需要,因为这样做才能支持Xbox 360硬件,它完全不同于PC的显卡。在PC上,您可以直接访问渲染目标使之用在Post-Screen Shader,但在Xbox 360渲染目标被放至在一个只写的位置,硬件无法访问。你必须复制渲染目标到内部纹理。这一过程需要一些时间,但它仍然非常快,所以不要担心。

在Resolve渲染目标后,你要将渲染目标重置使回到背景缓冲,否则,您仍然渲染到渲染目标,在屏幕上什么也看不到。为了做到这一点,只需调用BaseGame中的ResetRenderTarget方法,清除任何开始的渲染目标。如果你没有任何渲染目标,这个方法仍会工作,但仅return,不采取任何行动。

// Get the sceneMap texture content

sceneMapTexture.Resolve();

// Do a full reset back to the back buffer

BaseGame.ResetRenderTarget(true);

这几乎就是RenderToTexture类的所有内容了。另外的功能现在用不到(直到本书的最后一章)。去看看RenderToTexture的单元测试代码去了解更多东西。

PostScreenDarkenBorder类

在写PostScreenDarkenBorder类之前你就应先定义一个单元测试,并编写想在Post-Screen Shader类中包含的所有功能。请注意,这个类和Pre-Screen Shader一样从ShaderEffect类继承,有一些获取effect类和参数的简化过程,但你仍要在PostScreenDarkenBorder.fx中设置一些新的参数。

看看Post-Screen Shader的单元测试代码:

public static void TestPostScreenDarkenBorder()

{

PreScreenSkyCubeMapping skyCube = null;

Model testModel = null;

PostScreenDarkenBorder postScreenShader = null;

TestGame.Start("TestPostScreenDarkenBorder",

delegate {

skyCube = new PreScreenSkyCubeMapping();

testModel = new Model("Asteroid4");

postScreenShader = new PostScreenDarkenBorder();

},

delegate {

// Start post screen shader to render to our sceneMap

postScreenShader.Start();

// Draw background sky cube

skyCube.RenderSky();

// And our testModel (the asteroid)

testModel.Render(Matrix.CreateScale(10));

// And finally show post screen shader

postScreenShader.Show();

});

} // TestPostScreenDarkenBorder()

请始终用同样的方式命名shader和源代码,这样检查bug更容易。你能做的另一件漂亮的事是:如果以后要编写的新类功能类似,可以从Post-Screen Shader类继承,这个技巧能节省编写相同的效果参数代码量,也避免了重复编写渲染代码。

单元测试中使用了三个变量:

看一下PostScreenDarkenBorder.cs中的单元测试,你会发现一些另外的代码,它们的功能是切换Post-Screen Shader的有无、在屏幕上显示帮助信息,这是以后添加的,只是为了改善shader 的可用性,基本布局是相同的。

Post-Screen Shader的布局与PreScreenSkyCube Mapping类似。你只需一个新的Start和Show方法和一些内部变量存储新的effect参数,并检查Post-Screen Shader是否开始(见图8-11)。

图8-11

Start方法为场景贴图调用SetRenderTarget,重要代码如下:

/// <summary>

/// Execute shaders and show result on screen, Start(..) must have been

/// called before and the scene should be rendered to sceneMapTexture.

/// </summary>

public virtual void Show()

{

// Only apply post screen glow if texture and effect are valid

if (sceneMapTexture == null || Valid == false || started == false)

return;

started = false;

// Resolve sceneMapTexture render target for Xbox360 support

sceneMapTexture.Resolve();

// Don't use or write to the z buffer

BaseGame.Device.RenderState.DepthBufferEnable = false;

BaseGame.Device.RenderState.DepthBufferWriteEnable = false;

// Also don't use any kind of blending.

BaseGame.Device.RenderState.AlphaBlendEnable = false;

if (windowSize != null)

windowSize.SetValue(new float[] { sceneMapTexture.Width, sceneMapTexture.Height });

if (sceneMap != null)

sceneMap.SetValue(sceneMapTexture.XnaTexture);

effect.CurrentTechnique = effect.Techniques["ScreenDarkenBorder"];

// We must have exactly 1 pass!

if (effect.CurrentTechnique.Passes.Count != 1)

throw new Exception("This shader should have exactly 1 pass!");

effect.Begin();

for (int pass= 0; pass < effect.CurrentTechnique.Passes.Count; pass++)

{

if (pass == 0)

// Do a full reset back to the back buffer

BaseGame.ResetRenderTarget(true);

EffectPass effectPass = effect.CurrentTechnique.Passes[pass];

effectPass.Begin();

VBScreenHelper.Render();

effectPass.End();

} // for (pass, <, ++)

effect.End();

// Restore z buffer state

BaseGame.Device.RenderState.DepthBufferEnable = true;

BaseGame.Device.RenderState.DepthBufferWriteEnable = true;

} // Show()

如果仍然有shader运行,首先检查Start是否被正确调用,否则,内容管道中的场景贴图渲染目标将不包含任何有用的数据。然后Resolv渲染目标以支持Xbox 360,设置所有的参数和technique。你可能会问,为什么这里使用technique名调用technique,通过reference不是更快吗,这样可以在构造函数中加以初始化?你是对的!但这并不重要,你每帧调用shader一次,在分析工具中看不出有多少性能差异(几千行代码中的一行并不会影响太多性能)。如果更频繁地调用shader,你最好定义technique reference缓存,这样就用不着每一帧重复调用了。可参见ShaderEffect类详细了解这种优化。

现在开始shader,像以前一样你使用VBScreenHelper类渲染屏幕,但你必须确保为每一个pass提供正确的渲染目标。这里只有一个pass,你只需把它重置到后备缓冲(你应该在post-screen shader中最后一个pass执行这个操作,否则你将不会在屏幕上看到任何东西)。对于更复杂的例子您就可以参考PostScreenGlow类。

单元测试结果

在渲染到设备后,渲染状态被储存(也许你还想在显示shader后渲染3D数据)。如果看一下代码,你还可以看到一些额外的try和catch代码块,只是为了确保渲染引擎在shader发生错误时运行正常。在这种情况下,shader被设置成不正常状态而不再被使用,错误被写到日志,shader效果看不到,但游戏其余部分仍能工作。

如果一些代码不工作或崩溃你应该总能提供选择。大部分情况下代码只提供视觉效果。不使用shader游戏仍能运行,只是看起来不漂亮。本书中的游戏shader永远不会崩溃,你不必担心,但如果在自己的开发过程中您很容易搞砸的effect参数或在像素着色尝试新事物,你不想让您的整个游戏或单元测试崩溃,它仍然应该继续运行。当运行TestPostScreenDarkenBorder单元测试后,您应该看到如图8-12的结果。您也可以调整一个shader参数或代码,例如,尝试渲染一个较小的目标只提供四分之一的画面,看看最终输出效果。

图8-12

如果你有Xbox 360,你也应尽早在X360上测试所有的shader,这是非常重要的,因为shader有时表现得并不一样,你必须确保所有渲染目标也能适应Xbox 360的内存并性能良好。PostScreenDarkenBorder和RenderToTexture类完全兼容Xbox 360,在Xbox 360游戏机工作得很好。


发布时间:2008/9/23 上午8:42:24  阅读次数:7783

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

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号