Buffer Kedalaman Hirarkis


Ulasan singkat


Penyangga kedalaman hierarkis adalah penyangga kedalaman multi-level (penyangga-Z) yang digunakan sebagai struktur percepatan untuk kueri kedalaman. Seperti dalam kasus rantai mip tekstur, ukuran masing-masing tingkat biasanya merupakan hasil dari pembagian oleh dua ukuran buffer resolusi penuh. Pada artikel ini, saya akan berbicara tentang dua cara untuk menghasilkan buffer kedalaman hirarkis dari buffer resolusi penuh.

Pertama, saya akan menunjukkan cara menghasilkan rantai mip lengkap untuk buffer kedalaman, yang menjaga keakuratan kueri kedalaman dalam ruang koordinat tekstur (atau NDC) bahkan untuk ukuran buffer kedalaman tidak sama dengan kekuatan dua. (Di Internet, saya menemukan contoh kode yang tidak menjamin keakuratan ini, yang mempersulit pelaksanaan kueri akurat pada level mip tinggi.)

Kemudian, untuk kasus-kasus di mana hanya satu level downsampling diperlukan, saya akan mendemonstrasikan bagaimana menghasilkan level ini dengan satu panggilan ke shader komputasi yang menggunakan operasi atom dalam memori bersama dari workgroup. Untuk aplikasi saya, di mana hanya diperlukan resolusi 1/16 x 1/16 (mip level 4), metode dengan shader komputasi 2-3 kali lebih cepat daripada pendekatan biasa dengan downsampling rantai mip dalam beberapa lintasan.

pengantar


Hierarchical Depths (juga disebut Hi-Z) adalah teknik yang sering digunakan dalam grafik 3D. Hal ini digunakan untuk mempercepat pemangkasan geometri tak terlihat (oklusi culling) (di CPU , serta di GPU ), perhitungan refleksi di ruang layar , kabut volumetrik, dan banyak lagi.

Selain itu, GPU Hi-Z sering diimplementasikan sebagai bagian dari pipa rasterisasi. Operasi pencarian Hi-Z yang cepat dalam cache pada chip memungkinkan Anda untuk melompati ubin fragmen sepenuhnya jika tertutup sepenuhnya oleh primitif yang sebelumnya diberikan.

Ide dasar Hi-Z adalah untuk mempercepat operasi kueri mendalam dengan membaca dari buffer resolusi lebih rendah. Ini lebih cepat daripada membaca kedalaman resolusi penuh dari buffer, karena dua alasan:

  1. Satu texel (atau hanya beberapa texels) dari buffer resolusi yang lebih rendah dapat digunakan sebagai nilai perkiraan sejumlah pluralitas dari buffer resolusi tinggi.
  2. Penyangga resolusi yang lebih rendah bisa cukup kecil dan di-cache, yang sangat mempercepat pelaksanaan operasi pencarian (terutama dengan akses acak).

Isi level buffer Hi-Z sampel-rendah tergantung pada bagaimana mereka digunakan (apakah buffer kedalaman akan "terbalik" , jenis permintaan apa yang harus digunakan). Secara umum, sebuah texel di level penyangga Hi-Z menyimpan minatau maxsemua texel yang sesuai dengannya di level sebelumnya. Terkadang nilai-nilai mindan disimpan pada saat yang sama max. Nilai rata-rata sederhana (yang sering digunakan dalam level mip dari tekstur reguler) jarang digunakan karena jarang digunakan untuk jenis pertanyaan seperti itu.

Buffer Hi-Z paling sering diminta segera di pintu keluar untuk menghindari pemrosesan lebih lanjut dan operasi pencarian yang lebih akurat dalam buffer resolusi penuh. Misalnya, jika kita menyimpan nilaimaxuntuk buffer kedalaman non-terbalik (di mana semakin besar nilai kedalaman, semakin jauh objeknya), kita dapat dengan cepat menentukan dengan tepat apakah posisi tertentu di ruang layar ditutupi oleh buffer kedalaman (jika koordinatnya Z> adalah nilai (maks) yang disimpan dalam beberapa tingkat yang lebih tinggi (mis., resolusi lebih rendah) dari buffer Hi-Z).

