Прогресивне трасування шляху з явним відбором світла


14

Я зрозумів логіку, що стоїть за важливістю вибірки для частини БРР. Однак, коли мова йде про чіткий вибір джерел світла, все стає заплутаним. Наприклад, якщо в моїй сцені є одне точкове джерело світла і якщо я безпосередньо випробовую його на кожному кадрі постійно, чи слід вважати це ще одним зразком для інтеграції monte carlo? Тобто я беру один зразок із розподіленого за косинусом розподілу, а інший - із точкового світла. Це всього два зразки чи лише один? Крім того, чи слід ділити сяйво, що виходить від прямої вибірки, на будь-який термін?

Відповіді:


19

У відстеженні шляху є кілька областей, які можуть бути важливими для вибірки. Крім того, кожна з цих областей може також використовувати вибірку з великою важливістю, вперше запропоновану в статті Веача та Гібаса 1995 року . Для кращого пояснення давайте подивимось на зворотний трасування шляху:

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);
}

Англійською:

  1. Стріляйте промінь через сцену
  2. Перевірте, чи вдаримо ми щось. Якщо ні, ми повертаємо колір skybox і ламаємо.
  3. Перевірте, чи потрапили ми у світло. Якщо так, ми додаємо світло випромінювання до нашого накопичення кольорів
  4. Виберіть новий напрямок для наступного променя. Ми можемо зробити це рівномірно, або важливий зразок на основі BRDF
  5. Оцініть BRDF та накопичіть його. Тут ми повинні розділити на pdf обраного нами напрямку, щоб наслідувати алгоритм Монте-Карло.
  6. Створіть новий промінь на основі обраного нами напрямку та звідки ми щойно прийшли
  7. [Необов’язково] Використовуйте російську рулетку, щоб вибрати, чи слід скасовувати промінь
  8. Перейти 1

За допомогою цього коду ми отримуємо колір лише тоді, коли промінь врешті-решт потрапляє на світло. Крім того, він не підтримує пунктуальні джерела світла, оскільки у них немає ділянки.

Щоб виправити це, ми відбираємо проби безпосередньо при кожному відскоку. Ми повинні зробити кілька невеликих змін:

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);
}

Спочатку додаємо "color + = пропускну здатність * SampleLights (...)". Я трохи детальніше розповім про SampleLights (). Але, по суті, він проходить через усі світильники і повертає свій внесок у колір, ослаблений BSDF.

Це чудово, але нам потрібно зробити ще одну зміну, щоб зробити його правильним; конкретно, що відбувається, коли ми потрапили на світло. У старому коді ми додавали випромінювання світла до накопичення кольорів. Але тепер ми безпосередньо відбиваємо світло кожного відскоку, тож якби ми додали випромінювання світла, ми б "подвоїлися". Тому правильно робити це - нічого; ми пропускаємо накопичуючи випромінювання світла.

Однак є два найважливіших випадки:

  1. Перший промінь
  2. Ідеально дзеркальні відскоки (ака дзеркала)

Якщо перший промінь потрапить на світло, ви повинні побачити випромінювання світла безпосередньо. Тож якщо ми пропустимо його, всі світильники стануть чорними, навіть якщо поверхні навколо них освітлені.

Якщо ви потрапили на ідеально окулярні поверхні, ви не зможете безпосередньо пробити світло, оскільки вхідний промінь має лише один вихід. Ну, технічно, ми могли б перевірити, чи буде вхідний промінь потрапляти на світло, але немає сенсу; головний контур Шляху відстеження все одно буде робити це. Тому, якщо ми потрапили на світло відразу після того, як ми потрапимо на окулярну поверхню, нам потрібно накопичити колір. Якщо цього не зробити, у дзеркалах буде світло чорного кольору.

Тепер давайте поглибимось у 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;
}

