Для того, щоб зрозуміти російську рулетку, давайте подивимося на дуже основний відстежуючий зворотній шлях:
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. ми підстрибуємо навколо сцени, накопичуючи колір і ослаблення світла під час руху. Для того, щоб бути повністю математично неупередженим, відскоки повинні йти до нескінченності. Але це нереально, і, як ви зазначали, візуально не потрібно; для більшості сцен після певної кількості відмов, скажімо, 10, розмір внеску в остаточний колір дуже дуже мінімальний.
Таким чином, для економії обчислювальних ресурсів багато трасерів шляху мають жорстке обмеження кількості відмов. Це додає упередженості.
Однак, важко вибрати, якою має бути ця жорстка межа. Деякі сцени виглядають чудово після 2 відмов; інші (скажімо, з передачею або SSS) можуть зайняти до 10 або 20.
Якщо ми виберемо занадто низьке, зображення буде помітно упередженим. Але якщо ми обираємо занадто високу, ми витрачаємо на обчислювальну енергію та час.
Як ви зазначили, один із способів вирішити це - припинити шлях після досягнення певного порогу ослаблення. Це також додає упередженості.
Затискання після порогу спрацює , але знову ж таки, як ми обираємо поріг? Якщо ми виберемо занадто великі, зображення буде помітно упередженим, занадто малим, і ми витрачаємо ресурси.
Російська рулетка намагається вирішити ці проблеми неупереджено. По-перше, ось код:
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);
}
Російська рулетка випадковим чином завершує шлях з ймовірністю, обернено рівною пропускній здатності. Тож шляхи з низькою пропускною здатністю, які не сприятимуть великій кількості сцени, швидше за все припиняються.
Якщо ми зупинимось там, ми все ще упереджені. Ми «втрачаємо» енергію шляху, який ми випадково закінчуємо. Щоб зробити його неупередженим, ми збільшуємо енергію неприпинених контурів, імовірність їх припинення. Це, поряд із випадковістю, робить російську рулетку неупередженою.
Щоб відповісти на ваші останні запитання:
- Чи дає російська рулетка неупереджений результат?
- Чи потрібна російська рулетка для неупередженого результату?
- Залежить від того, що ви маєте на увазі неупереджено. Якщо ви маєте на увазі математично, то так. Однак якщо ви маєте на увазі візуально, то ні. Вам потрібно дуже обережно вибрати максимальну глибину шляху та поріг обрізання. Це може бути дуже нудним, оскільки може змінюватися від сцени до сцени.
- Чи можете ви скористатися фіксованою ймовірністю (скорочення), а потім перерозподілити «втрачену» енергію. Це неупереджено?
- Якщо ви використовуєте фіксовану ймовірність, ви додаєте упередженість. Перерозподіляючи «втрачену» енергію, ви зменшуєте упередження, але воно все ще є математично упередженим. Щоб бути абсолютно неупередженим, це повинно бути випадковим.
- Якщо енергія, яка була б втрачена при припиненні променя, не перерозподіляючи його енергію, врешті-решт втрачається (оскільки промені, на які він перерозподіляється, також врешті-решт припиняються), як це покращує ситуацію?
- Російська рулетка лише зупиняє підстрибування. Це не видаляє зразок повністю. Крім того, «втрачена» енергія враховується в очікуванні до припинення. Тож єдиним способом енергії, яка в остаточному підсумку все-таки втрачається, було б мати повністю чорну кімнату.
Зрештою, російська рулетка - це дуже простий алгоритм, який використовує дуже малу кількість додаткових обчислювальних ресурсів. В обмін це може заощадити велику кількість обчислювальних ресурсів. Тому я не можу реально побачити причину не використовувати його.
to be completely unbiased it must be random
. Я думаю, що ви все одно можете отримати математичні результати, використовуючи дробове зважування зразків, а не двійковий пропуск / краплю, яку нав'язує російська рулетка, це просто те, що рулетка збіжиться швидше, оскільки вона має ідеальну вибірку важливості.