Qual é a melhor camada de abstração para o gerenciamento de dados de vértices D3D9 e OpenGL?


8

Meu código de renderização sempre foi o OpenGL. Agora, preciso oferecer suporte a uma plataforma que não possua OpenGL, portanto, preciso adicionar uma camada de abstração que inclua o OpenGL e o Direct3D 9. Suporte o Direct3D 11 posteriormente.

TL; DR: as diferenças entre o OpenGL e o Direct3D causam redundância para o programador, e o layout dos dados parece esquisito.

Por enquanto, minha API funciona um pouco assim. É assim que um shader é criado:

Shader *shader = Shader::Create(
    " ... GLSL vertex shader ... ", " ... GLSL pixel shader ... ",
    " ... HLSL vertex shader ... ", " ... HLSL pixel shader ... ");
ShaderAttrib a1 = shader->GetAttribLocation("Point", VertexUsage::Position, 0);
ShaderAttrib a2 = shader->GetAttribLocation("TexCoord", VertexUsage::TexCoord, 0);
ShaderAttrib a3 = shader->GetAttribLocation("Data", VertexUsage::TexCoord, 1);
ShaderUniform u1 = shader->GetUniformLocation("WorldMatrix");
ShaderUniform u2 = shader->GetUniformLocation("Zoom");

Já existe um problema aqui: depois que um sombreador do Direct3D é compilado, não há como consultar um atributo de entrada por seu nome; aparentemente, apenas a semântica permanece significativa. É por isso GetAttribLocationque esses argumentos extras ficam ocultos ShaderAttrib.

Agora é assim que eu crio uma declaração de vértice e dois buffers de vértice:

VertexDeclaration *decl = VertexDeclaration::Create(
        VertexStream<vec3,vec2>(VertexUsage::Position, 0,
                                VertexUsage::TexCoord, 0),
        VertexStream<vec4>(VertexUsage::TexCoord, 1));

