Compartilhando código entre vários shaders GLSL


30

Muitas vezes me vejo copiando e colando código entre vários shaders. Isso inclui determinados cálculos ou dados compartilhados entre todos os shaders em um único pipeline e cálculos comuns de que todos os meus shaders de vértice precisam (ou qualquer outro estágio).

Claro, essa é uma prática horrível: se eu precisar alterar o código em qualquer lugar, preciso garantir que o mude em qualquer outro lugar.

Existe uma prática recomendada aceita para manter o DRY ? As pessoas apenas anexam um único arquivo comum a todos os shaders? Eles escrevem seu próprio pré-processador rudimentar de estilo C, que analisa #includediretivas? Se houver padrões aceitos no setor, eu gostaria de segui-los.


4
Essa pergunta pode ser um pouco controversa, porque vários outros sites de SE não querem perguntas sobre as melhores práticas. É intencional ver como esta comunidade se posiciona em relação a essas questões.
Martin Ender

2
Hmm, parece bom para mim. Eu diria que, em grande parte, somos um pouco "mais amplos" / "mais gerais" em nossas perguntas do que, digamos, StackOverflow.
Chris diz Reinstate Monica

2
O StackOverflow deixou de ser um 'pergunte-nos' a um 'não pergunte a menos que você precise agradar'.
7/08

Se é para determinar o tópico, então que tal uma pergunta Meta associada?
SL Barth - Restabelece Monica

Respostas:


18

Há várias abordagens, mas nenhuma é perfeita.

É possível compartilhar código usando glAttachShaderpara combinar shaders, mas isso não torna possível compartilhar coisas como declarações struct ou #defineconstantes -d. Ele funciona para compartilhar funções.

Algumas pessoas gostam de usar a matriz de seqüências de caracteres transmitidas glShaderSourcecomo uma maneira de anexar definições comuns antes do seu código, mas isso tem algumas desvantagens:

  1. É mais difícil controlar o que precisa ser incluído no shader (você precisa de um sistema separado para isso).
  2. Isso significa que o autor do sombreador não pode especificar o GLSL #version, devido à seguinte declaração na especificação do GLSL:

A diretiva #version deve ocorrer em um sombreador antes de qualquer outra coisa, exceto comentários e espaço em branco.

Devido a esta declaração, glShaderSourcenão pode ser usado para acrescentar texto antes das #versiondeclarações. Isso significa que a #versionlinha precisa ser incluída em seus glShaderSourceargumentos, o que significa que sua interface do compilador GLSL precisa, de alguma forma, ser informada sobre qual versão do GLSL deve ser usada. Além disso, a não especificação de a #versionfará o compilador GLSL usar como padrão o GLSL versão 1.10. Se você deseja permitir que os autores do shader especifiquem o #versionscript de uma maneira padrão, será necessário inserir #include-s após a #versioninstrução. Isso pode ser feito analisando explicitamente o sombreador GLSL para encontrar a #versionstring (se presente) e faça suas inclusões depois dela, mas tendo acesso a um#includediretiva pode ser preferível controlar mais facilmente quando essas inclusões precisam ser feitas. Por outro lado, como o GLSL ignora comentários antes da #versionlinha, você pode adicionar metadados para inclusões nos comentários na parte superior do seu arquivo (eca.)

A questão agora é: Existe uma solução padrão para #include, ou você precisa lançar sua própria extensão de pré-processador?

