Game Engine Design - Ubershader - Shader management design [chiuso]


18

Voglio implementare un sistema flessibile Ubershader, con ombreggiatura differita. La mia idea attuale è quella di creare shader dai moduli, che trattano alcune funzionalità, come FlatTexture, BumpTexture, Displacement Mapping, ecc. Ci sono anche piccoli moduli che decodificano il colore, eseguono la mappatura dei toni, ecc. Questo ha il vantaggio che posso sostituire alcuni tipi di moduli se la GPU non li supporta, quindi posso adattarmi alle funzionalità GPU correnti. Non sono sicuro che questo design sia buono. Temo di poter fare una cattiva scelta di design, ora e in seguito pagarla.

La mia domanda è dove trovo risorse, esempi, articoli su come implementare efficacemente un sistema di gestione degli shader? Qualcuno sa come fanno i grandi motori di gioco?


3
Non abbastanza per una vera risposta: con questo approccio andrai bene se inizi in piccolo e lo fai crescere organicamente in base alle tue esigenze invece di provare a costruire il MegaCity-One di shader in anticipo. Innanzitutto mitigate la vostra più grande preoccupazione di progettare troppo in anticipo e pagarlo in un secondo momento se non funziona, in secondo luogo evitate di fare un lavoro extra che non viene mai utilizzato.
Patrick Hughes,

Sfortunatamente, non accettiamo più domande sulla "richiesta di risorse".
Gnemlock,

Risposte:


23

Un approccio semi-comune è quello di rendere ciò che chiamo componenti shader , simile a quello che penso tu stia chiamando moduli.

L'idea è simile a un grafico di post-elaborazione. Scrivi blocchi di codice shader che include sia gli input necessari, gli output generati e quindi il codice per lavorare effettivamente su di essi. Hai un elenco che indica quali shader applicare in ogni situazione (se questo materiale ha bisogno di un componente di mappatura di rilievo, se il componente differito o in avanti è abilitato, ecc.).

Ora puoi prendere questo grafico e generare il codice shader da esso. Ciò significa principalmente "incollare" il codice dei blocchi in posizione, con il grafico che si è assicurato che siano già nell'ordine necessario e quindi incollare gli input / output dello shader come appropriato (in GLSL, questo significa definire il tuo "globale" in , out e variabili uniformi).

Questo non è lo stesso di un approccio ubershader. Gli Ubershader sono dove metti tutto il codice necessario per tutto in un unico set di shader, magari usando #ifdefs e uniformi e simili per attivare e disattivare le funzionalità durante la compilazione o l'esecuzione. Personalmente disprezzo l'approccio ubershader, ma alcuni motori AAA piuttosto impressionanti li usano (mi viene in mente Crytek).

È possibile gestire i blocchi shader in diversi modi. Il modo più avanzato - e utile se prevedi di supportare GLSL, HLSL e le console - è scrivere un parser per un linguaggio shader (probabilmente il più vicino possibile a HLSL / Cg o GLSL per la massima "comprensibilità" da parte dei tuoi sviluppatori ) che possono quindi essere utilizzati per le traduzioni da fonte a fonte. Un altro approccio è semplicemente quello di avvolgere blocchi di shader in file XML o simili, ad es

<shader name="example" type="pixel">
  <input name="color" type="float4" source="vertex" />
  <output name="color" type="float4" target="output" index="0" />
  <glsl><![CDATA[
     output.color = vec4(input.color.r, 0, 0, 1);
  ]]></glsl>
</shader>

Si noti che con questo approccio è possibile creare più sezioni di codice per diverse API o anche versioni della sezione di codice (in modo da poter avere una versione GLSL 1.20 e una versione GLSL 3.20). Il tuo grafico può anche escludere automaticamente i blocchi shader che non hanno una sezione di codice compatibile in modo da poter ottenere un degrado semi-grazioso su hardware più vecchio (quindi qualcosa come la normale mappatura o qualsiasi cosa sia appena esclusa su hardware più vecchio che non può supportarlo senza che il programmatore abbia bisogno di fare un sacco di controlli espliciti).

L'esempio XMl può quindi generare qualcosa di simile a (mi scuso se questo è GLSL non valido, è passato un po 'di tempo da quando mi sono sottoposto a quell'API):

layout (location=0) in vec4 input_color;
layout (location=0) out vec4 output_color;

struct Input {
  vec4 color;
};
struct Output {
  vec4 color;
}

void main() {
  Input input;
  input.color = input_color;
  Output output;

  // Source: example.shader
#line 5
  output.color = vec4(input.color.r, 0, 0, 1);

  output_color = output.color;
}

Potresti essere un po 'più intelligente e generare un codice più "efficiente", ma onestamente qualsiasi compilatore di shader che non sia una schifezza totale rimuoverà per te le ridondanze da quel codice generato. Forse la GLSL più recente ti consente di inserire anche il nome del file nei #linecomandi, ma so che le versioni precedenti sono molto carenti e non lo supportano.

Se hai più blocchi, i loro input (che non sono forniti come output da un blocco di antenati nella struttura ad albero) vengono concatenati nel blocco di input, così come gli output, e il codice viene semplicemente concatenato. Viene fatto un po 'di lavoro extra per garantire che le fasi coincidano (vertice vs frammento) e che i layout di input dell'attributo vertice "funzionino". Un altro bel vantaggio di questo approccio è che puoi scrivere indici espliciti di associazione uniforme e di input che non sono supportati nelle versioni precedenti di GLSL e gestirli nella tua libreria di generazione / associazione di shader. Allo stesso modo è possibile utilizzare i metadati nella configurazione dei VBO e delle glVertexAttribPointerchiamate per garantire la compatibilità e che tutto " funzioni ".

Sfortunatamente non esiste già una buona libreria cross-API come questa. Cg è un po 'vicino, ma ha il supporto di schifezze per OpenGL su schede AMD e può essere estremamente lento se si utilizzano le funzionalità di generazione del codice tranne quelle di base. Anche il framework degli effetti DirectX funziona ma ovviamente non ha supporto per nessuna lingua oltre a HLSL. Ci sono alcune librerie incomplete / buggy per GLSL che imitano le librerie DirectX ma dato il loro stato l'ultima volta che ho controllato avrei semplicemente scritto le mie.

L'approccio ubershader significa semplicemente definire direttive di preprocessore "ben note" per determinate caratteristiche e quindi ricompilare per materiali diversi con configurazione diversa. ad esempio, per qualsiasi materiale con una normale mappa è possibile definire USE_NORMAL_MAPPING=1e quindi nel proprio ubershader pixel-stage basta avere:

#if USE_NORMAL_MAPPING
  vec4 normal;
  // all your normal mapping code
#else
  vec4 normal = normalize(in_normal);
#endif

Un grosso problema qui è la gestione di questo per HLSL precompilato, in cui è necessario precompilare tutte le combinazioni in uso. Anche con GLSL è necessario essere in grado di generare correttamente una chiave di tutte le direttive del preprocessore in uso per evitare la ricompilazione / memorizzazione nella cache di shader identici. L'uso delle uniformi può ridurre la complessità ma, diversamente dalle uniformi del preprocessore, non riduce il conteggio delle istruzioni e può comunque avere un impatto minore sulle prestazioni.

Per essere chiari, entrambi gli approcci (oltre a scrivere manualmente una tonnellata di variazioni di shader) sono tutti utilizzati nello spazio AAA. Usa quello che funziona meglio per te.

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.