Англійською:

  1. Проведіть через усі вогні
  2. Пропустити світло, якщо ми потрапимо в нього
    • Не подвійне занурення
  3. Накопичуйте пряме освітлення від усіх вогнів
  4. Поверніть пряме освітлення

Нарешті, EstimateDirect () просто оцінюєBSDF(p,ωi,ωo)Li(p,ωi)

Для пунктуальних джерел світла це просто:

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;
}

Однак, якщо ми хочемо, щоб ліхтарі мали площу, спершу потрібно відібрати зразок точки на світлі. Тому повне визначення таке:

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;
}

Ми можемо реалізувати світло-> SampleLi, як тільки хочемо; ми можемо вибрати точку рівномірно, або зразок важливості. У будь-якому випадку ми розділимо радіовимірюваність на pdf вибору точки. Знову ж таки, щоб задовольнити вимоги Монте-Карло.

Якщо BRDF сильно залежить від зору, може бути краще вибрати точку на основі BRDF, а не випадкову точку на світлі. Але як ми обираємо? Зразок на основі світла, або на основі BRDF?

Чому б не так? Введіть вибірку декількох значень. Коротше кажучи, ми оцінюємо кілька разів, використовуючи різні методи відбору проб, а потім їх середнє оцінювання, використовуючи ваги на основі їх pdfs. У коді це: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;
}

Англійською:

  1. Спочатку ми пробимо світло
    • Це оновлення взаємодії.InputDirection
    • Дає нам Лі для світла
    • І pdf вибору тієї точки світла
  2. Перевірте, чи pdf є дійсним, а сяйво не нульовим
  3. Оцініть BSDF за допомогою вибіркового InputDirection
  4. Обчисліть pdf для BSDF з урахуванням вибіркового InputDirection
    • По суті, наскільки ймовірний цей зразок, якби ми брали вибірку за допомогою BSDF, а не світла
  5. Обчисліть вагу, використовуючи легкий pdf та BSDF pdf
    • Вейч і Гібас визначають кілька різних способів розрахунку ваги. Експериментально вони виявили потужність евристичної з потужністю 2, яка працює найкраще для більшості випадків. Для отримання більш детальної інформації я посилаюсь на цей документ. Впровадження нижче
  6. Помножте вагу на розрахунок прямого освітлення і розділіть на легкий pdf. (Для Монте-Карло) І додайте до прямого скупчення світла.
  7. Потім проводимо вибірку BRDF
    • Це оновлення взаємодії.InputDirection
  8. Оцініть BRDF
  9. Отримайте pdf для вибору цього напрямку на основі BRDF
  10. Обчисліть легкий pdf, враховуючи вибірковий InputDirection
    • Це дзеркало раніше. Наскільки ймовірний цей напрямок, якщо ми будемо пробивати світло
  11. Якщо lightPdf == 0,0f, то промінь пропустив світло, тому просто поверніть пряме освітлення зі зразка світла.
  12. В іншому випадку обчисліть вагу і додайте до накопичення пряме освітлення BSDF
  13. Нарешті, поверніть накопичене пряме освітлення

.

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);
}

Існує ряд оптимізацій / удосконалень, які ви можете виконати в цих функціях, але я створив їх, щоб спробувати полегшити їх розуміння. Якщо ви хочете, я можу поділитися деякими з цих вдосконалень.

Тільки відбір одного світла

У SampleLights () ми перебираємо всі світильники і отримуємо їх внесок. Для невеликої кількості вогнів це нормально, але для сотень чи тисяч вогнів це стає дорогим. На щастя, ми можемо використати той факт, що Монте-Карло Інтеграція - це гігантський середній показник. Приклад:

Давайте визначимось

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 повинен інтегруватися до 1, а на вибір є 2 функції.pdf=12

Англійською:

  1. Випадково виберіть або для оцінки.g ( x )f(x)g(x)
  2. Результат розділити на (оскільки є два пункти)12
  3. Середній

Коли N стає великим, оцінка перейде до правильного рішення.

