Tracciamento progressivo del percorso con campionamento leggero esplicito


14

Ho capito la logica dietro l'importanza del campionamento per la parte BRDF. Tuttavia, quando si tratta di campionare esplicitamente le sorgenti luminose, tutto diventa confuso. Ad esempio, se ho una sorgente luminosa puntiforme nella mia scena e se la campiono direttamente in ogni fotogramma costantemente, dovrei considerarla come un altro campione per l'integrazione Monte Carlo? Cioè, prendo un campione dalla distribuzione ponderata al coseno e l'altro dalla luce del punto. Sono due campioni in totale o solo uno? Inoltre, dovrei dividere la radianza proveniente dal campione diretto in qualsiasi termine?

Risposte:


19

Esistono più aree nel tracciato del percorso che possono essere campionate per importanza. Inoltre, ognuna di queste aree può anche utilizzare il campionamento di importanza multipla, proposto per la prima volta nel documento di Veach e Guibas del 1995 . Per spiegare meglio, diamo un'occhiata a un tracciatore di percorsi all'indietro:

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);
    SurfaceInteraction interaction;

    // Bounce the ray around the scene
    const uint maxBounces = 15;
    for (uint bounces = 0; bounces < maxBounces; ++bounces) {
        m_scene->Intersect(ray);

        // The ray missed. Return the background color
        if (ray.GeomID == INVALID_GEOMETRY_ID) {
            color += throughput * m_scene->BackgroundColor;
            break;
        }

        // 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 emission
        if (light != nullptr) {
            color += throughput * light->Le();
        }

        interaction.Position = ray.Origin + ray.Direction * ray.TFar;
        interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
        interaction.OutputDirection = normalize(-ray.Direction);


        // Get the new ray direction
        // Choose the direction based on the bsdf        
        material->bsdf->Sample(interaction, sampler);
        float pdf = material->bsdf->Pdf(interaction);

        // Accumulate the weight
        throughput = throughput * material->bsdf->Eval(interaction) / pdf;

        // Shoot a new ray

        // Set the origin at the intersection point
        ray.Origin = interaction.Position;

        // Reset the other ray properties
        ray.Direction = interaction.InputDirection;
        ray.TNear = 0.001f;
        ray.TFar = infinity;


        // Russian Roulette
        if (bounces > 3) {
            float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
            if (sampler->NextFloat() > p) {
                break;
            }

            throughput *= 1 / p;
        }
    }

    m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}

In inglese:

  1. Spara un raggio attraverso la scena
  2. Controlla se abbiamo colpito qualcosa. Altrimenti restituiamo il colore dello skybox e ci spezziamo.
  3. Controlla se accendiamo una luce. In tal caso, aggiungiamo l'emissione di luce al nostro accumulo di colore
  4. Scegli una nuova direzione per il raggio successivo. Possiamo farlo in modo uniforme o esempio di importanza basato sul BRDF
  5. Valuta il BRDF e accumulalo. Qui dobbiamo dividere per il pdf della nostra direzione prescelta, al fine di seguire l'algoritmo di Monte Carlo.
  6. Crea un nuovo raggio in base alla direzione scelta e da dove siamo appena arrivati
  7. [Opzionale] Usa Russian Roulette per scegliere se terminare il raggio
  8. Vai a 1

Con questo codice, otteniamo colore solo se il raggio alla fine colpisce una luce. Inoltre, non supporta fonti di luce puntuali, poiché non hanno area.

Per risolvere questo problema, campioniamo le luci direttamente ad ogni rimbalzo. Dobbiamo fare alcune piccole modifiche:

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);
    SurfaceInteraction interaction;

    // Bounce the ray around the scene
    const uint maxBounces = 15;
    for (uint bounces = 0; bounces < maxBounces; ++bounces) {
        m_scene->Intersect(ray);

        // The ray missed. Return the background color
        if (ray.GeomID == INVALID_GEOMETRY_ID) {
            color += throughput * m_scene->BackgroundColor;
            break;
        }

        // 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 this is the first bounce or if we just had a specular bounce,
        // we need to add the emmisive light
        if ((bounces == 0 || (interaction.SampledLobe & BSDFLobe::Specular) != 0) && light != nullptr) {
            color += throughput * light->Le();
        }

        interaction.Position = ray.Origin + ray.Direction * ray.TFar;
        interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
        interaction.OutputDirection = normalize(-ray.Direction);


        // Calculate the direct lighting
        color += throughput * SampleLights(sampler, interaction, material->bsdf, light);


        // Get the new ray direction
        // Choose the direction based on the bsdf        
        material->bsdf->Sample(interaction, sampler);
        float pdf = material->bsdf->Pdf(interaction);

        // Accumulate the weight
        throughput = throughput * material->bsdf->Eval(interaction) / pdf;

        // Shoot a new ray

        // Set the origin at the intersection point
        ray.Origin = interaction.Position;

        // Reset the other ray properties
        ray.Direction = interaction.InputDirection;
        ray.TNear = 0.001f;
        ray.TFar = infinity;


        // Russian Roulette
        if (bounces > 3) {
            float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
            if (sampler->NextFloat() > p) {
                break;
            }

            throughput *= 1 / p;
        }
    }

    m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}