Harap dicatat bahwa saya menggunakan frasa "tepat": jika koordinat Z <= nilai yang diterima (maks), maka tidak diketahui apakah buffernya tumpang tindih. Dalam beberapa aplikasi, dalam kasus ketidakpastian, mungkin perlu mencari buffer untuk kedalaman resolusi penuh; dalam kasus lain, ini tidak diperlukan (misalnya, jika hanya perhitungan yang tidak perlu dipertaruhkan, dan bukan render yang benar).

Aplikasi saya: rendering partikel dalam shader komputasi


Saya dihadapkan dengan kebutuhan untuk menggunakan Hi-Z ketika mengimplementasikan rendering partikel dalam shader komputasi di mesin aplikasi VR PARTICULATE saya . Karena teknik rendering ini tidak menggunakan rasterisasi dengan fungsi tetap, ia perlu menggunakan pemeriksaan kedalaman sendiri untuk setiap partikel dengan ukuran satu piksel. Dan karena partikel tidak diurutkan dengan cara apa pun, akses ke buffer kedalaman (dalam kasus terburuk) hampir acak.

Operasi pencarian pada tekstur akses acak layar penuh adalah cara untuk kinerja yang buruk. Untuk mengurangi beban, pertama-tama saya mencari kedalaman di buffer kedalaman yang dikurangi dengan resolusi 1/16 x 1/16 dari aslinya. Buffer ini berisi nilai kedalaman.min, yang memungkinkan shading rendering komputasi untuk sebagian besar partikel yang terlihat melewati tes kedalaman resolusi penuh. (Jika kedalaman partikel <kedalaman minimum disimpan dalam buffer resolusi lebih rendah, maka kita tahu bahwa itu benar-benar terlihat. Jika itu adalah = = min, maka kita perlu memeriksa buffer kedalaman resolusi penuh.)

Berkat ini, tes kedalaman untuk partikel yang terlihat dalam kasus umum menjadi operasi berbiaya rendah. (Untuk partikel yang tumpang tindih oleh geometri, itu lebih mahal, tetapi itu cocok untuk kita karena tidak menyebabkan biaya rendering, oleh karena itu partikel masih memerlukan sedikit perhitungan.)

Karena fakta bahwa pencarian pertama kali dilakukan dalam buffer kedalaman resolusi yang lebih rendah (seperti yang disebutkan di atas) , waktu rendering partikel berkurang maksimum 35%dibandingkan dengan kasus ketika pencarian hanya dilakukan dalam buffer resolusi penuh. Karena itu, untuk aplikasi saya, Hi-Z sangat bermanfaat.

Sekarang kita akan melihat dua teknik untuk menghasilkan buffer kedalaman hirarkis.

Teknik 1: menghasilkan rantai Mip lengkap


Dalam banyak aplikasi Hi-Z, penciptaan rantai mip buffer kedalaman lengkap diperlukan. Sebagai contoh, ketika melakukan oklusi pemusnahan menggunakan Hi-Z, volume pembatas diproyeksikan ke ruang layar dan ukuran yang diproyeksikan digunakan untuk memilih tingkat mip yang sesuai (sehingga sejumlah texel tetap terlibat dalam setiap uji tumpang tindih).

Menghasilkan rantai mip dari buffer kedalaman resolusi penuh biasanya merupakan tugas sederhana - untuk setiap texel pada level N kami mengambil max(atau min, atau keduanya) 4 texel yang sesuai pada level N-1 yang dihasilkan sebelumnya. Kami melakukan operan berurutan (setiap kali mengurangi ukurannya setengah) sampai kami mendapatkan ukuran mip level 1x1 terakhir.

Namun, dalam hal buffer kedalaman, ukuran yang tidak sesuai dengan kekuatan dua, semuanya lebih rumit. Karena Hi-Z untuk buffer dalam sering dibangun dari resolusi layar standar (yang jarang memiliki kekuatan dua), kita perlu menemukan solusi yang dapat diandalkan untuk masalah ini.

Pertama-tama mari kita putuskan apa artinya nilai dari setiap penyangga kedalaman tingkat-mex texel. Dalam artikel ini, kita akan mengasumsikan bahwa rantai mip menyimpan nilai min. Operasi pencarian kedalaman harus menggunakan pemfilteran tetangga terdekat, karena interpolasi nilai tidak minberguna bagi kami dan akan merusak sifat hierarkis rantai kedalaman mip yang dibuat.

