Step 14: Post-processing |
While we are modifying the graphics screen, we could add post-processing to apply some full-screen image filters.
Some advanced effects are only available for the HiDef graphics profile.
This requires more changes in MyGraphicsScreen.cs:
… using DigitalRune.Graphics.PostProcessing; // NEW … namespace 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:
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:
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:
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.
We will fix this in the next step.