Was ist eine bessere Abstraktionsschicht für die Verwaltung von D3D9- und OpenGL-Scheitelpunktdaten?

8

Mein Rendering-Code war immer OpenGL. Ich muss jetzt eine Plattform ohne OpenGL unterstützen, daher muss ich eine Abstraktionsschicht hinzufügen, die OpenGL und Direct3D 9 umschließt. Ich werde Direct3D 11 später unterstützen.

TL; DR: Die Unterschiede zwischen OpenGL und Direct3D führen zu Redundanz für den Programmierer, und das Datenlayout fühlt sich unzuverlässig an.

Im Moment funktioniert meine API ein bisschen so. So wird ein Shader erstellt:

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

Hier gibt es bereits ein Problem: Sobald ein Direct3D-Shader kompiliert ist, kann ein Eingabeattribut nicht mehr nach seinem Namen abgefragt werden. anscheinend bleibt nur die Semantik sinnvoll. Aus diesem Grund GetAttribLocationgibt es diese zusätzlichen Argumente, die versteckt werden ShaderAttrib.

So erstelle ich eine Scheitelpunktdeklaration und zwei Scheitelpunktpuffer:

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

Ein weiteres Problem: Die Informationen VertexUsage::Position, 0sind für das OpenGL / GLSL-Backend völlig nutzlos, da die Semantik keine Rolle spielt.

Sobald die Scheitelpunktpuffer mit Daten gefüllt wurden oder auf Daten zeigen, ist dies der Rendering-Code:

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

Sie sehen, das declist ein bisschen mehr als nur eine D3D-ähnliche Vertex-Deklaration, es kümmert sich auch um das Rendern. Ist das überhaupt sinnvoll? Was wäre ein saubereres Design? Oder eine gute Inspirationsquelle?

Sam Hocevar
quelle
Auf welche OpenGL-Version zielen Sie ab?
Nicol Bolas
@NicolBolas Ab sofort verwende ich OpenGL 2.1 und OpenGL ES 2.0 und ich plane, OpenGL 3.3 oder 4.0 zu unterstützen, aber ich habe nicht entschieden, ob ich die Unterstützung für frühere Versionen einstellen werde. Mein aktuelles Problem ist, dass ich auch eine Teilmenge des alten OpenGL auf der PS3 verwende, was suboptimal, aber ziemlich praktisch ist…
Sam Hocevar
Sie sind sich dessen wahrscheinlich bereits bewusst, aber sehen Sie in der Quelle von Ogre nach, wie sie es gemacht haben. Ogre3d.org
Aralox
4
@Aralox: OGRE ist ein von Singleton befallenes Chaos und ich würde niemals jemandem raten, seinem Design zu folgen.
DeadMG

Antworten:

8

Sie befinden sich im Grunde genommen in einer Situation, die NVIDIA Cg zu einer so attraktiven Software macht (abgesehen von der Tatsache, dass GL | ES, von dem Sie sagten, dass Sie es verwenden, nicht unterstützt wird).

Beachten Sie auch, dass Sie glGetAttribLocation wirklich nicht verwenden sollten. Diese Funktion ist ein schlechter Juju aus den ersten Tagen von GLSL, bevor die Verantwortlichen von GL wirklich anfingen zu überlegen, wie eine gute Schattierungssprache funktionieren sollte. Es ist nicht veraltet, da es gelegentlich verwendet wird. Bevorzugen Sie jedoch im Allgemeinen glBindAttibLocation oder die explizite Erweiterung des Attributstandorts (Kern in GL 3.3+).

Der Umgang mit den Unterschieden in den Shader-Sprachen ist bei weitem der schwierigste Teil der Portierungssoftware zwischen GL und D3D. Die API-Probleme, auf die Sie bei der Definition des Scheitelpunktlayouts stoßen, können auch nur als Shader-Sprachproblem angesehen werden, da GLSL-Versionen vor 3.30 keine explizite Attributposition (ähnlich der Attributsemantik in HLSL) und GLSL-Versionen zuvor unterstützen 4.10 iirc unterstützt keine expliziten einheitlichen Bindungen.

Der "beste" Ansatz besteht darin, eine übergeordnete Shading-Sprachbibliothek und ein Datenformat zu haben, die Ihre Shader-Pakete kapseln. Geben Sie NICHT einfach eine Menge rohes GLSL / HLSL in eine Thin Shader-Klasse ein und erwarten Sie, dass Sie eine vernünftige API entwickeln können.

Legen Sie stattdessen Ihre Shader in einer Datei ab. Wickeln Sie sie in ein paar Metadaten ein. Sie können XML verwenden und Shader-Pakete schreiben wie:

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

Das Schreiben eines minimalen Parsers dafür ist trivial (verwenden Sie zum Beispiel einfach TinyXML). Lassen Sie Ihre Shader-Bibliothek dieses Paket laden, wählen Sie das richtige Profil für Ihren aktuellen Ziel-Renderer aus und kompilieren Sie die Shader.

Beachten Sie auch, dass Sie die Quelle außerhalb der Shader-Definition behalten können, die Datei jedoch weiterhin vorhanden ist. Fügen Sie einfach Dateinamen anstelle von Quelle in die Quellelemente ein. Dies kann nützlich sein, wenn Sie beispielsweise Shader vorkompilieren möchten.

