Para entender a roleta russa, vejamos um rastreador de caminho muito básico para trás:
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. pulamos pela cena, acumulando cores e atenuação de luz à medida que avançamos. Para ser completamente matematicamente imparcial, os saltos devem ir para o infinito. Mas isso é irreal e, como você observou, não é visualmente necessário; para a maioria das cenas, após um certo número de rejeições, digamos 10, a quantidade de contribuição para a cor final é muito, muito mínima.
Portanto, para economizar recursos de computação, muitos rastreadores de caminho têm um limite rígido para o número de rejeições. Isso adiciona viés.
Dito isto, é difícil escolher qual deve ser esse limite. Algumas cenas ficam ótimas após 2 saltos; outros (digamos, com transmissão ou SSS) podem levar até 10 ou 20.
Se escolhermos muito baixo, a imagem será visivelmente distorcida. Mas se escolhermos muito alto, estamos desperdiçando energia e tempo de computação.
Uma maneira de resolver isso, como você observou, é finalizar o caminho depois de atingirmos algum limiar de atenuação. Isso também adiciona viés.
A fixação de um limite funcionará , mas novamente, como escolhemos o limite? Se escolhermos muito grande, a imagem será visivelmente tendenciosa, muito pequena e estamos desperdiçando recursos.
A Roleta Russa tenta resolver esses problemas de maneira imparcial. Primeiro, aqui está o código:
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);
}
A Roleta Russa termina aleatoriamente um caminho com uma probabilidade inversamente igual à taxa de transferência. Portanto, caminhos com baixo rendimento que não contribuem muito para a cena têm mais probabilidade de serem finalizados.
Se pararmos por aí, ainda somos tendenciosos. Nós "perdemos" a energia do caminho que terminamos aleatoriamente. Para torná-lo imparcial, aumentamos a energia dos caminhos não terminados pela probabilidade de serem terminados. Isso, além de aleatório, torna a Roleta Russa imparcial.
Para responder às suas últimas perguntas:
- A Roleta Russa oferece um resultado imparcial?
- A Roleta Russa é necessária para obter um resultado imparcial?
- Depende do que você quer dizer com imparcial. Se você quer dizer matematicamente, então sim. No entanto, se você quer dizer visualmente, então não. Você só precisa escolher a profundidade máxima do caminho e o limite de corte com muito cuidado. Isso pode ser muito entediante, pois pode mudar de cena para cena.
- Você pode usar uma probabilidade fixa (corte) e depois redistribuir a energia 'perdida'. Isso é imparcial?
- Se você usa uma probabilidade fixa, está adicionando viés. Ao redistribuir a energia 'perdida', você reduz o viés, mas ainda é matematicamente tendencioso. Para ser completamente imparcial, deve ser aleatório.
- Se a energia que seria perdida ao terminar um raio sem redistribuí-lo é eventualmente perdida (como os raios aos quais é redistribuído também acabam), como isso melhora a situação?
- A Roleta Russa só para de saltar. Não remove a amostra completamente. Além disso, a energia 'perdida' é contabilizada nos saltos até o término. Portanto, a única maneira de a energia ser "eventualmente perdida de qualquer maneira" seria ter um quarto completamente preto.
No final, a Roleta Russa é um algoritmo muito simples que utiliza uma quantidade muito pequena de recursos computacionais extras. Em troca, ele pode economizar uma grande quantidade de recursos computacionais. Portanto, não vejo realmente um motivo para não usá-lo.
to be completely unbiased it must be random
. Acho que você ainda pode obter resultados matemáticos, usando pesos fracionados de amostras, em vez da passagem / queda binária que a roleta russa impõe, é só que a roleta convergirá mais rapidamente porque está operando uma amostragem de importância perfeita.