Ми можемо застосувати цей самий принцип до легкої вибірки. Замість вибірки кожного світла ми вибираємо випадковим чином і множимо результат на кількість вогнів (Це те саме, що ділити на дробовий pdf):

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);
}

У цьому коді всі ліхтарі мають рівні шанси на вибір. Однак ми можемо мати важливу вибірку, якщо нам це подобається. Наприклад, ми можемо дати більшим вогням більший шанс бути вибраним або ближче до поверхні удару. Вам просто потрібно розділити результат на pdf, який більше не буде .1numLights

Множинне значення вибірки напрямку «Новий промінь»

Поточний код має значення лише для вибірки напрямку "New Ray" на базі BSDF. Що робити, якщо ми хочемо також важливий зразок на основі розташування вогнів?

З огляду на те, що ми дізналися вище, одним із методів було б знімати два "нові" промені та вагу кожного, виходячи зі своїх pdfs. Однак це як обчислювально дорого, так і важко здійснити без рекурсії.

Щоб подолати це, ми можемо застосувати ті самі принципи, які ми дізналися, відібравши лише одне світло. Тобто, випадковим чином виберіть один для вибірки та розділіть на pdf вибору.

// 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;
}

Все, що сказано, чи ми дійсно хочемо, щоб на вибір було відіграно напрямок "Нового променя"? На пряме освітлення на радіомісткість впливає як BSDF поверхні, так і напрямок світла. Але для непрямого освітлення радіовипромінювання майже виключно визначається BSDF потрапляє на поверхню раніше. Отже, додавання вибірки легкої важливості нам нічого не дає.

Тому загальною є лише вибірка зразка "Новий напрямок" з BSDF, але застосовувати вибірку з декількома значеннями для прямого освітлення.


Дякую за уточнюючу відповідь! Я розумію, що якби ми використовували трекер шляху без явного вибірки світла, ми б ніколи не потрапляли на точкове джерело світла. Отже, ми можемо в основному додати його внесок. З іншого боку, якщо ми відібрали місцеве джерело світла, ми повинні переконатися, що ми не повинні вдарити його знову за допомогою непрямого освітлення, щоб уникнути подвійного занурення
Mustafa Işık

Саме так! Чи є якась частина, на яку вам потрібні роз'яснення? Або недостатньо деталей?
RichieSams

Крім того, чи вибірка з великою важливістю використовується лише для розрахунку прямого освітлення? Можливо, я пропустив, але іншого прикладу цього не бачив. Якщо я знімаю лише один промінь за відбій у моєму траєкторі шляху, здається, що я не можу це зробити для розрахунку непрямого освітлення.
Мустафа Ішик

2
Відбір проб з декількома важливістю може бути застосований у будь-якому місці, де ви використовуєте важливий вибірки. Потужність вибірки багаторазової важливості полягає в тому, що ми можемо поєднати переваги багатьох методів вибірки. Наприклад, у деяких випадках вибірка легкої важливості буде кращою, ніж вибірка BSDF. В інших випадках, навпаки. MIS поєднає найкраще з обох світів. Однак якщо вибірка BSDF буде кращою в 100% часу, немає підстав додавати складність MIS. До відповіді я додав кілька розділів, щоб розширити цю
тему

1
Здається, ми розділили джерела випромінювання на дві частини як прямі та непрямі. Ми чітко відбираємо проби на пряму частину, і під час вибірки цієї деталі доцільно важливо відібрати пробні світильники, а також BSDF. З іншого боку, ми не маємо уявлення про те, який напрямок може дати нам більші значення сяйва, оскільки саме цю проблему ми хочемо вирішити. Однак ми можемо сказати, який напрямок може більше сприяти згідно з косинусним терміном та BSDF. Це я розумію. Виправте мене, якщо я помиляюся, і дякую за вашу приголомшливу відповідь.
Мустафа Ішик
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.