Per capire la roulette russa, 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. Per essere completamente matematicamente imparziali, i rimbalzi dovrebbero andare all'infinito. Ma questo non è realistico e, come hai notato, non è visivamente necessario; per la maggior parte delle scene, dopo un certo numero di rimbalzi, diciamo 10, la quantità di contributo al colore finale è molto minima.
Quindi, al fine di risparmiare risorse di calcolo, molti tracciatori di percorsi hanno un limite rigido al numero di rimbalzi. Questo aggiunge parzialità.
Detto questo, è difficile scegliere quale dovrebbe essere quel limite. Alcune scene sembrano fantastiche dopo 2 rimbalzi; altri (diciamo con trasmissione o SSS) possono richiedere fino a 10 o 20.
Se scegliamo troppo in basso, l'immagine sarà visibilmente distorta. Ma se scegliamo troppo in alto, stiamo sprecando energia e tempo di calcolo.
Un modo per risolverlo, come hai notato, è terminare il percorso dopo aver raggiunto una certa soglia di attenuazione. Questo aggiunge anche parzialità.
Il serraggio dopo una soglia funzionerà , ma ancora una volta, come possiamo scegliere la soglia? Se scegliamo troppo grande, l'immagine sarà visibilmente distorta, troppo piccola e stiamo sprecando risorse.
La roulette russa tenta di risolvere questi problemi in modo imparziale. Innanzitutto, ecco il codice:
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;
// Russian Roulette
// Randomly terminate a path with a probability inversely equal to the throughput
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
// Add the energy we 'lose' by randomly terminating paths
throughput *= 1 / p;
// 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);
}
La roulette russa termina casualmente un percorso con una probabilità inversamente uguale alla velocità effettiva. Quindi è più probabile che vengano interrotti i percorsi con un throughput basso che non contribuiranno molto alla scena.
Se ci fermiamo lì, siamo ancora di parte. 'Perdiamo' l'energia del percorso che terminiamo casualmente. Per renderlo imparziale, aumentiamo l'energia dei percorsi non terminati dalla loro probabilità di essere terminati. Questo, oltre ad essere casuale, rende la roulette russa imparziale.
Per rispondere alle tue ultime domande:
- La roulette russa dà un risultato imparziale?
- La roulette russa è necessaria per un risultato imparziale?
- Dipende da cosa intendi per imparziale. Se intendi matematicamente, allora sì. Tuttavia, se intendi visivamente, allora no. Devi solo scegliere la profondità massima del percorso e la soglia di taglio con molta attenzione. Questo può essere molto noioso poiché può cambiare da una scena all'altra.
- Puoi usare una probabilità fissa (cut-off) e quindi ridistribuire l'energia "persa". È imparziale?
- Se usi una probabilità fissa, stai aggiungendo distorsione. Ridistribuendo l'energia "persa", si riduce il pregiudizio, ma è ancora matematicamente distorto. Per essere completamente imparziale, deve essere casuale.
- Se l'energia che andrebbe persa terminando un raggio senza ridistribuire la sua energia alla fine viene persa comunque (man mano che i raggi a cui viene ridistribuita vengono infine interrotti), in che modo ciò migliora la situazione?
- La roulette russa ferma solo il rimbalzo. Non rimuove completamente il campione. Inoltre, l'energia "persa" è spiegata nei rimbalzi fino alla fine. Quindi l'unico modo in cui l'energia "alla fine potrebbe essere persa" sarebbe quella di avere una stanza completamente nera.
Alla fine, la roulette russa è un algoritmo molto semplice che utilizza una quantità molto piccola di risorse di calcolo extra. In cambio, può risparmiare una grande quantità di risorse computazionali. Pertanto, non riesco davvero a vedere un motivo per non usarlo.
to be completely unbiased it must be random
. Penso che puoi ancora ottenere risultati matematici ok usando la distribuzione frazionaria dei campioni, piuttosto che il passaggio / drop binario imposto dalla roulette russa, è solo che la roulette converge più velocemente perché sta eseguendo un campionamento di importanza perfetta.