Der schwierige Teil ist jetzt natürlich die Behandlung von GLSL und seinen Mängeln. Das Problem ist, dass Sie Attributpositionen an etwas binden müssen, das der HLSL-Semantik ähnelt. Dies kann erreicht werden, indem Sie diese Semantik in Ihrer API definieren und dann glBindAttribLocation verwenden, bevor Sie das GLSL-Profil verknüpfen. Ihr Shader-Paket-Framework kann dies explizit verarbeiten, ohne dass Ihre Grafik-API die Details offenlegen muss.

Sie können dies tun, indem Sie das obige XML-Format um einige neue Elemente im GLSL-Profil erweitern, um Attributpositionen explizit anzugeben, z

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

Ihr Shader-Paketcode würde alle Attributelemente im XML einlesen, den Namen und die Semantik daraus abrufen, den vordefinierten Attributindex für jede Semantik nachschlagen und dann beim Verknüpfen des Shaders automatisch glBindAttribLocation für Sie aufrufen.

Das Endergebnis ist, dass sich Ihre API jetzt viel besser anfühlt, als Ihr alter GL-Code wahrscheinlich jemals ausgesehen hat, und sogar ein bisschen sauberer, als D3D11 es erlauben würde:

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

Beachten Sie auch, dass Sie das Shader-Paketformat nicht unbedingt benötigen . Wenn Sie die Dinge einfach halten möchten, können Sie nur eine Funktion loadShader (const char * name) verwenden, die die GLSL-Dateien name.vs und name.fs im GL-Modus automatisch erfasst und kompiliert und verknüpft. Sie werden jedoch unbedingt diese Attribut-Metadaten wollen. Im einfachen Fall können Sie Ihren GLSL-Code mit speziellen, einfach zu analysierenden Kommentaren ergänzen, wie z.

#version 150

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

out vec4 vColor

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

Sie können beim Kommentieren von Kommentaren so ausgefallen werden, wie Sie möchten. Nicht wenige professionelle Engines werden so weit gehen, dass sie kleinere Spracherweiterungen vornehmen, die sie analysieren und sogar modifizieren, z. B. das direkte Hinzufügen semantischer Deklarationen im HLSL-Stil. Wenn Ihre Kenntnisse über das Parsen solide sind, sollten Sie in der Lage sein, diese erweiterten Deklarationen zuverlässig zu finden, die zusätzlichen Informationen zu extrahieren und den Text dann durch den GLSL-kompatiblen Code zu ersetzen.

Unabhängig davon, wie Sie es tun, besteht die Kurzversion darin, Ihre GLSL mit den fehlenden Attribut-Semantikinformationen zu erweitern und Ihre Shader-Loader-Abstraktion mit dem Aufruf von glBindAttribLocation zu befassen, um Probleme zu beheben und sie den einfachen und effizienten modernen GLSL-Versionen und HLSL ähnlicher zu machen.

Sean Middleditch
quelle
Vielen Dank für eine äußerst umfassende Antwort. Der zusätzliche Vorschlag zu semantischen Kommentaren ist einfach, aber sehr sinnvoll!
Sam Hocevar
Ich akzeptiere endlich Ihre Antwort, auch wenn sich andere als sehr hilfreich erwiesen haben. Ich habe viel Zeit damit verbracht, darüber nachzudenken, wie man es richtig macht, und am Ende habe ich einen vollständigen GLSL / HLSL-Parser geschrieben, der mir hilft, den expliziten Attributspeicherort zu emulieren, wenn er nicht unterstützt wird.
Sam Hocevar
5

Erstens würde ich vorschlagen, die Typensicherheit VertexBuffer<T>zu verbessern, aber zweitens denke ich, dass die Unterschiede zwischen den beiden APIs auf dieser Ebene zu groß sind. Ich persönlich würde die Renderer hinter einer Schnittstelle vollständig kapseln, die sich nicht mit Dingen wie Vertex-Deklarationen oder dem Festlegen von Shader-Attributen befasst.

DeadMG
quelle
Abgeordnet; Ihre Abstraktionsschicht befindet sich derzeit auf einem zu niedrigen Niveau und muss höher sein, um API-Unterschiede wirklich bewältigen zu können.
Maximus Minimus
2

Persönlich würde ich eine standardisierte Konvention für Attributindizes festlegen (und durchsetzen). Der GL-Index 0 ist die Position. GL-Index 1 ist die Farbe. Index 2 ist normal, mit 3 und 4 für Tangenten und Binormale (falls erforderlich). Index 5-7 sind Texturkoordinaten. Vielleicht sind 8 und 9 für Knochengewichte. 10 kann bei Bedarf eine zweite Farbe sein. Wenn Sie GL_ARB_explicit_attrib_locationGL 3.3+ nicht verwenden können, sollten Sie auch eine standardisierte Namenskonvention für Attribute festlegen .

Auf diese Weise hat D3D Konventionen und OpenGL Konventionen. Der Benutzer muss also nicht einmal nach dem Index einer "Position" fragen. Sie wissen, dass es 0 ist. Und Ihre Abstraktion weiß, dass 0 im D3D-Land bedeutet VertexUsage::Position.

Nicol Bolas
quelle