明示的な光サンプリングによるプログレッシブパストレース


14

BRDFパーツの重要度サンプリングの背後にあるロジックを理解しました。ただし、光源を明示的にサンプリングすることになると、すべてが混乱します。たとえば、シーンに1つの点光源があり、各フレームで常に直接サンプリングする場合、モンテカルロ統合のもう1つのサンプルとしてカウントする必要がありますか?つまり、1つのサンプルをコサイン加重分布から取得し、もう1つのサンプルを点光源から取得します。合計2つのサンプルですか、それとも1つのサンプルですか?また、直接サンプルからの放射輝度を任意の項に分割する必要がありますか?

回答:


19

パストレースには、重要度のサンプリングが可能な複数の領域があります。さらに、これらの各領域では、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);
}

英語で:

  1. シーンを通して光線を放つ
  2. 何かヒットしたかどうかを確認します。そうでない場合は、スカイボックスの色を返し、ブレークします。
  3. 光が当たったかどうかを確認してください。その場合、色の蓄積に発光を追加します
  4. 次の光線の新しい方向を選択します。これを均一に行うことも、BRDFに基づいた重要度サンプルを行うこともできます
  5. BRDFを評価して蓄積します。ここで、モンテカルロアルゴリズムに従うために、選択した方向のpdfで除算する必要があります。
  6. 選択した方向と元の場所に基づいて新しいレイを作成します
  7. [オプション]ロシアンルーレットを使用して、レイを終了するかどうかを選択します
  8. 後藤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. 最初の光線
  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;
}

英語で:

  1. すべてのライトをループします
  2. 当てたら光を飛ばす
    • ダブルディップしないでください
  3. すべてのライトからの直接照明を蓄積する
  4. 直接照明を返す

BSDFpωωoLpω

時間厳守の光源の場合、これは次のように簡単です。

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に基づいていますか?

BSDFpωωoLpω

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;
}

英語で:

  1. まず、光をサンプリングします
    • これにより、interaction.InputDirectionが更新されます
    • 私たちに光のLiを与えます
    • そして、光の点を選択するpdf
  2. PDFが有効であり、放射輝度がゼロでないことを確認してください
  3. サンプリングされたInputDirectionを使用してBSDFを評価します
  4. サンプリングされたInputDirectionが与えられたBSDFのPDFを計算します
    • 基本的に、光の代わりにBSDFを使用してサンプリングする場合、このサンプルはどのくらいありそうですか
  5. light pdfとBSDF pdfを使用して、重量を計算します
    • VeachとGuibasは、重量を計算するいくつかの異なる方法を定義しています。実験的に、ほとんどの場合に最適に機能するために、2のべき乗のヒューリスティックな発見がありました。詳細については、この論文を参照してください。実装は以下です
  6. 重みに直接照明の計算を乗算し、光のpdfで除算します。(モンテカルロ用)そして、直接光の蓄積に追加します。
  7. 次に、BRDFをサンプリングします
    • これにより、interaction.InputDirectionが更新されます
  8. BRDFを評価する
  9. BRDFに基づいてこの方向を選択するためのPDFを取得します
  10. サンプリングされたInputDirectionが与えられた場合、ライトpdfを計算します
    • これは以前のミラーです。光をサンプリングする場合、この方向はどのくらいありそうですか
  11. lightPdf == 0.0fの場合、レイはライトを逃したため、ライトサンプルから直接照明を返すだけです。
  12. それ以外の場合は、重量を計算し、累積にBSDF直接照明を追加します
  13. 最後に、蓄積された直接照明を返します

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バツ=fバツ+gバツ

hバツ

hバツ=1N=1Nfバツ+gバツ

fバツgバツ

hバツ=1N=1Nrζバツpdf

ζrζバツ

rζバツ={fバツ0.0ζ<0.5gバツ0.5ζ<1.0

pdf=12

英語で:

  1. fバツgバツ
  2. 結果を除算する12
  3. 平均

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で「新しい方向」を重要度でサンプリングするだけで、直接照明には複数の重要度サンプリングを適用するのが一般的です。


明確な答えをありがとう!明示的な光サンプリングなしでパストレーサーを使用すると、点光源に到達することはないことを理解しています。したがって、基本的にその貢献を追加できます。一方、エリア光源をサンプリングする場合は、二重の落ち込みを避けるために間接照明で再び当たらないようにする必要があります
ムスタファイシュク

丁度!説明が必要な部分はありますか?または、十分な詳細がありませんか?
-RichieSams

また、多重照明サンプリングは直接照明計算にのみ使用されますか?たぶん私は逃したが、私はそれの別の例を見ていない。パストレーサーでバウンスごとにレイを1つだけ撮影すると、間接照明の計算ではできないようです。
ムスタファイシュク

2
複数の重要度サンプリングは、重要度サンプリングを使用する任意の場所に適用できます。多重重要度サンプリングの利点は、複数のサンプリング手法の利点を組み合わせることができることです。たとえば、場合によっては、重要度の軽いサンプリングの方がBSDFサンプリングよりも優れています。他の場合には、その逆も同様です。MISは両方の長所を組み合わせます。ただし、BSDFサンプリングが常に100%優れている場合、MISの複雑さを追加する理由はありません。この点を拡張するためにいくつかのセクションを回答に追加しました
-RichieSams

1
入ってくる放射輝度源を直接と間接の2つの部分に分けたようです。直接部分のライトを明示的にサンプリングします。この部分をサンプリングする間、ライトとBSDFをサンプリングすることは重要です。ただし、間接部分については、解決したいのは問題自体なので、どの方向がより高い放射輝度値を与える可能性があるかについてはわかりません。ただし、コサイン項とBSDFによれば、どちらの方向がより寄与できるかを言うことができます。これは私が理解していることです。私が間違っている場合は私を修正し、あなたの素晴らしい答えをありがとう。
ムスタファイシュク
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.