Usando dois shaders em vez de um com instruções IF

9

Eu tenho trabalhado na portabilidade de uma fonte opengl ES 1.1 relativamente grande para o ES 2.0.

No OpenGL ES 2.0 (o que significa que tudo usa shaders), quero desenhar um bule de chá três vezes.

  1. O primeiro, com uma cor uniforme (ala o antigo glColor4f).

  2. O segundo, com uma cor por vértice (o bule de chá também possui sua matriz de cores)

  3. O terceiro, com textura por vértice

  4. E talvez um quarto, com textura e cor por vértice. E então talvez um quinto, com normais também ..

Existem duas opções que tenho com a implementação, até onde eu sei. A primeira é criar um sombreador que suporte todas as opções acima, com um uniforme definido para alterar o comportamento (por exemplo, use o uniforme de cor singular ou o uniforme de cor por vértice).

A segunda opção é criar um shader diferente para cada situação. Com algum pré-processamento de shader personalizado, não é tão complicado de fazer, mas a preocupação é o custo de desempenho na troca de shaders entre objetos de desenho. Eu li que não é trivialmente pequeno.

Quero dizer, a melhor maneira de fazer isso é construir os dois e medir, mas seria bom ouvir qualquer entrada.

kamziro
fonte

Respostas:

10

O custo de desempenho da ramificação também não pode ser trivialmente pequeno. No seu caso, todos os vértices e fragmentos desenhados seguirão o mesmo caminho nos shaders; portanto, no hardware de desktop moderno , não seria tão ruim quanto poderia ser, mas você está usando o ES2, o que implica que não está usando o moderno hardware de desktop.

O pior caso da ramificação será mais ou menos assim:

  • ambos os lados do ramo são avaliados.
  • uma instrução "mix" ou "step" será gerada pelo compilador de sombreador e inserida no seu código para decidir qual lado usar.

E todas essas instruções extras serão executadas para cada vértice ou fragmento que você desenhar. São potencialmente milhões de instruções extras a serem pesadas em relação ao custo de uma alteração no shader.

O " Guia de programação do OpenGL ES para iOS " da Apple (que pode ser considerado representativo para o seu hardware de destino) tem a dizer sobre ramificação:

Evite ramificação

As ramificações são desencorajadas em shaders, pois podem reduzir a capacidade de executar operações em paralelo nos processadores gráficos 3D. Se seus shaders precisam usar ramificações, siga estas recomendações:

  • Melhor desempenho: ramifique uma constante conhecida quando o sombreador é compilado.
  • Aceitável: ramifica em uma variável uniforme.
  • Potencialmente lento: ramificação em um valor calculado dentro do shader.

Em vez de criar um shader grande com muitos botões e alavancas, crie shaders menores especializados para tarefas de renderização específicas. Há uma troca entre reduzir o número de ramificações em seus shaders e aumentar o número de shaders que você cria. Teste diferentes opções e escolha a solução mais rápida.

Mesmo se você estiver satisfeito por estar no slot "Aceitável" aqui, ainda precisará considerar que, com 4 ou 5 casos para selecionar, você aumentará a contagem de instruções em seus shaders. Você deve estar ciente dos limites de contagem de instruções no hardware de destino e garantir que você não os ultrapasse, citando novamente no link da Apple acima:

As implementações do OpenGL ES não são necessárias para implementar um fallback de software quando esses limites são excedidos; em vez disso, o sombreador simplesmente falha ao compilar ou vincular.

Nada disso significa que a ramificação não é a melhor solução para sua necessidade. Você identificou corretamente o fato de que deve criar um perfil das duas abordagens, portanto essa é a recomendação final. Mas lembre-se de que, à medida que os sombreadores se tornam mais complexos, uma solução baseada em ramificação pode gerar uma sobrecarga muito maior do que algumas alterações no sombreador.

Maximus Minimus
fonte
3

O custo de vincular shaders pode não ser trivial, mas não será o seu gargalo, a menos que você esteja processando milhares de itens sem agrupar todos os objetos que usam os mesmos shaders.

Embora eu não tenha certeza se isso se aplica a dispositivos móveis, as GPUs não são terrivelmente lentas com ramificações se a condição estiver entre constante e uniforme. Ambos são válidos, ambos foram usados ​​no passado e continuarão a ser usados ​​no futuro, escolha o que você achar que seria mais limpo no seu caso.

Além disso, existem algumas outras maneiras de conseguir isso: "Uber-shaders" e um pouco de truque com a maneira como os programas de OpenGL shader são vinculados.

"Uber-shaders" são essencialmente a primeira escolha, menos a ramificação, mas você terá vários shaders. Em vez de usar ifdeclarações, você pode usar o pré-processador - #define, #ifdef, #else, #endif, e compilar versões diferentes, incluindo os adequados #defines para o que você precisa.

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

Você também pode dividir o shader em funções separadas. Tenha um shader que defina protótipos para todas as funções e as chame, vincule vários shaders extras que incluem as implementações apropriadas. Usei esse truque para o mapeamento de sombras, para facilitar a troca de como a filtragem é feita em todos os objetos sem precisar modificar todos os shaders.

//ins, outs, uniforms

float getShadowCoefficient();

void main()
{
    //shading stuff goes here

    gl_FragColor = color * getShadowCoefficient();
}

Então, eu poderia ter vários outros arquivos de sombreador que definem getShadowCoefficient(), os uniformes necessários e nada mais. Por exemplo, shadow_none.glslcontém:

float getShadowCoefficient()
{
    return 1;
}

E shadow_simple.glslcontém (simplificado do meu shader que implementa os CSMs):

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 você pode simplesmente escolher se deseja ou não sombreamento vinculando um shadow_*sombreador diferente . Essa solução pode muito bem ter mais sobrecarga, mas eu gostaria de pensar que o compilador GLSL é bom o suficiente para otimizar qualquer sobrecarga extra em comparação com outras maneiras de fazer isso. Ainda não testei isso, mas é dessa maneira que gosto de fazê-lo.

Robert Rouhani
fonte