Прогрессивная трассировка пути с явной выборкой света


14

Я понял логику отсчёта важности для части BRDF. Однако, когда дело доходит до явной выборки источников света, все становится запутанным. Например, если у меня есть один точечный источник света в моей сцене и если я постоянно сэмплирую его в каждом кадре, должен ли я считать его еще одним образцом для интеграции Монте-Карло? То есть я беру одну выборку из косинус-взвешенного распределения, а другую - из точечного источника. Всего два образца или один? Кроме того, я должен разделить яркость, идущую от прямой выборки к любому члену?

Ответы:


19

Есть несколько областей в трассировке пути, которые могут быть выбраны по важности. Кроме того, в каждой из этих областей также может использоваться выборка по нескольким значениям, впервые предложенная в работе Veach и Guibas 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. Проверьте, ударили ли мы что-нибудь. Если нет, мы возвращаем цвет скайбокса и ломаемся.
  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 + = throughput * SampleLights (...)». Я немного подробнее расскажу о SampleLights (). Но, по сути, он проходит через все источники света и возвращает их вклад в цвет, ослабленный BSDF.

Это здорово, но нам нужно сделать еще одно изменение, чтобы исправить это; в частности, что происходит, когда мы попадаем на свет. В старом коде мы добавили излучение света к накоплению цвета. Но теперь мы непосредственно отбираем свет при каждом отражении, поэтому, если мы добавим излучение света, мы будем «дважды падать». Следовательно, правильная вещь ... ничего; мы пропускаем накопление излучения света.

Однако есть два угловых случая:

  1. Первый луч
  2. Совершенно зеркальные отскоки (зеркала)

Если первый луч попадает на свет, вы должны увидеть излучение света напрямую. Поэтому, если мы пропустим это, все огни будут отображаться черными, даже если поверхности вокруг них освещены.

Когда вы попадаете на совершенно зеркальные поверхности, вы не можете напрямую пробовать свет, потому что у входного луча есть только один выход. Ну, технически, мы могли бы проверить, попадет ли входной луч на свет, но в этом нет никакого смысла; основной цикл Path Tracing будет делать это в любом случае. Поэтому, если мы попадаем на свет сразу после того, как попадаем на зеркальную поверхность, нам нужно накапливать цвет. Если мы этого не сделаем, огни будут черными в зеркалах.

Теперь давайте углубимся в 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. Вернуть прямое освещение

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

Мы можем реализовать light-> SampleLi так, как хотим; мы можем выбрать точку равномерно или важность образца. В любом случае, мы делим лучистость на pdf выбора точки. Опять же, чтобы удовлетворить требования Монте-Карло.

Если BRDF сильно зависит от вида, может быть лучше выбрать точку на основе BRDF, а не случайную точку на источнике света. Но как мы выбираем? Образец на основе света или на основе BRDF?

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
    • Veach и Guibas определяют несколько разных способов расчета веса. Экспериментально они обнаружили, что мощность эвристики со степенью 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=12

По-английски:

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

1numLights

Многократное значение Отбор проб в направлении «Новый луч»

В текущем коде важна только выборка направления «Новый луч» на основе BSDF. Что делать, если мы хотим, чтобы значение выборки также основывалось на расположении источников света?

Исходя из того, что мы узнали выше, одним из методов будет съемка двух «новых» лучей и веса каждого на основе их PDF-файлов. Однако это не только вычислительно дорого, но и трудно реализовать без рекурсии.

Чтобы преодолеть это, мы можем применить те же принципы, которые мы узнали, отбирая только один свет. То есть, случайным образом выберите один образец и разделите его на 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. Я добавил несколько разделов к ответу, чтобы расширить этот вопрос
RichieSams,

1
Кажется, мы разделили источники входящего излучения на две части, как прямые и косвенные. Мы отбираем источники света в явном виде для прямой части, и, отбирая образцы этой части, целесообразно провести выборку источников света, а также BSDF. Однако для косвенной части мы не имеем представления о том, какое направление потенциально может дать нам более высокие значения яркости, поскольку именно эту проблему мы хотим решить. Тем не менее, мы можем сказать, какое направление может внести больший вклад в соответствии с косинус-термином и BSDF. Это то, что я понимаю. Поправь меня, если я ошибаюсь, и спасибо за твой потрясающий ответ.
Мустафа
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.