Existe a GL_ARB_shading_language_includeextensão, mas tem algumas desvantagens:

  1. É suportado apenas pela NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Ele funciona especificando as seqüências de inclusão antecipadamente. Portanto, antes de compilar, você precisa especificar que a sequência "/buffers.glsl"(conforme usada em #include "/buffers.glsl") corresponde ao conteúdo do arquivo buffer.glsl(que você carregou anteriormente).
  3. Como você deve ter notado no ponto (2), seus caminhos precisam começar "/", como caminhos absolutos no estilo Linux. Essa notação geralmente não é familiar para programadores C e significa que você não pode especificar caminhos relativos.

Um design comum é implementar seu próprio #includemecanismo, mas isso pode ser complicado, pois você também precisa analisar (e avaliar) outras instruções do pré-processador, como #ifpara lidar adequadamente com a compilação condicional (como proteções de cabeçalho).

Se você implementar o seu próprio #include, também terá algumas liberdades em como deseja implementá-lo:

  • Você pode passar as strings antes do tempo (como GL_ARB_shading_language_include).
  • Você pode especificar um retorno de chamada de inclusão (isso é feito pela biblioteca D3DCompiler do DirectX.)
  • Você pode implementar um sistema que sempre lê diretamente do sistema de arquivos, como é feito em aplicativos C.

Como simplificação, você pode inserir automaticamente protetores de cabeçalho para cada inclusão na camada de pré-processamento, para que a camada do processador se pareça com:

if (#include and not_included_yet) include_file();

(Agradecemos a Trent Reed por me mostrar a técnica acima.)

Em conclusão , não existe solução automática, padrão e simples. Em uma solução futura, você pode usar alguma interface SPIR-V OpenGL; nesse caso, o compilador GLSL para SPIR-V pode estar fora da API GL. Ter o compilador fora do tempo de execução do OpenGL simplifica muito a implementação de coisas como #includeé um local mais apropriado para a interface com o sistema de arquivos. Acredito que o atual método difundido é apenas implementar um pré-processador personalizado que funcione da maneira que qualquer programador C deve estar familiarizado.


Os sombreadores também podem ser separados em módulos usando o glslify , embora funcione apenas com o node.js.
Anderson Green

9

Geralmente, apenas uso o fato de que glShaderSource (...) aceita uma matriz de strings como entrada.

Eu uso um arquivo de definição de sombreador baseado em json, que especifica como um sombreador (ou um programa para ser mais correto) é composto, e lá especifico o pré-processador que posso precisar, os uniformes que ele usa, o arquivo de sombreadores de vértices / fragmentos, e todos os arquivos adicionais de "dependência". Essas são apenas coleções de funções que são anexadas à fonte antes da fonte real do shader.

Apenas para adicionar, AFAIK, o Unreal Engine 4 usa uma diretiva #include que é analisada e anexa todos os arquivos relevantes, antes da compilação, como você estava sugerindo.


4

Eu não acho que exista uma convenção comum, mas se eu adivinhasse, diria que quase todo mundo implementa alguma forma simples de inclusão de texto como uma etapa de pré-processamento (uma #includeextensão), porque é muito fácil de fazer então. (Em JavaScript / WebGL, você pode fazer isso com uma expressão regular simples, por exemplo). A vantagem disso é que você pode executar o pré-processamento em uma etapa offline para compilações de "liberação", quando o código do sombreador não precisar mais ser alterado.

Na verdade, uma indicação de que esta abordagem é comum é o fato de que uma extensão ARB foi introduzido para que: GL_ARB_shading_language_include. Não tenho certeza se isso se tornou um recurso principal até agora ou não, mas a extensão foi escrita no OpenGL 3.2.


2
GL_ARB_shading_language_include não é um recurso principal. De fato, apenas a NVIDIA suporta. ( Delphigl.de/glcapsviewer/... )
Nicolas Louis Guillemot

4

Algumas pessoas já apontaram que glShaderSourcepodem ter uma variedade de strings.

Além disso, no GLSL, a compilação ( glShaderSource, glCompileShader) e a vinculação ( glAttachShader, glLinkProgram) do shader são separadas.

Eu usei isso em alguns projetos para dividir shaders entre a parte específica e as partes comuns à maioria dos shaders, que são compilados e compartilhados com todos os programas de shader. Isso funciona e não é difícil de implementar: você apenas precisa manter uma lista de dependências.

Porém, em termos de manutenção, não tenho certeza se é uma vitória. A observação foi a mesma, vamos tentar fatorar. Embora de fato evite a repetição, a sobrecarga da técnica parece significativa. Além disso, o shader final é mais difícil de extrair: você não pode apenas concatenar as fontes do shader, pois as declarações terminam em uma ordem que alguns compiladores rejeitarão ou serão duplicados. Portanto, fica mais difícil fazer um teste rápido de shader em uma ferramenta separada.

No final, esta técnica aborda alguns problemas DRY, mas está longe de ser o ideal.

Em um tópico paralelo, não tenho certeza se essa abordagem tem algum impacto em termos de tempo de compilação; Eu li que alguns drivers realmente compilam o programa shader na vinculação, mas eu não medi.


Pelo meu entendimento, acho que isso não resolve o problema de compartilhar definições de estrutura.
Nicolas Louis Guillemot

@NicolasLouisGuillemot: sim, você está certo, apenas o código de instruções é compartilhado dessa maneira, não as declarações.
Julien Guertault
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.