Design do Game Engine - Ubershader - Design de gerenciamento do Shader [fechado]

18

Quero implementar um sistema flexível de Ubershader, com sombreamento adiado. Minha idéia atual é criar shaders a partir de módulos, que lidam com certos recursos, como FlatTexture, BumpTexture, Displacement Mapping, etc. Existem também pequenos módulos que decodificam cores, fazem mapeamento de tons etc. Isso tem a vantagem que posso substitua certos tipos de módulos se a GPU não os suportar, para que eu possa me adaptar aos recursos atuais da GPU. Não tenho certeza se esse design é bom. Receio poder fazer uma má escolha de design agora e depois pagar por isso.

Minha pergunta é onde encontro recursos, exemplos, artigos sobre como implementar um sistema de gerenciamento de sombreador de maneira eficaz? Alguém sabe como os motores de jogos grandes fazem isso?

Michael Staud
fonte
3
Não há tempo suficiente para uma resposta real: você se sairá bem com essa abordagem se começar pequena e deixá-la crescer organicamente, de acordo com suas necessidades, em vez de tentar construir o MegaCity-One de shaders na frente. Em primeiro lugar, você atenua sua maior preocupação de criar muito design antecipadamente e pagá-lo mais tarde, se não der certo; em segundo lugar, evite fazer um trabalho extra que nunca será usado.
Patrick Hughes
Infelizmente, não aceitamos mais perguntas sobre "solicitação de recursos".
Gnemlock 9/17

Respostas:

23

Uma abordagem semi-comum é criar o que eu chamo de componentes shader , semelhante ao que acho que você está chamando de módulos.

A ideia é semelhante a um gráfico de pós-processamento. Você escreve pedaços de código de sombreador que incluem as entradas necessárias, as saídas geradas e o código para realmente trabalhar nelas. Você tem uma lista que indica quais shaders aplicar em qualquer situação (se esse material precisa de um componente de mapeamento de resposta, se o componente adiado ou adiado está ativado, etc.).

Agora você pode pegar esse gráfico e gerar código de sombreador a partir dele. Isso geralmente significa "colar" o código dos pedaços no lugar, com o gráfico garantindo que eles já estejam na ordem necessária e, em seguida, colando nas entradas / saídas do shader conforme apropriado (no GLSL, isso significa definir seu "global" em , out e variáveis ​​uniformes).

Isso não é o mesmo que uma abordagem de ubershader. As Ubershaders são onde você coloca todo o código necessário para tudo em um único conjunto de shader, talvez usando #ifdefs e uniformes e similares para ativar e desativar recursos ao compilar ou executá-los. Eu pessoalmente desprezo a abordagem do ubershader, mas alguns mecanismos AAA bastante impressionantes os usam (a Crytek em particular me vem à mente).

Você pode lidar com os pedaços de shader de várias maneiras. A maneira mais avançada - e útil se você planeja suportar GLSL, HLSL e os consoles - é escrever um analisador para uma linguagem de sombreador (provavelmente o mais próximo possível de HLSL / Cg ou GLSL, para obter o máximo de "compreensão" por seus desenvolvedores ) que podem ser usados ​​para traduções de fonte a fonte. Outra abordagem é apenas agrupar pedaços de shader em arquivos XML ou similares, por exemplo

<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>

Observe com essa abordagem que você pode criar várias seções de código para diferentes APIs ou até mesmo a versão da seção de código (para que você possa ter uma versão do GLSL 1.20 e uma versão do GLSL 3.20). Seu gráfico pode até excluir automaticamente blocos de shader que não possuem uma seção de código compatível, para que você possa obter uma degradação semi-graciosa em hardware mais antigo (algo como mapeamento normal ou qualquer outra coisa que seja excluída em hardware antigo que não pode suportá-lo sem que o programador precise faça várias verificações explícitas).

O exemplo XMl pode gerar algo semelhante a (desculpas se isso for GLSL inválido, já faz um tempo desde que me submeti a essa 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;
}

Você pode ser um pouco mais inteligente e gerar um código mais "eficiente", mas honestamente, qualquer compilador de shader que não seja uma porcaria total removerá as redundâncias desse código gerado para você. Talvez o GLSL mais recente permita que você coloque o nome do arquivo em#line comandos agora também, mas eu sei que as versões mais antigas são muito deficientes e não suportam isso.

Se você tiver vários blocos, suas entradas (que não são fornecidas como saída por um bloco ancestral na árvore) são concatenadas no bloco de entrada, assim como as saídas, e o código é apenas concatenado. Um pouco de trabalho extra é feito para garantir a correspondência entre estágios (vértice e fragmento) e que os layouts de entrada de atributo de vértice "simplesmente funcionem". Outro benefício interessante dessa abordagem é que você pode escrever índices explícitos de ligação uniforme e de atributo de entrada que não são suportados em versões mais antigas do GLSL e manipulá-los na sua biblioteca de geração / ligação de shader. Da mesma forma, você pode usar os metadados na configuração de seus VBOs e glVertexAttribPointerchamadas para garantir a compatibilidade e que tudo "simplesmente funcione".

Infelizmente, não existe uma boa biblioteca entre APIs como essa. O Cg chega bem perto, mas tem suporte para o OpenGL em placas AMD e pode ser extremamente lento se você usar qualquer um, exceto os recursos mais básicos de geração de código. A estrutura de efeitos do DirectX também funciona, mas é claro que não oferece suporte a nenhum idioma além do HLSL. Existem algumas bibliotecas incompletas / com erros para o GLSL que imitam as bibliotecas do DirectX, mas, devido ao estado delas na última vez que verifiquei, eu escreveria apenas as minhas.

A abordagem do ubershader significa apenas definir diretivas de pré-processador "conhecidas" para certos recursos e recompilar para diferentes materiais com configurações diferentes. por exemplo, para qualquer material com um mapa normal, você pode definir USE_NORMAL_MAPPING=1e, em seu ubershader no estágio de pixel, basta ter:

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

Um grande problema aqui é lidar com isso para o HLSL pré-compilado, onde você precisa pré-compilar todas as combinações em uso. Mesmo com o GLSL, você precisa gerar adequadamente uma chave de todas as diretivas de pré-processador em uso para evitar recompilar / armazenar em cache shaders idênticos. O uso de uniformes pode reduzir a complexidade, mas, diferentemente dos uniformes do pré-processador, não reduz a contagem de instruções e ainda pode ter um impacto menor no desempenho.

Só para esclarecer, ambas as abordagens (além de escrever manualmente uma tonelada de variações de shaders) são usadas no espaço AAA. Use o que for melhor para você.

Sean Middleditch
fonte