¿Cuál es una mejor capa de abstracción para la gestión de datos de vértices D3D9 y OpenGL?

8

Mi código de representación siempre ha sido OpenGL. Ahora necesito admitir una plataforma que no tenga OpenGL, así que tengo que agregar una capa de abstracción que envuelva OpenGL y Direct3D 9. Soportaré Direct3D 11 más adelante.

TL; DR: las diferencias entre OpenGL y Direct3D causan redundancia para el programador, y el diseño de los datos parece escaso.

Por ahora, mi API funciona un poco así. Así es como se crea un sombreador:

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

Ya hay un problema aquí: una vez que se compila un sombreador Direct3D, no hay forma de consultar un atributo de entrada por su nombre; aparentemente, la semántica sigue siendo significativa. Es por eso que GetAttribLocationtiene estos argumentos adicionales, que se ocultan ShaderAttrib.

Ahora, así es como creo una declaración de vértice y dos búferes de vértices:

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

Otro problema: la información VertexUsage::Position, 0es totalmente inútil para el backend de OpenGL / GLSL porque no le importa la semántica.

Una vez que los búferes de vértices se han llenado o señalado con datos, este es el código de representación:

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

Verás que decles un poco más que una simple declaración de vértice tipo D3D, también se encarga de renderizar. ¿Tiene esto algún sentido? ¿Cuál sería un diseño más limpio? ¿O una buena fuente de inspiración?

sam hocevar
fuente
¿A qué versión de OpenGL estás apuntando?
Nicol Bolas
@NicolBolas a partir de ahora uso OpenGL 2.1 y OpenGL ES 2.0 y planeo admitir OpenGL 3.3 o 4.0, pero no he decidido si dejaré de admitir versiones anteriores. Mi problema actual es que también utilizo un subconjunto de edad OpenGL en la PS3, que es subóptima, sino más bien conveniente ...
Sam Hocevar
Probablemente ya lo sepa, pero consulte la fuente de Ogre para ver cómo lo hicieron ogre3d.org
Aralox
44
@Aralox: OGRE es un desastre infestado de Singleton y nunca, nunca recomendaría a nadie que siga su diseño.
DeadMG

Respuestas:

8

Básicamente, se encuentra con el tipo de situación que hace que NVIDIA Cg sea una pieza de software tan atractiva (aparte del hecho de que no es compatible con GL | ES, que usted dijo que estaba usando).

También tenga en cuenta que realmente no debe usar glGetAttribLocation. Esa función es mala juju desde los primeros días de GLSL antes de que la gente a cargo de GL realmente comenzara a entender cómo debería funcionar un buen lenguaje de sombreado. No está en desuso, ya que tiene un uso ocasional, pero en general, prefiere glBindAttibLocation o la extensión de ubicación de atributo explícita (núcleo en GL 3.3+).

Lidiar con las diferencias en los lenguajes de sombreado es, con mucho, la parte más difícil de portar software entre GL y D3D. Los problemas de API con los que se encuentra en relación con la definición de diseño de vértices también pueden considerarse como un problema de lenguaje de sombreador, ya que las versiones GLSL anteriores a la 3.30 no admiten la ubicación explícita de atributos (similar en espíritu a la semántica de atributos en HLSL) y las versiones GLSL anteriores 4.10 iirc no admite enlaces uniformes explícitos.

El "mejor" enfoque es tener una biblioteca de lenguaje de sombreado de alto nivel y un formato de datos que encapsule sus paquetes de sombreadores. NO alimente simplemente un montón de GLSL / HLSL sin procesar a una clase de Shader delgada y espere poder crear cualquier tipo de API sana.

En su lugar, coloca tus sombreadores en un archivo. Envuélvalos en un poco de metadatos. Podría usar XML y escribir paquetes de sombreadores 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>

Escribir un analizador mínimo para eso es trivial (solo use TinyXML por ejemplo). Deje que su biblioteca de sombreadores cargue ese paquete, seleccione el perfil adecuado para su renderizador de destino actual y compile los sombreadores.

También tenga en cuenta que si lo prefiere, puede mantener la fuente externa a la definición del sombreador, pero aún tiene el archivo. Simplemente ponga nombres de archivo en lugar de fuente en los elementos fuente. Esto puede ser beneficioso si planea precompilar sombreadores, por ejemplo.

