WBOIT di OpenGL: transparansi tanpa pengurutan

Posting ini adalah tentang transparansi campuran pesanan-independen tertimbang (WBOIT) - trik yang dicakup dalam JCGT pada 2013.

Ketika beberapa objek transparan muncul di layar, warna piksel bergantung pada yang mana yang lebih dekat dengan penampil. Berikut adalah operator pencampuran terkenal yang digunakan dalam kasus itu:

\ begin {matrix} C = C_ {near} \ alpha + C_ {far} (1- \ alpha) && (1) \ end {matrix}



Pemesanan fragmen penting. Operator berisi warna (C dekat ) dan opacity ( Ξ± ) dari fragmen dekat dan keseluruhan warna (C jauh ) dari semua fragmen di belakangnya. Opacity dapat berkisar dari 0 hingga 1; 0 berarti objek sepenuhnya transparan (tidak terlihat) dan 1 berarti sepenuhnya buram.

Untuk menggunakan operator ini, Anda perlu mengurutkan fragmen berdasarkan kedalaman. Bayangkan betapa kutukan itu. Secara umum, Anda perlu membuat satu penyortiran per bingkai. Jika Anda menyortir objek maka Anda mungkin harus berurusan dengan permukaan berbentuk tidak teratur yang harus dipotong menjadi beberapa bagian, dan kemudian cut-off PARTS dari permukaan tersebut harus diurutkan (Anda pasti perlu melakukannya untuk memotong permukaan). Jika Anda menyortir fragmen maka Anda akan menempatkan penyortiran yang sebenarnya di shader Anda. Metode ini dikenal sebagai "Transparansi independen pesanan" (OIT), dan didasarkan pada daftar tertaut yang disimpan dalam memori video. Hampir mustahil untuk memperkirakan berapa banyak memori yang harus dialokasikan untuk daftar itu. Dan jika Anda kekurangan memori, Anda mendapatkan artefak di layar.

Anggap diri Anda beruntung jika Anda dapat mengatur jumlah objek transparan pada adegan Anda dan sesuaikan posisi relatifnya. Tetapi jika Anda mengembangkan CAD maka itu tergantung pada pengguna untuk memposisikan objek mereka, sehingga akan ada objek sebanyak yang mereka inginkan, dan penempatan mereka akan jelas sewenang-wenang.

Sekarang Anda tahu mengapa begitu menggoda untuk menemukan operator campuran yang tidak memerlukan penyortiran awal. Dan ada operator seperti itu - dalam sebuah makalah yang saya sebutkan di awal. Sebenarnya, ada beberapa formula, tetapi salah satunya yang penulis (dan saya sendiri) anggap sebagai yang terbaik:

\ begin {matrix} C = {{\ sum_ {i = 1} ^ {n} C_i \ alpha_i} \ over {\ sum_ {i = 1} ^ {n} \ alpha_i}} (1- \ prod_ {i = 1} ^ {n} (1- \ alpha_i)) + C_0 \ prod_ {i = 1} ^ {n} (1- \ alpha_i) && (2) \ end {matrix}





Pada tangkapan layar orang dapat melihat kelompok segitiga transparan yang disusun pada empat lapisan kedalaman. Di sisi kiri mereka diberikan dengan WBOIT, dan di sisi kanan campuran klasik yang tergantung pesanan - dengan rumus (1) - digunakan (saya akan menyebutnya CODB mulai sekarang).

Sebelum kita dapat mulai membuat objek transparan, kita perlu membuat semua yang tidak transparan. Setelah itu, objek transparan dirender dengan uji kedalaman tetapi tanpa menulis apa pun ke buffer kedalaman (dapat dilakukan dengan cara ini: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); ).

Sekarang, mari kita lihat apa yang terjadi di beberapa titik dengan koordinat layar-ruang (x, y). Fragmen transparan - yang kebetulan lebih dekat daripada yang tidak transparan - lulus uji kedalaman, tidak peduli bagaimana mereka ditempatkan relatif terhadap fragmen transparan yang telah diberikan. Fragmen transparan yang jatuh di belakang yang tidak transparan - well, mereka tidak lulus uji kedalaman dan dibuang secara alami.

C 0 dalam rumus (2) adalah warna dari fragmen non-transparan yang diberikan pada titik itu (x, y). Kami memiliki n fragmen transparan total yang lulus uji kedalaman, dan mereka memiliki indeks i ∈ [1, n]. C i adalah warna dari fragmen transparan dan α i adalah opacity-nya.

