Risposta breve:
Il campionamento dell'importanza è un metodo per ridurre la varianza nell'integrazione Monte Carlo scegliendo uno stimatore vicino alla forma della funzione effettiva.
PDF è un'abbreviazione per Probability Density Function . A pdf(x) fornisce la probabilità che un campione casuale generato sia x .
Risposta lunga:
Per iniziare, esaminiamo cos'è l'integrazione Monte Carlo e come appare matematicamente.
L'integrazione Monte Carlo è una tecnica per stimare il valore di un integrale. In genere viene utilizzato quando non esiste una soluzione in formato chiuso per l'integrale. Sembra così:
∫f(x)dx≈1N∑i=1Nf(xi)pdf(xi)
In inglese, ciò dice che è possibile approssimare un integrale calcolando la media di campioni casuali successivi dalla funzione. Man mano che N diventa grande, l'approssimazione si avvicina sempre di più alla soluzione. pdf(xi) rappresenta la funzione di densità di probabilità di ciascun campione casuale.
Facciamo un esempio: Calcolare il valore dell'integrale I .
I=∫2π0e−xsin(x)dx
Usiamo l'integrazione Monte Carlo:
I≈1N∑i=1Ne−xsin(xi)pdf(xi)
Un semplice programma Python per calcolare questo è:
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)
Se eseguiamo il programma otteniamo I=0.4986941
Usando la separazione per parti, possiamo ottenere la soluzione esatta:
I=12(1−e−2π)=0.4990663
Noterai che la soluzione Monte Carlo non è del tutto corretta. Questo perché è una stima. Detto questo, mentre va all'infinito, la stima dovrebbe avvicinarsi sempre di più alla risposta corretta. Già a alcune corse sono quasi identiche alla risposta corretta.NN=2000
Una nota sul PDF: in questo semplice esempio, prendiamo sempre un campione casuale uniforme. Un campione casuale uniforme significa che ogni campione ha la stessa identica probabilità di essere scelto. Campioniamo nell'intervallo quindi,[0,2π]pdf(x)=1/(2π−0)
Il campionamento dell'importanza funziona non campionando in modo uniforme. Invece proviamo a scegliere più campioni che contribuiscono molto al risultato (importante) e meno campioni che contribuiscono solo un po 'al risultato (meno importante). Da qui il nome, campionamento di importanza.
Se si sceglie una funzione di campionamento il cui pdf si avvicina molto alla forma di , è possibile ridurre notevolmente la varianza, il che significa che è possibile prelevare meno campioni. Tuttavia, se si sceglie una funzione di campionamento il cui valore è molto diverso da , è possibile aumentare la varianza. Vedi l'immagine qui sotto:
Immagine dall'appendice A della tesi di Wojciech Jaroszff
Un esempio di campionamento di importanza in Path Tracing è come scegliere la direzione di un raggio dopo che colpisce una superficie. Se la superficie non è perfettamente speculare (cioè uno specchio o un vetro), il raggio in uscita può trovarsi ovunque nell'emisfero.
Abbiamo potuto uniformemente assaggiare l'emisfero per generare il nuovo raggio. Tuttavia, possiamo sfruttare il fatto che l'equazione di rendering ha un fattore coseno in essa:
Lo(p,ωo)=Le(p,ωo)+∫Ωf(p,ωi,ωo)Li(p,ωi)|cosθi|dωi
In particolare, sappiamo che eventuali raggi all'orizzonte saranno fortemente attenuati (in particolare, ). Quindi, i raggi generati vicino all'orizzonte non contribuiranno molto al valore finale.cos(x)
Per combattere questo, usiamo il campionamento di importanza. Se generiamo raggi secondo un emisfero ponderato per il coseno, ci assicuriamo che vengano generati più raggi ben sopra l'orizzonte e meno vicino all'orizzonte. Ciò ridurrà la varianza e ridurrà il rumore.
Nel tuo caso, hai specificato che utilizzerai un BRDF Cook-Torrance basato su microfacet. La forma comune è:
f(p,ωi,ωo)=F(ωi,h)G(ωi,ωo,h)D(h)4cos(θi)cos(θo)
dove
F(ωi,h)=Fresnel functionG(ωi,ωo,h)=Geometry Masking and Shadowing functionD(h)=Normal Distribution Function
Il blog "A Graphic's Guy's Note" ha un eccellente resoconto su come campionare i BRDF Cook-Torrance. Ti farò riferimento al suo post sul blog . Detto questo, proverò a creare una breve panoramica di seguito:
L'NDF è generalmente la porzione dominante del BRDF di Cook-Torrance, quindi se stiamo andando a campionare l'importanza, dovremmo campionare in base all'NDF.
Cook-Torrance non specifica un NDF specifico da utilizzare; siamo liberi di scegliere quello che più si adatta alla nostra fantasia. Detto questo, ci sono alcuni NDF popolari:
Ogni NDF ha la sua formula, quindi ognuno deve essere campionato in modo diverso. Mostrerò solo la funzione di campionamento finale per ciascuno. Se desideri vedere come viene derivata la formula, consulta il post sul blog.
GGX è definito come:
DGGX(m)=α2π((α2−1)cos2(θ)+1)2
Per campionare l'angolo delle coordinate sferiche , possiamo usare la formula:θ
θ=arccos(α2ξ1(α2−1)+1−−−−−−−−−−−−√)
dove è una variabile casuale uniforme.ξ
Partiamo dal presupposto che il NDF è isotropo, quindi possiamo campionare modo uniforme:ϕ
ϕ=ξ2
Beckmann è definito come:
DBeckmann(m)=1πα2cos4(θ)e−tan2(θ)α2
Che può essere campionato con:
θ=arccos(11=α2ln(1−ξ1)−−−−−−−−−−−−−−√)ϕ=ξ2
Infine, Blinn è definito come:
DBlinn(m)=α+22π(cos(θ))α
Che può essere campionato con:
θ=arccos(1ξα+11)ϕ=ξ2
Mettendolo in pratica
Diamo un'occhiata a un tracciatore di percorsi all'indietro di base:
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. rimbalziamo intorno alla scena, accumulando colore e attenuazione della luce mentre procediamo. Ad ogni rimbalzo, dobbiamo scegliere una nuova direzione per il raggio. Come accennato in precedenza, abbiamo potuto uniformemente assaggiare l'emisfero per generare il nuovo raggio. Tuttavia, il codice è più intelligente; importanza campiona la nuova direzione basata sul BRDF. (Nota: questa è la direzione di input, perché siamo un tracciatore di percorsi all'indietro)
// 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);
Che potrebbe essere implementato come:
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;
}
Dopo aver campionato inputDirection ('wi' nel codice), lo usiamo per calcolare il valore di BRDF. E poi dividiamo per il pdf secondo la formula di Monte Carlo:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Dove Eval () è solo la funzione BRDF stessa (Lambert, Blinn-Phong, Cook-Torrance, ecc.):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}