A Gentle Introduction to DirectX Raytracing 3

引言

本节是基础知识,我们将创建基于栅格化的 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 following RenderingPipeline:

如果您打开 Tutor03-RasterGBuffer.cpp,您会发现一个稍微复杂一些的主程序,它定义了以下 RenderingPipeline:

// Create our rendering pipeline
RenderingPipeline *pipeline = new RenderingPipeline();

// Add passes into our pipeline
pipeline->setPass(0, SimpleGBufferPass::create()); // This pass renders a g-buffer for the scene that's loaded
pipeline->setPass(1, CopyToOutputPass::create()); // This pass allows the user to select which g-buffer image to display

Now, there are two render passes: SimpleGBufferPass and CopyToOutputPass. For every frame, these are executed in sequence. First SimpleGBufferPass is executed, and it stores its output in textures managed by our ResourceManager. This allows subsequent passes, like CopyToOutputPass to access and reuse these intermediate results. This structure allows us to build modular and reusable code. In fact, we’ll reuse the SimpleGBufferPass we write here in Tutorial 5, without modification.

现在,有两个渲染通道: SimpleGBufferPassCopyToOutputPass。对于每个帧,这些操作按顺序执行。第一个 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 the kOutputChannel 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 the RenderPasses we wrote in Tutorials 1 and 2. The major difference is in our pass’ member variables:

首先查看 SimpleGBufferPass.h。这应该看起来很熟悉,因为样板与我们在教程1和2中编写的 RenderPasses 几乎相同。主要区别在于我们管线的成员变量:

// Internal pass state
GraphicsState::SharedPtr mpGfxState; ///< Our graphics pipeline state (i.e., culling, raster, blend settings)
Scene::SharedPtr mpScene; ///< A pointer to the scene we're rendering
RasterLaunch::SharedPtr mpRaster; ///< A wrapper managing the shader for our g-buffer creation

