A Gentle Introduction to DirectX Raytracing 5

引言

本节关于环境光遮蔽。从 G 缓冲区开始,追踪每个像素一条 AO 射线。

In Tutorial 4, we walked through our first example of spawning rays in DirectX and the new HLSL shader types needed to control them. This tutorial combines the rasterized G-Buffer we created in Tutorial 3 and then launches ambient occlusion rays from the locations of pixels in that G-buffer.

教程 4 中,我们演示了 DirectX 中生成光线的第一个示例以及控制它们所需的新 HLSL 着色器类型。本教程结合了我们在 教程 3 中创建的栅格化 G 缓冲区,然后从该 G 缓冲区中的像素位置启动环境光遮蔽光线。

(译者注:关于 AO 有一些比较通俗易懂的补充理解贴在下面。)

这个 AO 其实就是美术那边经常提到的 AO 贴图,当时被设计出来模拟全局光照的,现在的机子性能已经足以可以实时计算 AO。那它是怎么计算的?

首先用半球罩住,然后从半球发射无数条射线,如果直接可以反射回半球的我们就给它白色,如果因为遮挡反弹两次才回到半球我们就给它一个比较暗的颜色,反弹次数越多,得到的像素就越暗,如果没有返回到光源半球的光线,我们就给它一个黑色。

这是一种计算方式,当然还要其他的计算方式,光源可以不是半球,基本的思路差不多是这样。

Our Basic Ambient Occlusion Pipeline

If you open up Tutor05-AmbientOcclusion.cpp, you will find our new pipeline combines the SimpleGBufferPass from Tutorial 3 with the new AmbientOcclusionPass we’ll build below.

如果您打开 Tutor05-AmbientOcclusion.cpp,您会发现我们的新管道将 教程3 中的 SimpleGBufferPass与我们将在下面构建的新 AmbientOcclusionPass 相结合。

// Create our rendering pipeline
RenderingPipeline *pipeline = new RenderingPipeline();
pipeline->setPass(0, SimpleGBufferPass::create());
pipeline->setPass(1, AmbientOcclusionPass::create());

This pipeline is a hybrid rasterization and ray tracing pipeline, as it uses a standard rasterization pass to defer rendering, saving our primary hit points in a G-buffer and shading them in a later pass. In this example, our later pass will shade these rasterized points using ray traced ambient occlusion.

此渲染管线是混合光栅化和光线追踪管道,因为它使用标准光栅化通道来延迟渲染,将主要命中点保存在 G 缓冲区中,并在之后的管线中对其进行着色。在此示例中,我们后面的管线将使用光线追踪环境光遮蔽对这些栅格化点进行着色。

Launching DirectX Ambient Occlusion Rays From a G-Buffer

Continue by looking in AmbientOcclusionPass.h. This should look familiar, as the boilerplate is nearly identical to that from RayTracedGBufferPass in Tutorial 4. The major difference is our member variables to control the ambient occlusion:

继续查看 AmbientOcclusionPass.h。这应该看起来很熟悉,因为样板几乎与 教程4 中的 RayTracedGBufferPass 相同。主要区别在于我们用于控制环境光遮蔽的成员变量:

float     mAORadius = 0.0f;    // Our ambient occlusion radius
uint32_t mFrameCount = 0; // Used for unique random seeds each frame

Ambient occlusion gives an approximation of ambient lighting and shadowing due to nearby geometry at each pixel. Usually, ambient occlusion is limited to nearby geometry by having a radius that defines “nearby geometry.” In our rendering, this is controlled by mAORadius.

环境光遮蔽提供了由于每个像素处的附近几何形状而导致的环境光照和阴影的近似值。通常,环境光遮蔽仅限于附近的几何体,因为它具有定义”附近几何体”的半径。在我们的渲染中由 mAORadius 控制。

Ambient occlusion is a stochastic effect, so we’ll need a random number generator to select our rays. Our shader uses a pseudo random number generator to generate random samples, and to avoid generating the same rays every frame we need to update our random seeds each frame. We do this by hashing the pixel location and a frame count to initialize our random generator.

