Berbagi kode antara beberapa shader GLSL

30

Saya sering menemukan diri saya menyalin kode paste antara beberapa shader. Ini termasuk perhitungan atau data tertentu yang dibagikan di antara semua shader dalam satu pipa tunggal, dan perhitungan umum yang dibutuhkan oleh semua shader vertex saya (atau tahap lainnya).

Tentu saja, itu praktik yang mengerikan: jika saya perlu mengubah kode di mana saja, saya harus memastikan saya mengubahnya di tempat lain.

Apakah ada praktik terbaik yang diterima untuk menjaga KERING ? Apakah orang hanya menambahkan satu file biasa ke semua shader mereka? Apakah mereka menulis preprocessor C-style ala kadarnya sendiri yang menguraikan #includearahan? Jika ada pola yang diterima di industri, saya ingin mengikuti mereka.

Martin Ender
sumber
4
Pertanyaan ini mungkin sedikit kontroversial, karena beberapa situs SE lainnya tidak menginginkan pertanyaan tentang praktik terbaik. Ini disengaja untuk melihat bagaimana komunitas ini berdiri mengenai pertanyaan-pertanyaan seperti itu.
Martin Ender
2
Hmm, terlihat bagus untukku. Saya katakan kita sebagian besar sedikit "lebih luas" / "lebih umum" dalam pertanyaan kita daripada, katakanlah, StackOverflow.
Chris berkata Reinstate Monica
2
StackOverflow berubah dari menjadi 'minta kami' ke 'jangan tanya kami kecuali Anda harus menyenangkan' papan.
insidesin
Jika itu dimaksudkan untuk menentukan topik, lalu bagaimana dengan pertanyaan Meta terkait?
SL Barth - Reinstate Monica
2
Diskusi tentang meta.
Martin Ender

Jawaban:

18

Ada banyak pendekatan, tetapi tidak ada yang sempurna.

Dimungkinkan untuk berbagi kode dengan menggunakan glAttachShaderuntuk menggabungkan shader, tetapi ini tidak memungkinkan untuk membagikan hal-hal seperti deklarasi struct atau #definekonstanta -d. Itu berfungsi untuk fungsi berbagi.

Beberapa orang suka menggunakan array string yang diteruskan glShaderSourcesebagai cara untuk menambahkan definisi umum sebelum kode Anda, tetapi ini memiliki beberapa kelemahan:

  1. Lebih sulit untuk mengontrol apa yang perlu dimasukkan dari dalam shader (Anda memerlukan sistem terpisah untuk ini.)
  2. Ini berarti pembuat shader tidak dapat menentukan GLSL #version, karena pernyataan berikut dalam spesifikasi GLSL:

The # versi direktif harus terjadi dalam shader sebelum hal lain, kecuali untuk komentar dan ruang putih.

Karena pernyataan ini, glShaderSourcetidak dapat digunakan untuk menambahkan teks sebelum #versiondeklarasi. Ini berarti bahwa #versionbaris tersebut perlu dimasukkan dalam glShaderSourceargumen Anda , yang berarti bahwa antarmuka kompiler GLSL Anda perlu entah bagaimana diberitahu versi GLSL apa yang diharapkan akan digunakan. Selain itu, tidak menentukan #versionakan membuat kompiler GLSL default untuk menggunakan GLSL versi 1.10. Jika Anda ingin membiarkan penulis shader menentukan #versionskrip dalam cara standar, maka Anda harus memasukkan #include-s setelah #versionpernyataan tersebut. Ini bisa dilakukan dengan menguraikan GLSL shader secara eksplisit untuk menemukan #versionstring (jika ada) dan membuat inklusi Anda setelahnya, tetapi memiliki akses ke#includearahan mungkin lebih disukai untuk dikontrol dengan lebih mudah ketika inklusi tersebut perlu dibuat. Di sisi lain, karena GLSL mengabaikan komentar sebelum #versionbaris, Anda dapat menambahkan metadata untuk memasukkan dalam komentar di bagian atas file Anda (yuck.)

Pertanyaannya sekarang adalah: Apakah ada solusi standar untuk #include, atau Anda perlu menggulung ekstensi preprosesor Anda sendiri?

