Che cos'è il campionamento dell'importanza?


33

Che cos'è il campionamento per importanza? Ogni articolo che leggo menziona 'PDF' che cos'è anche quello?

Da quello che raccolgo, il campionamento di importanza è una tecnica per campionare solo le aree di un emisfero che contano più di altre. Quindi, idealmente, dovrei campionare i raggi verso fonti di luce per ridurre il rumore e aumentare la velocità. Inoltre, alcuni BRDF agli angoli di pascolo non hanno alcuna differenza nel calcolo, quindi usare il campionamento per importanza per evitare che sia buono?

Se dovessi implementare il campionamento di importanza per un BRDF Cook-Torrance come potrei farlo?


Questo link è una buona lettura che spiega cos'è un PDF. TL; DR un PDF è una funzione che descrive la probabilità di numeri casuali (continui o in virgola mobile). Generare numeri casuali da un PDF specifico può essere una sfida e ci sono alcune tecniche per farlo. Questo parla di uno di loro. L'articolo dopo questo parla di un altro modo. blog.demofox.org/2017/08/05/…
Alan Wolfe il

Risposte:


51

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)dx1Ni=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=02πexsin(x)dx

Usiamo l'integrazione Monte Carlo:

I1Ni=1Nexsin(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(1e2π)=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 JaroszffConfronto tra campionamento buono e campionamento negativo

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.

I raggi in uscita possono andare 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:

  • GGX
  • Beckmann
  • Blinn

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π((α21)cos2(θ)+1)2

Per campionare l'angolo delle coordinate sferiche , possiamo usare la formula:θ

θ=arccos(α2ξ1(α21)+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(θ)etan2(θ)α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ξ1α+1)ϕ=ξ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);
}

Bella risposta. L'OP ha anche chiesto il campionamento dell'importanza di Cook-Torrance che questa risposta non tocca.
PeteUK,

6
Ho aggiornato la risposta per aggiungere una sezione su Cook-Torrance
RichieSams

Ad esempio GGX, per campionare l'angolo di coordinate sferiche cos (θ) usiamo la formula campionata per calcolare l'angolo e usarla come al solito in GGX, giusto? O la formula sostituisce interamente GGX?
Arjan Singh,

3
Ho aggiunto una sezione per aiutarti a rispondere alle tue domande. Ma, in breve, il tuo primo metodo è corretto. Si utilizza la formula di campionamento per generare una direzione, quindi si utilizza quella nuova direzione nella normale formula GGX e per ottenere il pdf per la formula Monte Carlo.
RichieSams,

Per GGX come dovrei calcolare / campionare wi? Capisco come campionare l'angolo delle coordinate sferiche θ ma per il vettore di direzione reale come viene fatto?
Arjan Singh,

11

Se hai una funzione 1D e vuoi integrare questa funzione da 0 a 1, un modo per eseguire questa integrazione è prendere N campioni casuali nell'intervallo [0, 1], valutare per ogni campionare e calcolare la media dei campioni. Tuttavia, si dice che questa "ingenua" integrazione di Monte Carlo "converge lentamente", vale a dire che è necessario un gran numero di campioni per avvicinarsi alla verità del terreno, in particolare se la funzione ha alte frequenze.f(x)f(x)

Con il campionamento per importanza, invece di prelevare N campioni casuali nell'intervallo [0, 1], si prelevano più campioni nelle regioni "importanti" di che contribuiscono maggiormente al risultato finale. Tuttavia, poiché si predispone il campionamento verso le regioni importanti della funzione, questi campioni devono essere ponderati di meno per contrastare il bias, che è dove arriva il PDF (funzione di densità di probabilità). Il PDF indica la probabilità di un campione in una determinata posizione e viene utilizzato per calcolare la media ponderata dei campioni dividendo ciascun campione con il valore PDF in ciascuna posizione del campione.f(x)

Con il campionamento di importanza di Cook-Torrance la pratica comune è quella di distribuire campioni in base alla normale funzione di distribuzione NDF. Se NDF è già normalizzato, può fungere direttamente da PDF, il che è conveniente poiché annulla il termine fuori dalla valutazione BRDF. L'unica cosa che devi fare è quindi distribuire le posizioni campione basate su PDF e valutare BRDF senza il termine NDF, cioè E calcolare la media dei risultati del campione moltiplicata per l'angolo solido del dominio su cui si integra (ad es. per l'emisfero).

f=FGπ(nωi)(nωo)
2π

Per NDF è necessario calcolare la funzione di distribuzione cumulativa del PDF per convertire la posizione campione distribuita uniformemente in posizione campione ponderata PDF. Per NDF isotropico questo semplifica la funzione 1D a causa della simmetria della funzione. Per maggiori dettagli sulla derivazione di CDF puoi consultare questo vecchio articolo Gemme GPU .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.