环境光遮蔽是一种 随机效应,因此我们需要一个随机数发生器来选择我们的光线。我们的着色器使用 伪随机数生成器 来生成随机样本,并且为了避免每帧生成相同的光线,我们需要在每帧更新随机种子。我们通过散列像素位置和帧计数来初始化我们的随机生成器来做到这一点。

Initializing our Ambient Occlusion Pass

Our AmbientOcclusionPass::initialize() is fairly straightforward:

我们的 AmbientOcclusionPass::initialize() 相当简单:

bool AmbientOcclusionPass::initialize(RenderContext::SharedPtr pRenderContext, 
ResourceManager::SharedPtr pResManager)
{
// Stash a copy of our resource manager; request needed buffer resources
mpResManager = pResManager;
mpResManager->requestTextureResources( {"WorldPosition", "WorldNormal",
ResourceManager::kOutputChannel });

// Create our wrapper for our DirectX Raytracing launch.
mpRays = RayLaunch::create("aoTracing.hlsl", "AoRayGen");
mpRays->addMissShader("aoTracing.hlsl", "AoMiss");
mpRays->addHitShader("aoTracing.hlsl", "AoClosestHit", "AoAnyHit");

// Compile our shaders and pass in our scene
mpRays->compileRayProgram();
mpRays->setScene(mpScene);
return true;
}

We first request our rendering resources. We need our G-buffer fields "WorldPosition" and "WorldNormal" as inputs and we’re outputting a displayable color to kOutputChannel.

我们首先请求渲染资源。我们需要我们的 G 缓冲区字段"WorldPosition""WorldNormal"作为输入,并且我们将可显示的颜色输出到 kOutputChannel

Then we create a ray tracing wrapper mpRays that starts tracing rays at in the ray generation shader AoRayGen() in the file aoTracing.hlsl and has one ray type with shaders AoMiss, AoAnyHit, and AoClosestHit.

然后,我们创建一个光线追踪包装器 mpRays,该包装器在文件 aoTracing.hlsl 中的光线生成着色器 AoRayGen() 中开始追踪光线,并具有一种带有着色器 AoMissAoClosestHitAoAnyHit

// Create our wrapper for our DirectX Raytracing launch.
mpRays = RayLaunch::create("aoTracing.hlsl", "AoRayGen");
mpRays->addMissShader("aoTracing.hlsl", "AoMiss");
mpRays->addHitShader("aoTracing.hlsl", "AoClosestHit", "AoAnyHit");

Launching Ambient Occlusion Rays

Now that we initialized our rendering resources, we can shoot our ambient occlusion rays. Let’s walk through the AmbientOcclusionPass::execute pass below:

现在,我们初始化了渲染资源,可以发射环境光遮蔽光线了。让我们浏览一下 AmbientOcclusionPass::execute

void AmbientOcclusionPass::execute(RenderContext::SharedPtr pRenderContext)
{
// Get our output buffer; clear it to black.
auto dstTex = mpResManager->getClearedTexture(ResourceManager::kOutputChannel,
vec4(0.0f, 0.0f, 0.0f, 0.0f));

// Set our shader variables for our ambient occlusion shader
auto rayGenVars = mpRays->getRayGenVars();
rayGenVars["RayGenCB"]["gFrameCount"] = mFrameCount++;
rayGenVars["RayGenCB"]["gAORadius"] = mAORadius;
rayGenVars["RayGenCB"]["gMinT"] = mpResManager->getMinTDist();
rayGenVars["gPos"] = mpResManager->getTexture("WorldPosition");
rayGenVars["gNorm"] = mpResManager->getTexture("WorldNormal");
rayGenVars["gOutput"] = dstTex;

// Shoot our AO rays
mpRays->execute( pRenderContext, mpResManager->getScreenSize() );
}

First, we grab our output buffer and clear it to black.

首先,我们获取输出缓冲区并将其清除为黑色。

// Get our output buffer; clear it to black.
auto dstTex = mpResManager->getClearedTexture(ResourceManager::kOutputChannel,
vec4(0.0f, 0.0f, 0.0f, 0.0f));

