A Gentle Introduction to DirectX Raytracing 12

引言

本节我们将添加单反弹漫反射全局间接照明。

In Tutorial 11, we changed our Lambertian shading pass to stochastically shoot exactly one shadow ray per pixel, rather than shooting one ray per scene light. This demonstrates a very simplistic Monte Carlo integration process, which we will extend in this tutorial to compute one-bounce diffuse global illumination.

教程 11中,我们更改了 Lambertian 着色通道,以随机方式为每个像素精确地拍摄一条阴影光线,而不是为每个场景光拍摄一条光线。这演示了一个非常简单的 Monte Carlo 积分过程,我们将在本教程中对其进行扩展,以计算单次反射漫反射 全局照明

Changes to our C++ Render Pass

As in most of the later tutorials, differences in the C++ RenderPass are fairly minor. In our new SimpleDiffuseGIPass.h, we now add the following methods and variables:

与后面的大多数教程一样,C++RenderPass中的差异非常小。在我们的 new SimpleDiffuseGIPass.h 中,我们现在添加以下方法和变量:

// So we can use an environment map to illuminate the scene
bool usesEnvironmentMap() override { return true; }

// Some user controls allowing the UI to switch on/off certain rays
bool mDoIndirectGI = true;
bool mDoDirectShadows = true;

The major change in SimpleDiffuseGIPass::initialize() occurs because we now plan to shoot two types of rays: shadow rays and indirect bounce rays. This means, we need to define both types when we initialize our RayLaunce wrapper class:

主要变化 SimpleDiffuseGIPass::initialize() 是因为我们现在计划发射两种类型的光线: 阴影光线间接反弹光线。这意味着,我们需要在初始化 RayLaunce 包装类时定义这两种类型:

// Create our ray tracing wrapper and define the ray generation shader
std::string shaderFile = "simpleDiffuseGI.rt.hlsl";
mpRays = RayLaunch::create(shaderFile, "DiffuseRayGen");

// Define our miss shaders
mpRays->addMissShader(shaderFile, "ShadowMiss"); // Miss shader #0
mpRays->addMissShader(shaderFile, "IndirectMiss"); // Miss shader #1

// Define our hit groups (first is #0, second is #1)
mpRays->addHitShader(shaderFile, "ShadowClosest", "ShadowAny");
mpRays->addHitShader(shaderFile, "IndirectClosest", "IndirectAny");

// Finalize pass; compile and attach our scene
mpRays->compileRayProgram();
if (mpScene) mpRays->setScene(mpScene);

The only other changes our C++ code includes sending additional variables down to our DirectX shaders, see SimpleDiffuseGIPass::execute():

我们的 C++ 代码的唯一其他更改包括将额外的变量发送到我们的 DirectX 着色器,请参阅 SimpleDiffuseGIPass::execute()

// Send down our optional UI parameters
auto rayGenVars = mpRays->getRayGenVars();
rayGenVars["RayGenCB"]["gDoIndirectGI"] = mDoIndirectGI;
rayGenVars["RayGenCB"]["gDirectShadow"] = mDoDirectShadows;

// Set an environment map for indirect rays that miss geometry
auto missVars = mpRays->getMissVars(1); // Indirect rays use miss shader #1
missVars["gEnvMap"] = mpResManager->getTexture(ResourceManager::kEnvironmentMap);

HLSL Changes for Diffuse Global Illumination

Open up simpleDiffuseGI.rt.hlsl. Our first change is we define a new set of shaders for our indirect ray. The miss shader is essentially copied from our environment map lookup in Tutorial 10 and the any-hit shader is our standard any-hit shader that performs alpha testing:

打开 simpleDiffuseGI.rt.hlsl。我们的第一个变化是我们为间接光线定义了一组新的着色器。未命中着色器本质上是从教程 10中的环境贴图查找中复制而来的,而任意命中着色器是我们执行alpha 测试的标准任意命中着色器:

// Identical to our environment map code in Tutoral 10
[shader("miss")]
void IndirectMiss(inout IndirectPayload rayData)
{
float2 dims;
gEnvMap.GetDimensions(dims.x, dims.y);
float2 uv = wsVectorToLatLong(WorldRayDirection());
rayData.color = gEnvMap[uint2(uv * dims)].rgb;
}

// Identical to any hit shaders for most other rays we've defined
[shader("anyhit")]
void IndirectAny(inout IndirectPayload rayData,
BuiltinIntersectionAttribs attribs)
{
if (alphaTestFails(attribs)) IgnoreHit();
}

Our new indirect ray’s closest hit shader is somewhat more complex, but essentially reproduces the direct illumination code from Tutorial 11. Basically: when we our bounce rays hit another surface, we do simple Lambertian shading at the hit points, using the same math we previously defined for primary hits. To save cost, we only shoot one shadow ray from each hit (rather than looping through all scene lights):

我们新的间接光线的最近命中着色器稍微复杂一些,但基本上再现了教程 11中的直接照明代码。基本上:当我们的反弹光线照射到另一个表面时,我们会在命中点处进行简单的 Lambertian 着色,使用我们之前为主要命中定义的相同数学。为了节省成本,我们每次命中只拍摄一条阴影光线(而不是循环通过所有场景灯光):