Innanzitutto, aggiungiamo "color + = throughput * SampleLights (...)". Andrò nei dettagli su SampleLights () tra poco. Ma, essenzialmente, scorre attraverso tutte le luci e restituisce il loro contributo al colore, attenuato dal BSDF.

Questo è fantastico, ma dobbiamo apportare un altro cambiamento per renderlo corretto; in particolare, cosa succede quando colpiamo una luce. Nel vecchio codice, abbiamo aggiunto l'emissione di luce all'accumulo di colore. Ma ora campioniamo direttamente la luce ad ogni rimbalzo, quindi se aggiungessimo l'emissione della luce, faremmo una "doppia immersione". Pertanto, la cosa corretta da fare è ... nulla; saltiamo accumulando l'emissione di luce.

Tuttavia, ci sono due casi angolari:

  1. Il primo raggio
  2. Rimbalzi perfettamente speculari (aka specchi)

Se il primo raggio colpisce la luce, dovresti vedere direttamente l'emissione della luce. Quindi, se lo saltiamo, tutte le luci appariranno come nere, anche se le superfici intorno a loro sono illuminate.

Quando colpisci una superficie perfettamente speculare non puoi campionare direttamente una luce, perché un raggio di input ha solo un'uscita. Beh, tecnicamente, abbiamo potuto verificare che il raggio d'ingresso sta per colpire una luce, ma non c'è nessun punto; il loop principale di Path Tracing lo farà comunque. Pertanto, se colpiamo una luce subito dopo aver colpito una superficie speculare, dobbiamo accumulare il colore. In caso contrario, le luci saranno nere negli specchi.

Ora, approfondiamo SampleLights ():

float3 SampleLights(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
    std::size_t numLights = m_scene->NumLights();

    float3 L(0.0f);
    for (uint i = 0; i < numLights; ++i) {
        Light *light = &m_scene->Lights[i];

        // Don't let a light contribute light to itself
        if (light == hitLight) {
            continue;
        }

        L = L + EstimateDirect(light, sampler, interaction, bsdf);
    }

    return L;
}

In inglese:

  1. Attraversa tutte le luci
  2. Salta la luce se la colpiamo
    • Non fare doppio tuffo
  3. Accumula l'illuminazione diretta da tutte le luci
  4. Restituisce l'illuminazione diretta

BSDF(p,ωi,ωo)Li(p,ωi)

Per fonti di luce puntuali, questo è semplice come:

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        return float3(0.0f);
    }

    interaction.InputDirection = normalize(light->Origin - interaction.Position);
    return bsdf->Eval(interaction) * light->Li;
}

Tuttavia, se vogliamo che le luci abbiano un'area, dobbiamo prima campionare un punto sulla luce. Pertanto, la definizione completa è:

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    float3 directLighting = float3(0.0f);

    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        float pdf;
        float3 Li = light->SampleLi(sampler, m_scene, interaction, &pdf);

        // Make sure the pdf isn't zero and the radiance isn't black
        if (pdf != 0.0f && !all(Li)) {
            directLighting += bsdf->Eval(interaction) * Li / pdf;
        }
    }

    return directLighting;
}

Possiamo implementare light-> SampleLi come vogliamo; possiamo scegliere il punto in modo uniforme, o campione di importanza. In entrambi i casi, dividiamo la radiosità per il pdf della scelta del punto. Ancora una volta, per soddisfare le esigenze di Monte Carlo.

Se il BRDF è altamente dipendente dalla vista, potrebbe essere meglio scegliere un punto basato sul BRDF, anziché un punto casuale sulla luce. Ma come scegliamo? Campione basato sulla luce o basato sul BRDF?

