Click or drag to resize
DigitalRuneStep 14: Post-processing

While we are modifying the graphics screen, we could add post-processing to apply some full-screen image filters.

Change graphics profile to HiDef

Some advanced effects are only available for the HiDef graphics profile.

  1. In the Solution Explorer right-click the MyGame project.
  2. Select Properties…
  3. Select the XNA game studio tab and set the Game Profile to HiDef.
    Tutorial-01-22
Add post-processing

This requires more changes in MyGraphicsScreen.cs:

MyGraphicsScreen.cs
using DigitalRune.Graphics.PostProcessing;                                              // NEWnamespace MyGame
{
    public class MyGraphicsScreen : GraphicsScreen
    {
        private MeshRenderer _meshRenderer;
        private BillboardRenderer _billboardRenderer;
        private SkyRenderer _skyRenderer;
        private PostProcessorChain _postProcessors;                                     // NEW

        …

        public MyGraphicsScreen(IGraphicsService graphicsService)
          : base(graphicsService)
        {
            _meshRenderer = new MeshRenderer();
            _billboardRenderer = new BillboardRenderer(graphicsService, 2048);
            _skyRenderer = new SkyRenderer(graphicsService);

            _postProcessors = new PostProcessorChain(graphicsService);                  // NEW
            _postProcessors.Add(new SharpeningFilter(graphicsService));                 // NEW
            _postProcessors.Add(new GrainFilter(graphicsService));                      // NEW

            var spriteFont = graphicsService.Content.Load<SpriteFont>("SpriteFont1");
            DebugRenderer = new DebugRenderer(graphicsService, spriteFont);

            Scene = new Scene();
        }

        …

        protected override void OnRender(RenderContext context)
        {
            var screenRenderTarget = context.RenderTarget;                              // NEW
            var screenViewport = context.Viewport;                                      // NEW

            var graphicsDevice = GraphicsService.GraphicsDevice;

            var intermediateRenderTarget = GraphicsService.RenderTargetPool.Obtain2D(   // NEW
                new RenderTargetFormat(                                                 // NEW
                    context.Viewport.Width,                                             // NEW
                    context.Viewport.Height,                                            // NEW
                    false,                                                              // NEW
                    SurfaceFormat.Color,                                                // NEW
                    DepthFormat.Depth24Stencil8));                                      // NEW
     
            graphicsDevice.SetRenderTarget(intermediateRenderTarget);                   // NEW
            context.RenderTarget = intermediateRenderTarget;                            // NEW
            context.Viewport = graphicsDevice.Viewport;                                 // NEW
            
            graphicsDevice.Clear(Color.CornflowerBlue);

            context.CameraNode = CameraNode;
            context.Scene = Scene;

            // Frustum Culling: Get all the scene nodes that intersect the camera frustum.
            var query = Scene.Query<CameraFrustumQuery>(context.CameraNode, context);

            // Render opaque meshes that are visible from the camera
            graphicsDevice.DepthStencilState = DepthStencilState.Default;
            graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            graphicsDevice.BlendState = BlendState.Opaque;
            graphicsDevice.SamplerStates[0] = SamplerState.AnisotropicWrap;
            context.RenderPass = "Default";
            _meshRenderer.Render(query.SceneNodes, context);
            context.RenderPass = null;

            _skyRenderer.Render(query.SceneNodes, context);

            graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
            _billboardRenderer.Render(query.SceneNodes, context, RenderOrder.BackToFront);

            context.SourceTexture = intermediateRenderTarget;                           // NEW
            context.RenderTarget = screenRenderTarget;                                  // NEW
            context.Viewport = screenViewport;                                          // NEW
            _postProcessors.Process(context);                                           // NEW
            context.SourceTexture = null;                                               // NEW

            DebugRenderer.Render(context);

            GraphicsService.RenderTargetPool.Recycle(intermediateRenderTarget);         // NEW
            context.Scene = null;
            context.CameraNode = null;
        }
    }
}

This code creates a PostProcessorChain. The PostProcessorChain is a collection of post-process filters which are executed in sequence. Two post-processors are added to the PostProcessorChain: A SharpeningFilter and a GrainFilter.

OnRender becomes more complex. The post-processors are image filters. Therefore, we have to render the scene into an render target instead of the default back buffer.

First, we remember the orginal render target and viewport:

C#
var screenRenderTarget = context.RenderTarget;
var screenViewport = context.Viewport;

Usually the context.Viewport is the whole screen and context.RenderTarget is null, which means we are rendering into the back buffer. However, in complex rendering scenarios (e.g. several stacked graphics screen) this could be different.

Next, an intermediate render target is created. The RenderTargetPool manages a pool of render targets. We use the pool to get the intermediate render target

The immediate render target is set on the graphics device. After changing the render target, we update the information in the render context:

C#
graphicsDevice.SetRenderTarget(intermediateRenderTarget);
context.RenderTarget = intermediateRenderTarget;
context.Viewport = graphicsDevice.Viewport;

All subsequent render operations (rendering meshes, sky and billboards) draw into the intermediate render target.

Then we call the post-processors:

C#
context.SourceTexture = intermediateRenderTarget;
context.RenderTarget = screenRenderTarget;
context.Viewport = screenViewport;
_postProcessors.Process(context);
context.SourceTexture = null;

The post-processors read the intermediate render target and write the result into the final render target.

At the end of OnRender, we return the render target to the pool. The render target will be reused in the next frame. (Alternatively, we could also call intermediateRenderTarget.Dispose() to free the memory. This is very important. If we do not recycle or dispose our render targets after use, we could run into out-of-memory problems!)

It is also good practice to restore any information that was changed in the render context.

What happens if we run the game? We get a ContentLoadException: Error loading "DigitalRune\XXX". File not found.

Tutorial-01-23

We will fix this in the next step.