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 #include
diretivas? Se houver padrões aceitos no setor, eu gostaria de segui-los.
Respostas:
Há várias abordagens, mas nenhuma é perfeita.
É possível compartilhar código usando
glAttachShader
para combinar shaders, mas isso não torna possível compartilhar coisas como declarações struct ou#define
constantes -d. Ele funciona para compartilhar funções.Algumas pessoas gostam de usar a matriz de seqüências de caracteres transmitidas
glShaderSource
como uma maneira de anexar definições comuns antes do seu código, mas isso tem algumas desvantagens:#version
, devido à seguinte declaração na especificação do GLSL:Devido a esta declaração,
glShaderSource
não pode ser usado para acrescentar texto antes das#version
declarações. Isso significa que a#version
linha precisa ser incluída em seusglShaderSource
argumentos, 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#version
fará o compilador GLSL usar como padrão o GLSL versão 1.10. Se você deseja permitir que os autores do shader especifiquem o#version
script de uma maneira padrão, será necessário inserir#include
-s após a#version
instrução. Isso pode ser feito analisando explicitamente o sombreador GLSL para encontrar a#version
string (se presente) e faça suas inclusões depois dela, mas tendo acesso a um#include
diretiva 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#version
linha, 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_include
extensão, mas tem algumas desvantagens:"/buffers.glsl"
(conforme usada em#include "/buffers.glsl"
) corresponde ao conteúdo do arquivobuffer.glsl
(que você carregou anteriormente)."/"
, 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
#include
mecanismo, mas isso pode ser complicado, pois você também precisa analisar (e avaliar) outras instruções do pré-processador, como#if
para 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:GL_ARB_shading_language_include
).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:
(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.fonte
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.
fonte
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
#include
extensã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.fonte
Algumas pessoas já apontaram que
glShaderSource
podem 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.
fonte