Kita akan berbicara tentang "Transparansi tertimbang urutan-independen-independen" (selanjutnya disebut WBOIT) - teknik yang dijelaskan dalam JCGT pada 2013 (
tautan ).
Ketika beberapa objek transparan muncul di layar, warna pixel tergantung pada mana yang lebih dekat dengan pengamat. Berikut ini adalah formula pencampuran warna yang terkenal untuk kasus ini:
\ begin {matrix} C = C_ {near} \ alpha + C_ {far} (1- \ alpha) && (1) \ end {matrix}
Urutan pengaturan fragmen penting untuknya: warna fragmen dekat dan opacitynya dilambangkan sebagai C
dekat dan
α , dan warna yang dihasilkan dari semua fragmen yang terletak di belakangnya dilambangkan sebagai C
jauh . Opacity adalah properti yang mengambil nilai dari 0 hingga 1, di mana 0 berarti objek sangat transparan sehingga tidak terlihat, dan 1 - sangat buram sehingga tidak ada yang terlihat di
belakangnya .
Untuk menggunakan rumus ini, Anda harus terlebih dahulu mengurutkan fragmen berdasarkan kedalaman. Bayangkan betapa sakit kepala ini melibatkan! Secara umum, penyortiran harus dilakukan di setiap frame. Jika Anda menyortir objek, maka beberapa objek dengan bentuk kompleks harus dipotong-potong dan disortir berdasarkan kedalaman bagian yang dipotong (khususnya, untuk permukaan berpotongan, ini pasti perlu dilakukan). Jika Anda menyortir fragmen, maka penyortiran akan terjadi di shader. Pendekatan ini disebut "Transparansi bebas pesanan" (OIT), dan menggunakan daftar tertaut yang disimpan dalam memori kartu video. Untuk memperkirakan berapa banyak memori yang harus dialokasikan untuk daftar ini hampir tidak realistis. Dan jika tidak ada cukup memori, artefak akan muncul di layar.
Beruntung bagi mereka yang dapat mengontrol berapa banyak objek tembus ditempatkan di atas panggung, dan di mana mereka relatif satu sama lain. Tetapi jika Anda melakukan CAD, maka Anda akan memiliki objek transparan sebanyak yang diinginkan pengguna, dan mereka akan ditempatkan secara acak.
Sekarang Anda memahami keinginan beberapa orang untuk menyederhanakan hidup mereka dan menghasilkan formula untuk memadukan warna yang tidak memerlukan penyortiran. Formula seperti itu ada di artikel yang saya sebutkan di awal. Bahkan ada beberapa formula di sana, tetapi yang terbaik menurut penulis (dan menurut saya juga) adalah ini:
\ 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}

Dalam tangkapan layar adalah kelompok segitiga tembus pandang yang terletak di empat lapisan secara mendalam. Di sebelah kiri, mereka dirender menggunakan teknik WBOIT. Di sebelah kanan adalah gambar yang diperoleh dengan menggunakan rumus (1), campuran warna klasik, dengan mempertimbangkan urutan susunan fragmen. Selanjutnya saya akan menyebutnya CODB (pencampuran tergantung pesanan Klasik).
Sebelum kita mulai membuat objek transparan, kita harus membuat semua yang buram. Setelah itu, objek transparan dirender dengan uji kedalaman, tetapi tanpa menulis ke buffer kedalaman (ini dilakukan seperti ini:
glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE);
). Artinya, inilah yang terjadi pada suatu titik dengan beberapa koordinat layar (x, y): fragmen transparan yang lebih dekat daripada buram lulus uji kedalaman, terlepas dari bagaimana mereka berada di kedalaman relatif terhadap fragmen transparan yang sudah ditarik, dan fragmen transparan yang muncul lebih jauh buram, jangan lulus uji kedalaman, dan, karenanya, dibuang.
C
0 dalam rumus (2) adalah warna dari sebuah fragmen yang buram, yang di atasnya fragmen transparan digambar, di mana kita memiliki n buah, yang ditunjukkan oleh indeks 1 sampai n. C
i adalah warna dari fragmen transparan ke-i,
α i adalah opacity-nya.
Jika Anda perhatikan lebih teliti, maka rumus (2) sedikit mirip rumus (1). Jika Anda bayangkan itu

Apakah C
dekat , C
0 adalah C
jauh , dan

- ini
α , maka ini akan menjadi formula 1, 1-1. Dan sungguh

