Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

My rendering code has always been OpenGL. I now need to support a platform that does not have OpenGL, so I have to add an abstraction layer that wraps OpenGL and Direct3D 9. I will support Direct3D 11 later.

TL;DR: the differences between OpenGL and Direct3D cause redundancy for the programmer, and the data layout feels flaky.

For now, my API works a bit like this. This is how a shader is created:

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

There is already a problem here: once a Direct3D shader is compiled, there is no way to query an input attribute by its name; apparently only the semantics stay meaningful. This is why GetAttribLocation has these extra arguments, which get hidden in ShaderAttrib.

Now this is how I create a vertex declaration and two vertex buffers:

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

Another problem: the information VertexUsage::Position, 0 is totally useless to the OpenGL/GLSL backend because it does not care about semantics.

Once the vertex buffers have been filled with or pointed at data, this is the 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();

You see that decl is a bit more than just a D3D-like vertex declaration, it kinda takes care of rendering as well. Does this make sense at all? What would be a cleaner design? Or a good source of inspiration?

share|improve this question
    
What version of OpenGL are you targeting? –  Nicol Bolas Apr 15 '12 at 8:31
    
@NicolBolas as of now I use OpenGL 2.1 and OpenGL ES 2.0 and I plan to support OpenGL 3.3 or 4.0, but I have not decided whether I will drop support for previous versions. My current problem is that I also use a subset of old OpenGL on the PS3, which is suboptimal but rather convenient… –  Sam Hocevar Apr 15 '12 at 8:47
    
You are probably already aware of this, but check out Ogre's source to see how they did it ogre3d.org –  Aralox Apr 15 '12 at 11:17
4  
@Aralox: OGRE is a Singleton-infested mess and I would never, ever, advise anyone to follow their design. –  DeadMG Apr 15 '12 at 22:56

3 Answers 3

up vote 8 down vote accepted

You're basically running into the kind of situation that makes NVIDIA Cg such an attractive piece of software (aside from the fact that it doesn't support GL|ES, which you said you're using).

Also note that you really shouldn't use glGetAttribLocation. That function is bad juju from the initial days of GLSL before the folks in charge of GL really started to grok how a good shading language should work. It's not deprecated as it has the occasional use, but in general, prefer glBindAttibLocation or the explicit attribute location extension (core in GL 3.3+).

Dealing with the differences in shader languages is by far the hardest part of porting software between GL and D3D. The API issues you're running into regarding vertex layout definition can also just be looked at as a shader language problem, as GLSL versions before 3.30 don't support explicit attribute location (similar in spirit to attribute semantics in HLSL) and GLSL versions before 4.10 iirc don't support explicit uniform bindings.

The "best" approach is to have a high-level shading language library and data format that encapsulates your shader packages. Do NOT simply feed in a bunch of raw GLSL/HLSL to a thin Shader class and expect to be able to come up with any kind of sane API.

Instead, put your shaders out into a file. Wrap them up in a bit of meta-data. You could use XML, and write shader packages like:

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

Writing a minimal parser for that is trivial (just use TinyXML for instance). Let your shader library load up that package, select the proper profile for your current target renderer, and compile the shaders.

Also note that if you prefer, you can keep the source external to the shader definition, but still have the file. Just put file names instead of source into the source elements. This may be beneficial if you plan on precompiling shaders, for instance.

The hard part now of course is dealing with GLSL and its deficiencies. The problem is that you need to binding attribute locations to something akin to HLSL semantics. This can be done by defining those semantics in your API and then using glBindAttribLocation before linking the GLSL profile. Your shader package framework can handle this explicitly, with absolutely no need for your graphics API to expose the details.

You can do that by extending the above XML format with some new elements in the GLSL profile to explicitly specify attribute locations, e.g.

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

Your shader package code would read in all the attrib elements in the XML, grab the name and the semantic from them, look up the pre-defined attribute index for each semantic, and then automatically call glBindAttribLocation for you when linking the shader.

The end result is that your API can now feel much nicer than your old GL code likely ever looked, and even a bit cleaner than D3D11 would allow:

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

Also note that you don't strictly need the shader package format. If you want to keep things simple, you're free to just have a loadShader(const char* name) kind of function that automatically grabs the name.vs and name.fs GLSL files in GL mode and compiles and links them. However, you're absolutely going to want that attribute metadata. In the simple case, you can augment your GLSL code with special easy-to-parse comments, like:

#version 150

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

out vec4 vColor

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

You can get as fancy as you're comfortable with in the comment parsing. More than a few professional engines will go so far as to make minor language extensions that they parse out and modify, even, such as just outright adding HLSL style semantic declarations. If your knowledge of parsing is robust, you should be able to reliably find those extended declarations, extract the extra information, and then replace the text with the GLSL compatible code.

No matter how you do it, the short version is to augment your GLSL with the missing attribute semantic information and have your shader loader abstraction deal with calling glBindAttribLocation to fix things up and make them more like the easy and efficient modern GLSL versions and HLSL.

share|improve this answer
    
Thanks for an extremely comprehensive answer. The additional suggestion about semantic comments is simple yet makes a lot sense! –  Sam Hocevar Apr 20 '12 at 11:49
    
I am finally accepting your answer, even if others have proved very helpful. I spent a lot of time pondering how to do it properly, and I ended up writing a full GLSL / HLSL parser that helps me emulate explicit attribute location when not supported. –  Sam Hocevar Jan 17 '13 at 13:48

Firstly, I'd suggest using VertexBuffer<T> to improve type safety, but secondly, I think the differences between the two APIs are too much at this level. I personally would fully encapsulate the renderers behind an interface which does not deal with things like vertex declarations or setting shader attributes.

share|improve this answer
    
Seconded; your abstraction layer is currently at too low a level and needs to be higher in order to really be able to cope with API differences. –  Jimmy Shelter Apr 15 '12 at 22:53

Personally, I'd establish (and enforce) a standardized convention for attribute indices. GL index 0 is the position. GL index 1 is the color. Index 2 is a normal, with 3 and 4 for tangents and binormals (if needed). Index 5-7 are texture coordinates. Maybe 8 and 9 are for bone weights. 10 can be a second color if needs be. If you're not able to use GL_ARB_explicit_attrib_location or GL 3.3+, then you should also establish a standardized attribute naming convention.

That way, D3D has conventions and OpenGL has conventions. So the user doesn't even have to ask what the index of a "position" is; they know it's 0. And your abstraction knows that 0 means, in D3D land, VertexUsage::Position.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.