I am trying to make a 2D game engine using OpenGL ES 2.0 (iOS for now). I've written Application layer in Objective C and a separate self contained RendererGLES20 in C++. No GL specific call is made outside the renderer. It is working perfectly.
But I have some design issues when using shaders. Each shader has its own unique attributes and uniforms that need to be set just before the main draw call (glDrawArrays in this case). For instance, in order to draw some geometry I would do:
void RendererGLES20::render(Model * model)
{
// Set a bunch of uniforms
glUniformMatrix4fv(.......);
// Enable specific attributes, can be many
glEnableVertexAttribArray(......);
// Set a bunch of vertex attribute pointers:
glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, m->pCoords);
// Now actually Draw the geometry
glDrawArrays(GL_TRIANGLES, 0, m->vertexCount);
// After drawing, disable any vertex attributes:
glDisableVertexAttribArray(.......);
}
As you can see this code is extremely rigid. If I were to use another shader, say ripple effect, i would be needing to pass extra uniforms, vertex attribs etc. In other words I would have to change the RendererGLES20 render source code just to incorporate the new shader.
Is there any way to make the shader object totally generic? Like What if I just want to change the shader object and not worry about game source re-compiling? Any way to make the renderer agnostic of uniforms and attributes etc?. Even though we need to pass data to uniforms, what is the best place to do that? Model class? Is the model class aware of shader specific uniforms and attributes?
Following shows Actor class:
class Actor : public ISceneNode
{
ModelController * model;
AIController * AI;
};
Model controller class: class ModelController { class IShader * shader; int textureId; vec4 tint; float alpha; struct Vertex * vertexArray; };
Shader class just contains the shader object, compiling and linking sub-routines etc.
In Game Logic class I am actually rendering the object:
void GameLogic::update(float dt)
{
IRenderer * renderer = g_application->GetRenderer();
Actor * a = GetActor(id);
renderer->render(a->model);
}
Please note that even though Actor extends ISceneNode, I haven't started implementing SceneGraph yet. I will do that as soon as I resolve this issue.
Any ideas how to improve this? Related design patterns etc?
Thank you for reading the question.
Answer
It's possible to make your shader system more data-driven, so that you don't have so much shader-specific code laying around for uniforms and vertex formats, but rather set them programmatically based on metadata attached to the shaders.
First the disclaimer: a data-driven system can make it easier to add new shaders, but on the other hand it comes with costs in terms of the increased complexity of the system, which makes it harder to maintain and debug. So it's a good idea to think carefully about just how much data-drivenness will be good for you (for a small project, the answer might well be "none"), and don't try to build a system that's overly generalized.
Okay, let's talk about vertex formats (attributes) first. You can create a data description by making a struct that contains the data to pass to glVertexAttribPointer
- the index, type, size, etc. of a single attribute - and having an array of those structs to represent the whole vertex format. Given this information, you can programmatically set up all the GL state related to the vertex attributes.
Where does the data to populate this description come from? Conceptually, I think the cleanest way is to have it belong to the shader. When you build the vertex data for a mesh, you would look up which shader is used on the mesh, find the vertex format required by that shader, and build the vertex buffer accordingly. You just need some way for each shader to specify the vertex format it expects.
There are a variety of ways to do this; for instance, you could have a standard set of names for the attributes in the shader ("attrPosition", "attrNormal", etc.) plus some hard-coded rules like "position is 3 floats". Then you use glGetAttribLocation
or similiar to query which attributes the shader uses, and apply the rules to build the vertex format. Another way is to have an XML snippet defining the format, embedded in a comment in the shader source and extracted by your tools, or something along those lines.
For uniforms, if you can use OpenGL 3.1 or later it's a good idea to use uniform buffer objects (the OpenGL equivalent of D3D's constant buffers). Alas, GL ES 2.0 doesn't have those, so uniforms have to be handled individually. One way to do it would be to create a struct containing the uniform location for each parameter you want to set - the camera matrix, specular power, world matrix etc. Sampler locations could be in here too. This approach depends on there being a standard set of parameters shared across all shaders. Not every shader has to use every single parameter, but all parameters would have to be in this struct.
Each shader would have an instance of this struct, and when you load a shader you'd query it for the locations of all the parameters, using glGetUniformLocation
and standardized names. Then whenever you need to set a uniform from code, you can check if it's present in that shader, and just look up its location and set it.
No comments:
Post a Comment