Ada GL_ARB_shading_language_includeekstensi, tetapi memiliki beberapa kelemahan:

  1. Ini hanya didukung oleh NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Ini bekerja dengan menentukan string termasuk sebelumnya. Oleh karena itu, sebelum kompilasi, Anda perlu menentukan bahwa string "/buffers.glsl"(seperti yang digunakan dalam #include "/buffers.glsl") sesuai dengan isi file buffer.glsl(yang telah Anda muat sebelumnya).
  3. Seperti yang mungkin Anda perhatikan pada poin (2), jalur Anda harus memulainya "/", seperti jalur absolut gaya Linux. Notasi ini umumnya tidak dikenal oleh programmer C, dan berarti Anda tidak dapat menentukan jalur relatif.

Desain umum adalah untuk mengimplementasikan #includemekanisme Anda sendiri , tetapi ini bisa rumit karena Anda juga perlu mem-parsing (dan mengevaluasi) instruksi preprosesor lain seperti #ifuntuk menangani dengan benar kompilasi bersyarat (seperti pelindung kepala.)

Jika Anda menerapkan sendiri #include, Anda juga memiliki beberapa kebebasan dalam cara Anda ingin menerapkannya:

  • Anda dapat melewati string sebelumnya (seperti GL_ARB_shading_language_include).
  • Anda bisa menentukan callback include (ini dilakukan oleh perpustakaan D3DCompiler DirectX.)
  • Anda bisa mengimplementasikan sistem yang selalu membaca langsung dari sistem file, seperti yang dilakukan pada aplikasi C yang khas.

Sebagai penyederhanaan, Anda dapat secara otomatis memasukkan pelindung tajuk untuk masing-masing termasuk dalam lapisan preprocessing Anda, sehingga lapisan prosesor Anda terlihat seperti:

if (#include and not_included_yet) include_file();

(Penghargaan untuk Trent Reed karena menunjukkan saya teknik di atas)

Kesimpulannya , tidak ada solusi otomatis, standar, dan sederhana. Dalam solusi masa depan, Anda bisa menggunakan beberapa antarmuka OpenGL SPIR-V, dalam hal ini kompiler GLSL ke SPIR-V bisa berada di luar API GL. Memiliki kompiler di luar runtime OpenGL sangat menyederhanakan menerapkan hal-hal seperti #includekarena ini adalah tempat yang lebih tepat untuk berinteraksi dengan sistem file. Saya percaya metode luas saat ini adalah dengan hanya mengimplementasikan preprocessor kustom yang bekerja dengan cara yang harus akrab dengan programmer C.

Nicolas Louis Guillemot
sumber
Shader juga dapat dipisahkan menjadi modul menggunakan glslify , meskipun hanya bekerja dengan node.js.
Anderson Green
9

Saya biasanya hanya menggunakan fakta bahwa glShaderSource (...) menerima array string sebagai inputnya.

Saya menggunakan file definisi shader berbasis json, yang menentukan bagaimana shader (atau program menjadi lebih benar) dikomposisikan, dan di sana saya menentukan preprocessor mendefinisikan yang saya butuhkan, seragam yang digunakan, file shader vertex / fragmen, dan semua file "ketergantungan" tambahan. Ini hanya kumpulan fungsi yang ditambahkan ke sumber sebelum sumber shader yang sebenarnya.

Sekadar menambahkan, AFAIK, Unreal Engine 4 menggunakan arahan #include yang diuraikan dan menambahkan semua file yang relevan, sebelum kompilasi, seperti yang Anda sarankan.

Matteo Bertello
sumber
4

Saya tidak berpikir ada konvensi umum, tetapi jika saya menebak, saya akan mengatakan bahwa hampir semua orang menerapkan beberapa bentuk inklusi teks sederhana sebagai langkah preprocessing ( #includeekstensi), karena sangat mudah dilakukan begitu. (Dalam JavaScript / WebGL, Anda dapat melakukannya dengan ekspresi reguler sederhana, misalnya). Kelebihannya adalah Anda dapat melakukan preprocessing dalam langkah offline untuk build "release", ketika kode shader tidak lagi perlu diubah.

Bahkan, indikasi bahwa pendekatan ini adalah umum adalah kenyataan bahwa perpanjangan ARB diperkenalkan untuk itu: GL_ARB_shading_language_include. Saya tidak yakin apakah ini menjadi fitur inti sekarang atau tidak, tetapi ekstensi itu ditulis terhadap OpenGL 3.2.

Glampert
sumber
2
GL_ARB_shading_language_include bukan fitur inti. Bahkan, hanya NVIDIA yang mendukungnya. ( delphigl.de/glcapsviewer/… )
Nicolas Louis Guillemot
4

Beberapa orang telah menunjukkan bahwa glShaderSourcedapat mengambil serangkaian string.

Selain itu, dalam GLSL kompilasi ( glShaderSource, glCompileShader) dan menghubungkan ( glAttachShader, glLinkProgram) shader terpisah.

Saya telah menggunakan itu dalam beberapa proyek untuk membagi shader antara bagian tertentu, dan bagian yang umum bagi sebagian besar shader, yang kemudian dikompilasi dan dibagi dengan semua program shader. Ini berfungsi dan tidak sulit untuk diterapkan: Anda hanya perlu mempertahankan daftar ketergantungan.

Dalam hal rawatan, saya tidak yakin itu menang. Pengamatannya sama, mari kita coba membuat faktor. Meskipun memang menghindari pengulangan, overhead teknik terasa signifikan. Selain itu, shader terakhir lebih sulit untuk diekstrak: Anda tidak bisa hanya menggabungkan sumber shader, karena deklarasi berakhir dalam urutan yang akan ditolak oleh beberapa kompiler, atau akan diduplikasi. Sehingga membuatnya lebih sulit untuk melakukan tes shader cepat di alat yang terpisah.

Pada akhirnya teknik ini membahas beberapa masalah KERING, tetapi jauh dari ideal.

Pada topik sampingan, saya tidak yakin apakah pendekatan ini memiliki dampak dalam hal waktu kompilasi; Saya telah membaca bahwa beberapa driver hanya benar-benar mengkompilasi program shader di tautan, tetapi saya belum mengukur.

Julien Guertault
sumber
Dari pemahaman saya, saya pikir ini tidak menyelesaikan masalah berbagi definisi struct.
Nicolas Louis Guillemot
@NicolasLouisGuillemot: ya Anda benar, hanya kode instruksi yang dibagikan dengan cara ini, bukan deklarasi.
Julien Guertault