Setelah kemunculan kartu grafis Nvidia RTX musim panas lalu, ray tracing mendapatkan kembali popularitasnya. Selama beberapa bulan terakhir, umpan Twitter saya telah diisi dengan aliran perbandingan grafis tanpa akhir dengan RTX yang diaktifkan dan dinonaktifkan.
Setelah mengagumi begitu banyak gambar yang indah, saya ingin mencoba menggabungkan renderer maju klasik dengan pelacak sinar sendiri.
Menderita
sindrom penolakan terhadap perkembangan orang lain , sebagai hasilnya, saya membuat mesin rendering hybrid saya sendiri yang berbasis WebGL1. Anda dapat bermain dengan rendering tingkat demo dari Wolfenstein 3D dengan bola (yang saya gunakan karena ray tracing) di
sini .
Prototipe
Saya memulai proyek ini dengan membuat prototipe, mencoba menciptakan kembali
pencahayaan global dengan ray tracing dari Metro Exodus .
Prototipe pertama yang menunjukkan pencahayaan global difus (Diffuse GI)Prototipe didasarkan pada renderer maju, yang menjadikan seluruh geometri adegan. Shader yang digunakan untuk meraster geometri tidak hanya menghitung iluminasi langsung, tetapi juga memancarkan sinar acak dari permukaan geometri yang diberikan untuk terakumulasi menggunakan pantulan sinar tidak langsung pantulan cahaya yang timbul dari permukaan yang tidak mengkilap (Diffuse GI).
Pada gambar di atas, Anda dapat melihat bagaimana semua bola diterangi dengan benar hanya oleh pencahayaan tidak langsung (sinar cahaya dipantulkan dari dinding di belakang kamera). Sumber cahaya itu sendiri ditutupi oleh dinding cokelat di sisi kiri gambar.
Wolfenstein 3D
Prototipe menggunakan adegan yang sangat sederhana. Ini hanya memiliki satu sumber cahaya dan hanya beberapa bola dan kubus yang diberikan. Berkat ini, kode pelacakan sinar di shader sangat sederhana. Siklus memeriksa brute-force di mana balok diuji untuk persimpangan dengan semua kubus dan bola di tempat kejadian masih cukup cepat untuk program untuk mengeksekusinya secara real time.
Setelah membuat prototipe ini, saya ingin melakukan sesuatu yang lebih kompleks dengan menambahkan lebih banyak geometri dan banyak sumber cahaya ke TKP.
Masalah dengan lingkungan yang lebih kompleks adalah bahwa saya masih harus dapat melacak sinar di TKP secara real time. Biasanya, struktur
hierarki volume pembatas (BVH) akan digunakan untuk mempercepat proses pelacakan ray, tetapi keputusan saya untuk membuat proyek ini di WebGL1 tidak memungkinkan ini: tidak mungkin memuat data 16-bit ke dalam tekstur di WebGL1 dan operasi biner tidak dapat digunakan dalam shader. Ini mempersulit perhitungan awal dan aplikasi BVH di shader WebGL1.
Itulah mengapa saya memutuskan untuk menggunakan level demo 3D Wolfenstein untuk ini. Pada 2013, saya membuat satu
shader WebGL terfragmentasi di
Shadertoy yang tidak hanya membuat tingkat seperti Wolfenstein, tetapi juga secara prosedural menciptakan semua tekstur yang diperlukan. Dari pengalaman saya bekerja pada shader ini, saya tahu bahwa desain level berbasis grid dari Wolfenstein juga dapat digunakan sebagai struktur akselerasi yang cepat dan mudah, dan ray tracing pada struktur ini akan sangat cepat.
Di bawah ini adalah tangkapan layar demo, dan dalam mode layar penuh Anda dapat memainkannya di sini:
https://reindernijhoff.net/wolfrt .
Deskripsi singkat
Demo ini menggunakan mesin rendering hybrid. Untuk membuat semua poligon dalam bingkai, ia menggunakan rasterisasi tradisional, dan kemudian menggabungkan hasilnya dengan bayangan, GI difus, dan refleksi yang dibuat oleh ray tracing.
BayanganGi difusRefleksiRender proaktif
Kartu Wolfenstein dapat dikodekan sepenuhnya menjadi kisi dua dimensi 64 × 64. Peta yang digunakan dalam demo didasarkan pada
level pertama episode 1 dari Wolfenstein 3D.
Saat start-up, semua geometri yang diperlukan untuk lulus rendering proaktif dibuat. Jala dinding dihasilkan dari data peta. Ini juga menciptakan bidang lantai dan langit-langit, jerat terpisah untuk lampu, pintu, dan bola acak.
Semua tekstur yang digunakan untuk dinding dan pintu dikemas dalam satu atlas tekstur tunggal, sehingga semua dinding dapat ditarik dalam satu panggilan draw.
Bayangan dan Pencahayaan
Penerangan langsung dihitung dalam shader yang digunakan untuk umpan render maju. Setiap fragmen dapat diterangi (maksimum) oleh empat sumber berbeda. Untuk mengetahui sumber mana yang dapat memengaruhi fragmen dalam shader, saat demo dimulai, tekstur pencarian sudah dihitung sebelumnya. Tekstur pencarian ini memiliki ukuran 64 x 128 dan mengkodekan posisi 4 sumber cahaya terdekat untuk setiap posisi dalam kisi peta.
varying vec3 vWorldPos; varying vec3 vNormal; void main(void) { vec3 ro = vWorldPos; vec3 normal = normalize(vNormal); vec3 light = vec3(0); for (int i=0; i<LIGHTS_ENCODED_IN_MAP; i++) { light += sampleLight(i, ro, normal); }
Untuk mendapatkan bayangan lembut untuk setiap fragmen dan sumber cahaya, posisi acak dalam sumber cahaya disampel. Menggunakan kode pelacakan sinar di shader (lihat bagian Ray Tracing di bawah), bayangan bayangan dipancarkan ke titik pengambilan sampel untuk menentukan visibilitas sumber cahaya.
Setelah menambahkan refleksi (tambahan) (lihat bagian Refleksi di bawah), difusi GI ditambahkan ke warna yang dihitung dari fragmen dengan melakukan pencarian di Target Render GI Render (lihat di bawah).
Ray tracing
Meskipun prototipe ray tracing code untuk difuse GI dikombinasikan dengan preemptive shader, dalam demo saya memutuskan untuk memisahkan mereka.
Saya memisahkan mereka dengan melakukan rendering kedua dari semua geometri menjadi target render yang terpisah (Diffuse GI Render Target) menggunakan shader lain yang hanya memancarkan sinar acak untuk mengumpulkan GI difus (lihat bagian “Diffuse GI” di bawah). Pencahayaan yang dikumpulkan dalam target render ini ditambahkan ke pencahayaan langsung yang dihitung dalam bagian render maju.
Dengan memisahkan lintasan proaktif dan difusi GI, kita dapat memancarkan kurang dari satu sinar GI per piksel layar. Ini dapat dilakukan dengan mengurangi Skala Buffer (dengan menggerakkan slider pada opsi di sudut kanan atas layar).
Misalnya, jika Skala Penyangga adalah 0,5, maka hanya satu sinar yang akan dipancarkan untuk setiap empat piksel layar. Ini memberikan peningkatan besar dalam produktivitas. Menggunakan UI yang sama di sudut kanan atas layar, Anda juga dapat mengubah jumlah sampel per piksel dalam target render (SPP) dan jumlah pantulan sinar.
Dipancarkan sinar
Agar dapat memancarkan sinar ke tempat kejadian, semua geometri level harus memiliki format yang dapat digunakan pelacak sinar dalam shader. Lapisan Wolfenstein mengkodekan kisi 64 × 64, sehingga cukup mudah untuk menyandikan semua data menjadi satu tekstur 64 × 64:
- Dalam saluran merah warna tekstur, semua objek yang terletak di sel yang sesuai x, y dari kisi peta dikodekan. Jika nilai saluran merah adalah nol, maka tidak ada benda di dalam sel, jika tidak, itu ditempati oleh dinding (nilai 1 hingga 64), pintu, sumber cahaya atau bola yang perlu diperiksa untuk persimpangan.
- Jika sebuah bola menempati sel grid level, maka saluran hijau, biru dan alfa digunakan untuk mengkodekan radius dan koordinat relatif x dan y dari bola di dalam sel grid.
Sinar dipancarkan dalam sebuah adegan dengan melintasi tekstur menggunakan kode berikut:
bool worldHit(n vec3 ro,in vec3 rd,in float t_min, in float t_max, inout vec3 recPos, inout vec3 recNormal, inout vec3 recColor) { vec3 pos = floor(ro); vec3 ri = 1.0/rd; vec3 rs = sign(rd); vec3 dis = (pos-ro + 0.5 + rs*0.5) * ri; for( int i=0; i<MAXSTEPS; i++ ) { vec3 mm = step(dis.xyz, dis.zyx); dis += mm * rs * ri; pos += mm * rs; vec4 mapType = texture2D(_MapTexture, pos.xz * (1. / 64.)); if (isWall(mapType)) { ... return true; } } return false; }
Kode penelusuran mesh mesh yang serupa dapat ditemukan di
shader Wolfenstein di Shadertoy ini.
Setelah menghitung titik persimpangan dengan dinding atau pintu (menggunakan
tes persimpangan dengan jajar genjang ), mencari di atlas tekstur yang sama yang digunakan untuk melewati rendering proaktif memberi kita titik persimpangan Albedo. Spheres memiliki warna yang ditentukan secara prosedural berdasarkan koordinat
x, y dalam kisi dan
fungsi gradien warna .
Pintu sedikit lebih rumit karena mereka bergerak. Agar representasi adegan dalam CPU (digunakan untuk membuat jerat pada pass rendering maju) harus sama dengan representasi adegan dalam GPU (digunakan untuk penelusuran sinar), semua pintu bergerak secara otomatis dan deterministik, berdasarkan jarak dari kamera ke pintu.
Gi difus
Pencahayaan Global Tersebar (GI difus) dihitung dengan memancarkan sinar dalam shader, yang digunakan untuk menggambar semua geometri di Target Diffuser GI Render. Arah sinar-sinar ini tergantung pada normal ke permukaan, ditentukan dengan mengambil sampel belahan yang berbobot kosinus.
Memiliki arah sinar
rd dan titik awal
ro , iluminasi yang dipantulkan dapat dihitung dengan menggunakan siklus berikut:
vec3 getBounceCol(in vec3 ro, in vec3 rd, in vec3 col) { vec3 emitted = vec3(0); vec3 recPos, recNormal, recColor; for (int i=0; i<MAX_RECURSION; i++) { if (worldHit(ro, rd, 0.001, 20., recPos, recNormal, recColor)) {
Untuk mengurangi kebisingan, pengambilan sampel cahaya langsung ditambahkan ke loop. Ini mirip dengan teknik yang digunakan dalam shader
Cornell Box saya yang
lain di Shadertoy.
Refleksi
Berkat kemampuan untuk melacak adegan dengan sinar di shader, sangat mudah untuk menambahkan pantulan. Dalam demo saya, refleksi ditambahkan dengan memanggil metode
getBounceCol yang sama
seperti yang ditunjukkan di atas menggunakan berkas pantulan kamera:
#ifdef REFLECTION col = mix(col, getReflectionCol(ro, reflect(normalize(vWorldPos - _CamPos), normal), albedo), .15); #endif
Refleksi ditambahkan dalam lintasan render maju, oleh karena itu, satu sinar pantulan akan selalu memancarkan satu sinar pantulan.
Anti-aliasing sementara
Karena kedua bayangan lembut di lintasan render maju dan pendekatan GI difus menggunakan kira-kira satu sampel per piksel, hasil akhirnya sangat bising. Untuk mengurangi jumlah kebisingan, temporal anti-aliasing (TAA) digunakan berdasarkan TAA
Playdead :
Temporal Reprojection Anti-Aliasing di INSIDE .
Proyeksi ulang
Gagasan di balik TAA cukup sederhana: TAA menghitung satu subpixel per frame, dan kemudian rata-rata nilainya dengan pixel yang berkorelasi dari frame sebelumnya.
Untuk mengetahui di mana piksel saat ini berada dalam bingkai sebelumnya, posisi fragmen diproyeksikan kembali menggunakan matriks model-view-proyeksi dari frame sebelumnya.
Jatuhkan sampel dan batasi lingkungan
Dalam beberapa kasus, sampel yang disimpan dari masa lalu tidak valid, misalnya, ketika kamera bergerak sedemikian rupa sehingga sebuah fragmen dari frame saat ini di frame sebelumnya ditutup oleh geometri. Untuk membuang sampel yang tidak valid tersebut, pembatasan lingkungan digunakan. Saya memilih jenis pembatasan paling sederhana:
vec3 history = texture2D(_History, uvOld ).rgb; for (float x = -1.; x <= 1.; x+=1.) { for (float y = -1.; y <= 1.; y+=1.) { vec3 n = texture2D(_New, vUV + vec2(x,y) / _Resolution).rgb; mx = max(n, mx); mn = min(n, mn); } } vec3 history_clamped = clamp(history, mn, mx);
Saya juga mencoba menggunakan metode pembatasan berdasarkan pada jajar genjang, tetapi tidak melihat banyak perbedaan dengan solusi saya. Ini mungkin terjadi karena dalam adegan dari demo ada banyak warna gelap yang identik dan hampir tidak ada benda bergerak.
Getaran kamera
Untuk mendapatkan anti-aliasing, kamera di setiap frame berosilasi karena penggunaan pergeseran subpiksel acak (semu). Ini diimplementasikan dengan mengubah matriks proyeksi:
this._projectionMatrix[2 * 4 + 0] += (this.getHaltonSequence(frame % 51, 2) - .5) / renderWidth; this._projectionMatrix[2 * 4 + 1] += (this.getHaltonSequence(frame % 41, 3) - .5) / renderHeight;
Kebisingan
Noise adalah dasar dari algoritma yang digunakan untuk menghitung difusi GI dan bayangan halus. Menggunakan
noise yang baik sangat memengaruhi kualitas gambar, sementara noise yang buruk menciptakan artefak atau memperlambat konvergensi gambar.
Saya takut white noise yang digunakan dalam demo ini tidak terlalu bagus.
Menggunakan noise yang baik mungkin merupakan aspek terpenting untuk meningkatkan kualitas gambar dalam demo ini. Misalnya, Anda dapat menggunakan
noise biru .
Saya melakukan eksperimen dengan noise berdasarkan rasio emas, tetapi mereka tidak berhasil. Sejauh ini,
Hash terkenal
tanpa Sine of Dave Hoskins digunakan:
vec2 hash2() { vec3 p3 = fract(vec3(g_seed += 0.1) * HASHSCALE3); p3 += dot(p3, p3.yzx + 19.19); return fract((p3.xx+p3.yz)*p3.zy); }
Pengurangan kebisingan
Bahkan dengan TAA diaktifkan, demo masih menunjukkan banyak kebisingan. Terutama sulit untuk membuat langit-langit, karena hanya diterangi oleh pencahayaan tidak langsung. Fakta bahwa langit-langit adalah permukaan datar besar yang diisi dengan warna solid tidak menyederhanakan situasi: jika memiliki detail tekstur atau geometris, kebisingan akan menjadi kurang terlihat.
Saya tidak ingin menghabiskan banyak waktu di bagian demo ini, jadi saya mencoba menerapkan hanya satu filter pengurangan kebisingan: Median3x3 dari
Morgan McGuire dan Kyle Witson . Sayangnya, filter ini tidak bekerja dengan baik dengan grafik "pixel art" dari tekstur dinding: filter ini menghilangkan semua detail di kejauhan dan memutari sudut-sudut piksel dinding di dekatnya.
Dalam percobaan lain, saya menerapkan filter yang sama ke Target Render GI Render. Meskipun ia sedikit mengurangi kebisingan, pada saat yang sama hampir tanpa mengubah detail tekstur dinding, saya memutuskan bahwa peningkatan ini tidak sebanding dengan milidetik tambahan yang dihabiskan.
Demo
Anda dapat memainkan demo di sini .