Qu'est-ce qu'une meilleure couche d'abstraction pour la gestion des données de vertex D3D9 et OpenGL?

8

Mon code de rendu a toujours été OpenGL. Je dois maintenant prendre en charge une plate-forme qui n'a pas OpenGL, je dois donc ajouter une couche d'abstraction qui enveloppe OpenGL et Direct3D 9. Je prendrai en charge Direct3D 11 plus tard.

TL; DR: les différences entre OpenGL et Direct3D entraînent une redondance pour le programmeur, et la disposition des données semble floconneuse.

Pour l'instant, mon API fonctionne un peu comme ça. Voici comment un shader est créé:

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");

Il y a déjà un problème ici: une fois qu'un shader Direct3D est compilé, il n'y a aucun moyen d'interroger un attribut d'entrée par son nom; apparemment, seule la sémantique reste significative. C'est pourquoi GetAttribLocationa ces arguments supplémentaires, qui se cachent ShaderAttrib.

Maintenant, voici comment je crée une déclaration de sommet et deux tampons de sommet:

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

Autre problème: les informations VertexUsage::Position, 0sont totalement inutiles pour le backend OpenGL / GLSL car elles ne se soucient pas de la sémantique.

Une fois que les tampons de sommet ont été remplis ou pointés sur des données, voici le code de rendu:

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();

Vous voyez que declc'est un peu plus qu'une simple déclaration de sommet de type D3D, cela prend également en charge le rendu. Est-ce que cela a du sens? Quelle serait une conception plus propre? Ou une bonne source d'inspiration?

sam hocevar
la source
Quelle version d'OpenGL visez-vous?
Nicol Bolas
@NicolBolas à partir de maintenant, j'utilise OpenGL 2.1 et OpenGL ES 2.0 et je prévois de prendre en charge OpenGL 3.3 ou 4.0, mais je n'ai pas décidé si je supprimerai le support pour les versions précédentes. Mon problème actuel est que j'utilise également un sous-ensemble de vieux OpenGL sur la PS3, qui est sous-optimal mais plutôt pratique…
sam hocevar
Vous en êtes probablement déjà au courant, mais consultez la source d'Ogre pour voir comment ils l'ont fait ogre3d.org
Aralox
4
@Aralox: OGRE est un gâchis infesté de singleton et je ne conseillerais jamais à personne de suivre leur conception.
DeadMG

Réponses:

8