BSDF(p,ωi,ωo)Li(p,ωi)

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    float3 directLighting = float3(0.0f);
    float3 f;
    float lightPdf, scatteringPdf;


    // Sample lighting with multiple importance sampling
    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        float3 Li = light->SampleLi(sampler, m_scene, interaction, &lightPdf);

        // Make sure the pdf isn't zero and the radiance isn't black
        if (lightPdf != 0.0f && !all(Li)) {
            // Calculate the brdf value
            f = bsdf->Eval(interaction);
            scatteringPdf = bsdf->Pdf(interaction);

            if (scatteringPdf != 0.0f && !all(f)) {
                float weight = PowerHeuristic(1, lightPdf, 1, scatteringPdf);
                directLighting += f * Li * weight / lightPdf;
            }
        }
    }


    // Sample brdf with multiple importance sampling
    bsdf->Sample(interaction, sampler);
    f = bsdf->Eval(interaction);
    scatteringPdf = bsdf->Pdf(interaction);
    if (scatteringPdf != 0.0f && !all(f)) {
        lightPdf = light->PdfLi(m_scene, interaction);
        if (lightPdf == 0.0f) {
            // We didn't hit anything, so ignore the brdf sample
            return directLighting;
        }

        float weight = PowerHeuristic(1, scatteringPdf, 1, lightPdf);
        float3 Li = light->Le();
        directLighting += f * Li * weight / scatteringPdf;
    }

    return directLighting;
}

In inglese:

  1. Innanzitutto, campioniamo la luce
    • Questo aggiorna interazione.InputDirection
    • Ci dà il Li per la luce
    • E il pdf di scegliere quel punto sulla luce
  2. Verifica che il pdf sia valido e che la radianza sia diversa da zero
  3. Valutare BSDF utilizzando InputDirection campionato
  4. Calcola il pdf per il BSDF dato l'InputDirection campionato
    • In sostanza, quanto è probabile questo campione, se dovessimo campionare usando il BSDF, invece della luce
  5. Calcola il peso, usando il pdf leggero e il pdf BSDF
    • Veach e Guibas definiscono un paio di modi diversi per calcolare il peso. Sperimentalmente, hanno trovato l'euristica del potere con una potenza di 2 per funzionare al meglio nella maggior parte dei casi. Ti rimando al documento per maggiori dettagli. L'implementazione è di seguito
  6. Moltiplicare il peso con il calcolo dell'illuminazione diretta e dividere per la luce pdf. (Per Monte Carlo) E aggiungere all'accumulo di luce diretta.
  7. Quindi, campioniamo il BRDF
    • Questo aggiorna interazione.InputDirection
  8. Valuta il BRDF
  9. Ottieni il pdf per scegliere questa direzione in base al BRDF
  10. Calcola il pdf leggero, in base alla InputDirection campionata
    • Questo è lo specchio di prima. Quanto è probabile questa direzione, se dovessimo campionare la luce
  11. Se lightPdf == 0.0f, il raggio ha perso la luce, quindi restituisci l'illuminazione diretta dal campione di luce.
  12. Altrimenti, calcola il peso e aggiungi l'illuminazione diretta BSDF all'accumulo
  13. Infine, restituisci l'illuminazione diretta accumulata

.

inline float PowerHeuristic(uint numf, float fPdf, uint numg, float gPdf) {
    float f = numf * fPdf;
    float g = numg * gPdf;

    return (f * f) / (f * f + g * g);
}

Ci sono una serie di ottimizzazioni / miglioramenti che puoi fare in queste funzioni, ma le ho ridotte per cercare di renderle più facili da comprendere. Se lo desideri, posso condividere alcuni di questi miglioramenti.

Campionamento di una sola luce

In SampleLights () analizziamo tutte le luci e otteniamo il loro contributo. Per un piccolo numero di luci, va bene, ma per centinaia o migliaia di luci, questo diventa costoso. Fortunatamente, possiamo sfruttare il fatto che l'integrazione di Monte Carlo è una media gigantesca. Esempio:

Definiamo

h(x)=f(x)+g(x)

h(x)

h(x)=1Ni=1Nf(xi)+g(xi)

f(x)g(x)

h(x)=1Ni=1Nr(ζ,x)pdf

ζr(ζ,x)