Jadi, apa sebenarnya nilai dari masing-masing texel pada mip-level N yang diperoleh oleh kita? Ini harus menjadi nilai minimum (min) dari semua texels dari buffer kedalaman layar penuh yang menempati ruang yang sama dalam ruang koordinat tekstur (dinormalisasi).

Dengan kata lain, jika koordinat terpisah dari tekstur (dalam interval)[0,1]2) dipetakan (dengan memfilter tetangga terdekat) ke masing-masing texel buffer resolusi penuh, maka texel resolusi penuh ini harus dianggap sebagai kandidat untuk nilai yang mindihitung untuk texel pada setiap tingkat mip berikutnya yang lebih tinggi yang dipetakan dengan koordinat tekstur yang sama.

Jika korespondensi ini dijamin, maka kami akan yakin bahwa operasi pencarian pada level mip tinggi tidak akan pernah mengembalikan nilai kedalaman> nilai texel dalam tekstur yang sama sesuai dengan buffer resolusi penuh (level 0). Dalam kasus N yang terpisah, jaminan ini dipertahankan untuk semua level di bawahnya (<N).

Untuk dimensi genap (dan dalam hal buffer resolusi penuh, yang merupakan kekuatan dua, dimensi genap di setiap level hingga yang terakhir, di mana dimensi menjadi sama dengan 1) akan mudah dilakukan. Dalam kasus satu dimensi, untuk texel dengan indeksi pada level N kita perlu mengambil texels pada level N-1 dengan indeks 2dan 2i+1dan temukan artinya min. YaituDN[i]=min(DNβˆ’1[2i],DNβˆ’1[2i+1]). Kita dapat langsung membandingkan texel dalam rasio "2 banding 1" (dan karena itu ukuran koordinat tekstur), karena ukuran setiap level tepat dua kali lebih kecil dari yang sebelumnya.


Contoh ukuran level genap: 6 texels pada level ini dikurangi menjadi 3 pada level yang lebih tinggi. Ukuran koordinat tekstur dari masing-masing tiga texel tingkat tinggi tepat ditumpangkan pada setiap dua texel tingkat rendah. (Titik adalah pusat dari texels, dan kotak adalah dimensi dari koordinat tekstur ketika menyaring tetangga terdekat.)

Dalam kasus ukuran tingkat ganjil (dan buffer resolusi penuh yang bukan kekuatan dua akan memiliki setidaknya satu tingkat dengan ukuran ganjil) semuanya semakin sulit. Untuk level N-1 ukuran anehdimNβˆ’1 ukuran level berikutnya (N) akan sama dimN=⌊dimNβˆ’12βŒ‹, yaitu β‰ dimNβˆ’12.

Ini berarti bahwa sekarang kita tidak memiliki pemetaan 2-ke-1 yang jelas dari texel level N-1 ke texels level N. Sekarang ukuran koordinat tekstur dari setiap texel pada level N ditumpangkan pada ukuran 3 texels pada level N-1.


Contoh ukuran level ganjil: 7 texels dari level ini dikurangi menjadi 3 texels di level berikutnya. Dimensi koordinat tekstur tiga texel tingkat tinggi ditumpangkan pada ukuran tiga texels dari tingkat yang lebih rendah.

KarenanyaDN[i]=min(DNβˆ’1[2i],DNβˆ’1[2i+1],DNβˆ’1[2i+2]). Ini berarti bahwa satu texel pada level N-1 kadang-kadang mempengaruhi nilai yang mindihitung untuk 2 texel pada level N. Hal ini diperlukan untuk mempertahankan perbandingan yang dijelaskan di atas.

Deskripsi di atas telah disajikan hanya dalam satu dimensi untuk kesederhanaan. Dalam dua dimensi, jika kedua dimensi level N-1 genap, maka wilayah 2x2 texel di level N-1 dipetakan ke satu texel di level N. Jika salah satu dimensi ganjil, wilayah 2x3 atau 3x2 di level N-1 dipetakan ke satu texel pada level N. Jika kedua dimensi ganjil , maka texel β€œangular” juga harus diperhitungkan, yaitu, wilayah 3x3 pada level N-1 dibandingkan dengan satu texel pada level N.

Contoh kode


Kode shader GLSL yang ditunjukkan di bawah ini mengimplementasikan algoritma yang kami jelaskan. Itu harus dijalankan untuk setiap mip berikutnya, mulai dari level 1 (level 0 adalah level resolusi penuh).

