Em um sistema de material baseado em gráficos, como posso suportar uma variedade de tipos de entrada e saída?


11

insira a descrição da imagem aqui

Eu estou tentando envolver minha cabeça em torno de como os sistemas de materiais como esse , isso são implementadas. Esses sistemas poderosos e fáceis de usar, semelhantes a gráficos, parecem ser relativamente comuns como um método para permitir que programadores e não programadores criem shaders rapidamente. No entanto, da minha experiência relativamente limitada com programação gráfica, não tenho muita certeza de como eles funcionam.


Fundo:

Portanto, quando eu programei sistemas simples de renderização OpenGL antes, normalmente crio uma classe Material que carrega, compila e vincula shaders de arquivos GLSL estáticos que eu criei manualmente. Também costumo criar essa classe como um invólucro simples para acessar variáveis ​​uniformes GLSL. Como um exemplo simples, imagine que eu tenho um shader de vértice básico e um shader de fragmento, com um Texture2D extra uniforme para passar uma textura. Minha classe Material simplesmente carregava e compilava esses dois shaders em um material e, a partir desse momento, expunha uma interface simples para ler / escrever o uniforme Texture2D desse shader.

Para tornar esse sistema um pouco mais flexível, costumo escrevê-lo de uma maneira que me permita passar uniformes de qualquer nome / tipo [ie: SetUniform_Vec4 ("AmbientColor", colorVec4); que definiria o uniforme AmbientColor como um vetor 4d específico chamado "colorVec4" se esse uniforme existir no material.] .

class Material
{
    private:
       int shaderID;
       string vertShaderPath;
       string fragSahderPath;

       void loadShaderFiles(); //load shaders from files at internal paths.
       void buildMaterial(); //link, compile, buffer with OpenGL, etc.      

    public:
        void SetGenericUniform( string uniformName, int param );
        void SetGenericUniform( string uniformName, float param );
        void SetGenericUniform( string uniformName, vec4 param );
        //overrides for various types, etc...

        int GetUniform( string uniformName );
        float GetUniform( string uniformName );
        vec4 GetUniform( string uniformName );
        //etc...

        //ctor, dtor, etc., omitted for clarity..
}

Isso funciona, mas parece um sistema ruim, devido ao fato de que o cliente da classe Material precisa acessar os uniformes apenas pela fé - o usuário deve estar um pouco ciente dos uniformes que estão em cada objeto material, porque eles são forçados a passe-os pelo nome GLSL. Não é um grande negócio quando apenas 1-2 pessoas trabalham com o sistema, mas não consigo imaginar que esse sistema fosse muito bem dimensionado e, antes de fazer minha próxima tentativa de programar um sistema de renderização OpenGL, quero nivelar um pouco.


Questão:

É onde estou até agora, então tenho tentado estudar como outros mecanismos de renderização lidam com seus sistemas de materiais.

Essa abordagem baseada em nós é ótima e parece ser um sistema extremamente comum para a criação de sistemas de materiais fáceis de usar em ferramentas e motores modernos. Pelo que posso dizer, eles são baseados em uma estrutura de dados gráficos, onde cada nó representa algum aspecto de sombreamento do seu material e cada caminho representa algum tipo de relacionamento entre eles.

Pelo que sei, implementar esse tipo de sistema seria uma classe MaterialNode tão simples com uma variedade de subclasses (TextureNode, FloatNode, LerpNode, etc.). Onde cada subclasse MaterialNode teria MaterialConnections.

class MaterialConnection
{
    MatNode_Out * fromNode;
    MatNode_In * toNode;
}

class LerpNode : MaterialNode
{
    MatNode_In x;
    MatNode_In y;
    MatNode_In alpha;

    MatNode_Out result;
}

Essa é a ideia muito básica , mas estou um pouco incerto sobre como alguns aspectos desse sistema funcionariam:

1.) Se você observar as várias 'Expressões materiais' (nós) que o Unreal Engine 4 usa , verá que cada uma delas possui conexões de entrada e saída de vários tipos. Alguns nós flutuam de saída, alguns vetores de saída2, alguns vetores de saída4 etc. Como posso melhorar os nós e as conexões acima para que eles possam suportar uma variedade de tipos de entrada e saída? A subclasse de MatNode_Out com MatNode_Out_Float e MatNode_Out_Vec4 (e assim por diante) seria uma escolha sábia?

2.) Finalmente, como esse tipo de sistema se relaciona aos shaders GLSL? Olhando novamente para UE4 (e da mesma forma para os outros sistemas vinculados acima), é necessário que o usuário conecte algum nó de material a um nó grande com vários parâmetros que representam parâmetros de sombreador (cor base, metalidade, brilho, emissividade etc.) . Minha suposição original era que o UE4 tinha algum tipo de 'shader mestre' codificado com uma variedade de uniformes, e tudo o que o usuário faz em seu 'material' é simplesmente passado para o 'shader mestre' quando eles conectam seus nós ao ' nó principal '.

No entanto, a documentação UE4 afirma:

"Cada nó contém um trecho de código HLSL, designado para executar uma tarefa específica. Isso significa que, ao construir um Material, você está criando um código HLSL através de scripts visuais."

Se isso for verdade, esse sistema gera um script de sombreador real? Como, exatamente, isso funciona?


1
Relacionado à sua pergunta: gameangst.com/?p=441
glampert

Respostas:


10

Tentarei responder da melhor maneira possível, com pouco conhecimento sobre o caso específico do UE4, mas sobre a técnica geral.