r(ζ,x)={f(x),0.0ζ<0.5g(x),0.5ζ<1.0

pdf=12

In inglese:

  1. f(x)g(x)
  2. 12
  3. Media

Man mano che N diventa grande, la stima converge alla soluzione corretta.

Possiamo applicare questo stesso principio al campionamento leggero. Invece di campionare ogni luce, scegliamo casualmente una luce e moltiplichiamo il risultato per il numero di luci (è lo stesso che dividiamo per il pdf frazionario):

float3 SampleOneLight(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
    std::size_t numLights = m_scene->NumLights();

    // Return black if there are no lights
    // And don't let a light contribute light to itself
    // Aka, if we hit a light
    // This is the special case where there is only 1 light
    if (numLights == 0 || numLights == 1 && hitLight != nullptr) {
        return float3(0.0f);
    }

    // Don't let a light contribute light to itself
    // Choose another one
    Light *light;
    do {
        light = m_scene->RandomOneLight(sampler);
    } while (light == hitLight);

    return numLights * EstimateDirect(light, sampler, interaction, bsdf);
}

1numLights

Importanza multipla Campionamento della direzione "Nuovo raggio"

L'importanza del codice corrente campiona solo la direzione "New Ray" basata sul BSDF. E se vogliamo anche dare importanza al campione in base alla posizione delle luci?

Prendendo da ciò che abbiamo appreso sopra, un metodo sarebbe quello di sparare due "nuovi" raggi e pesare ciascuno sulla base dei loro pdf. Tuttavia, questo è sia costoso dal punto di vista computazionale, sia difficile da implementare senza ricorsione.

Per ovviare a questo, possiamo applicare gli stessi principi che abbiamo imparato campionando solo una luce. Cioè, scegli uno a caso da campionare e dividi per il pdf di sceglierlo.

// Get the new ray direction

// Randomly (uniform) choose whether to sample based on the BSDF or the Lights
float p = sampler->NextFloat();

Light *light = m_scene->RandomLight();

if (p < 0.5f) {
    // Choose the direction based on the bsdf 
    material->bsdf->Sample(interaction, sampler);
    float bsdfPdf = material->bsdf->Pdf(interaction);

    float lightPdf = light->PdfLi(m_scene, interaction);
    float weight = PowerHeuristic(1, bsdfPdf, 1, lightPdf);

    // Accumulate the throughput
    throughput = throughput * weight * material->bsdf->Eval(interaction) / bsdfPdf;

} else {
    // Choose the direction based on a light
    float lightPdf;
    light->SampleLi(sampler, m_scene, interaction, &lightPdf);

    float bsdfPdf = material->bsdf->Pdf(interaction);
    float weight = PowerHeuristic(1, lightPdf, 1, bsdfPdf);

    // Accumulate the throughput
    throughput = throughput * weight * material->bsdf->Eval(interaction) / lightPdf;
}

Che tutti Detto questo, abbiamo davvero voglia di campione importanza direzione "Nuovo Ray" in base alla luce? Per l' illuminazione diretta , la radiosità è influenzata sia dal BSDF della superficie, sia dalla direzione della luce. Ma per l' illuminazione indiretta , la radiosity è quasi esclusivamente definita dal BSDF della superficie colpita prima. Quindi, l'aggiunta di campionamenti di importanza leggera non ci dà nulla.

Pertanto, è comune campionare solo l'importanza della "Nuova direzione" con il BSDF, ma applicare il campionamento di importanza multipla all'illuminazione diretta.


Grazie per la risposta chiarificatrice! Capisco che se dovessimo usare un tracciatore di percorsi senza campionamento esplicito della luce, non colpiremmo mai una sorgente luminosa puntuale. Quindi, possiamo sostanzialmente aggiungere il suo contributo. D'altra parte, se campioniamo una sorgente luminosa di area, dobbiamo assicurarci di non colpirla di nuovo con l'illuminazione indiretta per evitare il doppio calo
Mustafa Işık

Esattamente! C'è qualche parte su cui hai bisogno di chiarimenti? O non ci sono abbastanza dettagli?
RichieSams,

Inoltre, il campionamento a importanza multipla viene utilizzato solo per il calcolo dell'illuminazione diretta? Forse mi sono perso ma non ne ho visto un altro esempio. Se scatto un solo raggio per rimbalzo nel mio tracciatore di percorso, sembra che non riesca a farlo per il calcolo dell'illuminazione indiretta.
Mustafa Işık,

2
Il campionamento per importanza multipla può essere applicato ovunque si usi il campionamento per importanza. Il potere del campionamento di importanza multipla è che possiamo combinare i vantaggi di più tecniche di campionamento. Ad esempio, in alcuni casi, il campionamento di importanza leggera sarà migliore del campionamento BSDF. In altri casi, viceversa. MIS combinerà il meglio di entrambi i mondi. Tuttavia, se il campionamento BSDF sarà migliore del 100% delle volte, non c'è motivo di aggiungere la complessità del MIS. Ho aggiunto alcune sezioni alla risposta per
approfondire

1
Sembra che abbiamo separato le fonti di radianza in arrivo in due parti come dirette e indirette. Campioniamo esplicitamente le luci per la parte diretta e, durante il campionamento di questa parte, è ragionevole prendere in considerazione sia le luci che i BSDF. Per la parte indiretta, tuttavia, non abbiamo idea di quale direzione possa potenzialmente darci valori di radianza più elevati poiché è il problema stesso che vogliamo risolvere. Tuttavia, possiamo dire quale direzione può contribuire maggiormente in base al termine del coseno e al BSDF. Questo è quello che capisco. Correggimi se sbaglio e grazie per la tua fantastica risposta.
Mustafa Işık,
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.