Есть несколько областей в трассировке пути, которые могут быть выбраны по важности. Кроме того, в каждой из этих областей также может использоваться выборка по нескольким значениям, впервые предложенная в работе 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);
}
По-английски:
- Стрелять лучом через сцену
- Проверьте, ударили ли мы что-нибудь. Если нет, мы возвращаем цвет скайбокса и ломаемся.
- Проверьте, не попали ли мы в свет. Если это так, мы добавляем излучение света к нашему накоплению цвета
- Выберите новое направление для следующего луча. Мы можем сделать это равномерно, или образец важности на основе BRDF
- Оцените BRDF и накопите его. Здесь мы должны разделить на pdf выбранного нами направления, чтобы следовать алгоритму Монте-Карло.
- Создайте новый луч на основе выбранного нами направления и того, откуда мы только что пришли
- [Необязательно] Используйте русскую рулетку, чтобы выбрать, следует ли нам прекратить луч
- Перейти к 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.
Это здорово, но нам нужно сделать еще одно изменение, чтобы исправить это; в частности, что происходит, когда мы попадаем на свет. В старом коде мы добавили излучение света к накоплению цвета. Но теперь мы непосредственно отбираем свет при каждом отражении, поэтому, если мы добавим излучение света, мы будем «дважды падать». Следовательно, правильная вещь ... ничего; мы пропускаем накопление излучения света.
Однако есть два угловых случая:
- Первый луч
- Совершенно зеркальные отскоки (зеркала)
Если первый луч попадает на свет, вы должны увидеть излучение света напрямую. Поэтому, если мы пропустим это, все огни будут отображаться черными, даже если поверхности вокруг них освещены.
Когда вы попадаете на совершенно зеркальные поверхности, вы не можете напрямую пробовать свет, потому что у входного луча есть только один выход. Ну, технически, мы могли бы проверить, попадет ли входной луч на свет, но в этом нет никакого смысла; основной цикл 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;
}
По-английски:
- Перебрать все огни
- Пропустить свет, если мы нажмем на него
- Накопить прямое освещение от всех огней
- Вернуть прямое освещение
B SD F( р , ωя, ωо) Lя( р , ωя)
Для точечных источников света это просто как:
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?
B SD F( р , ωя, ωо) Lя( р , ωя)
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;
}
По-английски:
- Сначала мы пробуем свет
- Это обновляет взаимодействие. InputDirection
- Дает нам Ли для света
- И PDF выбора этой точки на свете
- Проверьте, что PDF-файл действителен и яркость не равна нулю
- Оцените BSDF, используя выбранный InputDirection
- Рассчитать PDF для BSDF, учитывая выборку InputDirection
- По сути, насколько вероятен этот образец, если бы мы взяли образец с использованием BSDF вместо света
- Рассчитать вес, используя легкий PDF и BSDF PDF
- Veach и Guibas определяют несколько разных способов расчета веса. Экспериментально они обнаружили, что мощность эвристики со степенью 2 работает лучше всего для большинства случаев. Я отсылаю вас к статье для более подробной информации. Реализация ниже
- Умножьте вес при расчете прямого освещения и разделите на свет pdf. (Для Монте-Карло) И добавить к прямому накоплению света.
- Затем мы пробуем BRDF
- Это обновляет взаимодействие. InputDirection
- Оценить BRDF
- Получить PDF для выбора этого направления на основе BRDF
- Рассчитать легкий PDF, учитывая выборку InputDirection
- Это зеркало раньше. Насколько вероятно это направление, если мы будем пробовать свет
- Если lightPdf == 0.0f, то луч пропустил свет, поэтому просто верните прямое освещение от образца света.
- В противном случае рассчитайте вес и добавьте прямое освещение BSDF к накоплению.
- Наконец, верните накопленное прямое освещение
,
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( х ) + г( х )
ч ( х )
ч ( х ) = 1NΣя = 1Nе( хя) + г( хя)
е( х )грамм( х )
ч ( х ) = 1NΣя = 1Nr ( ζ, Х )р де
ζr ( ζ, Х )
r ( ζ, х ) = { ф( х ) ,грамм( х ) ,0.0 ≤ ζ< 0,50,5 ≤ ζ< 1,0
р де= 12
По-английски:
- е( х )грамм( х )
- 12
- Средний
Когда 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, но применять выборочную выборку по нескольким значениям для прямого освещения.