У відстеженні шляху є кілька областей, які можуть бути важливими для вибірки. Крім того, кожна з цих областей може також використовувати вибірку з великою важливістю, вперше запропоновану в статті Веача та Гібаса 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);
}
Англійською:
- Стріляйте промінь через сцену
- Перевірте, чи вдаримо ми щось. Якщо ні, ми повертаємо колір skybox і ламаємо.
- Перевірте, чи потрапили ми у світло. Якщо так, ми додаємо світло випромінювання до нашого накопичення кольорів
- Виберіть новий напрямок для наступного променя. Ми можемо зробити це рівномірно, або важливий зразок на основі 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 + = пропускну здатність * SampleLights (...)". Я трохи детальніше розповім про SampleLights (). Але, по суті, він проходить через усі світильники і повертає свій внесок у колір, ослаблений BSDF.
Це чудово, але нам потрібно зробити ще одну зміну, щоб зробити його правильним; конкретно, що відбувається, коли ми потрапили на світло. У старому коді ми додавали випромінювання світла до накопичення кольорів. Але тепер ми безпосередньо відбиваємо світло кожного відскоку, тож якби ми додали випромінювання світла, ми б "подвоїлися". Тому правильно робити це - нічого; ми пропускаємо накопичуючи випромінювання світла.
Однак є два найважливіших випадки:
- Перший промінь
- Ідеально дзеркальні відскоки (ака дзеркала)
Якщо перший промінь потрапить на світло, ви повинні побачити випромінювання світла безпосередньо. Тож якщо ми пропустимо його, всі світильники стануть чорними, навіть якщо поверхні навколо них освітлені.
Якщо ви потрапили на ідеально окулярні поверхні, ви не зможете безпосередньо пробити світло, оскільки вхідний промінь має лише один вихід. Ну, технічно, ми могли б перевірити, чи буде вхідний промінь потрапляти на світло, але немає сенсу; головний контур Шляху відстеження все одно буде робити це. Тому, якщо ми потрапили на світло відразу після того, як ми потрапимо на окулярну поверхню, нам потрібно накопичити колір. Якщо цього не зробити, у дзеркалах буде світло чорного кольору.
Тепер давайте поглибимось у 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;
}
Англійською:
- Проведіть через усі вогні
- Пропустити світло, якщо ми потрапимо в нього
- Накопичуйте пряме освітлення від усіх вогнів
- Поверніть пряме освітлення
Нарешті, EstimateDirect () просто оцінюєB SД Ж( р , ωi, ωо) Li( р , ω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. У коді це:B SД Ж( р , ωi, ωо) Li( р , ω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;
}
Англійською:
- Спочатку ми пробимо світло
- Це оновлення взаємодії.InputDirection
- Дає нам Лі для світла
- І pdf вибору тієї точки світла
- Перевірте, чи pdf є дійсним, а сяйво не нульовим
- Оцініть BSDF за допомогою вибіркового InputDirection
- Обчисліть pdf для BSDF з урахуванням вибіркового InputDirection
- По суті, наскільки ймовірний цей зразок, якби ми брали вибірку за допомогою BSDF, а не світла
- Обчисліть вагу, використовуючи легкий pdf та BSDF pdf
- Вейч і Гібас визначають кілька різних способів розрахунку ваги. Експериментально вони виявили потужність евристичної з потужністю 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( x ) + g( х )
Наразі ми оцінюємо :h(x)
h(x)=1N∑i=1Nf(xi)+g(xi)
Але обчислення і є дорогим, тож замість цього ми робимо:f(x)g(x)
h(x)=1N∑i=1Nr(ζ,x)pdf
Де - рівномірна випадкова величина, а визначається як:ζr(ζ,x)
r(ζ,x)={f(x),g(x),0.0≤ζ<0.50.5≤ζ<1.0
У цьому випадку оскільки pdf повинен інтегруватися до 1, а на вибір є 2 функції.pdf=12
Англійською:
- Випадково виберіть або для оцінки.g ( x )f(x)g(x)
- Результат розділити на (оскільки є два пункти)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);
}
У цьому коді всі ліхтарі мають рівні шанси на вибір. Однак ми можемо мати важливу вибірку, якщо нам це подобається. Наприклад, ми можемо дати більшим вогням більший шанс бути вибраним або ближче до поверхні удару. Вам просто потрібно розділити результат на 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, але застосовувати вибірку з декількома значеннями для прямого освітлення.