Formula (2) sedikit mirip dengan formula (1), meskipun tidak terlalu jelas. Ganti dengan C dekat , C 0 dengan C jauh dan dengan Ξ± dan formula (1) akan persis seperti yang Anda dapatkan. Memang, adalah rata - rata aritmatika tertimbang dari warna semua fragmen transparan (ada rumus serupa dalam mekanika untuk "pusat massa"), dan akan pergi untuk warna fragmen dekat C dekat . C 0 adalah warna dari fragmen non-transparan di balik semua fragmen transparan yang kami hitung rata-rata aritmatika tertimbang. Dengan kata lain, kami mengganti semua fragmen transparan dengan satu fragmen "rata-rata tertimbang" dan menggunakan operator pencampuran standar - formula (1). Sekarang, ada rumus yang agak canggih untuk Ξ± , dan kami belum mengetahui artinya.

 alpha=1βˆ’ prodi=1n(1βˆ’ alphai)


Ini adalah fungsi skalar dalam ruang n-dimensi. Semua Ξ± i terkandung dalam [0, 1] sehingga turunan parsialnya berkenaan dengan salah satu Ξ± i adalah konstanta non-negatif. Ini berarti bahwa opacity dari fragmen "weighted mean" meningkat ketika Anda meningkatkan opacity dari salah satu fragmen transparan, yang persis seperti yang kita inginkan. Selain itu, meningkat secara linear.

Jika opacity dari beberapa fragmen adalah 0 maka itu sama sekali tidak terlihat. Itu tidak berkontribusi pada warna yang dihasilkan sama sekali.

Jika setidaknya satu fragmen memiliki opacity 1 maka Ξ± adalah 1 juga. Artinya, fragmen non-transparan menjadi tidak terlihat, yang bagus. Masalahnya adalah, fragmen transparan lainnya (di belakang fragmen ini dengan opacity = 1) masih dapat dilihat melaluinya dan berkontribusi pada warna yang dihasilkan:



Segitiga oranye pada gambar ini terletak di bagian atas, segitiga hijau terletak di bawahnya dan di bawah segitiga hijau terletak segitiga abu-abu dan cyan. Latar belakangnya hitam. Opacity segitiga oranye adalah 1; yang lainnya memiliki opacity = 0,5. Di sini Anda dapat melihat bahwa WBOIT terlihat sangat miskin. Satu-satunya tempat di mana warna oranye sejati muncul adalah tepi segitiga hijau yang diuraikan dengan garis putih tidak transparan. Seperti yang saya sebutkan, fragmen non-transparan tidak terlihat jika memiliki fragmen transparan di atasnya dengan opacity = 1.

Itu terlihat lebih baik pada gambar berikut:



Opacity segitiga oranye adalah 1, segitiga hijau dengan transparansi dimatikan hanya ditampilkan dengan objek yang tidak transparan. Sepertinya warna HIJAU dari segitiga di belakang menyaring melalui segitiga atas sebagai warna ORANGE.

Cara paling sederhana untuk membuat gambar Anda terlihat masuk akal adalah dengan tidak mengatur opacity tinggi untuk objek Anda. Dalam sebuah proyek di mana saya menggunakan teknik ini, saya tidak mengatur opacity lebih dari 0,5. Ini adalah CAD 3D di mana objek digambar secara skematis dan tidak perlu terlihat sangat realistis, sehingga batasan ini dapat diterima.

Dengan opasitas rendah, gambar kiri dan kanan terlihat sangat mirip:



Dan mereka jelas berbeda dengan opasitas tinggi:



Berikut ini adalah polyhedron transparan:




Ini memiliki wajah sisi oranye dan wajah horisontal hijau, yang sayangnya tidak jelas, yang berarti bahwa gambar tidak terlihat kredibel. Di mana pun wajah oranye berada di atas, warnanya harus lebih oranye, dan di mana di balik wajah hijau, warnanya harus lebih hijau. Lebih baik menggambarnya dengan satu warna:



Suntikkan kedalaman ke operator blending


Untuk mengkompensasi kurangnya penyortiran kedalaman, penulis makalah JCGT tersebut datang dengan beberapa cara untuk menyuntikkan kedalaman dalam formula (2). Ini mempersulit implementasi dan membuat hasilnya kurang dapat diprediksi. Untuk membuatnya berfungsi, blending parameter harus disesuaikan dengan adegan 3D tertentu. Saya tidak mempelajari topik ini secara mendalam, jadi jika Anda ingin tahu lebih banyak, baca koran.

