Ada beberapa area dalam pelacakan jalur yang bisa menjadi sampel penting. Selain itu, masing-masing area tersebut juga dapat menggunakan Multiple Importance Sampling, yang pertama kali diusulkan dalam makalah Veach dan Guibas 1995 . Untuk lebih jelasnya, mari kita lihat tracer jalur mundur:
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);
}
Dalam Bahasa Inggris:
- Tembak sinar melalui adegan
- Periksa apakah kita menabrak sesuatu. Jika tidak, kami mengembalikan warna skybox dan istirahat.
- Periksa apakah kita menyalakan lampu. Jika demikian, kami menambahkan emisi cahaya ke akumulasi warna kami
- Pilih arah baru untuk ray berikutnya. Kami dapat melakukan ini secara seragam, atau sampel penting berdasarkan BRDF
- Evaluasi BRDF dan akumulasikan. Di sini kita harus membagi dengan pdf dari arah yang kita pilih, untuk mengikuti Algoritma Monte Carlo.
- Buat sinar baru berdasarkan arah yang kita pilih dan dari mana kita berasal
- [Opsional] Gunakan Roulette Rusia untuk memilih apakah kami harus menghentikan ray
- Kebagian 1
Dengan kode ini, kita hanya mendapatkan warna jika sinar akhirnya menyentuh cahaya. Selain itu, itu tidak mendukung sumber cahaya tepat waktu, karena mereka tidak memiliki area.
Untuk mengatasinya, kami mencicipi lampu secara langsung di setiap pantulan. Kami harus melakukan beberapa perubahan kecil:
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);
}
Pertama, kami menambahkan "color + = throughput * SampleLights (...)". Saya akan masuk ke detail tentang SampleLights () sedikit. Tapi, pada dasarnya, ia melewati semua lampu, dan mengembalikan kontribusinya pada warna, dilemahkan oleh BSDF.
Ini hebat, tetapi kita perlu membuat satu perubahan lagi untuk membuatnya benar; secara khusus, apa yang terjadi ketika kita menyalakan lampu. Dalam kode lama, kami menambahkan emisi cahaya ke akumulasi warna. Tapi sekarang kami langsung mencicipi cahaya setiap pantulan, jadi jika kami menambahkan emisi cahaya, kami akan "melipatgandakan". Karena itu, hal yang benar untuk dilakukan adalah ... tidak ada; kita melewatkan akumulasi emisi cahaya.
Namun, ada dua kasus sudut:
- Sinar pertama
- Terpental dengan sempurna (alias mirror)
Jika sinar pertama mengenai cahaya, Anda harus melihat emisi cahaya secara langsung. Jadi jika kita lewati, semua lampu akan tampak hitam, meskipun permukaan di sekitarnya menyala.
Ketika Anda menekan permukaan specular sempurna Anda tidak dapat langsung mengambil sampel cahaya, karena sinar input hanya memiliki satu output. Nah, secara teknis, kita bisa memeriksa apakah sinar input akan mengenai cahaya, tetapi tidak ada gunanya; lintasan Path Tracing utama akan tetap melakukannya. Karena itu, jika kita mengenai cahaya tepat setelah kita mengenai permukaan specular, kita perlu mengakumulasi warnanya. Jika tidak, lampu akan menjadi hitam di cermin.
Sekarang, mari kita mempelajari 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;
}
Dalam Bahasa Inggris:
- Lingkari semua lampu
- Lewati cahaya jika kita menabraknya
- Akumulasi pencahayaan langsung dari semua lampu
- Kembalikan pencahayaan langsung
BSDF(p,ωi,ωo)Li(p,ωi)
Untuk sumber cahaya tepat waktu, ini sederhana seperti:
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;
}
Namun, jika kita ingin lampu memiliki area, pertama-tama kita perlu mengambil sampel titik pada lampu. Oleh karena itu, definisi lengkapnya adalah:
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;
}
Kita dapat mengimplementasikan light-> SampleLi sesuai keinginan kita; kita dapat memilih titik secara seragam, atau sampel penting. Dalam kedua kasus, kami membagi radiositas dengan pdf memilih titik. Sekali lagi, untuk memenuhi persyaratan Monte Carlo.
Jika BRDF sangat bergantung pada tampilan, mungkin lebih baik untuk memilih titik berdasarkan BRDF, daripada titik acak pada lampu. Tetapi bagaimana kita memilih? Sampel berdasarkan lampu, atau berdasarkan 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;
}
Dalam Bahasa Inggris:
- Pertama, kami mencicipi cahaya
- Ini memperbarui interaksi.InputDirection
- Memberi kami Li untuk cahaya
- Dan pdf memilih titik itu pada cahaya
- Pastikan pdf itu valid dan pancarannya tidak nol
- Evaluasi BSDF menggunakan InputDirection sampel
- Hitung pdf untuk BSDF diberikan InputDirection sampel
- Intinya, seberapa besar kemungkinan sampel ini, jika kami mengambil sampel menggunakan BSDF, alih-alih cahaya
- Hitung beratnya, menggunakan pdf ringan dan pdf BSDF
- Veach dan Guibas mendefinisikan beberapa cara berbeda untuk menghitung berat. Secara eksperimental, mereka menemukan kekuatan heuristik dengan kekuatan 2 untuk bekerja yang terbaik untuk kebanyakan kasus. Saya merujuk Anda ke koran untuk lebih jelasnya. Implementasinya di bawah ini
- Lipat gandakan bobot dengan perhitungan pencahayaan langsung dan bagi dengan pdf cahaya. (Untuk Monte Carlo) Dan tambahkan ke akumulasi cahaya langsung.
- Kemudian, kami mencicipi BRDF
- Ini memperbarui interaksi.InputDirection
- Mengevaluasi BRDF
- Dapatkan pdf untuk memilih arah ini berdasarkan BRDF
- Hitung pdf cahaya, diberi InputDirection sampel
- Ini adalah cermin dari sebelumnya. Seberapa besar kemungkinan arah ini, jika kita mengambil sampel cahaya
- Jika lightPdf == 0,0f, maka sinar tersebut melewatkan cahaya, jadi kembalikan saja pencahayaan langsung dari sampel cahaya.
- Kalau tidak, hitung beratnya, dan tambahkan pencahayaan langsung BSDF ke akumulasi
- Akhirnya, kembalikan pencahayaan langsung yang terakumulasi
.
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);
}
Ada sejumlah optimasi / peningkatan yang dapat Anda lakukan dalam fungsi-fungsi ini, tetapi saya telah menguranginya untuk mencoba membuatnya lebih mudah untuk dipahami. Jika Anda mau, saya dapat membagikan beberapa peningkatan ini.
Hanya Sampling Satu Cahaya
Dalam SampleLights () kami memutari semua lampu, dan mendapatkan kontribusinya. Untuk sejumlah kecil lampu, ini bagus, tetapi untuk ratusan atau ribuan lampu, ini menjadi mahal. Untungnya, kita dapat memanfaatkan fakta bahwa Integrasi Monte Carlo adalah rata-rata raksasa. Contoh:
Mari kita definisikan
h ( x ) = f( x ) + g( x )
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=12
Dalam Bahasa Inggris:
- f(x)g(x)
- 12
- Rata-rata
Ketika N bertambah besar, estimasi akan menyatu ke solusi yang benar.
Kita dapat menerapkan prinsip yang sama ini pada sampling cahaya. Alih-alih mengambil sampel setiap cahaya, kami memilih secara acak, dan mengalikan hasilnya dengan jumlah lampu (Ini sama dengan membaginya dengan pdf fraksional):
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
Beberapa Kepentingan Mengambil Sampel Arah "Sinar Baru"
Kode saat ini hanya sampel penting arah "New Ray" berdasarkan BSDF. Bagaimana jika kita ingin sampel juga penting berdasarkan lokasi lampu?
Mengambil dari apa yang kita pelajari di atas, satu metode akan menembak dua sinar "baru" dan berat masing-masing berdasarkan pdf mereka. Namun, ini mahal secara komputasi, dan sulit untuk diterapkan tanpa rekursi.
Untuk mengatasinya, kita dapat menerapkan prinsip yang sama yang kita pelajari dengan mengambil sampel hanya satu cahaya. Yaitu, secara acak memilih satu untuk sampel, dan membaginya dengan pdf untuk memilihnya.
// 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;
}
Itu semua mengatakan, apakah kita benar - benar ingin mengambil sampel penting arah "New Ray" berdasarkan cahaya? Untuk penerangan langsung , radiositas dipengaruhi oleh BSDF permukaan, dan arah cahaya. Tetapi untuk pencahayaan tidak langsung , radiositas hampir secara eksklusif ditentukan oleh BSDF dari permukaan yang terkena sebelumnya. Jadi, menambahkan sampel cahaya penting tidak memberi kita apa pun.
Oleh karena itu, umum untuk hanya sampel penting "Arah Baru" dengan BSDF, tetapi menerapkan Multiple Importance Sampling ke pencahayaan langsung.