- ini adalah rata-rata tertimbang warna dari fragmen transparan (pusat massa ditentukan dalam mekanika dengan rumus yang sama), itu akan menjadi warna fragmen terdekat C di
dekat . C
0 adalah warna fragmen buram yang terletak di belakang semua fragmen, yang kami hitung rata-rata tertimbang ini, dan itu akan berlaku untuk C
jauh . Artinya, kami mengganti semua fragmen transparan dengan satu fragmen “rata-rata” dan menerapkan formula standar untuk pencampuran warna - formula (1). Apa rumus licik untuk
α yang ditawarkan oleh penulis artikel asli?
Ini adalah fungsi skalar dalam ruang n-dimensi, jadi mari kita ingat analisis diferensial fungsi beberapa variabel. Mengingat bahwa semua
α i termasuk dalam rentang dari 0 hingga 1, turunan parsial sehubungan dengan salah satu variabel akan selalu menjadi konstanta non-negatif. Ini berarti bahwa opacity dari fragmen "rata-rata" meningkat dengan meningkatnya opacity dari fragmen transparan, dan inilah yang kita butuhkan. Selain itu, meningkat secara linear.
Jika opacity dari sebuah fragmen adalah 0, maka itu tidak terlihat sama sekali, itu tidak mempengaruhi warna yang dihasilkan.
Jika opacity dari setidaknya satu fragmen adalah 1, maka
α adalah 1. Artinya, fragmen opak menjadi tidak terlihat, yang umumnya baik. Hanya fragmen transparan yang terletak di belakang fragmen dengan opacity = 1 masih bersinar dan memengaruhi warna yang dihasilkan:

Di sini, sebuah segitiga oranye terletak di atas, hijau di bawahnya, dan abu-abu dan cyan di bawah hijau, dan semua ini dengan latar belakang hitam. Opacity biru = 1, semua lainnya - 0,5. Gambar di sebelah kanan adalah apa yang seharusnya. Seperti yang Anda lihat, WBOIT terlihat menjijikkan. Satu-satunya tempat di mana warna oranye normal muncul adalah tepi segitiga hijau, dikelilingi oleh garis putih buram. Seperti yang baru saja saya katakan, sebuah fragmen buram tidak terlihat jika opacity dari fragmen transparan adalah 1.
Ini bahkan lebih baik dilihat di sini:

Segitiga oranye memiliki opacity 1, yang hijau dengan transparansi dimatikan hanya ditarik dengan objek buram. Sepertinya segitiga HIJAU bersinar melalui ORANGE melalui segitiga oranye.
Untuk membuat gambar terlihat layak, cara termudah adalah tidak menetapkan objek dengan opacity tinggi. Dalam proyek kerja saya, saya tidak mengizinkan pengaturan opacity lebih besar dari 0,5. Ini adalah CAD 3D, di mana objek digambar secara skematis, dan realisme khusus tidak diperlukan, jadi pembatasan seperti itu diperbolehkan di sana.
Dengan nilai opacity rendah, gambar di sebelah kiri dan kanan terlihat hampir sama:

Dan dengan tinggi mereka sangat berbeda:

Seperti inilah bentuk polyhedron transparan:


Polyhedron memiliki wajah horizontal oranye dan hijau oranye. Sayangnya, Anda tidak akan memahami ini pada pandangan pertama, yaitu gambar tidak terlihat meyakinkan. Di mana ada dinding oranye di depan, Anda membutuhkan lebih dari oranye, dan di mana hijau lebih dari hijau. Akan jauh lebih baik untuk menggambar wajah dalam satu warna:

WBOIT berbasis kedalaman
Untuk mengkompensasi kurangnya penyortiran berdasarkan kedalaman, penulis artikel ini memberikan beberapa opsi untuk menambahkan kedalaman pada formula (2). Ini membuat implementasi lebih sulit, dan hasilnya kurang dapat diprediksi dan tergantung pada karakteristik adegan tiga dimensi tertentu. Saya tidak mempelajari topik ini, jadi siapa yang peduli - saya mengusulkan untuk membaca artikel.
Dikatakan bahwa WBOIT kadang-kadang mampu melakukan sesuatu yang tidak bisa diurutkan oleh transparansi klasik. Misalnya, Anda menggambar asap sebagai sistem partikel yang hanya menggunakan dua partikel - dengan asap gelap dan terang. Ketika satu partikel melewati yang lain, pencampuran warna klasik dengan penyortiran memberikan hasil yang buruk - warna asap dari cahaya dengan tajam menjadi gelap. Artikel tersebut mengatakan bahwa WBOIT yang sensitif terhadap kedalaman memungkinkan transisi yang mulus dan terlihat lebih dapat dipercaya. Hal yang sama dapat dikatakan tentang pemodelan bulu dan rambut dalam bentuk tabung tipis.
Kode
Sekarang tentang bagaimana menerapkan formula (2) pada OpenGL. Contoh kode ada di Github (
tautan ), dan sebagian besar gambar dalam artikel berasal dari sana. Anda dapat mengumpulkan dan bermain dengan segitiga saya. Kerangka kerja Qt digunakan.
Bagi mereka yang baru mulai mempelajari rendering objek transparan, saya merekomendasikan dua artikel ini:
→
Pelajari OpenGL. Pelajaran 4.3 - Mencampur Warna→
Algoritme Transparansi Independen Pesanan menggunakan daftar tertaut pada Direct3D 11 dan OpenGL 4Namun, yang kedua tidak begitu penting untuk memahami materi ini, tetapi yang pertama harus dibaca.
Untuk menghitung rumus (2), kita membutuhkan 2 framebuffer tambahan, 3 tekstur multisampel dan buffer render, di mana kita akan menulis kedalamannya. Dalam tekstur pertama - colorTextureNT (NT berarti non-transparan) - kita akan membuat objek buram. Itu memiliki tipe GL_RGB10_A2. Tekstur kedua (colorTexture) akan bertipe GL_RGBA16F; dalam 3 komponen pertama dari tekstur ini, kita akan menulis rumus ini (2):