uniform sampler2D u_depthBuffer;
uniform int u_previousLevel;
uniform ivec2 u_previousLevelDimensions;

void main() {
	ivec2 thisLevelTexelCoord = ivec2(gl_FragCoord);
	ivec2 previousLevelBaseTexelCoord = 2 * thisLevelTexelCoord;

	vec4 depthTexelValues;
	depthTexelValues.x = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord,
                                    u_previousLevel).r;
	depthTexelValues.y = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 0),
                                    u_previousLevel).r;
	depthTexelValues.z = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 1),
                                    u_previousLevel).r;
	depthTexelValues.w = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(0, 1),
                                    u_previousLevel).r;

	float minDepth = min(min(depthTexelValues.x, depthTexelValues.y),
                         min(depthTexelValues.z, depthTexelValues.w));

    // Incorporate additional texels if the previous level's width or height (or both) 
    // are odd. 
	bool shouldIncludeExtraColumnFromPreviousLevel = ((u_previousLevelDimensions.x & 1) != 0);
	bool shouldIncludeExtraRowFromPreviousLevel = ((u_previousLevelDimensions.y & 1) != 0);
	if (shouldIncludeExtraColumnFromPreviousLevel) {
		vec2 extraColumnTexelValues;
		extraColumnTexelValues.x = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 0),
                                              u_previousLevel).r;
		extraColumnTexelValues.y = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 1),
                                              u_previousLevel).r;

		// In the case where the width and height are both odd, need to include the 
        // 'corner' value as well. 
		if (shouldIncludeExtraRowFromPreviousLevel) {
			float cornerTexelValue = texelFetch(u_depthBuffer,
                                                previousLevelBaseTexelCoord + ivec2(2, 2),
                                                u_previousLevel).r;
			minDepth = min(minDepth, cornerTexelValue);
		}
		minDepth = min(minDepth, min(extraColumnTexelValues.x, extraColumnTexelValues.y));
	}
	if (shouldIncludeExtraRowFromPreviousLevel) {
		vec2 extraRowTexelValues;
		extraRowTexelValues.x = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(0, 2),
                                           u_previousLevel).r;
		extraRowTexelValues.y = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(1, 2),
                                           u_previousLevel).r;
		minDepth = min(minDepth, min(extraRowTexelValues.x, extraRowTexelValues.y));
	}

	gl_FragDepth = minDepth;
}

Kelemahan dalam kode ini


Pertama, dalam hal buffer kedalaman resolusi penuh yang satu dimensi lebih dari dua kali ukuran dimensi lain, indeks panggilan texelFetchmungkin melampaui u_depthBuffer. (Dalam kasus seperti itu, dimensi yang lebih kecil berubah menjadi 1 sebelum yang lain.) Saya ingin menggunakan dalam contoh ini texelFetch(menggunakan koordinat bilangan bulat), sehingga apa yang terjadi sejelas mungkin, dan tidak secara pribadi menemukan penyangga kedalaman lebar / tinggi seperti itu. Jika Anda mengalami masalah seperti itu, Anda dapat membatasi ( clamp) texelFetchkoordinat yang ditransmisikan atau menggunakan texturekoordinat tekstur yang dinormalisasi (dalam sampler, menetapkan batas di tepi). Ketika menghitung minatau maxharus selalu mempertimbangkan satu texel beberapa kali untuk adanya kasus batas.

Kedua, meskipun fakta bahwa empat panggilan pertama texelFetchdapat diganti dengan satu textureGather, ini mempersulit hal-hal (karena textureGathertingkat mip tidak dapat diindikasikan); Selain itu, saya tidak melihat peningkatan kecepatan saat menggunakan textureGather.

Performa


Saya menggunakan shader fragmen di atas untuk menghasilkan dua rantai mip penuh untuk dua (satu untuk setiap mata) buffer kedalaman di mesin VR saya. Dalam tes tersebut, resolusi untuk setiap mata adalah 1648x1776, yang menyebabkan terciptanya 10 level mip tambahan yang dikurangi (yang berarti 10 lintasan). Butuh 0,25 ms pada NVIDIA GTX 980 dan 0,30 ms pada AMD R9 290 untuk menghasilkan rantai penuh untuk kedua mata.



Mip- 4, 5 6, , . ( , , , .) Mip- 4 β€” , (103x111) 2.

mip-


