引言
本节是基础知识,我们将创建基于栅格化的 G 缓冲区通道并渲染加载的场景。
As discussed in our tutorial introduction, our goal is to provide a simple infrastructure for getting a DirectX Raytracing application up and running without digging around in low-level API specification documents. Tutorial 3 continues with our sequence covering some infrastructure basics before we get to the meat of implementing a path tracer. If you wish to move on to a tutorial with actual DirectX Raytracing programming, jump ahead to Tutorial 4.
正如我们的教程简介中所讨论的,我们的目标是提供一个简单的基础结构来启动和运行 DirectX Raytracing 应用程序,而无需在低级 API 规范文档中进行挖掘。教程 3 继续我们的教程,介绍一些基础结构基础知识,然后再介绍实现路径跟踪器。如果您希望继续学习包含实际 DirectX 光线追踪编程的教程,请跳到教程 4.
Why Create a G-Buffer?
Tutorial 2 showed you how to use a more complex
RenderPass
to launch a simple HLSL pixel shader. Before moving on to actually using ray tracing in Tutorial 4, we’ll walk through how to interact with Falcor-loaded scene files and create a set of traditional vertex and pixel shaders that run over this geometry during rasterization.
教程 2 向您展示了如何使用更复杂的 RenderPass
启动简单的 HLSL 像素着色器。在教程4中实际使用光线追踪之前,我们将演示如何与 Falcor 加载的场景文件进行交互,并创建一组在栅格化期间在此几何图形上运行的传统顶点和像素着色器。
The shaders we use to demonstrate this will create a G-Buffer that we can use to accelerate ray tracing in later tutorials. In fact, Tutorial 5 uses a hybrid renderer that rasterizes primary visibility and only uses DirectX Raytracing to shoot shadow rays.
我们用于演示这一点的着色器将创建一个G 缓冲区,我们可以在后面的教程中使用它来加速光线追踪。实际上,教程 5使用混合渲染器来栅格化主可见性,并且仅使用 DirectX 光线追踪来拍摄阴影光线。
As an additional benefit, in order to extract the data to populate our G-buffer, we walk through various Falcor shader utilities that allow you to access scene properties like textures and materials.
作为另一个好处,为了提取数据以填充我们的G缓冲区,我们演示了各种 Falcor 着色器应用程序,这些程序允许您访问纹理和材质等场景属性。
A More Complex Rendering Pipeline
If you open up
Tutor03-RasterGBuffer.cpp
, you will find a slightly more complex main program that defines the followingRenderingPipeline
:
如果您打开 Tutor03-RasterGBuffer.cpp
,您会发现一个稍微复杂一些的主程序,它定义了以下 RenderingPipeline
:
// Create our rendering pipeline |
Now, there are two render passes:
SimpleGBufferPass
andCopyToOutputPass
. For every frame, these are executed in sequence. FirstSimpleGBufferPass
is executed, and it stores its output in textures managed by ourResourceManager
. This allows subsequent passes, likeCopyToOutputPass
to access and reuse these intermediate results. This structure allows us to build modular and reusable code. In fact, we’ll reuse theSimpleGBufferPass
we write here in Tutorial 5, without modification.
现在,有两个渲染通道: SimpleGBufferPass
和 CopyToOutputPass
。对于每个帧,这些操作按顺序执行。第一个 SimpleGBufferPass
被执行,并将其输出存储在由我们的 ResourceManager
管理的纹理中。这允许后续的管线如 CopyToOutputPass
访问和重用这些中间结果。这种结构允许我们构建模块化和可重用的代码。实际上,我们将重用我们在教程5中编写的 SimpleGBufferPass
而无需修改。
In this particular tutorial,
SimpleGBufferPass
creates a G-Buffer containing each pixel’s position, surface normal, diffuse color, specular color, and z-buffer.CopyToOutputPass
simply allows the user to select, via the GUI, which of those outputs to show and then copies the appropriate buffer to thekOutputChannel
to display.
在本教程中 SimpleGBufferPass
创建了一个G 缓冲区,其中包含每个像素的位置、表面法线、漫反射颜色、镜面色和 z 缓冲区。 CopyToOutputPass
只是允许用户通过 GUI 选择要显示的输出中的哪一个,然后将适当的缓冲区复制到 kOutputChannel
进行显示。
Handling the Falcor Scene and Launching Rasterization
Start by looking in
SimpleGBufferPass.h
. This should look familiar, as the boilerplate is nearly identical to that from theRenderPasses
we wrote in Tutorials 1 and 2. The major difference is in our pass’ member variables:
首先查看 SimpleGBufferPass.h
。这应该看起来很熟悉,因为样板与我们在教程1和2中编写的 RenderPasses
几乎相同。主要区别在于我们管线的成员变量:
// Internal pass state |
As in Tutorial 2, the
GraphicsState
class encapsulates various DirectX rendering state like the depth, rasterization, blending, and culling settings. TheScene
class encapsultes Falcor’s scene representation. It has a variety of accessor methods to provide access the cameras, lights, geometry, and other details. For these tutorials, we will mostly passmpScene
into our rendering wrappers and let Falcor automatically send the data to the GPU.
如教程 2所示 GraphicsState
类封装了各种 DirectX 呈现状态,如深度、栅格化、混合和剔除设置。Scene
类封装了 Falcor 的场景表示。它具有多种访问器方法以提供对摄像机、灯光、几何体和其他详细信息的访问。对于这些教程,我们主要将 mpScene
传递到我们的渲染封装中,并让 Falcor 自动将数据发送到 GPU。
The
RasterLaunch
is similar to theFullscreenLaunch
class from Tutorial 2, except it encapsulates state for rasterizing complex scene geometry (rather than a screen-aligned quad).
RasterLaunch
类似于教程 2中的 FullscreenLaunch
类,只是它封装了栅格化复杂场景几何体(而不是屏幕对齐的四边形)的状态。
Initializing our G-Buffer Pass
Our
SimpleGBufferPass::initialize()
method is slightly more complex that in our prior passes:
我们的 SimpleGBufferPass::initialize()
方法比我们之前的传递稍微复杂一些:
bool SimpleGBufferPass::initialize(RenderContext* pRenderContext, ResourceManager::SharedPtr pResManager) |
There’s a couple of important things to note here:
- We no longer write to
kOutputChannel
. Thus,SimpleGBufferPass
does not form a complete rendering pipeline. If no subsequent pass uses our intermediate results to write tokOutputChannel
, nothing will appear on screen!- When requesting buffers, the names are unimportant. If a subsequent pass requests access to a buffer with the same name, it will be shared.
- For the
Z-Buffer
, we use a more complexrequestTextureResource()
call. The second parameter specifies the resource format (using a 24 bit depth and 8 bit stencil). We also need to specify how it can be bound, since DirectX depth buffers have different limitations. The constantkDepthBufferFlags
stores good defaults for a depth buffer.- When not using the more complex request, buffers default to RGBA textures using 32-bit floats for each channel. The bind flags default to
kDefaultFlags
, which provide good defaults for all textures that are not used for depth or stencil.
这里有几件重要的事情需要注意:
- 我们不再编写
kOutputChannel
,因此SimpleGBufferPass
不会形成完整的渲染管线。如果没有后续传递使用我们的中间结果写入kOutputChannel
,屏幕上不会显示任何内容! - 请求缓冲区时,名称不重要。如果后续传递请求访问具有相同名称的缓冲区,则该缓冲区将被共享。
- 对于
Z-Buffer
,我们调用更复杂的requestTextureResource()
。第二个参数指定资源格式(使用 24 位深度和 8 位模具)。我们还需要指定如何绑定它,因为 DirectX 深度缓冲区具有不同的限制。常量kDepthBufferFlags
存储深度缓冲区的良好默认值。 - 不使用更复杂的请求时,缓冲区默认为每个通道使用 32 位浮点数的 RGBA 纹理。绑定标志默认为
kDefaultFlags
,这为所有不用于深度或模具的纹理提供了良好的默认值。
We then create our raster wrapper
mpRaster
by pointing it to our vertex and pixel shaders. For ourRasterLaunch
, it needs to know what scene to use. In case our scene has already been loaded prior to initialization, we pass the scene into our wrapper.
然后,我们通过将栅格包装器 mpRaster
指向顶点和像素着色器来创建栅格包装器。对于我们的 RasterLaunch
,它需要知道要使用哪个场景。如果我们的场景在初始化之前已经加载,我们将场景传递到我们的包装器中。
// Create our wrapper for a scene-rasterization pass. |
Handling Scene Loading
Our tutorial application automatically adds a GUI button to allow users to open a scene file. When Falcor loads a scene, all passes have the option to process it by overriding the
RenderPass::initScene()
method:
我们的教程应用程序会自动添加一个 GUI 按钮,以允许用户打开场景文件。当 Falcor 加载场景时,所有管道都可以选择通过重载 RenderPass::initScene()
方法来处理它:
void SimpleGBufferPass::initScene(RenderContext::SharedPtr pRenderContext, |
For our G-buffer class, this is very simple:
- Store a copy of the scene pointer so we can access it later.
- Tell our raster pass that we’re using a new scene.
对于我们的 G 缓冲区类,这非常简单:
- 存储场景指针的副本,以便我们以后可以访问它
- 告知栅格通道我们正在使用新场景
Launching our G-Buffer Rasterization pass
Now that we initialized our rendering resources and loaded our scene file, we can launch our G-buffer rasterization.
现在,我们已初始化渲染资源并加载了场景文件,可以启动 G 缓冲区栅格化了。
void SimpleGBufferPass::execute(RenderContext::SharedPtr pRenderContext) |
First, we need a framebuffer to write the results of our rendering pass. As in Tutorial 2, we call
createManagedFbo()
, albeit with a more complex set of parameters. Again, this creation should not occur once per frame for performance reasons, though here we do for simplicity and clarity.
首先,我们需要一个帧缓冲器来编写渲染通道的结果。如教程 2所示,尽管有一组更复杂的参数,我们还是选择调用 createManagedFbo()
。同样,出于性能原因,这种创建不应该每帧发生一次,尽管我们在这里这样做是为了简单明了。
When calling
createManagedFbo
, the first parameter is a list of names of resources managed by ourResourceManager
. (Note that these buffers were all requested during initialization.) These will be the color buffers in our framebuffer, and are bound in the order specified (so"WorldPosition"
isSV_Target0
in our DirectX shader and"MaterialExtraParams"
isSV_Target4
). The second parameter is the name of the resource to bind as a depth texture.
当调用 createManagedFbo
时,第一个参数是由我们的 ResourceManager
管理的资源名称列表。(请注意,这些缓冲区都是在初始化期间请求的。这些将是我们的帧缓冲器中的颜色缓冲区,并按指定的顺序绑定(因此 "WorldPosition"
在我们的 DirectX 着色器中SV_Target0
和 "MaterialExtraParams"
为 SV_Target4
)。第二个参数是要绑定为深度纹理的资源的名称。
We then clear this newly created framebuffer using a Falcor built-in. This method clears all 5 color buffers to black, clears the depth buffer to 1.0f and the stencil buffer to 0.
然后,我们使用内置的 Falcor 清除这个新创建的帧缓冲器。此方法将所有 5 个颜色缓冲区清除为黑色,将深度缓冲区清除到 1.0f,将模具缓冲区清除为 0。
// Clear g-buffer. Clear colors to black, depth to 1, stencil to 0, but then clear diffuse texture to our bg color |
Finally, we launch our rasterization pass.
execute()
requres the DirectX context, the DirectX graphics state to use, and the framebuffer to store results.
最后,我们启动栅格化管道 execute()
需要 DirectX context,要使用的 DirectX 图形状态以及用于存储结果的帧缓冲区。
// Execute our rasterization pass. Note: Falcor will populate many built-in shader variables |
The DirectX HLSL for Our G-Buffer Rasterization
Our vertex shader appears somewhat cryptic, since we use Falcor utility functions to access the scene data and pass it to our pixel shader appropriately:
我们的顶点着色器看起来有些神秘,因为我们使用 Falcor 应用程序函数来访问场景数据并将其适当地传递到像素着色器:
// ---- gBuffer.vs.hlsl ---- |
Falcor has a default vertex shader called
defaultVS
that we can use after the__import DefaultVS;
(The code is in the fileDefaultVS.slang
.) This default shader accesses standard scene attributes (seeVertexAttrib.h
), applies appropriate viewing, animation, and camera matrices, and stores the results into aVertexOut
structure (which is also defined inDefaultVS.slang
). Note that the__import
lines are not standard HLSL, but rather invoke our framework’s shader preprocessor / special-purpose compiler, Slang.
Falcor 有一个名为 defaultVS
的默认顶点着色器,我们可以 __import DefaultVS;
后使用; (代码位于文件 DefaultVS.slang
中)。此默认着色器访问标准场景属性(请参阅 VertexAttrib.h
)应用合适的视口、动画和摄像机矩阵,并将结果存储到 VertexOut
结构(也在 DefaultVS.slang
中定义)中。请注意 __import
行不是标准的 HLSL,而是调用我们框架的着色器预处理器/专用编译器 Slang.
Fundamentally, this is a very simple vertex shader that applies a few matrices to the vertex positions and normals, but the default shader gracefully handles different scenes geometry that may or may not have any combination of: normals, bitangents, texture coordinates, lightmaps, geometric skinning, plus a few other advanced features.
从根本上说,这是一个非常简单的顶点着色器,它将一些矩阵应用于顶点位置和法线,但默认着色器可以优雅地处理不同的场景几何图形,这些几何体可能具有也可能不具有以下任何组合:法线、双切线、纹理坐标、光照贴图、几何蒙皮以及一些其他高级功能。
The more interesting shader, our pixel shader, follows:
我们更有趣的像素着色器,如下:
// ---- gBuffer.ps.hlsl ---- |
The first couple lines include Falcor built-ins by asking our Slang shader preprocessor to import various common definitions and functions.
前几行包括 Falcor 内置功能,要求我们的 Slang 着色器预处理器导入各种通用定义和函数。
// ---- gBuffer.ps.hlsl ---- |
We then declare the structure of our output framebuffer’s render targets. This needs to match what we specified in the C++ code (when we created our framebuffer via
createManagedFbo
).
然后,我们声明输出帧缓冲器的渲染目标的结构。这需要与我们在C++代码中指定的内容相匹配(当我们通过 createManagedFbo
创建帧缓冲器时)。
Finally, we define our
main
routine for our pixel shader to take in theVertexOut
structure from our vertex shader and outputs to the framebuffer of the approriate format. We start by calling a Falcor built-in that uses our interploated geometry attributes, the scene’s materials (in the Falcor-defined shader variablegMaterial
) and the current camera position (in Falcor variablegCamera
) to extract commonly used data needed for shading. We then store some of this data out into our G-buffer:
- Our pixel’s world-space position
hitPt.posW
- Our pixel’s world-space normal
hitPt.N
and distance from fragment to the camera.- Our diffuse material (including texture) color and alpha value.
- Our specular reflectance and surface roughness.
- A miscellanous buffer containing index-of-refraction and a flag determining if the surface should be considered double-sided when shading.
最后,我们为像素着色器定义了 main
例程,以从顶点着色器接收 VertexOut
结构,并将其输出到适当格式的帧缓冲器。我们首先调用一个 Falcor 内置,该内置函数使用我们的间移几何属性、场景的材质(在 Falcor 定义的着色器变量 gMaterial
中)和当前摄像机位置(在 Falcor 变量 gCamera
中)来提取着色所需的常用数据。然后,我们将其中一些数据存储到我们的G缓冲区中:
- 我们像素的世界空间位置
hitPt.posW
- 我们的像素的世界空间正常
hitPt.N
和从片断到相机的距离 - 我们的漫反射材料(包括纹理)颜色和阿尔法值
- 我们的镜面反射率和表面粗糙度
- 包含折射率和标志的杂项缓冲区,用于确定着色时表面是否应被视为双面
This is an extremely verbose G-buffer, using five 128-bit buffers, which is significantly more than most people would consider reasonable. However, this is done for simplicity and clarity. It should be straightforward to compress this data into a more compact format.
这是一个非常冗长的G缓冲区,使用五个128位缓冲区,这比大多数人认为合理的要多得多。但是这样做是为了简单明了。将此数据压缩为更紧凑的格式应该很简单。
Implementing our CopyToOutputPass
As noted above, our
SimpleGBufferPass
does not write to the shared resourcekOutputChannel
, so another pass is required to generate an image. OurCopyToOutputPass
executes the following code when it renders:
如上所述,我们的 SimpleGBufferPass
不会写入共享资源 kOutputChannel
,因此需要另一次传递才能生成图像。我们的 CopyToOutputPass
在呈现时执行以下代码:
void CopyToOutputPass::execute(RenderContext::SharedPtr pRenderContext) |
From the
ResourceManager
we get the texture the user requested and our output buffer. Here we use the methodgetClearedTexture()
to first clear the output to black before returning it.
从 ResourceManager
中,我们获取用户请求的纹理和输出缓冲区。在这里,我们使用方法 getClearedTexture()
首先将输出清除为黑色,然后再将其返回。
If our input texture is valid, we then copy the input to the output using the Falcor built-in
blit()
. (Blit is an older graphics term that often means copy, or more specifically block image transfer.)
如果我们的输入纹理有效,则使用 Falcor 内置 blit()
将输入复制到输出。(Blit是一个较旧的图形术语,通常意味着复制,或者更具体地说是阻止图像传输。)
We allow the user to select
mSelectedBuffer
via a GUI dropdown widget from the list of options inmDisplayableBuffers
:
我们允许用户通过 GUI 下拉小部件从 mDisplayableBuffers
、mSelectedBuffer
:
void CopyToOutputPass::renderGui(Gui* pGui) |
A new method in
CopyToOutputPass
ispipelineUpdated()
, which gets called whenever passes get added or removed from ourRenderingPipeline
. The idea here is to create a GUI dropdown list containing all possible textures in the resource manager that we can display:
CopyToOutputPass
中的一个新方法是 pipelineUpdated()
,每当在我们的RenderingPipeline
中添加或删除传递时,就会调用它。这里的想法是创建一个 GUI 下拉列表,其中包含资源管理器中可以显示的所有可能的纹理:
void CopyToOutputPass::pipelineUpdated(ResourceManager::SharedPtr pResManager) |
What Does it Look Like?
That covers the important points of this tutorial. Now if you run it, you get a result similar to this:
这涵盖了本教程的要点。现在,如果您运行它,您将获得类似于以下内容的结果:
Hopefully, this tutorial demonstrated:
- How to build pipelines of multiple
RenderPasses
that share resources.- How to access Falcor scenes inside your render passes.
- How to rasterize these scenes using fairly basic HLSL vertex and fragment shaders.
希望本节教程能够演示:
- 如何构建共享资源的多个
RenderPasses
管道 - 如何在渲染通道中访问 Falcor 场景
- 如何使用相当基本的 HLSL 顶点和片段着色器对这些场景进行栅格化
When you are ready, continue on to Tutorial 4, where we finally learn how to spawn rays using DirectX Raytracing.
准备就绪后,请继续学习教程 4,我们终于学会了如何使用 DirectX 光线追踪生成光线。