Condivisione del codice tra più shader GLSL


30

Mi ritrovo spesso a copiare e incollare codice tra diversi shader. Ciò include sia determinati calcoli o dati condivisi tra tutti gli shader in una singola pipeline, sia i calcoli comuni di cui hanno bisogno tutti i miei shader di vertice (o qualsiasi altro stadio).

Certo, è una pratica orribile: se devo cambiare il codice ovunque, devo assicurarmi di cambiarlo ovunque.

Esiste una best practice accettata per mantenere il DRY ? Le persone antepongono semplicemente un singolo file comune a tutti i loro shader? Scrivono il proprio preprocessore rudimentale in stile C che analizza le #includedirettive? Se ci sono modelli accettati nel settore, mi piacerebbe seguirli.


4
Questa domanda può essere un po 'controversa, perché molti altri siti SE non vogliono domande sulle migliori pratiche. Questo è intenzionale per vedere come sta questa comunità riguardo a tali domande.
Martin Ender,

2
Hmm, mi sta bene. Direi che siamo in larga misura un po 'più "ampi" / "più generali" nelle nostre domande rispetto, per esempio, a StackOverflow.
Chris dice di reintegrare Monica il

2
StackOverflow è passato dall'essere un "chiedici" a un "non chiederci a meno che non sia necessario per favore".
insidesin

Se ha lo scopo di determinare l'argomento, allora che ne dici di una Meta domanda associata?
SL Barth - Ripristina Monica il

Risposte:


18

C'è un sacco di approcci, ma nessuno è perfetto.

È possibile condividere il codice usando glAttachShaderper combinare shader, ma ciò non rende possibile condividere cose come dichiarazioni di struttura o #definecostanti -d. Funziona per condividere le funzioni.

Ad alcune persone piace usare la matrice di stringhe passate glShaderSourcecome modo per anteporre definizioni comuni prima del codice, ma questo presenta alcuni svantaggi:

  1. È più difficile controllare ciò che deve essere incluso all'interno dello shader (per questo è necessario un sistema separato).
  2. Significa che l'autore dello shader non può specificare il GLSL #version, a causa della seguente dichiarazione nelle specifiche GLSL:

La direttiva #version deve essere presente in uno shader prima di ogni altra cosa, ad eccezione dei commenti e degli spazi bianchi.

A causa di questa affermazione, glShaderSourcenon può essere utilizzato per anteporre il testo prima delle #versiondichiarazioni. Ciò significa che la #versionlinea deve essere inclusa nei tuoi glShaderSourceargomenti, il che significa che l'interfaccia del compilatore GLSL deve essere in qualche modo informata su quale versione di GLSL dovrebbe essere utilizzata. Inoltre, non specificando a #versionsi renderà il compilatore GLSL predefinito usando GLSL versione 1.10. Se vuoi lasciare che gli autori dello shader specifichino l' #versioninterno dello script in modo standard, allora devi in ​​qualche modo inserire #include-s dopo l' #versionistruzione. Questo potrebbe essere fatto analizzando esplicitamente lo shader GLSL per trovare la #versionstringa (se presente) e fare le tue inclusioni dopo di essa, ma avere accesso a un#includela direttiva potrebbe essere preferibile per controllare più facilmente quando tali inclusioni devono essere fatte. D'altra parte, poiché GLSL ignora i commenti prima della #versionriga, è possibile aggiungere metadati per le inclusioni all'interno dei commenti nella parte superiore del file (yuck.)

La domanda ora è: esiste una soluzione standard per #includeo è necessario implementare la propria estensione preprocessore?