Tugas algoritma yang dijelaskan di atas adalah untuk menjaga keakuratan kueri kedalaman dalam ruang koordinat tekstur (atau NDC). Untuk kelengkapan (dan karena saya menolak jaminan ini dalam teknik di bawah 2), saya ingin menunjukkan metode lain yang saya temui (misalnya, dalam artikel ini ).

Perhatikan bahwa, seperti yang sebelumnya, metode alternatif ini dirancang untuk bekerja dengan buffer resolusi penuh yang ukurannya bukan kekuatan dua (tetapi, tentu saja, mereka bekerja dengan ukuran yang sama dengan kekuatan dua).

Dalam metode alternatif ini, ketika melakukan downsampling level dengan lebar ganjil (atau tinggi) alih-alih menambahkan untuk setiap output texel kolom tambahan (atau baris) dari texels dari level sebelumnya (lebih rendah), kami melakukan operasi ini hanya untuk texels output dengan indeks maksimum (texels "ekstrim" ) Satu-satunya hal yang berubah dalam fragmen shader yang disajikan di atas adalah menetapkan nilai shouldIncludeExtraColumnFromPreviousLeveldan shouldIncludeExtraRowFromPreviousLevel:

// If the previous level's width is odd and this is the highest-indexed "edge" texel for 
// this level, incorporate the rightmost edge texels from the previous level. The same goes 
// for the height. 
bool shouldIncludeExtraColumnFromPreviousLevel =
    (previousMipLevelBaseTexelCoords.x == u_previousLevelDimensions.x - 3);
bool shouldIncludeExtraRowFromPreviousLevel =
    (previousMipLevelBaseTexelCoords.y == u_previousLevelDimensions.y - 3);

Karena itu, texels ekstrem dengan indeks tertinggi menjadi sangat "tebal", karena setiap divisi dengan 2 dimensi ganjil mengarah pada fakta bahwa mereka menempati interval yang lebih besar secara proporsional dari ruang koordinat tekstur yang dinormalisasi.

Kerugian dari pendekatan ini adalah bahwa menjadi lebih sulit untuk melakukan query kedalaman level mip tinggi. Alih-alih hanya menggunakan koordinat tekstur yang dinormalisasi, pertama-tama kita perlu menentukan resolusi penuh texel yang sesuai dengan koordinat ini, dan kemudian mentransfer koordinat texel ini ke koordinat tingkat mip yang sesuai, permintaan yang sedang dieksekusi.

Cuplikan kode di bawah ini bermigrasi dari ruang NDC[βˆ’1,1]2untuk mengirim koordinat di tingkat mip higherMipLevel:

vec2 windowCoords = (0.5 * ndc.xy + vec2(0.5)) * textureSize(u_depthBuffer, 0);
// Account for texel centers being halfway between integers. 
ivec2 texelCoords = ivec2(round(windowCoords.xy - vec2(0.5)));
ivec2 higherMipLevelTexelCoords =
    min(texelCoords / (1 << higherMipLevel),
        textureSize(u_depthBuffer, higherMipLevel).xy - ivec2(1));

Teknik 2: Hasilkan Level Hi-Z Tunggal Menggunakan Compader Shader


Menghasilkan rantai mip lengkap cukup cepat, tetapi agak mengganggu saya bahwa aplikasi saya menghasilkan semua level ini dan hanya menggunakan salah satunya (level 4). Selain menghilangkan sedikit inefisiensi ini, saya juga ingin melihat seberapa banyak semuanya bisa dipercepat jika saya hanya menggunakan satu panggilan shader komputasi untuk menghasilkan level yang saya butuhkan. (Perlu dicatat bahwa aplikasi saya dapat berhenti pada level 4 saat menggunakan solusi dengan shader fragmen multi-pass, jadi pada akhir bagian ini saya menggunakannya sebagai dasar untuk membandingkan runtimes.)

Di sebagian besar aplikasi Hi-Z, hanya satu tingkat kedalaman yang diperlukan, jadi saya menemukan situasi ini biasa. Saya menulis shader komputasi untuk persyaratan spesifik saya sendiri (menghasilkan level 4, yang memiliki resolusi 1/16 x 1/16 dari aslinya). Kode serupa dapat digunakan untuk menghasilkan level yang berbeda.

