Peta Bayangan Reflektif: Bagian 2 - Implementasi

Halo, Habr! Artikel ini menyajikan implementasi sederhana Peta Bayangan Reflektif (algoritme dijelaskan dalam artikel sebelumnya ). Selanjutnya, saya akan menjelaskan bagaimana saya melakukannya dan apa jebakan itu. Beberapa kemungkinan optimasi juga akan dipertimbangkan.

gambar
Gambar 1: Dari kiri ke kanan: tanpa RSM, dengan RSM, perbedaan

Hasil


Pada Gambar 1, Anda dapat melihat hasil yang diperoleh menggunakan RSM . Untuk membuat gambar-gambar ini, "Stanford Rabbit" dan tiga segi empat digunakan. Pada gambar di sebelah kiri Anda dapat melihat hasil rendering tanpa RSM , hanya menggunakan lampu spot . Segala sesuatu di tempat teduh benar-benar hitam. Gambar di tengah menunjukkan hasil dengan RSM . Perbedaan berikut terlihat: di mana-mana ada warna yang lebih cerah, merah muda, membanjiri lantai dan kelinci, bayangan tidak sepenuhnya hitam. Gambar terakhir menunjukkan perbedaan antara yang pertama dan kedua, dan oleh karena itu kontribusi RSM . Tepi dan artefak yang lebih kencang terlihat di gambar tengah, tetapi ini dapat diatasi dengan menyesuaikan ukuran inti, intensitas pencahayaan tidak langsung, dan jumlah sampel.

Implementasi


Algoritma diimplementasikan pada mesinnya sendiri. Shader ditulis dalam HLSL, dan rendernya ada di DirectX 11. Saya sudah menyiapkan pemetaan bayangan dan bayangan untuk pencahayaan terarah (sumber cahaya terarah) sebelum menulis artikel ini. Pertama, saya menerapkan RSM untuk lampu arah dan hanya setelah saya menambahkan dukungan untuk peta bayangan dan RSM untuk lampu sorot.

Ekstensi peta bayangan


Secara tradisional, Shadow Maps (SM) tidak lebih dari peta kedalaman. Ini berarti Anda bahkan tidak memerlukan pixel / fragmen shader untuk mengisi SM. Namun, untuk RSM Anda membutuhkan beberapa buffer tambahan. Anda perlu menyimpan posisi ruang-dunia, ruang-dunia normal dan fluks (keluaran cahaya). Ini berarti bahwa Anda memerlukan pixel / fragmen shader dengan beberapa target render. Ingatlah bahwa untuk teknik ini, Anda perlu memotong pemusnahan wajah , bukan bagian depan.

Menggunakan tepi depan pemusnahan wajah adalah cara yang banyak digunakan untuk menghindari artefak bayangan, tetapi ini tidak bekerja dengan RSM .

Anda melewati posisi ruang-dunia dan normals ke pixel shader dan menulisnya ke buffer yang sesuai. Jika Anda menggunakan pemetaan normal , maka hitung juga dalam pixel shader. Fluks dihitung di sana, dengan mengalikan bahan albedo dengan warna sumber cahaya. Untuk cahaya spot, Anda perlu mengalikan nilai yang dihasilkan dengan sudut kejadian. Untuk cahaya arah, gambar yang tidak diarsir diperoleh.

Mempersiapkan perhitungan pencahayaan


Ada beberapa hal yang perlu Anda lakukan untuk bagian utama. Anda harus mengikat semua buffer yang digunakan dalam shadow pass sebagai tekstur. Anda juga membutuhkan nomor acak. Artikel resmi mengatakan bahwa Anda perlu melakukan pra-perhitungan angka-angka ini dan menyimpannya dalam buffer untuk mengurangi jumlah operasi dalam pengambilan sampel RSM . Karena algoritma ini berat dalam hal kinerja, saya sepenuhnya setuju dengan artikel resmi. Disarankan juga untuk mematuhi koherensi temporal (gunakan pola pengambilan sampel yang sama untuk semua perhitungan pencahayaan tidak langsung). Ini akan mencegah berkedip ketika setiap frame menggunakan bayangan yang berbeda.

Anda memerlukan dua angka floating point acak dalam rentang [0, 1] untuk setiap sampel. Angka acak ini akan digunakan untuk menentukan koordinat sampel. Anda juga akan membutuhkan matriks yang sama yang Anda gunakan untuk mengubah posisi dari ruang dunia (ruang dunia) ke ruang bayangan (ruang sumber cahaya). Anda juga akan memerlukan parameter seperti itu untuk pengambilan sampel, yang akan memberi warna hitam jika Anda mengambil sampel di luar batas tekstur.