Then we pass our G-buffer inputs, output texture, and user-controllable variables down to the ray generation shader. Unlike Tutorial 4, we do not send variables to either the miss shader or closest hit shader. Instead, our variables will only be visibile in the ray generation shader.

然后,我们将 G 缓冲区输入、输出纹理和用户可控制的变量传递到光线生成着色器。与教程 4 不同,我们不会将变量发送到未命中着色器(miss shader)或最接近命中着色器(closest shader)。相反,我们的变量仅在光线生成着色器中可见。

rayGenVars["RayGenCB"]["gFrameCount"]  = mFrameCount++;
rayGenVars["RayGenCB"]["gAORadius"] = mAORadius;
rayGenVars["RayGenCB"]["gMinT"] = mpResManager->getMinTDist();
rayGenVars["gPos"] = mpResManager->getTexture("WorldPosition");
rayGenVars["gNorm"] = mpResManager->getTexture("WorldNormal");
rayGenVars["gOutput"] = dstTex;

We pass down our mFrameCount to seed our per-pixel random number generator, our user-controllable mAORadius to change the amount of occlusion seen, and a minimum t value to use when shooting our rays. This gMinT variable will control how much bias to add at the beginning of our rays to avoid self-intersection on surfaces.

我们传入 mFrameCount 来设定每像素随机数生成器的种子,用户控制 mAORadius 来更改看到的遮挡量以及拍摄光线时要使用的最小 t 值。这个 gMinT 变量将控制在光线开始时添加多少偏差,以避免在表面上自交。

rayGenVars["RayGenCB"]["gFrameCount"]  = mFrameCount++;
rayGenVars["RayGenCB"]["gAORadius"] = mAORadius;
rayGenVars["RayGenCB"]["gMinT"] = mpResManager->getMinTDist();

We also pass down our input textures gPos and gNorm from our prior G-buffer pass and our default pipeline output to gOutput.

我们还将输入纹理 gPosgNorm 从之前的 G 缓冲区管线传递,并将默认管线输出传递到 gOutput

rayGenVars["gPos"]    = mpResManager->getTexture("WorldPosition");
rayGenVars["gNorm"] = mpResManager->getTexture("WorldNormal");
rayGenVars["gOutput"] = dstTex;

DirectX Raytracing for Ambient Occlusion

Let’s start by reading our ambient occlusion shader’s ray generation shader AoRayGen():

让我们开始深入环境光遮蔽着色器的光线生成着色器 AoRayGen() :

cbuffer RayGenCB {
float gAORadius, gMinT;
uint gFrameCount;
}

Texture2D<float4> gPos, gNorm;
RWTexture2D<float4> gOutput;

[shader("raygeneration")]
void AoRayGen()
{
// Where is this thread's ray on screen?
uint2 pixIdx = DispatchRaysIndex();
uint2 numPix = DispatchRaysDimensions();

// Initialize a random seed, per-pixel and per-frame
uint randSeed = initRand( pixIdx.x + pixIdx.y * numPix.x, gFrameCount );

// Load the position and normal from our g-buffer
float4 worldPos = gPos[pixIdx];
float3 worldNorm = gNorm[pixIdx].xyz;

// Default ambient occlusion value if we hit the background
float aoVal = 1.0f;

// worldPos.w == 0 for background pixels; only shoot AO rays elsewhere
if (worldPos.w != 0.0f)
{
// Random ray, sampled on cosine-weighted hemisphere around normal
float3 worldDir = getCosHemisphereSample(randSeed, worldNorm);

// Shoot our ambient occlusion ray and update the final AO value
aoVal = shootAmbientOcclusionRay(worldPos.xyz, worldDir, gMinT, gAORadius);
}

gOutput[pixIdx] = float4(aoVal, aoVal, aoVal, 1.0f);
}

First, we discover what pixel we’re at and how big our ray launch is. We use this data to initialize our psuedo random number generator (see tutorial code for details on initRand()) and to load the surfaces stored in our G-buffer (i.e., in gPos and gNorm).

