Kurze Antwort:
Die Wichtigkeitsabtastung ist eine Methode zur Verringerung der Varianz in der Monte-Carlo-Integration, indem ein Schätzer ausgewählt wird, der der Form der tatsächlichen Funktion nahe kommt.
PDF ist eine Abkürzung für Probability Density Function . A p df( x ) gibt die Wahrscheinlichkeit an, dass eine erzeugte Zufallsstichprobe x .
Lange Antwort:
Lassen Sie uns zunächst untersuchen, was Monte-Carlo-Integration ist und wie sie mathematisch aussieht.
Die Monte-Carlo-Integration ist eine Technik zur Schätzung des Wertes eines Integrals. Es wird normalerweise verwendet, wenn es keine geschlossene Formlösung für das Integral gibt. Es sieht aus wie das:
∫f( x )dx ≤ 1N∑i = 1Nf( xich)p df( xich)
Auf Englisch bedeutet dies, dass Sie ein Integral approximieren können, indem Sie aufeinanderfolgende Zufallsstichproben aus der Funktion mitteln. Wenn N groß wird, kommt die Approximation der Lösung immer näher. p df( xich) repräsentiert die Wahrscheinlichkeitsdichtefunktion jeder Zufallsstichprobe.
Lassen Sie uns ein Beispiel: Berechnen Sie den Wert des Integrals ich .
ich= ∫2 π0e- xSünde( x ) dx
Nutzen wir die Monte Carlo Integration:
ich≈ 1N∑i = 1Ne- xSünde( xich)p df( xich)
Ein einfaches Python-Programm, um dies zu berechnen, ist:
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)
Wenn wir das Programm ausführen, erhalten wir I=0.4986941
Durch die Trennung nach Teilen erhalten wir die genaue Lösung:
ich= 12( 1 - e - 2 π ) = 0,4990663
Sie werden feststellen, dass die Monte-Carlo-Lösung nicht ganz korrekt ist. Dies liegt daran, dass es sich um eine Schätzung handelt. Das heißt, wenn N gegen unendlich geht, sollte die Schätzung immer näher an die richtige Antwort heranreichen. Bereits bei N= 2000 einige Läufe fast identisch mit der richtigen Antwort.
Ein Hinweis zum PDF: In diesem einfachen Beispiel nehmen wir immer eine einheitliche Zufallsstichprobe. Eine einheitliche Zufallsstichprobe bedeutet, dass jede Stichprobe mit genau der gleichen Wahrscheinlichkeit ausgewählt wird. Wir tasten im Bereich [ 0 , 2 & pgr;] also ist p df( X ) = 1 / ( 2 π- 0 )
Die Stichprobenerhebung erfolgt durch nicht einheitliche Stichprobenerhebung. Stattdessen versuchen wir, mehr Samples auszuwählen, die viel zum Ergebnis beitragen (wichtig), und weniger Samples, die nur wenig zum Ergebnis beitragen (weniger wichtig). Daher der Name, die Bedeutung der Stichprobe.
Wenn Sie eine Sampling-Funktion wählen, deren PDF-Datei sehr genau der Form von f , können Sie die Varianz stark reduzieren, was bedeutet, dass Sie weniger Samples nehmen können. Wenn Sie jedoch eine Abtastfunktion wählen, deren Wert sich stark von f , können Sie die Varianz erhöhen . Siehe folgendes Bild:
Bild aus der Dissertation von Wojciech Jarosz, Anhang A
Ein Beispiel für eine wichtige Abtastung bei der Pfadverfolgung ist die Auswahl der Richtung eines Strahls, nachdem er auf eine Oberfläche auftrifft. Wenn die Oberfläche nicht perfekt spiegelnd ist (z. B. ein Spiegel oder ein Glas), kann der austretende Strahl überall auf der Hemisphäre sein.
Wir könnten die Hemisphäre gleichmäßig abtasten, um den neuen Strahl zu erzeugen. Wir können jedoch die Tatsache ausnutzen, dass die Rendering-Gleichung einen Kosinusfaktor enthält:
LO( p , ωO) = Le( p , ωO) + ∫Ωf( p , ωich, ωO)Li(p,ωi)|cosθi|dωi
Insbesondere wissen wir, dass alle Strahlen am Horizont stark abgeschwächt werden (speziell cos(x) ). Nahe am Horizont erzeugte Strahlen tragen also nicht viel zum Endwert bei.
Um dem entgegenzuwirken, verwenden wir Wichtigkeitsproben. Wenn wir Strahlen gemäß einer Kosinus-Halbkugel erzeugen, stellen wir sicher, dass mehr Strahlen weit über dem Horizont und weniger in der Nähe des Horizonts erzeugt werden. Dies verringert die Varianz und reduziert das Rauschen.
In Ihrem Fall haben Sie angegeben, dass Sie ein auf Mikrofacetten basierendes Cook-Torrance-BRDF verwenden. Die übliche Form ist:
f(p,ωi,ωo)=F(ωi,h)G(ωi,ωo,h)D(h)4cos(θi)cos(θo)
woher
F(ωi,h)=Fresnel functionG(ωi,ωo,h)=Geometry Masking and Shadowing functionD(h)=Normal Distribution Function
Der Blog "A Graphic's Guy's Note" bietet hervorragende Informationen zum Probieren von Cook-Torrance-BRDFs. Ich werde Sie auf seinen Blog-Beitrag verweisen . Trotzdem werde ich versuchen, im Folgenden eine kurze Übersicht zu erstellen:
Der NDF ist im Allgemeinen der dominierende Anteil des Cook-Torrance-BRDF. Wenn wir also eine Stichprobe von Bedeutung durchführen, sollten wir die Stichprobe auf der Grundlage des NDF durchführen.
Cook-Torrance gibt keinen bestimmten NDF an, der verwendet werden soll. es steht uns frei zu wählen, was unseren Vorstellungen entspricht. Das heißt, es gibt ein paar beliebte NDFs:
Jeder NDF hat eine eigene Formel, daher muss jede anders abgetastet werden. Ich werde nur die endgültige Sampling-Funktion für jeden zeigen. Wenn Sie sehen möchten, wie die Formel abgeleitet wird, lesen Sie den Blog-Beitrag.
GGX ist definiert als:
DG G X( m ) = α2π( ( α2- 1 ) cos2( θ ) + 1 )2
Zum Abtasten des sphärischen Koordinatenwinkels θkönnen wir die Formel verwenden:
θ = Arccos( α2ξ1( α2- 1 ) + 1------------√)
woher ξ ist eine einheitliche Zufallsvariable.
Wir gehen davon aus, dass der NDF isotrop ist, damit wir eine Probe entnehmen können ϕ gleichmäßig:
ϕ = ξ2
Beckmann ist definiert als:
DB e c k m a n n( m ) = 1πα2cos4( θ )e- Bräune2( θ )α2
Welche kann mit probiert werden:
θ = Arccos( 11 = α2ln( 1 - ξ1)--------------√)ϕ = ξ2
Schließlich ist Blinn definiert als:
DB l i n n( m ) = α + 22 π( cos( θ ) )α
Welche kann mit probiert werden:
θ = Arccos( 1ξα + 11)ϕ = ξ2
Umsetzen in die Praxis
Schauen wir uns einen grundlegenden Pfadfinder für Rückwärtsgänge an:
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. Wir hüpfen durch die Szene und akkumulieren dabei Farbe und Lichtschwächung. Bei jedem Sprung müssen wir eine neue Richtung für den Strahl wählen. Wie oben erwähnt, könnten wir die Hemisphäre gleichmäßig abtasten, um den neuen Strahl zu erzeugen. Der Code ist jedoch schlauer. es ist wichtig, die neue Richtung auf der Grundlage der BRDF abzutasten. (Hinweis: Dies ist die Eingaberichtung, da wir ein Rückwärtspfadverfolger sind.)
// 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);
Welche könnte implementiert werden als:
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;
}
Nachdem wir die inputDirection ('wi' im Code) abgetastet haben, verwenden wir diese, um den Wert der BRDF zu berechnen. Und dann teilen wir nach der Monte-Carlo-Formel durch das pdf:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Wobei Eval () nur die BRDF-Funktion selbst ist (Lambert, Blinn-Phong, Cook-Torrance usw.):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}