A Gentle Introduction to DirectX Raytracing 6

引言

本节将继续完善环境光遮蔽。随时间累积帧,为 AO 提供每像素任意多条光线。

In Tutorial 5, we built a hybrid ray tracer that generates a rasterized G-Buffer and then in a second pass shoots rays into the environment to discover nearby occluders. However, for most 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.

教程5中,我们构建了一个混合光线追踪器,它生成一个栅格化的G-Buffer,然后在第二次传递中将光线射入环境以发现附近的遮挡物。然而,对于大多数随机算法我们的随机射线会引入很多噪声。减少噪点的最简单方法是通过发射更多光线来改善我们的近似值 - 一次全部,或者将它们累积到多个帧中。

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.

在此演示中,我们将创建一个新的 RenderPass,它将内部状态保持在平均多个帧,以获得高质量的环境光遮蔽渲染。我们将在大多数后续教程中重用此 SimpleAccumulationPass 以生成非常高质量的图像。

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, the AmbientOcclusionPass from Tutorial 5 and the new SimpleAccumulationPass we’ll build below.

如果您打开 Tutor06-TemporalAccumulation.cpp,您会发现我们的新管道结合了教程3中的SimpleGBufferPass教程5中的 AmbientOcclusionPass 和我们将在下面构建的新 SimpleAccumulationPass

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

这个 SimpleAccumulationPass 将共享纹理作为输入进行累积。从逻辑上讲,此管道(a)渲染出一个 G 缓冲区,(b)计算环境光遮蔽,将输出存储在 kOutputChannel 中,然后(c)将图像与先前帧的渲染一起以 kOutputChannel 为单位累积,并将累积结果输出回 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.

当然,我们的 SimpleAccumulationPass 更复杂一些,因为它允许您在移动相机或对程序设置进行其他更改时清除累积。

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.

继续查看 SimpleAccumulationPass.h。与过去的几个演示不同,此管线不需要任何光线追踪。相反,我们在全屏(即 FullscreenLaunch 包装器)上使用光栅化来执行我们的时间累积。因此,此通道在外观上更接近 教程 2 中的正弦渲染。

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:

RenderPass 声明样板开始看起来很熟悉。这里有几个关键的变化,例如此管线将覆盖一些新的 RenderPass 方法:

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.

resize() 回调在屏幕分辨率更改时执行。由于我们的累积传递是多个帧的结果的平均值,因此当分辨率更改时,我们需要更新内部存储以匹配新的帧大小。

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.

每当任何其他管线注意到其设置已更改时,就会执行 stateRefreshed() 回调。在这种情况下,我们的图像可能会发生重大变化,我们应该重置累积的结果。

We also store various new internal data:

我们还存储各种新的内部数据:

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

特别是,我们需要一个地方来存储从先前帧中累积的颜色(我们将其放在 mpLastFrame 中),并计算我们累积了多少帧(以 mAccumCount 为单位)。).

Texture::SharedPtr  mpLastFrame;         // Our accumulated result
uint32_t mAccumCount = 0; // Total frames have we accumulated

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

为了避免在运动过程中累积(这会产生丑陋的涂抹外观),我们需要检测相机何时移动,以便我们可以停止累积。我们通过记住场景并将当前相机状态与最后一帧 mpScenempLastCameraMatrix 进行比较来做到这一点。).

Scene::SharedPtr    mpScene;             // What scene are we using?
mat4 mpLastCameraMatrix; // The last camera matrx

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.

我们还需要一个帧缓冲器对象用于全屏光栅传递,我们在初始化时创建一个(在 mpInternalFbo 中),并在每帧重用它。

Initializing our Accumulation Pass

Our SimpleAccumulationPass::initialize() is straightforward:

我们的 SimpleAccumulationPass::initialize() 很简单:

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.

首先,我们要求访问我们将要累积的纹理。这是在 Tutor06-TemporalAccumulation.cpp 中作为输入传递给我们的传递构造函数的管线名称.cpp。

// Stash a copy of our resource manager; request needed buffer resources
mpResManager = pResManager;
mpResManager->requestTextureResource( mAccumChannel ); // Pass input

Then we create a rasterization pipeline graphics state and our wrapper for our fullscreeen accumulation shader.

然后,我们创建一个光栅化图形渲染管线和为全屏积累着色器的封装器。

