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 1: Dari kiri ke kanan: tanpa RSM, dengan RSM, perbedaanHasil
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 2: Persamaan pencahayaan titik dengan posisi x dan sumber cahaya normal n pixel arah pHasil 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.
OpenglOpenGL 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 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.
Untuk | Terhadap |
Algoritma mudah dimengerti | Sama sekali tidak berteman dengan cache |
Terintegrasi dengan baik dengan penyaji tangguhan | Diperlukan 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 sampel | FPS untuk Tanpa RSM | FPS untuk RSM Naif | FPS untuk RSM Terinterpolasi |
100 | ~ 700 | 152 | 264 |
200 | ~ 700 | 89 | 179 |
300 | ~ 700 | 62 | 138 |
400 | ~ 700 | 44 | 116 |