Shader komputasi sangat cocok untuk tugas ini, karena dapat menggunakan memori kelompok kerja bersama untuk bertukar data antara utas. Setiap kelompok kerja bertanggung jawab untuk satu texel keluaran (dikurangi dengan downsampling buffer), dan utas kelompok kerja berbagi pekerjaan menghitung mintexel yang sesuai dengan resolusi penuh, membagikan hasilnya melalui memori bersama.

Saya mencoba dua solusi utama berdasarkan komputasi shader. Di bagian pertama, setiap utas meminta atomicMinsatu variabel memori bersama.

Perhatikan bahwa karena programmer tidak dapat (tanpa ekstensi untuk perangkat keras dari pabrikan tertentu) melakukan operasi atom pada nilai-nilai non-integer (dan kedalaman saya disimpan sebagai float), beberapa trik diperlukan di sini. Karena nilai floating-point non-negatif dari standar IEEE 754 menjaga urutannya ketika bit mereka diproses sebagai nilai integer yang tidak ditandatangani, kita dapat menggunakan floatBitsToUintuntuk membawa (menggunakan reinterpretasi gips) nilai kedalaman floatke uint, dan kemudian memanggil atomicMin(untuk kemudian mengeksekusi uintBitsToFloatuntuk nilai minimum selesai uint) .

Solusi paling jelas atomicMinadalah membuat grup thread 16x16 di mana setiap thread menerima satu texel dan kemudian mengeksekusinya atomicMindengan nilai di memori bersama. Saya membandingkan pendekatan ini menggunakan blok aliran yang lebih kecil (8x8, 4x8, 4x4, 2x4, 2x2), di mana setiap aliran menerima wilayah texel dan menghitung minimum lokalnya sendiri, dan kemudian memanggil atomicMin.

Yang tercepat dari semua solusi teruji iniatomicMinNVIDIA dan AMD ternyata memiliki solusi dengan blok aliran 4x4 (di mana setiap aliran itu sendiri menerima area texel 4x4). Saya tidak begitu mengerti mengapa opsi ini ternyata menjadi yang tercepat, tetapi mungkin ini mencerminkan kompromi antara persaingan operasi atom dan perhitungan dalam arus independen. Perlu juga dicatat bahwa ukuran workgroup 4x4 hanya menggunakan 16 utas per warp / wave (dan juga mungkin menggunakan 32 atau 64), yang menarik. Contoh di bawah ini menerapkan pendekatan ini.

Sebagai alternatif untuk menggunakan, atomicMinsaya mencoba melakukan reduksi paralel menggunakan teknik yang digunakan dalam presentasi NVIDIA yang dikutip secara aktif ini. (Ide dasarnya adalah menggunakan array memori bersama dengan ukuran yang sama dengan jumlah utas di workgroup, sertalog2⁑(n)lulus untuk perhitungan bersama berurutan dari minminimum setiap aliran sampai minimum akhir dari seluruh kelompok kerja diperoleh.)

Saya mencoba solusi ini dengan semua ukuran yang sama dari kelompok kerja seperti dalam solusi c atomicMin. Bahkan dengan semua optimasi yang dijelaskan dalam presentasi NVIDIA, solusi reduksi paralel sedikit lebih lambat (dalam sepuluh GPU pada kedua GPU) daripada solusi yang atomicMinsaya gunakan. Selain itu, solusi dengan atomicMinjauh lebih sederhana dalam hal kode.

Contoh kode


Dengan metode ini, cara termudah adalah tidak mencoba mempertahankan konsistensi dalam ruang koordinat tekstur yang dinormalisasi antara texel buffer yang direduksi dan resolusi penuh. Anda cukup melakukan konversi dari koordinat texel resolusi penuh ke koordinat texel resolusi berkurang:

ivec2 reducedResTexelCoords = texelCoords / ivec2(downscalingFactor);

Dalam kasus saya (menghasilkan setara dengan mip-level 4) downscalingFactoradalah 16.

Seperti disebutkan di atas, shader komputasi GLSL ini mengimplementasikan solusi dengan atomicMinukuran workgroup 4x4, di mana setiap thread menerima area texel 4x4 dari buffer resolusi penuh. Buffer kedalaman nilai yang dikurangi yang dihasilkan minadalah 1/16 x 1/16 dari ukuran buffer resolusi penuh (dibulatkan ketika ukuran resolusi penuh tidak habis dibagi 16 oleh sepenuhnya).

uniform sampler2D u_inputDepthBuffer;
uniform restrict writeonly image2DArray u_outputDownsampledMinDepthBufferImage;
// The dimension in normalized texture coordinate space of a single texel in 
// u_inputDepthBuffer. 
uniform vec2 u_texelDimensions;

