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:
- Spara un raggio attraverso la scena
- Controlla se abbiamo colpito qualcosa. Altrimenti restituiamo il colore dello skybox e ci spezziamo.
- Controlla se accendiamo una luce. In tal caso, aggiungiamo l'emissione di luce al nostro accumulo di colore
- Scegli una nuova direzione per il raggio successivo. Possiamo farlo in modo uniforme o esempio di importanza basato sul BRDF
- Valuta il BRDF e accumulalo. Qui dobbiamo dividere per il pdf della nostra direzione prescelta, al fine di seguire l'algoritmo di Monte Carlo.
- Crea un nuovo raggio in base alla direzione scelta e da dove siamo appena arrivati
- [Opzionale] Usa Russian Roulette per scegliere se terminare il raggio
- 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:
- Il primo raggio
- 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:
- Attraversa tutte le luci
- Salta la luce se la colpiamo
- Accumula l'illuminazione diretta da tutte le luci
- Restituisce l'illuminazione diretta
B SD F( p , ωio, ωo) Lio( p , ωio)
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?
B SD F( p , ωio, ωo) Lio( p , ωio)
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:
- Innanzitutto, campioniamo la luce
- Questo aggiorna interazione.InputDirection
- Ci dà il Li per la luce
- E il pdf di scegliere quel punto sulla luce
- Verifica che il pdf sia valido e che la radianza sia diversa da zero
- Valutare BSDF utilizzando InputDirection campionato
- 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
- 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
- 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.
- Quindi, campioniamo il BRDF
- Questo aggiorna interazione.InputDirection
- Valuta il BRDF
- Ottieni il pdf per scegliere questa direzione in base al BRDF
- Calcola il pdf leggero, in base alla InputDirection campionata
- Questo è lo specchio di prima. Quanto è probabile questa direzione, se dovessimo campionare la luce
- Se lightPdf == 0.0f, il raggio ha perso la luce, quindi restituisci l'illuminazione diretta dal campione di luce.
- Altrimenti, calcola il peso e aggiungi l'illuminazione diretta BSDF all'accumulo
- 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 ) = 1NΣi = 1Nf( xio) + g( xio)
f( x )g( x )
h ( x ) = 1NΣi = 1Nr ( ζ, x )p df
ζr ( ζ, x )
r ( ζ, x ) = { f( x ) ,g( x ) ,0,0 ≤ ζ< 0,50,5 ≤ ζ< 1.0
p df= 12
In inglese:
- f( x )g( x )
- 12
- 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.