// Create our graphics state and an accumulation shader
mpGfxState = GraphicsState::create();
mpAccumShader = FullscreenLaunch::create("accumulate.ps.hlsl");
return true;

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:

现在我们初始化了渲染资源,我们可以进行时间累积了。让我们通过下面的 SimpleAccumulationPass::execute pass进行演示:

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.

首先,我们获取我们正在积累的输入纹理。

// Get our output buffer; clear it to black.
auto accumTex = mpResManager->getTexture(mAccumChannel);

Next, we check if we need to reset our accumulation due to any camera movement in the last frame. See the hasCameraMoved() utility below.

接下来,我们检查是否需要由于最后一帧中的任何相机移动而重置累积。请参阅下面的 hasCameraMoved() 函数。

// If camera moved, reset accumulation
if (hasCameraMoved()) {
mAccumCount = 0;
mLastCameraMatrix = mpScene->getActiveCamera()->getViewMatrix();
}

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.

最后,我们将累积的结果复制回输出缓冲区和临时缓冲区(保留下一帧的运行总计)。请注意,我们使用 Falcor 内置函数 blit() 来执行此复制。

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”.

SRV 表示 着色器资源视图,这是一个 DirectX 术语,大致意思是”用于在着色器中读取的纹理访问器”。 RTV 表示 渲染目标视图,这是一个 DirectX 术语,大致意思是”用于写入此纹理作为输出的纹理访问器”。

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.

我们什么时候需要停止使用先前帧的累积,而是从头开始重置累积?我们确定了要处理的三种情况:

  • 每当相机移动时
  • 每当调整屏幕大小时
  • 每当我们的 RenderPasses 告诉我们他们已经改变了状态。

The camera motion is handled in SimpleAccumulationPass::execute, with a little help from the following utility function:

摄像机运动在 SimpleAccumulationPass::execute 中处理,并得到以下实用程序函数的一点帮助:

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.

我们可以通过创建 resize() 回调来重置窗口大小时的累积,该回调在窗口更改大小时显式调用。初始化时也会调用此 resize() 回调,因为屏幕尺寸从 0 像素变为实际大小。

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.

这调整了我们的两个内部资源(纹理 mpLastFrame 和framebuffer mpInternalFbo。我们使用 Falcor 实用程序来创建纹理(最后 4 个参数指定纹理具有多少个数组切片和 mip 级别,使用哪些数据进行初始化,以及如何绑定以进行渲染)。我们使用资源管理器来创建帧缓冲区对象。

Finally we reset the mAccumCount which is actually what ensures we don’t reuse any samples from last frame.

最后,我们重置了mAccumCount。这实际上是确保我们不会重用最后一帧中的任何样本的原因。

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++ 类方法是 stateRefreshed(),这是一个当任何其他 RenderPasses 它们的 setRefreshFlag() 方法回调时,它会被执行。这对函数是管线通信的简单方法setRefreshFlag() 表示”我刚刚显著更改了状态”,而 stateRefreshed() 允许其他管线处理此内容。

void SimpleAccumulationPass::stateRefreshed() {
mAccumCount = 0;
}

In this case, our stateRefreshed() callback is very simple… it just resets accumulation by setting mAccumCount back to zero.

在这种情况下,我们的 stateRefreshed() 回调非常简单…它只是通过将 mAccumCount 设置回零来重置累积。

DirectX Accumulation Shader

Our accumulation shader is extremely simple:

我们的累积着色器非常简单:

cbuffer PerFrameCB {
uint gAccumCount; // How many frames have we already accumulated?
}

Texture2D<float4> 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.)

此着色器只是一个加权总和。它不是将此帧与最后一帧求平均,而是根据它包含的样本总数对最后一帧加权(当前帧的权重始终为 1)。

What Does it Look Like?

That covers the important points of this tutorial. When running, you get the following result:

这涵盖了本教程的要点。运行时,您将获得以下结果:

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.

希望本教程能够演示:

  • 如何使用全屏通道运行简单的后处理管线
  • 暂时将多个帧累积在一起
  • 使用 RenderPass 类中的一些更高级的回调。

When you are ready, continue on to Tutorial 7, 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.

准备就绪后,请继续学习教程7,其中引入了相机抖动。当与本教程中的时间累积相结合时,这可以让您获得高质量的抗锯齿,以避免上图中看到的锯齿。