短い答え:
重要度サンプリングは、実際の関数の形状に近い推定量を選択することにより、モンテカルロ積分の分散を減らす方法です。
PDFは、Probability Density Functionの略です。pdf(x)され生成されたランダムサンプルの確率与えるx。
長い答え:
まず、モンテカルロ統合とは何か、数学的にどのように見えるかを確認しましょう。
モンテカルロ積分は、積分の値を推定する手法です。通常、積分に対する閉形式の解決策がない場合に使用されます。次のようになります。
∫f(x)dx≈1N∑i=1Nf(xi)pdf(xi)
英語では、これは、関数からの連続するランダムサンプルを平均化することで積分を近似できることを示しています。N大きくなる、近似解に近づくと近づきます。pdf(xi)は、各ランダムサンプルの確率密度関数を表します。
例を見てみましょう:積分I値を計算します。
I=∫2π0e−xsin(x)dx
モンテカルロ統合を使用してみましょう:
I≈1N∑i=1Ne−xsin(xi)pdf(xi)
これを計算する簡単なpythonプログラムは次のとおりです。
import random
import math
N = 200000
TwoPi = 2.0 * math.pi
sum = 0.0
for i in range(N):
x = random.uniform(0, TwoPi)
fx = math.exp(-x) * math.sin(x)
pdf = 1 / (TwoPi - 0.0)
sum += fx / pdf
I = (1 / N) * sum
print(I)
プログラムを実行すると、I = 0.4986941が得られますI=0.4986941
部品による分離を使用して、正確なソリューションを取得できます。
I=12(1−e−2π)=0.4990663
あなたは、モンテカルロソリューションが全く正しくないことに気付くでしょう。これは推定値であるためです。つまり、Nが無限大になると、推定値は正解に近づいていくはずです。すでにN=2000いくつかの実行は正解とほとんど同じです。
PDFに関するメモ:この単純な例では、常に一定のランダムサンプルを使用します。均一なランダムサンプルとは、すべてのサンプルが選択される確率がまったく同じであることを意味します。我々は、範囲内のサンプル[0,2π]したがって、pdf(x)=1/(2π−0)
重要度サンプリングは、均一にサンプリングしないことで機能します。代わりに、結果に大きく寄与するサンプル(重要)を選択し、結果に少ししか貢献しないサンプル(重要度が低い)を選択します。したがって、名前、重要度サンプリング。
pdfがfの形状に非常に近いサンプリング関数を選択すると、分散を大幅に減らすことができます。つまり、より少ないサンプルを取得できます。ただし、値がfと非常に異なるサンプリング関数を選択した場合、分散を増やすことができます。下の写真を参照してください:
Wojciech Jaroszの論文からの画像付録A
パストレースでの重要度サンプリングの1つの例は、サーフェスに到達した後のレイの方向の選択方法です。表面が完全に鏡面ではない場合(つまり、鏡やガラス)、出射光線は半球のどこにでもあります。
我々は可能性が一様に新しい線を発生させるために半球をサンプリング。ただし、レンダリング方程式には余弦係数が含まれているという事実を活用できます。
Lo(p,ωo)=Le(p,ωo)+∫Ωf(p,ωi,ωo)Li(p,ωi)|cosθi|dωi
cos(x)
これに対処するために、重要度サンプリングを使用します。余弦重み付き半球に従って光線を生成する場合、水平線のかなり上でより多くの光線が生成され、水平線の近くでは生成されないようにします。これにより、分散が低下し、ノイズが減少します。
あなたのケースでは、マイクロファセットベースのBRDFであるCook-Torranceを使用することを指定しました。一般的な形式は次のとおりです。
f(p 、ω私、ωo)= F(ω私、h )G (ω私、ωo、h )D (h )4 cos(θ私)cos(θo)
どこで
F(ω私、h )= フレネル関数G (ω私、ωo、h )= ジオメトリマスキングおよびシャドウイング機能D (h )= 正規分布関数
ブログ「A Graphic's Guy's Note」には、Cook-Torrance BRDFのサンプリング方法に関する優れた記事があります。彼のブログ投稿を紹介します。とはいえ、以下の簡単な概要を作成してみます。
NDFは一般的にCook-Torrance BRDFの主要部分であるため、重要度のサンプリングを行う場合は、NDFに基づいてサンプリングする必要があります。
Cook-Torranceは、使用する特定のNDFを指定していません。好みに合わせて自由に選択できます。とはいえ、人気のあるNDFがいくつかあります。
各NDFには独自の式があるため、それぞれを別々にサンプリングする必要があります。それぞれの最終サンプリング関数のみを表示します。公式の導出方法を確認するには、ブログの投稿を参照してください。
GGXは次のように定義されます。
DG G X(m )= α2π((α2− 1 )cos2(θ )+ 1 )2
θ
θ = arccos(α2ξ1(α2− 1 )+ 1−−−−−−−−−−−−√)
ξ
ϕ
ϕ = ξ2
ベックマンは次のように定義されます:
DB e c k m a n n(m )= 1πα2cos4(θ )e− タン2(θ )α2
以下でサンプリングできます:
θ = arccos(11 = α2ln(1 - ξ1)−−−−−−−−−−−−−−√)ϕ = ξ2
最後に、Blinnは次のように定義されます。
DB l i n n(m )= α + 22個のπ(cos(θ ))α
以下でサンプリングできます:
θ = arccos(1ξα + 11)ϕ = ξ2
実践する
基本的な後方パストレーサーを見てみましょう。
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);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// 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 emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
IE。シーンをバウンスして、色と光の減衰を累積します。バウンスごとに、レイの新しい方向を選択する必要があります。上記のように、半球を均一にサンプリングして新しい光線を生成できます。ただし、コードはよりスマートです。重要性は、BRDFに基づいて新しい方向をサンプリングします。(注:これは入力方向です。これは、後方パストレーサーであるためです)
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
次のように実装できます:
void LambertBRDF::Sample(float3 outputDirection, float3 normal, UniformSampler *sampler) {
float rand = sampler->NextFloat();
float r = std::sqrtf(rand);
float theta = sampler->NextFloat() * 2.0f * M_PI;
float x = r * std::cosf(theta);
float y = r * std::sinf(theta);
// Project z up to the unit hemisphere
float z = std::sqrtf(1.0f - x * x - y * y);
return normalize(TransformToWorld(x, y, z, normal));
}
float3a TransformToWorld(float x, float y, float z, float3a &normal) {
// Find an axis that is not parallel to normal
float3a majorAxis;
if (abs(normal.x) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(1, 0, 0);
} else if (abs(normal.y) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(0, 1, 0);
} else {
majorAxis = float3a(0, 0, 1);
}
// Use majorAxis to create a coordinate system relative to world space
float3a u = normalize(cross(normal, majorAxis));
float3a v = cross(normal, u);
float3a w = normal;
// Transform from local coordinates to world coordinates
return u * x +
v * y +
w * z;
}
float LambertBRDF::Pdf(float3 inputDirection, float3 normal) {
return dot(inputDirection, normal) * M_1_PI;
}
inputDirection(コードでは「wi」)をサンプリングした後、それを使用してBRDFの値を計算します。そして、モンテカルロ公式に従ってpdfで除算します:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
ここで、評価は()だけBRDF関数自体である(ランバート、ブリン・フォン、クック・トーランス、など):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}