首先,我们发现了我们所处的像素以及光线发射有多大。我们使用这些数据来初始化我们的伪随机数生成器(有关 initRand() 的详细信息,请参阅教程代码),并加载存储在 G 缓冲区中的表面(即 gPosgNorm 中)).

// Where is this thread's ray on screen?
uint2 pixIdx = DispatchRaysIndex();
uint2 numPix = DispatchRaysDimensions();

// Initialize a random seed, per-pixel and per-frame
uint randSeed = initRand( pixIdx.x + pixIdx.y * numPix.x, gFrameCount );

// Load the position and normal from our g-buffer
float4 worldPos = gPos[pixIdx];
float3 worldNorm = gNorm[pixIdx].xyz;

// Default ambient occlusion value if we hit the background
float aoVal = 1.0f;

(译注:这里放下 initrand() 函数的定义)

uint initRand(uint val0, uint val1, uint backoff = 16)
{
uint v0 = val0, v1 = val1, s0 = 0;

[unroll]
for (uint n = 0; n < backoff; n++)
{
s0 += 0x9e3779b9;
v0 += ((v1 << 4) + 0xa341316c) ^ (v1 + s0) ^ ((v1 >> 5) + 0xc8013ea4);
v1 += ((v0 << 4) + 0xad90777d) ^ (v0 + s0) ^ ((v0 >> 5) + 0x7e95761e);
}
return v0;
}

Next we need to test if our G-buffer actually stores geometry or it the pixel represents background. (Our G-buffer stores this information in the alpha channel of the position: 0.0f means a background pixel, other values represent valid hit points.) For background pixels, our default AO value will be 1.0f representing fully lit.