Penulis mengklaim bahwa terkadang WBOIT mampu melakukan sesuatu yang tidak dapat dilakukan CODB. Misalnya, pertimbangkan menggambar asap sebagai sistem partikel dengan dua partikel: asap gelap dan asap lebih ringan. Ketika partikel bergerak dan satu partikel melewati yang lain, warna campurannya langsung beralih dari gelap ke terang, yang tidak baik. Operator WBOIT dengan kedalaman menghasilkan hasil yang lebih disukai dengan transisi warna yang halus. Rambut atau bulu yang dimodelkan sebagai satu set tabung tipis memiliki sifat yang sama.

Kodenya


Sekarang untuk implementasi OpenGL dari rumus (2). Anda dapat melihat implementasinya di GitHub. Ini adalah aplikasi berbasis Qt, dan gambar yang Anda lihat di sini sebagian besar berasal dari itu.

Jika Anda baru mengenal rendering transparan, berikut ini adalah materi entry-level yang bagus:
Pelajari OpenGL. Memadukan

Saya sarankan membacanya sebelum melanjutkan dengan posting ini.

Untuk mengevaluasi formula (2) kita membutuhkan 2 framebuffer tambahan, 3 tekstur multisamle dan renderbuffer kedalaman. Objek yang tidak transparan akan dirender ke tekstur pertama, colorTextureNT. Jenisnya adalah GL_RGB10_A2. Tekstur kedua (colorTexture) akan bertipe GL_RGBA16F. Tiga komponen pertama colorTexture akan mengandung bagian formula ini (2): , dan akan ditulis ke komponen keempat. Tekstur terakhir, alphaTexture, dari tipe GL_R16 akan berisi .

Pertama, kita perlu membuat semua objek itu dan mendapatkan pengenalnya dari OpenGL:
  f->glGenFramebuffers (1, &framebufferNT ); f->glGenTextures (1, &colorTextureNT ); f->glGenRenderbuffers(1, &depthRenderbuffer); f->glGenFramebuffers(1, &framebuffer ); f->glGenTextures (1, &colorTexture); f->glGenTextures (1, &alphaTexture); 

Saya menggunakan Qt framewok, seperti yang Anda ingat, dan semua panggilan ke OpenGL dibuat dari objek bertipe QOpenGLFunctions_4_5_Core, yang untuknya saya selalu menggunakan nama f.

Alokasi memori berikutnya:
  f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGB16F, w, h, GL_TRUE ); f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples, GL_DEPTH_COMPONENT, w, h ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGBA16F, w, h, GL_TRUE ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_R16F, w, h, GL_TRUE ); 

Pengaturan Framebuffer:
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0 ); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0 ); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0 ); GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; f->glDrawBuffers(2, attachments); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); 

Selama pass rendering kedua, output dari shader fragmen akan terdiri dari dua tekstur, yang harus ditentukan secara eksplisit dengan glDrawBuffers.
Sebagian besar kode ini dieksekusi satu kali, ketika program dimulai. Kode untuk alokasi memori tekstur dan renderbuffer dijalankan setiap kali ukuran jendela diubah. Sekarang kita lanjutkan ke kode yang dieksekusi setiap kali isi jendela diperbarui.
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ... rendering non-transparent objects ... // ....... // done! (you didn't expect me to explain how do I render primitives in OpenGL, did you? // It's not relevant for this topic 