As in Tutorial 2, the GraphicsState class encapsulates various DirectX rendering state like the depth, rasterization, blending, and culling settings. The Scene 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 pass mpScene 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 the FullscreenLaunch 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)
{
// Stash a copy of our resource manager so we can get rendering resources
mpResManager = pResManager;

// We need a bunch of textures to store our G-buffer. Ask for a list of them. They all get the same
// format (in this case, the default, RGBA32F) and size (in this case, the default, screen sized)
mpResManager->requestTextureResources({ "WorldPosition", "WorldNormal", "MaterialDiffuse",
"MaterialSpecRough", "MaterialExtraParams" });

// We also need a depth buffer to use when rendering our g-buffer. Ask for one, with appropriate format and binding flags.
mpResManager->requestTextureResource("Z-Buffer", ResourceFormat::D24UnormS8, ResourceManager::kDepthBufferFlags);

// Set the default scene to load
mpResManager->setDefaultSceneName("Data/pink_room/pink_room.fscene");

// Since we're rasterizing, we need to define our raster pipeline state (though we use the defaults)
mpGfxState = GraphicsState::create();

// Create our wrapper for a scene-rasterization pass.
mpRaster = RasterLaunch::createFromFiles(kGbufVertShader, kGbufFragShader);
mpRaster->setScene(mpScene);

return true;
}

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 to kOutputChannel, 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 complex requestTextureResource() 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 constant kDepthBufferFlags 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 our RasterLaunch, 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.
mpRaster = RasterLaunch::createFromFiles(kGbufVertShader, kGbufFragShader);
mpRaster->setScene(mpScene);

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, 
Scene::SharedPtr pScene)
{
// Stash a copy of the scene
mpScene = pScene;

// Update our raster pass wrapper with this scene
if (mpRaster)
mpRaster->setScene(mpScene);
}

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)
{
// Create a framebuffer for rendering. (Should avoid doing each frame)
Fbo::SharedPtr outputFbo = mpResManager->createManagedFbo(
{ "WorldPosition", "WorldNormal", "MaterialDiffuse",
"MaterialSpecRough", "MaterialExtraParams" },
"Z-Buffer" );

// Clear all color buffers to (0,0,0,0), depth to 1, stencil to 0
pRenderContext->clearFbo(outputFbo.get(), vec4(0, 0, 0, 0), 1.0f, 0);

// Rasterize! Note: Falcor will populate many built-in shader variables
mpRaster->execute(pRenderContext, mpGfxState, outputFbo);
}

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 our ResourceManager. (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" is SV_Target0 in our DirectX shader and "MaterialExtraParams" is SV_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
pRenderContext->clearFbo(outputFbo.get(), vec4(0, 0, 0, 0), 1.0f, 0);

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
mpRaster->execute(pRenderContext, mpGfxState, outputFbo);

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 ----
#include "VertexAttrib.h"
__import ShaderCommon;
__import DefaultVS;

VertexOut main(VertexIn vIn)
{
return defaultVS(vIn);
}

Falcor has a default vertex shader called defaultVS that we can use after the __import DefaultVS; (The code is in the file DefaultVS.slang.) This default shader accesses standard scene attributes (see VertexAttrib.h), applies appropriate viewing, animation, and camera matrices, and stores the results into a VertexOut structure (which is also defined in DefaultVS.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 ----
__import Shading; // To get ShadingData structure & shading helper funcs
__import DefaultVS; // To get the VertexOut declaration

struct GBuffer
{
float4 wsPos : SV_Target0; // Specific bindings here determined by the
float4 wsNorm : SV_Target1; // order of buffers in the call to
float4 matDif : SV_Target2; // createManagedFbo() in our method
float4 matSpec : SV_Target3; // SimpleGBufferPass::execute()
float4 matExtra : SV_Target4;
};

GBuffer main(VertexOut vsOut, float4 pos: SV_Position)
{
// A Falcor built-in to extract geometry and material data suitable for
// shading. (See ShaderCommon.slang for the structure and routines)
ShadingData hitPt = prepareShadingData(vsOut, gMaterial, gCamera.posW);

// Dump out our G buffer channels
GBuffer gBufOut;
gBufOut.wsPos = float4(hitPt.posW, 1.f);
gBufOut.wsNorm = float4(hitPt.N, length(hitPt.posW - gCamera.posW) );
gBufOut.matDif = float4(hitPt.diffuse, hitPt.opacity);
gBufOut.matSpec = float4(hitPt.specular, hitPt.linearRoughness);
gBufOut.matExtra = float4(hitPt.IoR,
hitPt.doubleSidedMaterial ? 1.f : 0.f,
0.f, 0.f);
return gBufOut;
}

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 ----
__import Shading; // To get ShadingData structure & shading helper funcs
__import DefaultVS; // To get the VertexOut declaration

struct GBuffer
{
float4 wsPos : SV_Target0; // Specific bindings here determined by the
float4 wsNorm : SV_Target1; // order of buffers in the call to
float4 matDif : SV_Target2; // createManagedFbo() in our method
float4 matSpec : SV_Target3; // SimpleGBufferPass::execute()
float4 matExtra : SV_Target4;
};

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 the VertexOut 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 variable gMaterial) and the current camera position (in Falcor variable gCamera) to extract commonly used data needed for shading. We then store some of this data out into our G-buffer:

  1. Our pixel’s world-space position hitPt.posW
  2. Our pixel’s world-space normal hitPt.N and distance from fragment to the camera.
  3. Our diffuse material (including texture) color and alpha value.
  4. Our specular reflectance and surface roughness.
  5. 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缓冲区中:

  1. 我们像素的世界空间位置 hitPt.posW
  2. 我们的像素的世界空间正常 hitPt.N 和从片断到相机的距离
  3. 我们的漫反射材料(包括纹理)颜色和阿尔法值
  4. 我们的镜面反射率和表面粗糙度
  5. 包含折射率和标志的杂项缓冲区,用于确定着色时表面是否应被视为双面

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 resource kOutputChannel, so another pass is required to generate an image. Our CopyToOutputPass executes the following code when it renders:

如上所述,我们的 SimpleGBufferPass 不会写入共享资源 kOutputChannel,因此需要另一次传递才能生成图像。我们的 CopyToOutputPass 在呈现时执行以下代码:

void CopyToOutputPass::execute(RenderContext::SharedPtr pRenderContext)
{
// Get the Falcor texture we're copying and our output buffer
Texture::SharedPtr inTex = mpResManager->getTexture(mSelectedBuffer);
Texture::SharedPtr outTex = mpResManager->getClearedTexture(
ResourceManager::kOutputChannel,
vec4(0.0, 0.0, 0.0, 0.0));

// If we selected an invalid texture, return.
if (!inTex) return;

// Copy the selected input buffer to our output buffer.
pRenderContext->blit( inTex->getSRV(), outTex->getRTV() );
}

From the ResourceManager we get the texture the user requested and our output buffer. Here we use the method getClearedTexture() 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 in mDisplayableBuffers:

我们允许用户通过 GUI 下拉小部件从 mDisplayableBuffersmSelectedBuffer :

void CopyToOutputPass::renderGui(Gui* pGui)
{
pGui->addDropdown("Displayed", mDisplayableBuffers, mSelectedBuffer);
}

A new method in CopyToOutputPass is pipelineUpdated(), which gets called whenever passes get added or removed from our RenderingPipeline. 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)
{
// If our resource manager changed, stash the new pointer
mpResManager = pResManager;

// Clear our GUI list
mDisplayableBuffers.clear();

// Look through all available texture resources
for (uint32_t i = 0; i < mpResManager->getTextureCount(); i++)
{
// If this one isn't the output buffer, add it to the displayables list
if (i == mpResManager->getTextureIndex(ResourceManager::kOutputChannel))
continue;
mDisplayableBuffers.push_back({int(i), mpResManager->getTextureName(i)});
}
}

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 光线追踪生成光线。