[shader("closesthit")]
void IndirectClosest(inout IndirectPayload rayData,
BuiltinIntersectionAttribs attribs)
{
// Extract data from our scene description to allow shading this point
ShadingData shadeData = getHitShadingData( attribs );

// Pick a random light from our scene to shoot a shadow ray towards
int lightToSample = min( int(gLightsCount * nextRand(rayData.rndSeed)),
gLightsCount - 1 );

// Query our scene to get this info about the current light
float distToLight; // How far away is it?
float3 lightColor; // What color is it?
float3 toLight; // What direction is it from our current hitpoint?

// A helper to query the Falcor scene to get light data
getLightData(lightIdx, shadeData.posW, toLight, lightColor, distToLight);

// Compute our Lambertion term (NL dot L)
float NdotL = saturate(dot(shadeData.N, toLight));

// Shoot our ray. Return 1.0 for lit, 0.0 for shadowed
float vis = shootShadowRay(shadeData.posW, toLight,
RayTMin(), distToLight);

// Return the shaded Lambertian shading at this indirect hit point
rayData.color = vis * NdotL * lightColor * shadeData.diffuse / M_PI;
}

We also add a utility function, similar to shootShadowRay that encapsulates the process of shooting an indirect ray and returning the incident color in the selected direction:

我们还添加了一个实用函数,类似于 shootShadowRay 封装了拍摄间接光线并返回所选方向的入射颜色的过程:

struct IndirectPayload
{
float3 color; // The color in the ray's direction
uint rndSeed; // Our current random seed
};

float3 shootIndirectRay(float3 orig, float3 dir, float minT, uint seed)
{
// Setup shadow ray and the default ray payload
RayDesc rayColor = { orig, minT, dir, 1.0e+38f };
IndirectPayload payload = { float3(0,0,0), seed };

// Trace our indirect ray. Use hit group #1 and miss shader #1 (of 2)
TraceRay(gRtScene, 0, 0xFF, 1, 2, 1, rayColor, payload);
return payload.color;
}

Changes to our Ray Generation Shader to Add Indirect Lighting

In our ray generation shader from Tutorial 11, we essentially have the following code:

教程 11中的光线生成着色器中,我们基本上有以下代码:

[shader("raygeneration")]
void SimpleDiffuseGIRayGen()
{
...
// Shoot a shadow ray for our direct lighting
float vis = shootShadowRay(worldPos.xyz, toLight, gMinT, distToLight);
shadeColor += vis * NdotL * lightColor * difMatlColor.rgb / M_PI;
...
}

After adding our color for direct illumination we need to add our color computations for indirect illumination:

添加我们的直接照明颜色后,我们需要添加我们的颜色计算 indirect illumination

...
// Pick a random direction for our indirect ray (in a cosine distribution)
float3 bounceDir = getCosHemisphereSample(randSeed, worldNorm.xyz);

// Get NdotL for our selected ray direction
float NdotL = saturate(dot(worldNorm.xyz, bounceDir));

// Shoot our indirect global illumination ray
float3 bounceColor = shootIndirectRay(worldPos.xyz, bounceDir,
gMinT, randSeed);

// Probability of sampling a cosine lobe
float sampleProb = NdotL / M_PI;

// Do Monte Carlo integration of indirect light (i.e., rendering equation)
// -> This is: (NdotL * incoming light * BRDF) / probability-of-sample
shadeColor += (NdotL * bounceColor * difMatlColor.rgb / M_PI) / sampleProb;
...

This is doing Monte Carlo integration of the rendering equation for just the indirect component of the lighting. In this tutorial, I explicitly define the sampling probability for our random ray.

这是对光照的间接分量进行渲染方程蒙特卡罗积分。在本教程中,我明确定义了随机射线的采样概率。

In more efficient code, you would obviously cancel the duplicate terms. But for novices (and even experts), cancelling terms is an easy way to forget the underlying math. Once you forget the math, you might decide to select your ray differently and forget to update the accumulation term with a new sampling probability. Almost every rendering researcher and engineer has stories of “magic coefficients” used to get a good results in some code base—forgetting the underlying mathematics is usually the cause of these spurious magic numbers.

在更高效的代码中,您显然会取消重复的术语。但是对于新手(甚至专家)来说,取消术语是一种忘记基础数学的简单方法。一旦你忘记了数学,你可能会决定以不同的方式选择你的射线,并忘记用新的采样概率更新累积项。几乎每个渲染研究人员和工程师都有关于在某些代码库中获得良好结果的“魔法系数”的故事——忘记基础数学通常是这些虚假魔法数字的原因。

What Does it Look Like?

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

这涵盖了本教程的重点。运行时,您会得到以下结果:

With this tutorial, you now have one-bounce diffuse global illumination (and the UI allows you to turn on and off direct and indirect light for comparison). We’re getting close to an interesting renderer. However, before continuing on to add more complex materials and multi-bounce global illumination, Tutorial 13 adds a detour to handle high dynamic range outputs.

通过本教程,您现在拥有单反射漫反射全局照明(并且 UI 允许您打开和关闭直接和间接光以进行比较)。我们正在接近一个有趣的渲染器。但是,在继续添加更复杂的材质和多反射全局照明之前, 教程 13增加了处理高动态范围 输出的弯路。

When rendering simple materials and lighting, displaying colors in a monitor’s arbitrary [0…1] range is typically sufficient. However, adding multiple bounces and HDR environment maps starts giving output colors that no longer fit in that range. This can give very dark or blown out images. In order to address this, Tutorial 13 allows you to turn on tone mapping to map a wider range of colors onto your monitor.

在渲染简单的材质和光照时,在监视器的任意 [0…1] 范围内显示颜色通常就足够了。但是,添加多个反弹和 HDR 环境贴图会开始提供不再适合该范围的输出颜色。这会产生非常暗或过曝的图像。为了解决这个问题,教程 13 允许您打开色调映射以将更广泛的颜色映射到显示器上。