// Resulting image is 1/16th x 1/16th resolution, but we fetch 4x4 texels per thread, hence 
// the divisions by 4 here. 
layout(local_size_x = 16/4, local_size_y = 16/4, local_size_z = 1) in;

// This is stored as uint because atomicMin only works on integer types. Luckily 
// (non-negative) floats maintain their order when their bits are interpreted as uint (using 
// floatBitsToUint). 
shared uint s_workgroupMinDepthEncodedAsUint;

void main() {
	if (gl_LocalInvocationIndex == 0) {
        // Initialize to 1.0 (max depth) before performing atomicMin's. 
		s_workgroupMinDepthEncodedAsUint = floatBitsToUint(1.0);
	}

	memoryBarrierShared();
	barrier();

	// Fetch a 4x4 texel region per thread with 4 calls to textureGather. 'gatherCoords' 
    // are set up to be equidistant from the centers of the 4 texels being gathered (which 
    // puts them on integer values). In my tests textureGather was not faster than 
    // individually fetching each texel - I use it here only for conciseness. 
    // 
    // Note that in the case of the full-res depth buffer's dimensions not being evenly 
    // divisible by the downscaling factor (16), these textureGather's may try to fetch 
    // out-of-bounds coordinates - that's fine as long as the texture sampler is set to 
    // clamp-to-edge, as redundant values don't affect the resulting min. 

	uvec2 baseTexelCoords = 4 * gl_GlobalInvocationID.xy;
	vec2 gatherCoords1 = (baseTexelCoords + uvec2(1, 1)) * u_texelDimensions;
	vec2 gatherCoords2 = (baseTexelCoords + uvec2(3, 1)) * u_texelDimensions;
	vec2 gatherCoords3 = (baseTexelCoords + uvec2(1, 3)) * u_texelDimensions;
	vec2 gatherCoords4 = (baseTexelCoords + uvec2(3, 3)) * u_texelDimensions;

	vec4 gatheredTexelValues1 = textureGather(u_inputDepthBuffer, gatherCoords1);
	vec4 gatheredTexelValues2 = textureGather(u_inputDepthBuffer, gatherCoords2);
	vec4 gatheredTexelValues3 = textureGather(u_inputDepthBuffer, gatherCoords3);
	vec4 gatheredTexelValues4 = textureGather(u_inputDepthBuffer, gatherCoords4);

	// Now find the min across the 4x4 region fetched, and apply that to the workgroup min 
    // using atomicMin. 
	vec4 gatheredTexelMins = min(min(gatheredTexelValues1, gatheredTexelValues2),
                                 min(gatheredTexelValues3, gatheredTexelValues4));
	float finalMin = min(min(gatheredTexelMins.x, gatheredTexelMins.y),
                         min(gatheredTexelMins.z, gatheredTexelMins.w));
	atomicMin(s_workgroupMinDepthEncodedAsUint, floatBitsToUint(finalMin));

	memoryBarrierShared();
	barrier();

    // Thread 0 writes workgroup-wide min to image. 
	if (gl_LocalInvocationIndex == 0) {
		float workgroupMinDepth = uintBitsToFloat(s_workgroupMinDepthEncodedAsUint);

		imageStore(u_outputDownsampledMinDepthBufferImage,
		           ivec2(gl_WorkGroupID.xy),
                   // imageStore can only be passed vec4, but only a float is stored. 
				   vec4(workgroupMinDepth));
	}
}

Performa


Saya menggunakan shader komputasi di atas untuk memproses buffer kedalaman resolusi penuh dengan dimensi yang sama yang digunakan untuk menghasilkan rantai mip penuh (buffer 1648x1776 untuk setiap mata). Ini berjalan dalam 0,12 ms pada NVIDIA GTX 980 dan 0,08 ms pada AMD R9 290. Jika kita bandingkan dengan waktu pembuatan hanya tingkat mip 1–4 (0,22 ms pada NVIDIA, 0,25 ms AMD), maka Solusi dengan komputasi shader ternyata menjadi 87% lebih cepat dengan GPU NVIDIA dan 197% lebih cepat dari GPU AMD .

Secara absolut, akselerasi tidak begitu besar, tetapi setiap 0,1 ms penting, terutama di VR :)

All Articles