Vous rencontrez essentiellement le genre de situation qui fait de NVIDIA Cg un logiciel si attrayant (à part le fait qu'il ne prend pas en charge GL | ES, que vous avez dit utiliser).

Notez également que vous ne devez vraiment pas utiliser glGetAttribLocation. Cette fonction est un mauvais juju depuis les premiers jours de GLSL avant que les responsables de GL ne commencent vraiment à comprendre comment un bon langage d'ombrage devrait fonctionner. Ce n'est pas obsolète car il a une utilisation occasionnelle, mais en général, préférez glBindAttibLocation ou l'extension de localisation d'attribut explicite (noyau dans GL 3.3+).

Gérer les différences dans les langages de shaders est de loin la partie la plus difficile du portage de logiciels entre GL et D3D. Les problèmes d'API que vous rencontrez concernant la définition de la disposition des sommets peuvent également être considérés comme un problème de langage de shader, car les versions GLSL avant 3.30 ne prennent pas en charge l'emplacement explicite des attributs (similaire dans l'esprit à la sémantique des attributs dans HLSL) et les versions GLSL avant 4.10 iirc ne prend pas en charge les liaisons uniformes explicites.

La "meilleure" approche consiste à disposer d'une bibliothèque de langages d'ombrage de haut niveau et d'un format de données qui encapsule vos packages de shaders. Ne vous contentez PAS d'alimenter un groupe de GLSL / HLSL bruts vers une classe Shader mince et attendez-vous à pouvoir proposer n'importe quel type d'API sensée.

Au lieu de cela, mettez vos shaders dans un fichier. Enveloppez-les dans un peu de métadonnées. Vous pouvez utiliser XML et écrire des packages de shader comme:

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

Écrire un analyseur minimal pour cela est trivial (utilisez simplement TinyXML par exemple). Laissez votre bibliothèque de shaders charger ce package, sélectionnez le profil approprié pour votre rendu cible actuel et compilez les shaders.

Notez également que si vous préférez, vous pouvez conserver la source externe à la définition du shader, mais conserver le fichier. Mettez simplement les noms de fichiers au lieu de source dans les éléments source. Cela peut être avantageux si vous prévoyez de précompiler des shaders, par exemple.

La partie la plus difficile maintenant est bien sûr de traiter avec GLSL et ses lacunes. Le problème est que vous devez lier les emplacements d'attributs à quelque chose qui ressemble à la sémantique HLSL. Cela peut être fait en définissant cette sémantique dans votre API, puis en utilisant glBindAttribLocation avant de lier le profil GLSL. Votre cadre de package de shader peut gérer cela explicitement, sans que votre API graphique ait besoin d'exposer les détails.

Vous pouvez le faire en étendant le format XML ci-dessus avec de nouveaux éléments dans le profil GLSL pour spécifier explicitement les emplacements des attributs, par exemple

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

Le code de votre package de shader lirait tous les éléments attrib dans le XML, en saisirait le nom et la sémantique, rechercherait l'index d'attribut prédéfini pour chaque sémantique, puis appeler automatiquement glBindAttribLocation pour vous lors de la liaison du shader.

Le résultat final est que votre API peut maintenant se sentir beaucoup plus agréable que votre ancien code GL n'a probablement jamais semblé, et même un peu plus propre que D3D11 permettrait:

// 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));

Notez également que vous n'avez pas strictement besoin du format de package de shader. Si vous voulez garder les choses simples, vous êtes libre d'avoir juste une sorte de fonction loadShader (const char * name) qui saisit automatiquement les fichiers GLSL name.vs et name.fs en mode GL et les compile et les relie. Cependant, vous allez absolument vouloir ces métadonnées d'attribut. Dans le cas simple, vous pouvez augmenter votre code GLSL avec des commentaires spéciaux faciles à analyser, comme:

#version 150

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

out vec4 vColor

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

Vous pouvez obtenir autant de fantaisie que vous le souhaitez dans l'analyse des commentaires. Plus de quelques moteurs professionnels iront jusqu'à faire des extensions de langage mineures qu'ils analyseront et modifieront même, comme simplement ajouter carrément des déclarations sémantiques de style HLSL. Si votre connaissance de l'analyse est solide, vous devriez pouvoir trouver de manière fiable ces déclarations étendues, extraire les informations supplémentaires, puis remplacer le texte par le code compatible GLSL.

Peu importe comment vous le faites, la version courte consiste à augmenter votre GLSL avec les informations sémantiques d'attribut manquantes et à faire en sorte que votre abstraction de chargeur de shader traite l'appel à glBindAttribLocation pour corriger les choses et les rendre plus semblables aux versions GLSL et HLSL modernes et faciles à utiliser.

Sean Middleditch
la source
Merci pour une réponse extrêmement complète. La suggestion supplémentaire concernant les commentaires sémantiques est simple mais a beaucoup de sens!
sam hocevar
J'accepte enfin votre réponse, même si d'autres se sont révélés très utiles. J'ai passé beaucoup de temps à réfléchir à la façon de le faire correctement, et j'ai fini par écrire un analyseur GLSL / HLSL complet qui m'aide à émuler l'emplacement d'attribut explicite lorsqu'il n'est pas pris en charge.
sam hocevar
5

Premièrement, je suggère d'utiliser VertexBuffer<T>pour améliorer la sécurité des types, mais deuxièmement, je pense que les différences entre les deux API sont trop importantes à ce niveau. Personnellement, j'encapsulerais complètement les moteurs de rendu derrière une interface qui ne traite pas de choses comme les déclarations de vertex ou la définition d'attributs de shader.

DeadMG
la source
Appuyé; votre couche d'abstraction est actuellement à un niveau trop bas et doit être plus élevée pour vraiment pouvoir faire face aux différences d'API.
Maximus Minimus
2

Personnellement, j'établirais (et ferais respecter) une convention normalisée pour les indices d'attribut. L'indice GL 0 est la position. L'indice GL 1 est la couleur. L'index 2 est normal, avec 3 et 4 pour les tangentes et binormales (si nécessaire). Les index 5-7 sont des coordonnées de texture. Peut-être que 8 et 9 correspondent au poids des os. 10 peut être une deuxième couleur si besoin est. Si vous ne pouvez pas utiliser GL_ARB_explicit_attrib_locationou GL 3.3+, vous devez également établir une convention de dénomination d' attribut normalisée .

De cette façon, D3D a des conventions et OpenGL a des conventions. Ainsi, l'utilisateur n'a même pas à demander quel est l'indice d'une "position"; ils savent que c'est 0. Et votre abstraction sait que 0 signifie, dans la terre D3D, VertexUsage::Position.

Nicol Bolas
la source