C'è l' GL_ARB_shading_language_includeestensione, ma ha alcuni svantaggi:

  1. È supportato solo da NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Funziona specificando in anticipo le stringhe di inclusione. Pertanto, prima di compilare, è necessario specificare che la stringa "/buffers.glsl"(come utilizzata in #include "/buffers.glsl") corrisponde al contenuto del file buffer.glsl(che è stato caricato in precedenza).
  3. Come avrai notato al punto (2), i tuoi percorsi devono iniziare con "/" , come i percorsi assoluti in stile Linux. Questa notazione è generalmente sconosciuta ai programmatori C e significa che non è possibile specificare percorsi relativi.

Un design comune è quello di implementare il tuo #include meccanismo, ma questo può essere complicato poiché è necessario analizzare (e valutare) altre istruzioni del preprocessore come #ifper gestire correttamente la compilazione condizionale (come le protezioni delle intestazioni).

Se implementi il ​​tuo #include , hai anche alcune libertà su come vuoi implementarlo:

  • Potresti passare le stringhe in anticipo (come GL_ARB_shading_language_include ).
  • È possibile specificare un callback include (questo viene fatto dalla libreria del compilatore D3DC di DirectX).
  • Potresti implementare un sistema che legge sempre direttamente dal filesystem, come fatto nelle tipiche applicazioni C.

Come semplificazione, è possibile inserire automaticamente protezioni di intestazione per ciascuna inclusione nel livello di preelaborazione, in modo che il livello del processore sia simile a:

if (#include and not_included_yet) include_file();

(Ringraziamo Trent Reed per avermi mostrato la tecnica sopra.)

In conclusione , non esiste una soluzione automatica, standard e semplice. In una soluzione futura, è possibile utilizzare un'interfaccia OpenGL SPIR-V, nel qual caso il compilatore da GLSL a SPIR-V potrebbe trovarsi al di fuori dell'API GL. Avere il compilatore fuori dal runtime OpenGL semplifica notevolmente l'implementazione di cose come #includepoiché è un posto più appropriato per interfacciarsi con il filesystem. Credo che l'attuale metodo diffuso sia semplicemente implementare un preprocessore personalizzato che funzioni in modo che qualsiasi programmatore C dovrebbe avere familiarità.


Gli shader possono anche essere separati in moduli usando glslify , sebbene funzioni solo con node.js.
Anderson Green,

9

In genere uso solo il fatto che glShaderSource (...) accetta una matrice di stringhe come input.

Uso un file di definizione dello shader basato su json, che specifica come è composto uno shader (o un programma per essere più corretti), e lì specifica il preprocessore che potrebbe essere necessario, le uniformi che usa, il file shader di vertici / frammenti, e tutti i file aggiuntivi di "dipendenza". Queste sono solo raccolte di funzioni che vengono aggiunte alla fonte prima della fonte effettiva dello shader.

Solo per aggiungere, AFAIK, Unreal Engine 4 usa una direttiva #include che viene analizzata e accoda tutti i file rilevanti, prima della compilazione, come stavi suggerendo.


4

Non penso che ci sia una convenzione comune, ma se provassi a indovinare, direi che quasi tutti implementano una semplice forma di inclusione testuale come fase di preelaborazione ( #includeun'estensione), perché è molto facile da fare così. (In JavaScript / WebGL, puoi farlo con una semplice espressione regolare, per esempio). Il vantaggio è che puoi eseguire la preelaborazione in un passaggio offline per build "release", quando il codice shader non deve più essere modificato.

Infatti, l'indicazione che questo approccio è comune il fatto che un'estensione ARB è stato introdotto che: GL_ARB_shading_language_include. Non sono sicuro se questa sia diventata una funzionalità di base ormai o no, ma l'estensione è stata scritta su OpenGL 3.2.


2
GL_ARB_shading_language_include non è una funzionalità principale. In effetti, solo NVIDIA lo supporta. ( delphigl.de/glcapsviewer/… )
Nicolas Louis Guillemot

4

Alcune persone hanno già sottolineato che glShaderSourcepuò richiedere una serie di stringhe.

Inoltre, in GLSL la compilazione ( glShaderSource, glCompileShader) e il collegamento ( glAttachShader, glLinkProgram) di shader sono separati.

L'ho usato in alcuni progetti per dividere gli shader tra la parte specifica e le parti comuni alla maggior parte degli shader, che viene quindi compilata e condivisa con tutti i programmi shader. Funziona e non è difficile da implementare: devi solo mantenere un elenco di dipendenze.

In termini di manutenibilità, non sono sicuro che sia una vittoria. L'osservazione è stata la stessa, proviamo a fattorizzare. Mentre evita davvero la ripetizione, il sovraccarico della tecnica sembra significativo. Inoltre, lo shader finale è più difficile da estrarre: non puoi semplicemente concatenare le fonti dello shader, poiché le dichiarazioni finiscono in un ordine che alcuni compilatori rifiuteranno o saranno duplicati. Quindi è più difficile eseguire un test rapido dello shader in uno strumento separato.

Alla fine questa tecnica affronta alcuni problemi di DEUMIDIFICAZIONE, ma è tutt'altro che ideale.

Su un argomento secondario, non sono sicuro che questo approccio abbia alcun impatto in termini di tempo di compilazione; Ho letto che alcuni driver compilano davvero solo il programma shader sul collegamento, ma non ho misurato.


Secondo la mia comprensione, penso che questo non risolva il problema della condivisione delle definizioni di struttura.
Nicolas Louis Guillemot,

@NicolasLouisGuillemot: sì, hai ragione, solo il codice delle istruzioni è condiviso in questo modo, non le dichiarazioni.
Julien Guertault,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.