Krótka odpowiedź:
Ważność próbkowania jest metodą zmniejszania wariancji w integracji Monte Carlo poprzez wybranie estymatora zbliżonego do kształtu rzeczywistej funkcji.
PDF jest skrótem od funkcji gęstości prawdopodobieństwa . A pdf(x) daje prawdopodobieństwo wygenerowania losowej próbki o wartości x .
Długa odpowiedź:
Na początek przejrzyjmy, czym jest integracja Monte Carlo i jak wygląda matematycznie.
Integracja Monte Carlo to technika szacowania wartości całki. Zwykle jest używany, gdy nie ma zamkniętego rozwiązania całki. To wygląda tak:
∫f(x)dx≈1N∑i=1Nf(xi)pdf(xi)
W języku angielskim oznacza to, że można przybliżać całkę, uśredniając kolejne losowe próbki z funkcji. Gdy N staje się duże, przybliżenie zbliża się coraz bardziej do rozwiązania. pdf(xi) reprezentuje funkcję gęstości prawdopodobieństwa każdej próbki losowej.
Zróbmy przykład: Oblicz wartość całki I .
I=∫2π0e−xsin(x)dx
Użyjmy integracji Monte Carlo:
I≈1N∑i=1Ne−xsin(xi)pdf(xi)
Prostym programem pythonowym do obliczenia tego jest:
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)
Jeśli uruchomimy program, otrzymamy I=0.4986941
Stosując separację według części, możemy uzyskać dokładne rozwiązanie:
I=12(1−e−2π)=0.4990663
Zauważysz, że rozwiązanie Monte Carlo nie jest całkiem poprawne. Jest tak, ponieważ jest to szacunek. To powiedziawszy, gdy przechodzi w nieskończoność, szacunki powinny być coraz bliżej poprawnej odpowiedzi. Już przy niektóre przebiegi są prawie identyczne z poprawną odpowiedzią.NN=2000
Uwaga na temat pliku PDF: W tym prostym przykładzie zawsze bierzemy jednolitą losową próbkę. Jednolita losowa próbka oznacza, że każda próbka ma dokładnie takie samo prawdopodobieństwo wyboru. Próbkujemy w zakresie więc[0,2π]pdf(x)=1/(2π−0)
Ważność próbkowania polega na tym, że próbkowanie nie jest jednolite. Zamiast tego staramy się wybierać więcej próbek, które mają duży wpływ na wynik (ważne), a mniej próbek, które tylko nieznacznie przyczyniają się do wyniku (mniej ważne). Stąd nazwa, ważność próbkowania.
Jeśli wybierzesz funkcję próbkowania, której pdf bardzo ściśle pasuje do kształtu , możesz znacznie zmniejszyć wariancję, co oznacza, że możesz pobrać mniej próbek. Jeśli jednak wybierzesz funkcję próbkowania, której wartość jest bardzo różna od , możesz zwiększyć wariancję. Zobacz zdjęcie poniżej:
Zdjęcie z rozprawy doktorskiej Wojciecha Jarosza Załącznik Aff
Jednym z przykładów ważnego próbkowania w śledzeniu ścieżki jest sposób wyboru kierunku promienia po uderzeniu w powierzchnię. Jeśli powierzchnia nie jest idealnie błyszcząca (tj. Lustro lub szkło), promień wychodzący może znajdować się w dowolnym miejscu na półkuli.
My mogliśmy równomiernie próbki półkulę, aby wygenerować nowy ray. Możemy jednak wykorzystać fakt, że równanie renderowania zawiera w sobie współczynnik cosinus:
Lo(p,ωo)=Le(p,ωo)+∫Ωf(p,ωi,ωo)Li(p,ωi)|cosθi|dωi
W szczególności wiemy, że wszelkie promienie na horyzoncie będą silnie osłabione (w szczególności ). Tak więc promienie generowane w pobliżu horyzontu nie przyczynią się zbytnio do końcowej wartości.cos(x)
Aby temu przeciwdziałać, używamy próbkowania według ważności. Jeśli generujemy promienie zgodnie z półkulą ważoną cosinusem, zapewniamy, że więcej promieni generuje się znacznie powyżej horyzontu, a mniej w pobliżu horyzontu. Obniży to wariancję i zmniejszy hałas.
W twoim przypadku podałeś, że będziesz korzystać z BRDF opartego na mikropacetach Cook-Torrance. Powszechną formą jest:
f(p,ωi,ωo)=F(ωi,h)G(ωi,ωo,h)D(h)4cos(θi)cos(θo)
gdzie
F(ωi,h)=Fresnel functionG(ωi,ωo,h)=Geometry Masking and Shadowing functionD(h)=Normal Distribution Function
Blog „Notka faceta z grafiki” ma doskonały opis próbkowania BRDF-ów Cook-Torrance. Odsyłam cię do jego postu na blogu . To powiedziawszy, postaram się stworzyć krótki przegląd poniżej:
NDF jest generalnie dominującą częścią Cook-Torrance BRDF, więc jeśli chcemy uzyskać ważność próbki, powinniśmy spróbować na podstawie NDF.
Cook-Torrance nie określa konkretnego NDF do użycia; mamy swobodę wyboru tego, co pasuje do naszych upodobań. To powiedziawszy, istnieje kilka popularnych NDF:
Każdy NDF ma własną formułę, dlatego każdy musi być próbkowany inaczej. Pokażę tylko ostateczną funkcję próbkowania dla każdego. Jeśli chcesz zobaczyć, jak formuła jest uzyskiwana, zobacz wpis na blogu.
GGX jest zdefiniowany jako:
DGGX(m)=α2π((α2−1)cos2(θ)+1)2
Aby pobrać próbkę sferycznego kąta współrzędnych , możemy użyć wzoru:θ
θ=arccos(α2ξ1(α2−1)+1−−−−−−−−−−−−√)
gdzie jest jednolitą zmienną losową.ξ
Zakładamy, że NDF jest izotropowy, więc możemy próbkować jednolicie:ϕ
ϕ=ξ2
Beckmann jest zdefiniowany jako:
DBeckmann(m)=1πα2cos4(θ)e−tan2(θ)α2
Które można próbkować za pomocą:
θ=arccos(11=α2ln(1−ξ1)−−−−−−−−−−−−−−√)ϕ=ξ2
Wreszcie, Blinn jest zdefiniowany jako:
DBlinn(m)=α+22π(cos(θ))α
Które można próbkować za pomocą:
θ=arccos(1ξα+11)ϕ=ξ2
Realizacja w praktyce
Spójrzmy na podstawowy znacznik ścieżki wstecz:
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);
}
TO ZNACZY. odbijamy się wokół sceny, gromadząc w miarę upływu czasu kolor i tłumienie światła. Przy każdym odbiciu musimy wybrać nowy kierunek promienia. Jak wspomniano powyżej, mogłyby równomiernie próbki półkulę, aby wygenerować nowy ray. Jednak kod jest mądrzejszy; znaczenie pobiera próbki nowego kierunku na podstawie BRDF. (Uwaga: To jest kierunek wprowadzania, ponieważ jesteśmy znacznikiem ścieżki do tyłu)
// 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);
Które można zaimplementować jako:
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;
}
Po próbkowaniu inputDirection („wi” w kodzie), używamy go do obliczenia wartości BRDF. Następnie dzielimy przez pdf zgodnie ze wzorem Monte Carlo:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Gdzie Eval () jest samą funkcją BRDF (Lambert, Blinn-Phong, Cook-Torrance itp.):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}