Utilizzo di due shader anziché uno con istruzioni IF


9

Ho lavorato sul porting di una sorgente ES 1.1 relativamente grande su ES 2.0.

In OpenGL ES 2.0 (il che significa che tutto usa shader), voglio disegnare una teiera tre volte.

  1. Il primo, con un colore uniforme (ala vecchio glColor4f).

  2. Il secondo, con un colore per vertice (anche la teiera ha il suo array di colori per vertici)

  3. Il terzo, con trama per vertice

  4. E forse un quarto, con trama e colore sia per vertice. E poi forse un quinto, anche con normali.

Per quanto ne so, ci sono due scelte che ho con l'implementazione. Il primo è quello di creare uno shader che supporti tutto quanto sopra, con un'uniforme impostata per modificare il comportamento (ad esempio utilizzare l'uniforme a colori singolare o l'uniforme a colori per vertice).

La seconda scelta è quella di creare uno shader diverso per ogni situazione. Con un po 'di pre-elaborazione di shader personalizzati, non è così complicato da fare, ma la preoccupazione è il costo delle prestazioni nel passaggio tra shader tra oggetti di disegno. Ho letto che non è banalmente piccolo.

Voglio dire, il modo migliore per farlo è costruire entrambi e misurare, ma sarebbe bello sentire qualsiasi input.

Risposte:


10

Il costo della ramificazione delle prestazioni non può essere anche banalmente piccolo. Nel tuo caso tutti i vertici e i frammenti disegnati prenderanno lo stesso percorso attraverso i tuoi shader, quindi sui moderni hardware desktop non sarebbe così male come potrebbe essere, ma stai usando ES2 che implica che non stai usando moderni hardware desktop.

Il caso peggiore con la ramificazione andrà in questo modo:

  • vengono valutati entrambi i lati del ramo.
  • un'istruzione "mix" o "step" verrà generata dal compilatore shader e inserita nel codice per decidere quale lato utilizzare.

E tutte queste istruzioni extra verranno eseguite per ogni vertice o frammento che disegni. Sono potenzialmente milioni di istruzioni extra da valutare rispetto al costo di un cambio di shader.

La " Guida alla programmazione OpenGL ES di Apple per iOS " (che può essere considerata come rappresentativa per l'hardware di destinazione) ha questo da dire sulla ramificazione:

Evita la ramificazione

I rami sono scoraggiati negli shader, poiché possono ridurre la capacità di eseguire operazioni in parallelo su processori grafici 3D. Se i tuoi shader devono usare i rami, segui questi consigli:

  • Prestazioni ottimali: diramazione su una costante nota durante la compilazione dello shader.
  • Accettabile: ramo su una variabile uniforme.
  • Potenzialmente lento: ramificazione su un valore calcolato all'interno dello shader.

Invece di creare un grande shader con molte manopole e leve, crea shader più piccoli specializzati per attività di rendering specifiche. C'è un compromesso tra la riduzione del numero di rami negli shader e l'aumento del numero di shader creati. Prova diverse opzioni e scegli la soluzione più veloce.

Anche se sei soddisfatto di essere nello slot "Accettabile" qui, devi comunque considerare che con 4 o 5 casi tra cui scegliere, aumenterai il conteggio delle istruzioni nei tuoi shader. Dovresti essere consapevole dei limiti di conteggio delle istruzioni sul tuo hardware di destinazione e assicurarti di non andare oltre, citando di nuovo dal link Apple sopra:

Le implementazioni OpenGL ES non sono necessarie per implementare un fallback software quando questi limiti vengono superati; invece, lo shader semplicemente non riesce a compilare o collegare.

Niente di tutto questo per dire che la ramificazione non è la soluzione migliore per le tue esigenze. Hai correttamente identificato il fatto che dovresti profilare entrambi gli approcci, quindi questa è la raccomandazione finale. Tuttavia, tieni presente che, man mano che gli shader diventano più complessi, una soluzione basata sulla ramificazione potrebbe comportare un sovraccarico molto più elevato rispetto ad alcuni cambi di shader.


3

Il costo di associazione degli shader potrebbe non essere banale, ma non sarà il collo di bottiglia a meno che non si stiano eseguendo il rendering di migliaia di articoli senza raggruppare tutti gli oggetti che utilizzano gli stessi shader.

Anche se non sono sicuro che questo si applichi ai dispositivi mobili, ma le GPU non sono tremendamente lente con i rami se la condizione è tra una costante e un'uniforme. Entrambi sono validi, entrambi sono stati utilizzati in passato e continueranno ad essere utilizzati in futuro, scegli quello che ritieni più pulito nel tuo caso.

Inoltre, ci sono alcuni altri modi per ottenere questo risultato: "Uber-shader" e un piccolo trucco con il modo in cui i programmi di shader OpenGL sono collegati.

Gli "Uber-shader" sono essenzialmente la prima scelta, meno la ramificazione, ma avrai più shader. Invece di usare ifaffermazioni, si utilizza il preprocessore - #define, #ifdef, #else, #endif, e compilare diverse versioni, tra cui la corretta #defines per quello che ti serve.

vec4 color;
#ifdef PER_VERTEX_COLOR
color = in_color;
#else
color = obj_color;
#endif

Puoi anche suddividere lo shader in funzioni separate. Avere uno shader che definisce i prototipi per tutte le funzioni e le chiama, collegare un gruppo di shader extra che includono le implementazioni appropriate. Ho usato questo trucco per la mappatura delle ombre, per semplificare lo scambio di filtri su tutti gli oggetti senza dover modificare tutti gli shader.

//ins, outs, uniforms

float getShadowCoefficient();

void main()
{
    //shading stuff goes here

    gl_FragColor = color * getShadowCoefficient();
}

Quindi, potrei avere più altri file shader che definiscono getShadowCoefficient(), le uniformi necessarie e nient'altro. Ad esempio, shadow_none.glslcontiene:

float getShadowCoefficient()
{
    return 1;
}

E shadow_simple.glslcontiene (semplificato dal mio shader che implementa CSM):

in vec4 eye_position;

uniform sampler2DShadow shad_tex;
uniform mat4 shad_mat;

float getShadowCoefficient()
{
    vec4 shad_coord = shad_mat * eye_position;
    return texture(shad_tex, shad_coord).x;
}

E puoi semplicemente scegliere se vuoi l'ombreggiatura collegando uno shadow_*shader diverso . Questa soluzione potrebbe benissimo avere più overhead, ma mi piacerebbe pensare che il compilatore GLSL sia abbastanza buono da ottimizzare qualsiasi overhead aggiuntivo rispetto ad altri modi per farlo. Non ho eseguito alcun test su questo, ma è il modo in cui mi piace farlo.

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.