引言
本节将继续完善环境光遮蔽。随时间累积帧,为 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 thisSimpleAccumulationPass
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 theSimpleGBufferPass
from Tutorial 3, theAmbientOcclusionPass
from Tutorial 5 and the newSimpleAccumulationPass
we’ll build below.
如果您打开 Tutor06-TemporalAccumulation.cpp
,您会发现我们的新管道结合了教程3中的SimpleGBufferPass
、教程5中的 AmbientOcclusionPass
和我们将在下面构建的新 SimpleAccumulationPass
。
// Create our rendering pipeline |
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 inkOutputChannel
, then (c) accumulates the image inkOutputChannel
with the renderings from prior frames and outputs the accumulate result back tokOutputChannel
.
这个 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., theFullscreenLaunch
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 newRenderPass
methods:
RenderPass
声明样板开始看起来很熟悉。这里有几个关键的变化,例如此管线将覆盖一些新的 RenderPass
方法:
void resize(uint32_t width, uint32_t height) 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 |
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 (inmAccumCount
).
特别是,我们需要一个地方来存储从先前帧中累积的颜色(我们将其放在 mpLastFrame
中),并计算我们累积了多少帧(以 mAccumCount
为单位)。).
Texture::SharedPtr mpLastFrame; // Our accumulated result |
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
andmpLastCameraMatrix
).
为了避免在运动过程中累积(这会产生丑陋的涂抹外观),我们需要检测相机何时移动,以便我们可以停止累积。我们通过记住场景并将当前相机状态与最后一帧 mpScene
和mpLastCameraMatrix
进行比较来做到这一点。).
Scene::SharedPtr mpScene; // What scene are we using? |
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, |
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 |
Then we create a rasterization pipeline graphics state and our wrapper for our fullscreeen accumulation shader.
然后,我们创建一个光栅化图形渲染管线和为全屏积累着色器的封装器。
// Create our graphics state and an 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:
现在我们初始化了渲染资源,我们可以进行时间累积了。让我们通过下面的 SimpleAccumulationPass::execute
pass进行演示:
void SimpleAccumulationPass::execute(RenderContext::SharedPtr pRenderContext) |
First, we grab our input texture that we’re accumulating.
首先,我们获取我们正在积累的输入纹理。
// Get our output buffer; clear it to black. |
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 |
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() { |
We can reset accumulation when the window is resized by creating a
resize()
callback, which is explicitly called when the window changes size. Thisresize()
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) { |
This resizes our two internal resources (the texture
mpLastFrame
and the framebuffermpInternalFbo
). 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 otherRenderPasses
call theirsetRefreshFlag()
method. This pair of functions is a simple way for passes to communicate:setRefreshFlag()
says “I just changed state significantly” andstateRefreshed()
allows other passes to process this.
我们最后一个重要的 C++ 类方法是 stateRefreshed()
,这是一个当任何其他 RenderPasses
它们的 setRefreshFlag()
方法回调时,它会被执行。这对函数是管线通信的简单方法setRefreshFlag()
表示”我刚刚显著更改了状态”,而 stateRefreshed()
允许其他管线处理此内容。
void SimpleAccumulationPass::stateRefreshed() { |
In this case, our
stateRefreshed()
callback is very simple… it just resets accumulation by settingmAccumCount
back to zero.
在这种情况下,我们的 stateRefreshed()
回调非常简单…它只是通过将 mAccumCount
设置回零来重置累积。
DirectX Accumulation Shader
Our accumulation shader is extremely simple:
我们的累积着色器非常简单:
cbuffer PerFrameCB { |
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,其中引入了相机抖动。当与本教程中的时间累积相结合时,这可以让您获得高质量的抗锯齿,以避免上图中看到的锯齿。