Passing Lighting


Sekarang bagian yang sulit untuk dipahami. Saya sarankan Anda menghitung pencahayaan tidak langsung setelah Anda menghitung pencahayaan langsung untuk sumber cahaya tertentu. Ini karena Anda memerlukan quad layar penuh untuk cahaya directional . Namun, untuk cahaya spot dan point, Anda biasanya ingin menggunakan jerat dengan bentuk tertentu dengan pemusnahan untuk mengisi lebih sedikit piksel.

Dalam kode di bawah ini, penerangan tidak langsung dihitung untuk piksel. Selanjutnya, saya akan menjelaskan apa yang terjadi di sana.

float3 DoReflectiveShadowMapping(float3 P, bool divideByW, float3 N) { float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); } 

Argumen pertama untuk fungsi adalah P , yang merupakan posisi ruang-dunia (dalam ruang dunia) untuk piksel tertentu. DivideByW digunakan untuk divisi prospektif yang diperlukan untuk mendapatkan nilai Z yang benar. N adalah ruang-dunia normal.

 float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; 

Pada bagian kode ini, posisi ruang-cahaya (relatif terhadap sumber cahaya) dihitung, variabel pencahayaan tidak langsung diinisialisasi, di mana nilai-nilai yang dihitung dari setiap sampel akan dijumlahkan, dan variabel rMax diatur dari persamaan pencahayaan dalam artikel resmi , nilai yang akan saya jelaskan di bagian berikutnya.

 for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; 

Di sini kita memulai siklus dan menyiapkan variabel kita untuk persamaan. Untuk keperluan optimasi, sampel acak yang saya hitung sudah mengandung offset koordinat, yaitu, untuk mendapatkan koordinat UV, saya hanya perlu menambahkan rMax * rnd ke koordinat ruang-cahaya. Jika UV yang dihasilkan di luar kisaran [0,1], sampel harus berwarna hitam. Itu logis, karena mereka melampaui jangkauan pencahayaan.

  float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); 

Ini adalah bagian di mana persamaan pencahayaan tidak langsung dihitung ( Gambar 2 ), dan juga ditimbang sesuai dengan jarak dari koordinat ruang-cahaya ke sampel. Persamaannya terlihat menakutkan, dan kodenya tidak membantu untuk memahami segalanya, jadi saya akan menjelaskan lebih detail.

Variabel Ξ¦ (phi) adalah fluks cahaya, yang merupakan intensitas radiasi. Artikel sebelumnya menjelaskan fluks lebih terinci.

Timbangan fluks dengan dua karya seni skalar. Yang pertama adalah antara normal sumber cahaya (texel) dan arah dari sumber cahaya ke posisi saat ini. Yang kedua adalah antara arus normal dan vektor arah dari posisi saat ini ke posisi sumber cahaya (texel). Agar tidak memberikan kontribusi negatif pada pencahayaan (ternyata jika pikselnya tidak menyala), produk skalar terbatas pada kisaran [0, ∞]. Dalam persamaan ini, normalisasi dilakukan pada akhirnya, saya kira, untuk alasan kinerja. Hal yang sama dapat diterima untuk menormalkan vektor arah sebelum melakukan produk skalar.

gambar
Gambar 2: Persamaan pencahayaan titik dengan posisi x dan sumber cahaya normal n pixel arah p

Hasil pass ini dapat dicampur dengan backbuffer (pencahayaan langsung), dan hasilnya akan seperti pada Gambar 1 .

Perangkap


Ketika menerapkan algoritma ini, saya mengalami beberapa masalah. Saya akan berbicara tentang masalah ini sehingga Anda tidak menginjak penggaruk yang sama.

Sampler salah


Saya menghabiskan banyak waktu untuk mencari tahu mengapa pencahayaan tidak langsung saya terlihat berulang. Tekstur Crytek Sponza disembunyikan, jadi Anda perlu sampler terbungkus untuk itu. Tetapi untuk RSM itu sangat tidak cocok.

Opengl
OpenGL menetapkan tekstur RSM ke GL_CLAMP_TO_BORDER

Nilai khusus