di keempat -

. Tekstur lain dari tipe GL_R16 (alphaTexture) akan berisi

.
Pertama, Anda perlu membuat objek-objek ini untuk mendapatkan pengidentifikasi 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);
Seperti yang saya katakan, kerangka kerja Qt digunakan di sini, dan semua panggilan OpenGL melalui objek bertipe QOpenGLFunctions_4_5_Core, yang selalu disebut sebagai f untuk saya.
Sekarang Anda harus mengalokasikan memori:
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 );
Dan konfigurasikan framebuffers:
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 );
Pada pass rendering kedua, output dari shader fragmen akan menuju ke dua tekstur sekaligus, dan ini harus ditentukan secara eksplisit menggunakan glDrawBuffers.
Sebagian besar kode ini dieksekusi satu kali, pada saat startup program. Kode yang mengalokasikan memori untuk tekstur dan membuat buffer dipanggil setiap kali jendela diubah ukurannya. Selanjutnya muncul kode rendering, yang dipanggil setiap kali jendela digambar ulang.
f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT);
Kami hanya menggambar semua objek buram pada tekstur colorTextureNT, dan menulis kedalaman ke buffer render. Sebelum menggunakan renderbuffer yang sama pada tahap gambar berikutnya, Anda perlu memastikan bahwa semua kedalaman objek buram sudah ditulis di sana. Untuk ini, GL_FRAMEBUFFER_BARRIER_BIT digunakan. Setelah merender objek transparan, kita memanggil fungsi ApplyTextures (), yang akan meluncurkan tahap akhir rendering, di mana shader fragmen akan membaca data dari colorTextureNT tekstur, colorTexture dan alphaTexture untuk menerapkan rumus (2). Tekstur seharusnya sudah sepenuhnya ditulis saat itu, jadi sebelum memanggil ApplyTextures () kami menggunakan GL_TEXTURE_FETCH_BARRIER_BIT.
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(); {
defaultFBO adalah framebuffer tempat kita menampilkan gambar. Dalam kebanyakan kasus itu adalah 0, tetapi dalam Qt adalah QOpenGLWidget :: defaultFramebufferObject ().
Setiap kali shader fragmen dipanggil, kami akan memiliki informasi tentang warna dan opacity dari fragmen saat ini. Tetapi pada output dalam tekstur colorTexture kami ingin mendapatkan jumlah (dan dalam produk alphaTexture tekstur) dari beberapa fungsi dari jumlah ini. Blending digunakan untuk ini. Selain itu, karena untuk tekstur pertama kami menghitung jumlah, dan untuk yang kedua - produk, pengaturan blending (glBlendFunc dan glBlendEquation) untuk setiap lampiran harus diatur secara terpisah.
Berikut adalah 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 dari fungsi CleanupAfterTransparentRendering ():
f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND);
Dalam shader fragmen saya, opacity ditunjukkan oleh huruf w. Produk warna oleh w dan w itu sendiri kita output ke satu parameter output, dan 1 - w ke yang lain. Untuk setiap parameter output, kualifikasi tata letak diatur dalam bentuk “location = X”, di mana X adalah indeks elemen dalam array lampiran, yang kami sampaikan ke glDrawBuffers di daftar ke-3 (khususnya, parameter output dengan lokasi = 0 dikirim ke tekstur terikat ke GL_COLOR_ATTACHMENT0 , dan parameter dengan lokasi = 1 - dalam tekstur yang dilampirkan ke GL_COLOR_ATTACHMENT1). Angka yang sama digunakan dalam fungsi glBlendFunci dan glBlendEquationi untuk menunjukkan nomor lampiran 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 kotak di atas seluruh jendela. Shader fragmen meminta data dari semua tekstur yang kami buat, menggunakan koordinat layar saat ini sebagai koordinat tekstur dan nomor sampel saat ini (gl_SampleID) sebagai nomor sampel dalam tekstur multisampel. Menggunakan variabel gl_SampleID dalam shader secara otomatis mengaktifkan mode ketika shader fragmen dipanggil satu kali untuk setiap sampel (dalam kondisi normal, itu disebut sekali untuk seluruh piksel, dan hasilnya ditulis untuk semua sampel yang ada di dalam primitif).
Tidak ada yang luar biasa di vertex shader:
#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, isi 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);
Yah, alangkah baiknya untuk membebaskan sumber daya OpenGL setelah selesai. Saya memiliki kode ini yang disebut sebagai penghancur 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);