パストレースには、重要度のサンプリングが可能な複数の領域があります。さらに、これらの各領域では、1995年の Veach and Guibasの論文で最初に提案されたMultiple Importance Samplingも使用できます。わかりやすく説明するために、後方パストレーサーを見てみましょう。
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
SurfaceInteraction interaction;
// Bounce the ray around the scene
const uint maxBounces = 15;
for (uint bounces = 0; bounces < maxBounces; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.GeomID == INVALID_GEOMETRY_ID) {
color += throughput * m_scene->BackgroundColor;
break;
}
// Fetch the material
Material *material = m_scene->GetMaterial(ray.GeomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.GeomID);
// If we hit a light, add the emission
if (light != nullptr) {
color += throughput * light->Le();
}
interaction.Position = ray.Origin + ray.Direction * ray.TFar;
interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
interaction.OutputDirection = normalize(-ray.Direction);
// Get the new ray direction
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float pdf = material->bsdf->Pdf(interaction);
// Accumulate the weight
throughput = throughput * material->bsdf->Eval(interaction) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
ray.TNear = 0.001f;
ray.TFar = infinity;
// Russian Roulette
if (bounces > 3) {
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
throughput *= 1 / p;
}
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
英語で:
- シーンを通して光線を放つ
- 何かヒットしたかどうかを確認します。そうでない場合は、スカイボックスの色を返し、ブレークします。
- 光が当たったかどうかを確認してください。その場合、色の蓄積に発光を追加します
- 次の光線の新しい方向を選択します。これを均一に行うことも、BRDFに基づいた重要度サンプルを行うこともできます
- BRDFを評価して蓄積します。ここで、モンテカルロアルゴリズムに従うために、選択した方向のpdfで除算する必要があります。
- 選択した方向と元の場所に基づいて新しいレイを作成します
- [オプション]ロシアンルーレットを使用して、レイを終了するかどうかを選択します
- 後藤1
このコードでは、光線が最終的に光に当たる場合にのみ色を取得します。さらに、エリアがないため、時間厳守の光源をサポートしていません。
これを修正するために、バウンスごとにライトを直接サンプリングします。いくつかの小さな変更を行う必要があります。
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
SurfaceInteraction interaction;
// Bounce the ray around the scene
const uint maxBounces = 15;
for (uint bounces = 0; bounces < maxBounces; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.GeomID == INVALID_GEOMETRY_ID) {
color += throughput * m_scene->BackgroundColor;
break;
}
// Fetch the material
Material *material = m_scene->GetMaterial(ray.GeomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.GeomID);
// If this is the first bounce or if we just had a specular bounce,
// we need to add the emmisive light
if ((bounces == 0 || (interaction.SampledLobe & BSDFLobe::Specular) != 0) && light != nullptr) {
color += throughput * light->Le();
}
interaction.Position = ray.Origin + ray.Direction * ray.TFar;
interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
interaction.OutputDirection = normalize(-ray.Direction);
// Calculate the direct lighting
color += throughput * SampleLights(sampler, interaction, material->bsdf, light);
// Get the new ray direction
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float pdf = material->bsdf->Pdf(interaction);
// Accumulate the weight
throughput = throughput * material->bsdf->Eval(interaction) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
ray.TNear = 0.001f;
ray.TFar = infinity;
// Russian Roulette
if (bounces > 3) {
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
throughput *= 1 / p;
}
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
最初に、「色+ =スループット* SampleLights(...)」を追加します。SampleLights()について少し詳しく説明します。しかし、本質的に、すべてのライトをループし、BSDFによって減衰された色への寄与を返します。
これはすばらしいことですが、それを修正するためにもう1つ変更を加える必要があります。具体的には、光を当てるとどうなりますか。古いコードでは、光の放出を色の蓄積に追加しました。しかし、今ではバウンスごとにライトを直接サンプリングするため、ライトの放射を追加すると、「ダブルディップ」になります。したがって、正しいことは...何もありません。光の放出を累積することはスキップします。
ただし、2つのコーナーケースがあります。
- 最初の光線
- 完全鏡面反射バウンス(別名ミラー)
最初の光線がライトに当たると、ライトの放射が直接表示されます。そのため、スキップすると、周囲の表面が照らされていても、すべてのライトが黒で表示されます。
入力レイには出力が1つしかないため、完全な鏡面に当たると、ライトを直接サンプリングすることはできません。技術的には、入力レイがライトに当たるかどうかを確認できますが、意味はありません。とにかく、メインのパストレースループがそれを行います。したがって、鏡面に当たった直後にライトを当てた場合、色を蓄積する必要があります。そうしないと、ミラーのライトが黒になります。
それでは、SampleLights()を詳しく見てみましょう。
float3 SampleLights(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
std::size_t numLights = m_scene->NumLights();
float3 L(0.0f);
for (uint i = 0; i < numLights; ++i) {
Light *light = &m_scene->Lights[i];
// Don't let a light contribute light to itself
if (light == hitLight) {
continue;
}
L = L + EstimateDirect(light, sampler, interaction, bsdf);
}
return L;
}
英語で:
- すべてのライトをループします
- 当てたら光を飛ばす
- すべてのライトからの直接照明を蓄積する
- 直接照明を返す
B SD F(p 、ω私、ωo)L私(p 、ω私)
時間厳守の光源の場合、これは次のように簡単です。
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
return float3(0.0f);
}
interaction.InputDirection = normalize(light->Origin - interaction.Position);
return bsdf->Eval(interaction) * light->Li;
}
ただし、ライトにエリアを持たせる場合は、まずライト上のポイントをサンプリングする必要があります。したがって、完全な定義は次のとおりです。
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
float3 directLighting = float3(0.0f);
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
float pdf;
float3 Li = light->SampleLi(sampler, m_scene, interaction, &pdf);
// Make sure the pdf isn't zero and the radiance isn't black
if (pdf != 0.0f && !all(Li)) {
directLighting += bsdf->Eval(interaction) * Li / pdf;
}
}
return directLighting;
}
必要に応じてlight-> SampleLiを実装できます。ポイントを均一に選択するか、重要度サンプルを選択できます。いずれの場合も、ラジオシティをポイントを選択したpdfで除算します。繰り返しますが、モンテカルロの要件を満たします。
BRDFがビューに大きく依存している場合、ライト上のランダムなポイントではなく、BRDFに基づいたポイントを選択する方が良い場合があります。しかし、どのように選択しますか?サンプルは光に基づいていますか、それともBRDFに基づいていますか?
B SD F(p 、ω私、ωo)L私(p 、ω私)
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
float3 directLighting = float3(0.0f);
float3 f;
float lightPdf, scatteringPdf;
// Sample lighting with multiple importance sampling
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
float3 Li = light->SampleLi(sampler, m_scene, interaction, &lightPdf);
// Make sure the pdf isn't zero and the radiance isn't black
if (lightPdf != 0.0f && !all(Li)) {
// Calculate the brdf value
f = bsdf->Eval(interaction);
scatteringPdf = bsdf->Pdf(interaction);
if (scatteringPdf != 0.0f && !all(f)) {
float weight = PowerHeuristic(1, lightPdf, 1, scatteringPdf);
directLighting += f * Li * weight / lightPdf;
}
}
}
// Sample brdf with multiple importance sampling
bsdf->Sample(interaction, sampler);
f = bsdf->Eval(interaction);
scatteringPdf = bsdf->Pdf(interaction);
if (scatteringPdf != 0.0f && !all(f)) {
lightPdf = light->PdfLi(m_scene, interaction);
if (lightPdf == 0.0f) {
// We didn't hit anything, so ignore the brdf sample
return directLighting;
}
float weight = PowerHeuristic(1, scatteringPdf, 1, lightPdf);
float3 Li = light->Le();
directLighting += f * Li * weight / scatteringPdf;
}
return directLighting;
}
英語で:
- まず、光をサンプリングします
- これにより、interaction.InputDirectionが更新されます
- 私たちに光のLiを与えます
- そして、光の点を選択するpdf
- PDFが有効であり、放射輝度がゼロでないことを確認してください
- サンプリングされたInputDirectionを使用してBSDFを評価します
- サンプリングされたInputDirectionが与えられたBSDFのPDFを計算します
- 基本的に、光の代わりにBSDFを使用してサンプリングする場合、このサンプルはどのくらいありそうですか
- light pdfとBSDF pdfを使用して、重量を計算します
- VeachとGuibasは、重量を計算するいくつかの異なる方法を定義しています。実験的に、ほとんどの場合に最適に機能するために、2のべき乗のヒューリスティックな発見がありました。詳細については、この論文を参照してください。実装は以下です
- 重みに直接照明の計算を乗算し、光のpdfで除算します。(モンテカルロ用)そして、直接光の蓄積に追加します。
- 次に、BRDFをサンプリングします
- これにより、interaction.InputDirectionが更新されます
- BRDFを評価する
- BRDFに基づいてこの方向を選択するためのPDFを取得します
- サンプリングされたInputDirectionが与えられた場合、ライトpdfを計算します
- これは以前のミラーです。光をサンプリングする場合、この方向はどのくらいありそうですか
- lightPdf == 0.0fの場合、レイはライトを逃したため、ライトサンプルから直接照明を返すだけです。
- それ以外の場合は、重量を計算し、累積にBSDF直接照明を追加します
- 最後に、蓄積された直接照明を返します
。
inline float PowerHeuristic(uint numf, float fPdf, uint numg, float gPdf) {
float f = numf * fPdf;
float g = numg * gPdf;
return (f * f) / (f * f + g * g);
}
これらの関数では、いくつかの最適化/改善を行うことができますが、理解しやすくするためにそれらを削減しました。必要に応じて、これらの改善点の一部を共有できます。
1つのライトのみをサンプリングする
SampleLights()では、すべてのライトをループし、その貢献度を取得します。少数のライトではこれで問題ありませんが、数百または数千のライトでは高価になります。幸いなことに、モンテカルロ統合が巨大な平均であるという事実を活用できます。例:
定義しましょう
h (x )= f(x )+ g(x )
h (x )
h (x )= 1N∑i = 1Nf(x私)+ g(x私)
f(x )g(x )
h (x )= 1N∑i = 1Nr (ζ、x )p df
ζr (ζ、x )
r (ζ、x )= { f(x )、g(x )、0.0 ≤ ζ< 0.50.5 ≤ ζ< 1.0
p df= 12
英語で:
- f(x )g(x )
- 結果を除算する12
- 平均
Nが大きくなると、推定値は正しい解に収束します。
この同じ原理を光サンプリングに適用できます。すべてのライトをサンプリングする代わりに、ランダムに1つを選択し、結果にライトの数を乗算します(これはフラクショナルpdfで除算するのと同じです)。
float3 SampleOneLight(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
std::size_t numLights = m_scene->NumLights();
// Return black if there are no lights
// And don't let a light contribute light to itself
// Aka, if we hit a light
// This is the special case where there is only 1 light
if (numLights == 0 || numLights == 1 && hitLight != nullptr) {
return float3(0.0f);
}
// Don't let a light contribute light to itself
// Choose another one
Light *light;
do {
light = m_scene->RandomOneLight(sampler);
} while (light == hitLight);
return numLights * EstimateDirect(light, sampler, interaction, bsdf);
}
1numLights
「新しい光線」方向をサンプリングする複数の重要性
現在のコードの重要性は、BSDFに基づいて「New Ray」方向のみをサンプリングすることです。ライトの位置に基づいてサンプルも重要にしたい場合はどうしますか?
上記で学んだことから、1つの方法は、2つの「新しい」レイを撮影し、それぞれのPDFに基づいて重み付けすることです。ただし、これは計算コストが高く、再帰なしで実装するのが困難です。
これを克服するために、1つのライトのみをサンプリングすることで学んだのと同じ原則を適用できます。つまり、サンプリングするものをランダムに選択し、それを選択したpdfで除算します。
// Get the new ray direction
// Randomly (uniform) choose whether to sample based on the BSDF or the Lights
float p = sampler->NextFloat();
Light *light = m_scene->RandomLight();
if (p < 0.5f) {
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float bsdfPdf = material->bsdf->Pdf(interaction);
float lightPdf = light->PdfLi(m_scene, interaction);
float weight = PowerHeuristic(1, bsdfPdf, 1, lightPdf);
// Accumulate the throughput
throughput = throughput * weight * material->bsdf->Eval(interaction) / bsdfPdf;
} else {
// Choose the direction based on a light
float lightPdf;
light->SampleLi(sampler, m_scene, interaction, &lightPdf);
float bsdfPdf = material->bsdf->Pdf(interaction);
float weight = PowerHeuristic(1, lightPdf, 1, bsdfPdf);
// Accumulate the throughput
throughput = throughput * weight * material->bsdf->Eval(interaction) / lightPdf;
}
すべては私たちが本当にない、と述べているしたいの重要性サンプルに光に基づく「新レイ」の方向を?以下のための直接照明、ラジオシティは、表面のBSDF、光の方向の両方に影響されます。しかし、間接照明の場合、ラジオシティはほぼ排他的に、以前にヒットしたサーフェスのBSDFによって定義されます。したがって、重要度の軽いサンプリングを追加しても何も得られません。
そのため、BSDFで「新しい方向」を重要度でサンプリングするだけで、直接照明には複数の重要度サンプリングを適用するのが一般的です。