**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