Kami baru saja memberikan semua objek yang tidak transparan ke colorTextureNT dan menulis kedalaman di renderbuffer. Sebelum menggunakan renderbuffer yang sama pada pass rendering berikutnya, kita perlu memastikan bahwa semua operasi penulisan dalam renderbuffer mendalam dari objek yang tidak transparan telah selesai. Ini dicapai dengan GL_FRAMEBUFFER_BARRIER_BIT. Setelah objek transparan di-render, kita akan memanggil fungsi ApplyTextures () yang akan melakukan proses rendering akhir di mana shader fragmen akan mengambil sampel dari tekstur colorTextureNT, colorTexture, dan alphaTexture untuk menerapkan rumus (2). Tekstur harus siap pada saat itu, jadi kami menggunakan GL_TEXTURE_FETCH_BARRIER_BIT sebelum memanggil ApplyTextures ().
  static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; static constexpr GLfloat clearAlpha = 1.0f; f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glClearBufferfv(GL_COLOR, 0, clearColor); f->glClearBufferfv(GL_COLOR, 1, &clearAlpha); f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); PrepareToTransparentRendering(); { // ... rendering transparent objects ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO adalah framebuffer yang kami gunakan untuk menampilkan gambar di layar. Dalam kebanyakan kasus itu adalah 0, tetapi dalam Qt adalah QOpenGLWidget :: defaultFramebufferObject ().

Dalam setiap doa shader fragmen kita akan memiliki akses ke warna dan opacity dari fragmen saat ini. Tetapi dalam colorTexture harus muncul jumlah (dan dalam alphaTexture produk) dari entitas tersebut. Untuk itu, kami akan menggunakan blending. Selain itu, mengingat bahwa untuk tekstur pertama kita menghitung jumlah sedangkan untuk yang kedua kita menghitung suatu produk, kita harus menyediakan pengaturan pencampuran yang berbeda (glBlendFunc dan glBlendEquation) untuk setiap lampiran.

Berikut ini isi dari fungsi SiapkanToTransparentRendering ():
  f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glDepthFunc(GL_LEQUAL); f->glDisable(GL_CULL_FACE); f->glEnable(GL_MULTISAMPLE); f->glEnable(GL_BLEND); f->glBlendFunci(0, GL_ONE, GL_ONE); f->glBlendEquationi(0, GL_FUNC_ADD); f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO); f->glBlendEquationi(1, GL_FUNC_ADD); 


Dan isi fungsi CleanupAfterTransparentRendering ():
  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

Dalam fragmen shader saya, w berarti opacity. Produk warna dan w - dan w itu sendiri - akan pergi ke parameter output pertama, dan 1 - w akan pergi ke parameter output kedua. Kualifikasi tata letak harus ditetapkan untuk setiap parameter output dalam bentuk "location = X", di mana X adalah indeks elemen dalam array lampiran - yang kami berikan ke fungsi glDrawBuffers. Lebih tepatnya, parameter output dengan location = 0 pergi ke tekstur terikat ke GL_COLOR_ATTACHMENT1, dan parameter dengan lokasi = 1 pergi ke tekstur terikat ke GL_COLOR_ATTACHMENT1. Angka-angka yang sama digunakan dalam fungsi glBlendFunci dan glBlendEquationi untuk menunjukkan lampiran warna mana yang kita atur parameter blending.

Shader fragmen:
 #version 450 core in vec3 color; layout (location = 0) out vec4 outData; layout (location = 1) out float alpha; layout (location = 2) uniform float w; void main() { outData = vec4(w * color, w); alpha = 1 - w; } 

Dalam fungsi ApplyTextures () kita cukup menggambar persegi panjang yang menutupi seluruh viewport. Sampel sampel shader fragmen dari ketiga tekstur menggunakan koordinat layar-ruang saat ini sebagai tekstur, dan indeks sampel saat ini (gl_SampleID) sebagai indeks sampel untuk tekstur multisampel. Kehadiran variabel gl_SampleID dalam kode shader membuat sistem untuk memanggil fragmen shader sekali per sampel (sementara biasanya itu dipanggil sekali per piksel, menulis outputnya ke semua sampel yang termasuk dalam primitif).

Vertex shader adalah sepele:
 #version 450 core const vec2 p[4] = vec2[4]( vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1) ); void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); } 


Shader fragmen:
 #version 450 core out vec4 outColor; layout (location = 0) uniform sampler2DMS colorTextureNT; layout (location = 1) uniform sampler2DMS colorTexture; layout (location = 2) uniform sampler2DMS alphaTexture; void main() { ivec2 upos = ivec2(gl_FragCoord.xy); vec4 cc = texelFetch(colorTexture, upos, gl_SampleID); vec3 sumOfColors = cc.rgb; float sumOfWeights = cc.a; vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb; if (sumOfWeights == 0) { outColor = vec4(colorNT, 1.0); return; } float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r; colorNT = sumOfColors / sumOfWeights * alpha + colorNT * (1 - alpha); outColor = vec4(colorNT, 1.0); } 

Dan akhirnya - fungsi ApplyTextures ():
  f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glUniform1i(0, 0); f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glUniform1i(1, 1); f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glUniform1i(2, 2); f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST); f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 


Pada akhirnya, sumber daya OpenGL harus dibebaskan. Saya melakukannya di destructor widget OpenGL saya:
  f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture); 

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


All Articles