Pelacakan Jalur Progresif dengan Sampling Cahaya Eksplisit

14

Saya memahami logika di balik pengambilan sampel yang penting untuk bagian BRDF. Namun, ketika datang ke pengambilan sampel sumber cahaya secara eksplisit, semua menjadi membingungkan. Misalnya, jika saya memiliki satu titik sumber cahaya dalam adegan saya dan jika saya langsung mengambil sampelnya di setiap frame secara konstan, haruskah saya menghitungnya sebagai satu sampel lagi untuk integrasi monte carlo? Yaitu, saya mengambil satu sampel dari distribusi berbobot kosinus dan lainnya dari titik cahaya. Apakah totalnya dua sampel atau hanya satu? Juga, haruskah saya membagi pancaran yang berasal dari sampel langsung ke istilah apa pun?

Mustafa Işık
sumber

Jawaban:

19

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:

  1. Tembak sinar melalui adegan
  2. Periksa apakah kita menabrak sesuatu. Jika tidak, kami mengembalikan warna skybox dan istirahat.
  3. Periksa apakah kita menyalakan lampu. Jika demikian, kami menambahkan emisi cahaya ke akumulasi warna kami
  4. Pilih arah baru untuk ray berikutnya. Kami dapat melakukan ini secara seragam, atau sampel penting berdasarkan BRDF
  5. Evaluasi BRDF dan akumulasikan. Di sini kita harus membagi dengan pdf dari arah yang kita pilih, untuk mengikuti Algoritma Monte Carlo.
  6. Buat sinar baru berdasarkan arah yang kita pilih dan dari mana kita berasal
  7. [Opsional] Gunakan Roulette Rusia untuk memilih apakah kami harus menghentikan ray
  8. 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:

  1. Sinar pertama
  2. 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:

  1. Lingkari semua lampu
  2. Lewati cahaya jika kita menabraknya
    • Jangan celupkan ganda
  3. Akumulasi pencahayaan langsung dari semua lampu
  4. 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:

  1. Pertama, kami mencicipi cahaya
    • Ini memperbarui interaksi.InputDirection
    • Memberi kami Li untuk cahaya
    • Dan pdf memilih titik itu pada cahaya
  2. Pastikan pdf itu valid dan pancarannya tidak nol
  3. Evaluasi BSDF menggunakan InputDirection sampel
  4. Hitung pdf untuk BSDF diberikan InputDirection sampel
    • Intinya, seberapa besar kemungkinan sampel ini, jika kami mengambil sampel menggunakan BSDF, alih-alih cahaya
  5. 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
  6. Lipat gandakan bobot dengan perhitungan pencahayaan langsung dan bagi dengan pdf cahaya. (Untuk Monte Carlo) Dan tambahkan ke akumulasi cahaya langsung.
  7. Kemudian, kami mencicipi BRDF
    • Ini memperbarui interaksi.InputDirection
  8. Mengevaluasi BRDF
  9. Dapatkan pdf untuk memilih arah ini berdasarkan BRDF
  10. Hitung pdf cahaya, diberi InputDirection sampel
    • Ini adalah cermin dari sebelumnya. Seberapa besar kemungkinan arah ini, jika kita mengambil sampel cahaya
  11. Jika lightPdf == 0,0f, maka sinar tersebut melewatkan cahaya, jadi kembalikan saja pencahayaan langsung dari sampel cahaya.
  12. Kalau tidak, hitung beratnya, dan tambahkan pencahayaan langsung BSDF ke akumulasi
  13. 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)=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

Dalam Bahasa Inggris:

  1. f(x)g(x)
  2. 12
  3. 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.

RichieSams
sumber
Terima kasih atas jawaban klarifikasi! Saya mengerti bahwa jika kita menggunakan pelacak jalur tanpa pengambilan sampel cahaya eksplisit, kita tidak akan pernah mengenai sumber cahaya titik. Jadi, pada dasarnya kita dapat menambahkan kontribusinya. Di sisi lain, jika kita mengambil sampel sumber cahaya area, kita harus memastikan bahwa kita tidak harus memukulnya lagi dengan pencahayaan tidak langsung untuk menghindari kemiringan ganda
Mustafa Işık
Persis! Apakah ada bagian yang perlu Anda klarifikasi? Atau tidak cukup detail?
RichieSams
Selain itu, apakah beberapa sampel penting hanya digunakan untuk perhitungan pencahayaan langsung? Mungkin saya merindukan tetapi saya tidak melihat contoh lain dari itu. Jika saya memotret hanya satu sinar per pantulan di pelacak jalur saya, sepertinya saya tidak dapat melakukannya untuk perhitungan pencahayaan tidak langsung.
Mustafa Işık
2
Multiple Importance Sampling dapat diterapkan di mana saja Anda menggunakan sampel penting. Kekuatan pengambilan sampel berganda penting adalah bahwa kita dapat menggabungkan manfaat teknik pengambilan sampel berganda. Misalnya, dalam beberapa kasus, pengambilan sampel dengan kepentingan cahaya akan lebih baik daripada pengambilan sampel BSDF. Dalam kasus lain, sebaliknya. MIS akan menggabungkan yang terbaik dari kedua dunia. Namun, jika pengambilan sampel BSDF akan lebih baik 100% dari waktu, tidak ada alasan untuk menambah kompleksitas SIM. Saya menambahkan beberapa bagian pada jawaban untuk memperluas pada titik ini
RichieSams
1
Tampaknya kita memisahkan sumber cahaya yang masuk menjadi dua bagian sebagai langsung dan tidak langsung. Kami sampel lampu secara eksplisit untuk bagian langsung dan sambil mengambil sampel bagian ini, masuk akal untuk sampel penting lampu serta BSDF. Namun, untuk bagian tidak langsung, kita tidak tahu arah mana yang berpotensi memberi kita nilai pancaran yang lebih tinggi karena masalah itu sendiri yang ingin kita pecahkan. Namun, kita dapat mengatakan arah mana yang dapat berkontribusi lebih sesuai dengan istilah cosinus dan BSDF. Inilah yang saya mengerti. Koreksi saya jika saya salah dan terima kasih atas jawaban Anda yang luar biasa.
Mustafa Işık