**DirectX Raytracing, Tutorial 6** **Adding a simple temporal accumulation pass** In [Tutorial 5](../Tutor5/tutorial05.md.html), we built a hybrid ray tracer that generates a rasterized [G-Buffer](https://en.wikipedia.org/wiki/Deferred_shading) and then in a second pass shoots rays into the environment to discover nearby occluders. However, for most [stochastic](https://en.wikipedia.org/wiki/Stochastic) algorithms our random rays introduce a lot of noise. The easiest way to reduce the noise is to improve our approximation by firing more rays -- either all at once, or by accumulating them over multiple frames. In this demo, we are going to create a new *`RenderPass`* that keeps internal state to average multiple frames to get a high quality ambient occlusion rendering. We will reuse this *`SimpleAccumulationPass`* in most of our subsequent tutorials to allow generation of very high quality images. Our Improved Temporal Accumulation Rendering Pipeline ================================================================================= If you open up *`Tutor06-TemporalAccumulation.cpp`*, you will find our new pipeline combines the *`SimpleGBufferPass`* from [Tutorial 3](../Tutor3/tutorial03.md.html), the *`AmbientOcclusionPass`* from [Tutorial 5](../Tutor5/tutorial05.md.html) and the new *`SimpleAccumulationPass`* we'll build below. ~~~~~~~~~~~~~~~~~~~~ C // Create our rendering pipeline RenderingPipeline *pipeline = new RenderingPipeline(); pipeline->setPass(0, SimpleGBufferPass::create()); pipeline->setPass(1, AmbientOcclusionPass::create()); pipeline->setPass(2, SimpleAccumulationPass::create(ResourceManager::kOutputChannel)); ~~~~~~~~~~~~~~~~~~~~ This *`SimpleAccumulationPass`* takes as input the shared texture to accumulate. Logically, this pipeline (a) renders a G-buffer, (b) computes ambient occludion, storing the output in *`kOutputChannel`*, then (c) accumulates the image in *`kOutputChannel`* with the renderings from prior frames and outputs the accumulate result back to *`kOutputChannel`*. Of course, our *`SimpleAccumulationPass`* is a bit more sophisticated, as it allows clearing the accumulation when you move the camera or make other changes to program settings. Accumulating Images Temporally ================================================================================= Continue by looking in *`SimpleAccumulationPass.h`*. Unlike the past few demos, this pass does not require any ray tracing. Instead, we use rasterization over the full screen (i.e., the *`FullscreenLaunch`* wrapper) to do our temporal accumulation. Thus, this pass is closer in appearance to our sinusoid rendering in [Tutorial 2](../Tutor2/tutorial02.md.html). Hopefully, the *`RenderPass`* declaration boilerplate is starting to look familiar. There are a couple key changes. For instance, this pass overrides a few new *`RenderPass`* methods: ~~~~~~~~~~~~~~~~~~~~C void resize(uint32_t width, uint32_t height) override; void stateRefreshed() override; ~~~~~~~~~~~~~~~~~~~~ The *`resize()`* callback gets executed when the screen resolution changes. Since our accumulation pass averages results over multiple frames, when the resolution changes we need to update our internal storage to match the new frame size. The *`stateRefreshed()`* callback gets executed whenever any other pass notes that its settings have changed. In this case, our image will likely change significantly and we should reset our accumulated result. We also store various new internal data: ~~~~~~~~~~~~~~~~~~~~C Texture::SharedPtr mpLastFrame; // Our accumulated result uint32_t mAccumCount = 0; // Total frames have we accumulated Scene::SharedPtr mpScene; // What scene are we using? mat4 mpLastCameraMatrix; // The last camera matrx Fbo::SharedPtr mpInternalFbo; // A temp framebuffer for rendering ~~~~~~~~~~~~~~~~~~~~ In particular, we need a place to store our accumulated color from prior frames (we put that in *`mpLastFrame`*) and a count of how many frames we have accumulated (in *`mAccumCount`*). To avoid accumulating during motion (which gives an ugly smeared look), we need to detect when the camera moves, so we can stop accumulating. We do this by remember the scene and comparing the current camera state with last frame (*`mpScene`* and *`mpLastCameraMatrix`*). We also need a framebuffer object for our full-screen raster pass, and we create one on initialization (in *`mpInternalFbo`*) and reuse it every frame. Initializing our Accumulation Pass ------------------------------ Our *`SimpleAccumulationPass::initialize()`* is straightforward: ~~~~~~~~~~~~~~~~~~~~ C bool SimpleAccumulationPass::initialize(RenderContext::SharedPtr pRenderContext, ResourceManager::SharedPtr pResManager) { // Stash a copy of our resource manager; request needed buffer resources mpResManager = pResManager; mpResManager->requestTextureResource( mAccumChannel ); // Pass input // Create our graphics state and an accumulation shader mpGfxState = GraphicsState::create(); mpAccumShader = FullscreenLaunch::create("accumulate.ps.hlsl"); return true; } ~~~~~~~~~~~~~~~~~~~~ First we ask to access the texture we're going to accumulate into. This is the channel name passed as input to our pass constructor in *`Tutor06-TemporalAccumulation.cpp`*. Then we create a rasterization pipeline graphics state and our wrapper for our fullscreeen accumulation shader. Accumulating Current Frame With Prior Frames ------------------------------ Now that we initialized our rendering resources, we can shoot do temporal accumulation. Let's walk through the *`SimpleAccumulationPass::execute`* pass below: ~~~~~~~~~~~~~~~~~~~~ C void SimpleAccumulationPass::execute(RenderContext::SharedPtr pRenderContext) { // Get our output buffer; clear it to black. auto accumTex = mpResManager->getTexture(mAccumChannel); // If camera moved, reset accumulation if (hasCameraMoved()) { mAccumCount = 0; mLastCameraMatrix = mpScene->getActiveCamera()->getViewMatrix(); } // Set our shader variables for our accumulation shader auto shaderVars = mpAccumShader->getVars(); shaderVars["PerFrameCB"]["gAccumCount"] = mAccumCount++; shaderVars["gLastFrame"] = mpLastFrame; // Running accumulation total shaderVars["gCurFrame"] = accumTex; // Current frame to accumulate // Execute the accumulation shader mpGfxState->setFbo( mpInternalFbo ); mpAccumShader->execute( pRenderContext, mpGfxState ); // Copy accumulated result (to output and our temporary buffer) auto outputTex = mpInternalFbo->getColorTexture(0); pRenderContext->blit( outputTex->getSRV(), accumTex->getRTV() ); pRenderContext->blit( outputTex->getSRV(), mpLastFrame->getRTV() ); } ~~~~~~~~~~~~~~~~~~~~ First, we grab our input texture that we're accumulating. Next, we check if we need to reset our accumulation due to any camera movement in the last frame. See the *`hasCameraMoved()`* utility below. Then we run a simple accumulation shader that takes our prior accumulated result, our current frame, and combines them (with appropriate weights) to get the new average considering the additional frame of input. Finally, we copy our accumulated result back to our output buffer and our temporary buffer (that keeps our running total for next frame). Note, we use a Falcor utility *`blit()`* to do this copy. *_SRV_* means *_shader resource view_*, which is a DirectX term meaning roughly "a texture accessor used for reading in our shader". *_RTV_* means *_render target view_*, which is a DirectX term meaning roughly "a texture accessor used to write to this texture as output". Resetting Accumulation ------------------------------ When do we need to stop accumulating with prior frames and instead resetart our accumulation from scratch? We identify three cases we want to handle: * Whenever the camera moves * Whenever the screen is resized * Whenever our *`RenderPasses`* in our pipline tell us they have changed state. The camera motion is handled in *`SimpleAccumulationPass::execute`*, with a little help from the following utility function: ~~~~~~~~~~~~~~~~~~~~C bool SimpleAccumulationPass::hasCameraMoved() { // No scene? No camera? Then the camera hasn't moved. if (!mpScene || !mpScene->getActiveCamera()) return false; // Check: Has our camera moved? return (mpLastCameraMatrix != mpScene->getActiveCamera()->getViewMatrix()); } ~~~~~~~~~~~~~~~~~~~~ We can reset accumulation when the window is resized by creating a *`resize()`* callback, which is explicitly called when the window changes size. This *`resize()`* callback is also called on initialization, since the screen dimensions change from 0 pixels to the actual size. ~~~~~~~~~~~~~~~~~~~~C bool SimpleAccumulationPass::resize(uint32_t width, uint32_t height) { // Resize our internal temporary buffer mpLastFrame = Texture::create2D(width, height, ResourceFormat::RGBA32Float, 1, 1, nullptr, ResourceManager::kDefaultFlags); // Recreate our framebuffer with the new size mpInternalFbo = ResourceManager::createFbo(width, height, ResourceFormat::RGBA32Float); // Force accumulation to restart by resetting the counter mAccumCount = 0; } ~~~~~~~~~~~~~~~~~~~~ This resizes our two internal resources (the texture *`mpLastFrame`* and the framebuffer *`mpInternalFbo`*). We use a Falcor utility to create our texture (the last 4 parameters specify how many array slices and mip levels the texture has, what data it is initialized with, and how it can be bound for rendering). We use our resource manager to create our framebuffer object. Finally we reset the *`mAccumCount`* which is actually what ensures we don't reuse any samples from last frame. Our last important C++ class method is *`stateRefreshed()`*, which is a callback that gets executed when any other *`RenderPasses`* call their *`setRefreshFlag()`* method. This pair of functions is a simple way for passes to communicate: *`setRefreshFlag()`* says "I just changed state significantly" and *`stateRefreshed()`* allows other passes to process this. ~~~~~~~~~~~~~~~~~~~~C void SimpleAccumulationPass::stateRefreshed() { mAccumCount = 0; } ~~~~~~~~~~~~~~~~~~~~ In this case, our *`stateRefreshed()`* callback is very simple... it just resets accumulation by setting *`mAccumCount`* back to zero. DirectX Accumulation Shader ============================================================================== Our accumulation shader is extremely simple: ~~~~~~~~~~~~~~~~~~~~~C cbuffer PerFrameCB { uint gAccumCount; // How many frames have we already accumulated? } Texture2D gLastFrame, gCurFrame; // Last and current frame inputs float4 main(float2 texC : TEXCOORD, float4 pos : SV_Position) : SV_Target0 { uint2 pixelPos = (uint2)pos.xy; // Where is this pixel on screen? float4 curColor = gCurFrame[pixelPos]; // Pixel color this frame float4 prevColor = gLastFrame[pixelPos]; // Pixel color last frame // Do a weighted sum, weighing last frame's color based on total count return (gAccumCount * prevColor + curColor) / (gAccumCount + 1); } ~~~~~~~~~~~~~~~~~~~~~ This shader is simply a weighted sum. Instead of averaging this frame with the last frame, it weights last frame based on the total number of samples it contains. (The current frame always gets a weight of 1.) What Does it Look Like? =============================================================================== That covers the important points of this tutorial. When running, you get the following result: ![Result of running Tutorial 6, after loading the scene "pink_room.fscene"](Tutor06-Output.png) Hopefully, this tutorial demonstrated: * How to use a full-screen pass to run a simple post processing pass * Accumulate multiple frames together temporally * Use some of the more advanced callbacks in the *`RenderPass`* class. When you are ready, continue on to [Tutorial 7](../Tutor7/tutorial07.md.html), which introduces a camera jitter. When combined with the temporal accumulation from this tutorial, this allows you to get high quality antialiasing to avoid the jaggies seen in the image above.