Implementing Post-Screen Shaders


A few helper classes like VBScreenHelper and PreScreenSkyCubeMapping were already introduced, but for post-screen shaders you really need render targets, which can be accessed and handled through the RenderTarget class in XNA. The problem with that class is that you still have to call a lot of methods and handle a lot of things yourself. This is especially true if you also want to use the depth buffer for render targets and restore them after they have been modified, which is useful for shadow mapping shaders.

RenderToTexture Class

For that reason a new helper class is introduced: RenderToTexture, which provides important methods to make post-screen shaders easy to handle (see Figure 8-10). The most important methods for you are the constructor, which takes SizeType as a parameter, Resolve, and SetRenderTarget. Getting the XnaTexture and the RenderTarget via the properties is also very useful. Please note that this class also inherits all the features from the Texture class.

image from book
Figure 8-10

For example, if you want to create a fullscreen scene map for the PostScreenDarkenBorder.fx shader you would use the following line:

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

The constructor does the following:

  /// <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) 

To use this render target and let everything be rendered into it you just have to call SetRenderTarget now, which uses some new helper methods in the BaseGame class to take care of multiple stacked render targets (required for more complex post-screen shaders):

  sceneMapTexture.SetRenderTarget(); 

After all the rendering is done you can call Resolve to copy the render target results to the internal texture (which can be accessed through the XnaTexture property). This step is new in XNA; it was not required in DirectX and the reason for it is to support the Xbox 360 hardware, which works quite differently than PC graphics cards. On the PC you can directly access the render target and use it for post-screen shaders, but on the Xbox 360 the render target results reside in a write-only hardware location and cannot be accessed. Instead you have to copy the render target over to the internal texture. This call takes some time but it is still very fast, so don’t worry about it.

After resolving the render target you have to reset the render target back to the default background buffer, or else you are still rendering into the render target and will see nothing on the screen. To do that just call the BaseGame ResetRenderTarget method and pass in true to get rid of any started render targets. The method will also work if you don’t have any render targets started. In that case it just returns and does nothing.

  // Get the sceneMap texture content sceneMapTexture.Resolve(); // Do a full reset back to the back buffer BaseGame.ResetRenderTarget(true); 

That is almost everything you have to know to work with the RenderToTexture class. Additional functionality will not be required for a while (until the last chapters of this book). Check out the unit tests of the RenderToTexture class to learn more about it.

PostScreenDarkenBorder Class

Before even thinking about writing the PostScreenDarkenBorder class you should define a unit test and specify the entire feature set you want to have in your post-screen class. Please note that you derive this class from ShaderEffect like for the pre-screen shader and have easier access to the effect class and the effect parameters. But you still have to specify the new effect parameters used in PostScreenDarkenBorder.fx.

Take a look at the unit test for this 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() 

By the way, always try to name your shader and source code files the same way; it will make it much easier when looking for bugs. Another nice thing you can sometimes do if shaders use similar features is to derive from one post-screen shader class when writing a new one. This trick is used for most of the following games to save coding the same effect parameters and render target code over and over again.

You use three variables in the unit test:

  • skyCube initializes and renders the sky cube mapping shader like in FX Composer.

  • testModel loads the asteroid4 model and displays it with a scaling factor of 10 after rendering the sky cube map background.

  • And finally, postScreenShader holds an instance of the PostScreenDarkenBorder class. Because this shader needs a valid scene map you have to call it twice, first with the Start method to set up the scene map render target and then at the end of the frame rendering. Show is then called to bring everything to the screen.

If you take a look at the final unit test in the PostScreenDarkenBorder.cs file you may notice some additional code to toggle the post-screen shader and show some help on the screen. This was added later and only improves the usability of the shader; the basic layout is the same.

The layout of post-screen shader classes is very similar to the one you already saw in PreScreenSkyCube Mapping. You just need a new Start and Show method and some internal variables to hold the new effect parameters and check whether or not the post-screen shader was started (see Figure 8-11).

image from book
Figure 8-11

The Start method just calls SetRenderTarget for the scene map texture; the important code is in the Show method:

  /// <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() 

First you are checking if Start was called properly and if you still have the shader started, otherwise the content in the scene map render target will not contain any useful data. Then you resolve the render target for full Xbox 360 support and you can start setting up all required effect parameters and the technique. You might ask why the technique is added by name here and you are right, it would be faster to do this by a reference to the technique, which could be initialized in the constructor. But it does not matter so much; you only call this shader once per frame and this performance difference will never show up in any profiler tool (because one line for a frame that goes through many thousand lines of code is not affecting performance that much). If you will call a shader more often you should definitely cache this technique reference and maybe not even set it again and again every frame (it is still set from the last frame). See the ShaderEffect class for more information about this kind of optimization.

Now the shader is started and you render the screen quad with the VBScreenHelper class as usual, but you have to make sure that the correct render target is set for each pass. In your case you just have one pass and all you have to do is to reset it to the back buffer (you will always have to do that for the last pass in post-screen shaders, or else you will not see anything on the screen). For a more complex example you can check out the PostScreenGlow class.

Unit Test Result

After you are done with rendering the device, render states are restored (maybe you still want to render some 3D data after showing the shader). If you look at the code in the project you can also see some additional try and catch blocks to make sure that the render engine stays stable even if a shader crashes. The shader will just set to an invalid state and not be used anymore. An error is logged and the user will no longer see the post-screen shader, but the rest of the game still runs.

You should always provide alternatives if some code does not work or crashes. Most of the time the code is only providing visual elements to your game like this post-screen shader. The game is still the same even if you don’t use this shader; it will just not look as pretty. In practice a shader will never crash and you don’t have to worry about this for your final game, but during development you can easily mess up an effect parameter or try new things in the Pixel Shader, and you don’t want your whole game or unit test to crash; it should still continue to run.

You should now see the result shown in Figure 8-12 when running the TestPostScreenDarkenBorder unit test. Feel free to play around with the shader and change code in your application. For example, you could try to render into a smaller render target like using only one quarter of the screen and see how this affects the final output (big blocky pixels).

image from book
Figure 8-12

You should also test out all post-screen shaders as early as possible on your Xbox 360 if you have one. This is very important because shaders can sometimes behave differently and you have to make sure that all render targets fit in the Xbox 360 memory and still perform well. The PostScreenDarkenBorder and the RenderToTexture classes are fully compatible with the Xbox 360; and they work great on Xbox 360 too.




Professional XNA Game Programming
Professional XNA Programming: Building Games for Xbox 360 and Windows with XNA Game Studio 2.0
ISBN: 0470261285
EAN: 2147483647
Year: 2007
Pages: 138

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net