接下来,我们需要测试我们的 G 缓冲区是否实际存储几何体,或者像素代表背景。(我们的 G 缓冲区将此信息存储在位置的 Alpha 通道中 0.0f 表示背景像素,其他值表示有效命中点。对于背景像素,我们的默认 AO 值将为 1.0f 表示完全亮起。

For pixels containing valid G-buffer hits, getCosHemisphereSample() computes a random ray (see code for details or various places online for the math of cosine-weighted sampling).

对于包含有效 G 缓冲区命中的像素 getCosHemisphereSample() 计算随机射线(有关详细信息,请参阅代码,或在线各个资源了解余弦加权采样的数学)。

// worldPos.w == 0 for background pixels; only shoot AO rays elsewhere
if (worldPos.w != 0.0f)
{
// Random ray, sampled on cosine-weighted hemisphere around normal
float3 worldDir = getCosHemisphereSample(randSeed, worldNorm);

// Shoot our ambient occlusion ray and update the final AO value
aoVal = shootAmbientOcclusionRay(worldPos.xyz, worldDir, gMinT, gAORadius);
}

gOutput[pixIdx] = float4(aoVal, aoVal, aoVal, 1.0f);

(译注:这里放下 getCosHemisphereSample() 函数的定义)

// Get a cosine-weighted random vector centered around a specified normal direction.
float3 getCosHemisphereSample(inout uint randSeed, float3 hitNorm)
{
// Get 2 random numbers to select our sample with
float2 randVal = float2(nextRand(randSeed), nextRand(randSeed));

// Cosine weighted hemisphere sample from RNG
float3 bitangent = getPerpendicularVector(hitNorm);
float3 tangent = cross(bitangent, hitNorm);
float r = sqrt(randVal.x);
float phi = 2.0f * 3.14159265f * randVal.y;

// Get our cosine-weighted hemisphere lobe sample direction
return tangent * (r * cos(phi).x) + bitangent * (r * sin(phi)) + hitNorm.xyz * sqrt(1 - randVal.x);
}

We trace our ambient occlusion ray by calling our helper function shootAmbientOcclusionRay():

我们通过调用我们的帮助程序函数 shootAmbientOcclusionRay() 遍历环境光遮蔽:

struct AORayPayload {
float aoVal; // Stores 0 on a ray hit, 1 on ray miss
};

float shootAmbientOcclusionRay( float3 orig, float3 dir,
float minT, float maxT )
{
// Setup AO payload. By default, assume our AO ray will *hit* (value = 0)
AORayPayload rayPayload = { 0.0f };

// Describe the ray we're shooting
RayDesc rayAO = { orig, minT, dir, maxT };

// We're going to tell our ray to never run the closest-hit shader and to
// stop as soon as we find *any* intersection
uint rayFlags = RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH |
RAY_FLAG_SKIP_CLOSEST_HIT_SHADER;

// Trace our ray.
TraceRay(gRtScene, rayFlags, 0xFF, 0, 1, 0, rayAO, rayPayload );

// Return our AO value out of the ray payload.
return rayPayload.aoVal;
}

This helper function sets up our RayDesc with the minimum t to avoid self intersection and the specified maximum t (of gAoRadius). It creates a ray payload that will toggle between 1.0f (it no intersection occurs) and 0.0f (if we hit a surface), then we trace a ray that never executes any closet hit shaders and stops as soon as it gets any confirmed intersections.

此帮助程序函数使用最小 t 设置 RayDesc 以避免自交和指定的最大t(gAoRadius)。 它创建了一个光线有效载荷,该有效载荷将在 1.0f(没有发生交叉点)和 0.0f(如果我们击中表面)之间切换,然后我们跟踪一条永远不会执行任何命中着色器的射线,并在获得任何确认的交叉点后立即停止。

We only have one hit group and one miss shader specified, so the 4th and 6th parameters to TraceRay() must be 0 and the 5th parameter must be 1.

我们只指定了一个命中组和一个未命中着色器,因此 TraceRay()的第 4 个和第 6 个参数必须为 0,第 5 个参数必须为 1。

// Trace our ray. 
TraceRay(gRtScene, rayFlags, 0xFF, 0, 1, 0, rayAO, rayPayload );

Our miss and hit shaders for AO rays are extremely simple:

我们针对AO射线的未命中和命中着色器非常简单:

[shader("miss")]
void AoMiss(inout AORayPayload rayData)
{
rayData.aoVal = 1.0f;
}

When our ambient occlusion ray misses, we need to toggle our payload value to store 1.0f, which is the value representing illuminated.

当我们的环境光遮蔽光线丢失时,我们需要切换有效载荷值以存储 1.0f 这是表示照明的值。

[shader("anyhit")]
void AoAnyHit(inout AORayPayload rayData, BuiltinIntersectionAttribs attribs)
{
if (alphaTestFails(attribs)) IgnoreHit();
}

When we have a potential hit, make sure it is valid by checking if there is an alpha mask and if the hit fails the alpha test.

当我们有潜在的命中时,通过检查是否存在 alpha 掩码以及命中是否未通过 alpha 测试来确保它是有效的。

[shader("closesthit")]
void AoClosestHit(inout AORayPayload, BuiltinIntersectionAttribs)
{
}

Out closest hit shader is never executed, so it can be a trivial no-op. Alternatively, this function could be removed from the shader and a null closest-hit shader specified in AmbientOcclusionPass::initialize().

最接近的命中着色器永远不会执行,因此它可能是一个微不足道的 no-op。或者,可以从着色器中删除此函数,并在 AmbientOcclusionPass::initialize() 中指定一个 null 最接近的着色器。.

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 launch DirectX rays from a previously rasterized G-buffer
  • Write basic ray tracing HLSL shaders to output ambient occlusion

希望本教程能够演示:

  • 如何从先前光栅化的 G 缓冲区启动 DirectX 光线
  • 编写基本光线追踪 HLSL 着色器以输出环境光遮蔽

When you are ready, continue on to Tutorial 6, which accumulates ambient occlusion samples over time so you can get ground-truth ambient occlusion rather than noisy one sample per pixel results.

准备就绪后,请继续学习教程 6,该教程会随时间累积环境光遮蔽样本,以便您可以获得真实环境光遮蔽,而不是每个像素一个样本的噪声结果。

(译注:以下是补充展示:)