La parte difícil ahora, por supuesto, es lidiar con GLSL y sus deficiencias. El problema es que necesita vincular ubicaciones de atributos a algo similar a la semántica de HLSL. Esto se puede hacer definiendo esas semánticas en su API y luego usando glBindAttribLocation antes de vincular el perfil GLSL. El marco del paquete de sombreadores puede manejar esto explícitamente, sin ninguna necesidad de que su API de gráficos exponga los detalles.

Puede hacerlo extendiendo el formato XML anterior con algunos elementos nuevos en el perfil GLSL para especificar explícitamente las ubicaciones de los atributos, p. Ej.

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

El código del paquete de sombreador leería en todos los elementos de atributo en el XML, tomaría el nombre y la semántica de ellos, buscaría el índice de atributo predefinido para cada semántica y luego llamaría automáticamente glBindAttribLocation por usted al vincular el sombreador.

El resultado final es que su API ahora puede sentirse mucho mejor de lo que probablemente se haya visto su antiguo código GL, e incluso un poco más limpio de lo que D3D11 permitiría:

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

También tenga en cuenta que no necesita estrictamente el formato del paquete de sombreado. Si desea simplificar las cosas, tiene la libertad de tener un tipo de función loadShader (const char * name) que toma automáticamente los archivos GLSL name.vs y name.fs en modo GL y los compila y los vincula. Sin embargo, absolutamente querrá los metadatos de ese atributo. En el caso simple, puede aumentar su código GLSL con comentarios especiales fáciles de analizar, 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;
}

Puede ser tan elegante como se sienta cómodo con el análisis de comentarios. Más de unos pocos motores profesionales llegarán a hacer extensiones de lenguaje menores que analizarán y modificarán, incluso, simplemente agregando directamente declaraciones semánticas de estilo HLSL. Si su conocimiento sobre el análisis es sólido, debería poder encontrar de manera confiable esas declaraciones extendidas, extraer la información adicional y luego reemplazar el texto con el código compatible con GLSL.

No importa cómo lo haga, la versión corta es aumentar su GLSL con la información semántica del atributo faltante y hacer que su abstracción del cargador de sombreadores se ocupe de llamar a glBindAttribLocation para arreglar las cosas y hacer que se parezcan más a las versiones modernas y eficientes de GLSL y HLSL.

Sean Middleditch
fuente
Gracias por una respuesta extremadamente completa. ¡La sugerencia adicional sobre los comentarios semánticos es simple pero tiene mucho sentido!
sam hocevar
Finalmente acepto su respuesta, incluso si otros han demostrado ser muy útiles. Pasé mucho tiempo reflexionando sobre cómo hacerlo correctamente, y terminé escribiendo un analizador GLSL / HLSL completo que me ayuda a emular la ubicación explícita de los atributos cuando no es compatible.
sam hocevar
5

En primer lugar, sugeriría usar VertexBuffer<T>para mejorar la seguridad de tipos, pero en segundo lugar, creo que las diferencias entre las dos API son demasiado en este nivel. Personalmente encapsularía completamente los renderizadores detrás de una interfaz que no se ocupa de cosas como declaraciones de vértices o configuración de atributos de sombreador.

DeadMG
fuente
Secundada; su capa de abstracción se encuentra actualmente en un nivel demasiado bajo y debe ser mayor para poder realmente hacer frente a las diferencias de API.
Maximus Minimus
2

Personalmente, establecería (y aplicaría) una convención estandarizada para los índices de atributos. El índice GL 0 es la posición. El índice GL 1 es el color. El índice 2 es normal, con 3 y 4 para tangentes y binormales (si es necesario). Los índices 5-7 son coordenadas de textura. Tal vez 8 y 9 son para pesas óseas. 10 puede ser un segundo color si es necesario. Si no puede usar GL_ARB_explicit_attrib_locationo GL 3.3+, también debe establecer una convención de nomenclatura de atributos estandarizada .

De esa manera, D3D tiene convenciones y OpenGL tiene convenciones. Por lo tanto, el usuario ni siquiera tiene que preguntar cuál es el índice de una "posición"; saben que es 0. Y su abstracción sabe que 0 significa, en D3D tierra, VertexUsage::Position.

Nicol Bolas
fuente