Untuk meningkatkan alur kerja, penting untuk dapat mengubah beberapa variabel dengan menekan tombol. Misalnya, intensitas iluminasi tidak langsung dan rentang pengambilan sampel ( rMax ). Parameter ini harus disesuaikan untuk setiap sumber cahaya. Jika Anda memiliki rentang pengambilan sampel yang besar, maka Anda mendapatkan pencahayaan tidak langsung dari mana-mana, yang berguna untuk adegan besar. Untuk pencahayaan tidak langsung yang lebih lokal, Anda akan membutuhkan rentang yang lebih kecil. Gambar 3 menunjukkan pencahayaan tidak langsung global dan lokal.

gambar
Gambar 3: Demonstrasi ketergantungan rMax .

Bagian yang terpisah


Pada awalnya, saya berpikir bahwa saya bisa membuat pencahayaan tidak langsung dalam shader, di mana saya mempertimbangkan pencahayaan langsung. Untuk cahaya directional, ini berfungsi karena Anda masih menggambar quad layar penuh. Namun, untuk cahaya spot dan point, Anda perlu mengoptimalkan perhitungan pencahayaan tidak langsung. Karena itu, saya menganggap pencahayaan tidak langsung sebagai bagian terpisah, yang diperlukan jika Anda juga ingin melakukan interpolasi layar-ruang .

Cache


Algoritma ini tidak ramah dengan cache sama sekali. Ini melakukan pengambilan sampel pada titik-titik acak dalam beberapa tekstur. Jumlah sampel tanpa optimisasi juga sangat besar. Dengan resolusi 1280 * 720 dan jumlah sampel RSM 400, Anda akan menghasilkan 1.105.920.000 sampel untuk setiap sumber cahaya.

Pro dan kontra


Saya akan mendaftar pro dan kontra dari algoritma perhitungan pencahayaan tidak langsung ini.
UntukTerhadap
Algoritma mudah dimengertiSama sekali tidak berteman dengan cache
Terintegrasi dengan baik dengan penyaji tangguhanDiperlukan pengaturan variabel
Dapat digunakan dalam algoritma lain ( LPV )Pilihan paksa antara penerangan tidak langsung lokal dan global

Optimalisasi


Saya melakukan beberapa upaya untuk meningkatkan kecepatan algoritma ini. Seperti dijelaskan dalam artikel resmi , Anda dapat menerapkan interpolasi layar-ruang . Saya melakukan ini, dan rendering sedikit lebih cepat. Di bawah ini saya akan menjelaskan beberapa optimasi, dan membuat perbandingan (dalam bingkai per detik) antara implementasi berikut, menggunakan adegan dengan 3 dinding dan kelinci: tanpa RSM , implementasi naif RSM , diinterpolasi oleh RSM .

Periksa-Z


Salah satu alasan RSM saya bekerja tidak efisien adalah karena saya juga menghitung pencahayaan tidak langsung untuk piksel yang merupakan bagian dari skybox. Skybox jelas tidak membutuhkannya.

Pengambilan sampel CPU secara acak


Perhitungan awal sampel tidak hanya akan memberikan koherensi temporal yang lebih besar, tetapi juga menyelamatkan Anda dari keharusan untuk menghitung kembali sampel-sampel ini dalam shader.

Interpolasi layar-ruang


Artikel resmi menyarankan menggunakan target render resolusi rendah untuk menghitung pencahayaan tidak langsung. Untuk adegan dengan banyak normal normal dan dinding lurus, informasi pencahayaan dapat dengan mudah diinterpolasi antara titik dengan resolusi lebih rendah. Saya tidak akan menjelaskan interpolasi secara rinci sehingga artikel ini sedikit lebih pendek.

Kesimpulan


Di bawah ini adalah hasil untuk jumlah sampel yang berbeda. Saya punya beberapa komentar mengenai hasil ini:

  • Secara logis, FPS tetap sekitar 700 untuk jumlah sampel yang berbeda ketika perhitungan RSM tidak dilakukan.
  • Interpolasi memberikan beberapa overhead dan tidak terlalu berguna dengan sejumlah kecil sampel.
  • Bahkan dengan 100 sampel, gambar akhir terlihat cukup bagus. Ini mungkin disebabkan oleh interpolasi, yang β€œmengaburkan” pencahayaan tidak langsung.

Jumlah sampelFPS untuk Tanpa RSMFPS untuk RSM NaifFPS untuk RSM Terinterpolasi
100~ 700152264
200~ 70089179
300~ 70062138
400~ 70044116

Source: https://habr.com/ru/post/id440570/


All Articles