VertexBuffer *vb1 = new VertexBuffer(NUM * (sizeof(vec3) + sizeof(vec2));
VertexBuffer *vb2 = new VertexBuffer(NUM * sizeof(vec4));

Outro problema: as informações VertexUsage::Position, 0são totalmente inúteis para o back-end OpenGL / GLSL porque não se preocupam com a semântica.

Depois que os buffers de vértice foram preenchidos ou apontados para dados, este é o código de renderização:

shader->Bind();
shader->SetUniform(u1, GetWorldMatrix());
shader->SetUniform(u2, blah);
decl->Bind();
decl->SetStream(vb1, a1, a2);
decl->SetStream(vb2, a3);
decl->DrawPrimitives(VertexPrimitive::Triangle, NUM / 3);
decl->Unbind();
shader->Unbind();

Você vê que declé um pouco mais do que apenas uma declaração de vértice do tipo D3D, ela também se encarrega de renderizar. Isso faz algum sentido? O que seria um design mais limpo? Ou uma boa fonte de inspiração?


Qual versão do OpenGL você está alvejando?
Nicol Bolas

@NicolBolas a partir de agora eu uso o OpenGL 2.1 e o OpenGL ES 2.0 e planejo oferecer suporte ao OpenGL 3.3 ou 4.0, mas ainda não decidi se vou abandonar o suporte para versões anteriores. Meu problema atual é que eu também uso um subconjunto de OpenGL velho no PS3, que é abaixo do ideal, mas bastante conveniente ...
Sam Hocevar

Você provavelmente já estão cientes disso, mas confira fonte de Ogre para ver como eles fizeram isso ogre3d.org
Aralox

4
@Aralox: OGRE é uma bagunça infestada de Singleton e eu nunca, jamais, aconselharia alguém a seguir seu design.
DeadMG

Respostas:


8

Você está basicamente enfrentando o tipo de situação que torna o NVIDIA Cg um software tão atraente (além do fato de que ele não suporta o GL | ES, que você disse que está usando).

Observe também que você realmente não deve usar glGetAttribLocation. Essa função é ruim desde os dias iniciais do GLSL, antes que o pessoal encarregado do GL realmente começasse a entender como uma boa linguagem de sombreamento deveria funcionar. Ele não foi descontinuado, pois tem uso ocasional, mas, em geral, prefere glBindAttibLocation ou a extensão explícita de localização de atributos (principal no GL 3.3+).

Lidar com as diferenças nas linguagens de sombreador é de longe a parte mais difícil de portar software entre GL e D3D. Os problemas de API que você está enfrentando em relação à definição de layout de vértice também podem ser vistos apenas como um problema de linguagem de sombreador, pois as versões GLSL anteriores à 3.30 não suportam a localização explícita do atributo (semelhante em espírito à semântica de atributos no HLSL) e versões GLSL anteriores 4.10 O iirc não suporta ligações uniformes explícitas.

A melhor "abordagem" é ter uma biblioteca de linguagem de sombreamento de alto nível e formato de dados que encapsule seus pacotes de sombreador. NÃO basta alimentar um monte de GLSL / HLSL bruto para uma classe Shader fina e esperar poder criar qualquer tipo de API sã.

Em vez disso, coloque seus shaders em um arquivo. Envolva-os em um pouco de metadados. Você pode usar XML e escrever pacotes shader como:

<shader name="bloom">
  <profile type="glsl" version="1.30">
    <source type="vertex"><![CDATA[
      glsl vertex shader code goes here
    ]]></source>
    <source type="fragment"><![CDATA[
      glsl fragment shader code goes here
    ]]></source>
  </profile>
  <profile type="hlsl" version="sm3">
    <source type="fx"><![CDATA[
      hlsl effects code goes here
      you could also split up the source elements for hlsl
    ]]></source>
  </profile>
</shader>

Escrever um analisador mínimo para isso é trivial (basta usar o TinyXML, por exemplo). Deixe sua biblioteca de sombreadores carregar esse pacote, selecione o perfil apropriado para seu renderizador de destino atual e compile os sombreadores.

Observe também que, se preferir, você pode manter a fonte externa à definição de sombreador, mas ainda assim possui o arquivo. Basta colocar nomes de arquivos em vez de fonte nos elementos de origem. Isso pode ser benéfico se você planeja pré-compilar shaders, por exemplo.

A parte difícil agora, é claro, é lidar com o GLSL e suas deficiências. O problema é que você precisa vincular os locais dos atributos a algo semelhante à semântica do HLSL. Isso pode ser feito definindo essas semânticas em sua API e usando glBindAttribLocation antes de vincular o perfil GLSL. Sua estrutura de pacotes de sombreador pode lidar com isso explicitamente, sem a necessidade de sua API de gráficos expor os detalhes.

Você pode fazer isso estendendo o formato XML acima com alguns novos elementos no perfil GLSL para especificar explicitamente os locais dos atributos, por exemplo

<shader name="bloom">
  <profile type="glsl" version="1.30">
    <attrib name="inPosition" semantic="POSITION"/>
    <attrib name="inColor" semantic="COLOR0"/>
    <source type="vertex"><![CDATA[
      #version 150
      in vec4 inPosition;
      in vec4 inColor;

      out vec4 vColor;

      void main() {
        vColor = inColor;
        gl_Position = position;
      }
    ]]></source>
  </profile>
</shader>

O código do pacote do shader lê todos os elementos de atributo no XML, pega o nome e a semântica a partir deles, consulta o índice de atributo predefinido para cada semântica e, em seguida, chama automaticamente glBindAttribLocation para você ao vincular o shader.

O resultado final é que sua API agora pode parecer muito melhor do que provavelmente seu código GL antigo já pareceu, e até um pouco mais limpo do que o D3D11 permitiria:

// simple example, easily improved
VertexLayout layout = api->createLayout();
layout.bind(gfx::POSITION, buffer0, gfx::FLOATx4, sizeof(Vertex), offsetof(Vertex, position));
layout.bind(gfx::COLOR0, buffer0, gfx::UBYTEx4, sizeof(Vertex), offsetof(Vertex, color));

Observe também que você não precisa estritamente do formato do pacote shader. Se você quiser manter as coisas simples, você está livre para ter apenas um tipo de função loadShader (const char * name) que pega automaticamente os arquivos GLSL name.vs e name.fs GLSL no modo GL e os compila e vincula. No entanto, você absolutamente desejará os metadados desse atributo. No caso simples, você pode aumentar seu código GLSL com comentários especiais fáceis de analisar, como:

#version 150

/// ATTRIB(inPosition,POSITION)
in vec4 inPosition;
/// ATTRIB(inColor,COLOR0)
in vec4 inColor;

out vec4 vColor

void main() {
  vColor = inColor;
  gl_Position = inPosition;
}

Você pode se sentir mais à vontade na análise de comentários. Mais do que alguns mecanismos profissionais chegarão ao ponto de criar pequenas extensões de linguagem que eles analisam e modificam, inclusive, como simplesmente adicionar declarações semânticas no estilo HLSL. Se seu conhecimento de análise for robusto, você poderá encontrar com segurança essas declarações estendidas, extrair informações adicionais e substituir o texto pelo código compatível com GLSL.

Não importa como você faz isso, a versão curta é aumentar seu GLSL com as informações semânticas dos atributos ausentes e fazer com que a abstração do shader loader lide com glBindAttribLocation para corrigir as coisas e torná-las mais parecidas com as versões modernas e fáceis e eficientes do GLSL e do HLSL.


Obrigado por uma resposta extremamente abrangente. A sugestão adicional sobre comentários semânticos é simples, mas faz muito sentido!
Sam Hocevar

Finalmente estou aceitando sua resposta, mesmo que outros tenham se mostrado muito úteis. Passei muito tempo pensando em como fazê-lo corretamente e acabei escrevendo um analisador GLSL / HLSL completo que me ajuda a emular a localização explícita de atributos quando não é suportada.
Sam Hocevar

5

Em primeiro lugar, sugiro usar VertexBuffer<T>para melhorar a segurança do tipo, mas em segundo lugar, acho que as diferenças entre as duas APIs são demais nesse nível. Pessoalmente, eu encapsularia totalmente os renderizadores por trás de uma interface que não lida com coisas como declarações de vértices ou configuração de atributos de sombreador.


Destacado; atualmente, sua camada de abstração está em um nível muito baixo e precisa ser maior para realmente poder lidar com as diferenças da API.
Maximus Minimus

2

Pessoalmente, eu estabeleceria (e aplicaria) uma convenção padronizada para índices de atributos. O índice GL 0 é a posição. O índice GL 1 é a cor. O índice 2 é normal, com 3 e 4 para tangentes e binormais (se necessário). Os índices 5-7 são coordenadas de textura. Talvez 8 e 9 sejam para pesos ósseos. 10 pode ser uma segunda cor, se necessário. Se você não conseguir usar o GL_ARB_explicit_attrib_locationGL 3.3+, também deverá estabelecer uma nomeação de atributo padronizada convenção .

Dessa forma, o D3D possui convenções e o OpenGL possui convenções. Portanto, o usuário nem precisa perguntar qual é o índice de uma "posição"; eles sabem que é 0. E sua abstração sabe que 0 significa, na terra D3D VertexUsage::Position,.

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.