§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类继承了所有功能。
例如,如果您想为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类继承,这个技巧能节省编写相同的效果参数代码量,也避免了重复编写渲染代码。
单元测试中使用了三个变量:
- skyCube初始化并渲染天空盒纹理。
- testModel载入asteroid4模型,并缩放10倍显示天空盒背景上。
- 最后,postScreenShader处理PostScreenDarkenBorder类的一个实例。由于这个shader需要一个有效的场景贴图,你必须调用两次,第一次在Start方法建立场景贴图渲染目标,第二次在渲染结束后,Show方法把一切物体都显示到屏幕上。
看一下PostScreenDarkenBorder.cs中的单元测试,你会发现一些另外的代码,它们的功能是切换Post-Screen Shader的有无、在屏幕上显示帮助信息,这是以后添加的,只是为了改善shader 的可用性,基本布局是相同的。
Post-Screen Shader的布局与PreScreenSkyCube Mapping类似。你只需一个新的Start和Show方法和一些内部变量存储新的effect参数,并检查Post-Screen Shader是否开始(见图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参数或代码,例如,尝试渲染一个较小的目标只提供四分之一的画面,看看最终输出效果。
如果你有Xbox 360,你也应尽早在X360上测试所有的shader,这是非常重要的,因为shader有时表现得并不一样,你必须确保所有渲染目标也能适应Xbox 360的内存并性能良好。PostScreenDarkenBorder和RenderToTexture类完全兼容Xbox 360,在Xbox 360游戏机工作得很好。
发布时间:2008/9/23 上午8:42:24 阅读次数:7783