Os materiais baseados em gráficos são tanto de programação quanto escrever o código você mesmo. Simplesmente não parece adequado para pessoas sem experiência em código, tornando-o aparentemente mais fácil. Portanto, quando um designer vincula um nó "Adicionar", ele está basicamente escrevendo add (valor1, valor2) e vinculando a saída a outra coisa. É isso que eles significam que cada nó gerará código HLSL, seja uma chamada de função ou apenas instruções simples.

No final, usar o gráfico de materiais é como programar shaders brutos com uma biblioteca de funções predefinidas que fazem algumas coisas úteis comuns, e é também isso que o UE4 faz. Possui uma biblioteca de código de sombreador que um compilador de sombreador irá receber e injetar na fonte final do sombreador, quando aplicável.

No caso do UE4, se eles reivindicam sua conversão para HLSL, presumo que eles usem uma ferramenta de conversão capaz de converter o código de byte HLSL em código de byte GLSL, portanto, é utilizável em plataformas GL. Mas outras bibliotecas têm apenas vários compiladores de sombreador, que lerão o gráfico e gerarão diretamente as fontes de linguagem de sombreamento necessárias.

O gráfico de material também é uma boa maneira de abstrair das especificidades da plataforma e focar no que importa do ponto de vista da direção de arte. Como não está vinculado a um idioma e a um nível muito mais alto, é mais fácil otimizar para a plataforma de destino e injetar dinamicamente outro código, como manipulação de luz no shader.

1) Agora, para responder suas perguntas mais diretamente, você deve ter uma abordagem orientada a dados para projetar esse sistema. Encontre um formato simples que possa ser definido em estruturas muito simples e até definido em um arquivo de texto. Em essência, cada gráfico deve ser uma matriz de nós, com um tipo, um conjunto de entradas e saídas, e cada um desses campos deve ter um link_id local para garantir que as conexões do gráfico não sejam ambíguas. Além disso, cada um desses campos pode ter configuração adicional para o que o campo suporta (que tipo de dados são suportados, por exemplo).

Com essa abordagem, você pode definir facilmente o campo de um nó como (float | double) e permitir inferir o tipo das conexões ou forçar um tipo a ele, sem hierarquias de classe ou superengenharia. Depende de você projetar essa estrutura de dados do gráfico tão rígida ou flexível quanto você desejar. Tudo o que você precisa é que ele tenha informações suficientes para que o gerador de código não tenha ambiguidade e, portanto, potencialmente manipule errado o que você deseja fazer. O importante é que, no nível básico da estrutura de dados, você o mantenha flexível e focado em resolver a tarefa de definir um material sozinho.

Quando digo "definir um material", estou me referindo muito especificamente à definição de uma superfície de malha, além do que a própria geometria fornece. Isso inclui o uso de atributos de vértice adicionais para configurar o aspecto da superfície, adicionar deslocamento a ele com um mapa de altura, perturbar os normais com normais por pixel, alterar parâmetros físicos, alterar os BRDFs e assim por diante. Você não deseja descrever nada como HDR, mapeamento de tom, animação de aparência, manipulação de luz ou muitas outras coisas feitas em shaders.

2) Cabe ao gerador de shader do renderizador percorrer essa estrutura de dados e, lendo suas informações, monte um conjunto de variáveis ​​e as vincule usando funções pré-fabricadas e injetando o código que calcula a iluminação e outros efeitos. Lembre-se de que os sombreadores variam não apenas das APIs gráficas diferentes, mas também entre os diferentes renderizadores (um renderizador diferido versus renderizador baseado em mosaico versus encaminhador exige todos os sombreadores diferentes para funcionar) e, com um sistema de material como esse, você pode abstrair do desagradável camada de baixo nível e concentre-se apenas na descrição da superfície.´

Para a UE4, eles criaram uma lista de itens para o nó de saída final que você mencionou, que eles pensam que descreve 99% das superfícies em jogos antigos e modernos. Eles desenvolveram esse conjunto de parâmetros ao longo de décadas e provaram isso com a quantidade insana de jogos que o mecanismo Unreal produziu até agora. Portanto, você ficará bem se fizer as coisas da mesma maneira que irreal.

Para finalizar, sugiro um arquivo .material apenas para lidar com cada gráfico. Durante o desenvolvimento, ele talvez contenha um formato baseado em texto para depuração e, em seguida, seja empacotado ou compilado no binário para liberação. Cada material. Seria composto por N nós e N conexões, como um banco de dados SQL. Cada nó teria N campos, com um nome e alguns sinalizadores para os tipos aceitos, se sua entrada ou saída, se os tipos forem inferidos, etc. A estrutura de dados de tempo de execução para armazenar o material carregado seria igualmente plana e simples; O editor pode adaptá-lo facilmente e salvar de volta no arquivo.

E então deixe o trabalho pesado real para a geração final do shader, que é realmente a parte mais difícil de fazer. A parte bonita é que seu material permanece independente da plataforma de renderização; em teoria, ele funcionaria com qualquer técnica de renderização e API, desde que você represente o material em sua linguagem de sombreamento apropriada.

Deixe-me saber se você precisar de detalhes adicionais ou alguma correção na minha resposta, perdi a visão geral de todo o texto.


Não posso agradecer o suficiente por escrever uma resposta tão elaborada e excelente. Eu sinto que tenho uma ótima idéia de onde devo ir daqui! Obrigado!
precisa saber é o seguinte

1
Não tem problema, fique à vontade para me enviar uma mensagem se precisar de mais ajuda. Na verdade, estou trabalhando em algo equivalente para minhas próprias ferramentas; portanto, se você quiser trocar ideias, fique à vontade! Tenha uma boa tarde: D
Grimshaw
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.