4 Hugues Ross - Blog: DFGame - Rendering Improvements
Hugues Ross

3/27/17

DFGame - Rendering Improvements

One of my main goals for DFGame is to make development fast and convenient. Some people might consider something like this to be impossible with a low-level language like C. However, there are a number of tricks that I've been using to improve the simplicity of DFGame. To see how this works, I'm going to compare and contrast the graphics code from before I started working to now.

Friendly warning: The contents of this post are going to get a bit technical. If you aren't fascinated by APIs, you may not be interested.

Shaders and Meshes

The first thing that we'll compare is how to create shaders and meshes.
The old method is simple, but not great. Both asset types can be loaded from files, but if you want to include any shaders in your code then you need to write them as strings. To help you visualize, here's an example from the old version of the code:

static const char* quad_fs[] =
{
    "#version 330\n"
    "uniform sampler2D texture;\n"
    "uniform vec4 color;\n"
    "in  vec2 v_uv;\n"
    "layout(location = 0) out vec4 f_color;\n"
    "void main() {\n"
    "f_color = texture2D(texture, v_uv) * color;\n"
    "}\n"
};
That's more or less how it looks. There's no syntax highlighting, and every line has to be quoted and end with a "\n".
To create a new mesh from code, you specify the number of vertices and the type of data that you intend to store. The result looks a little like this:
create_mesh(vertex_count, VT_POSITION | VT_TEXTURE);
Not too shabby, but we can do better.

Next, let's see how it's handled now!
The first big difference is that I no longer need to put shaders in my code in order to compile them in. After discovering a code generation feature in Meson, I wrote a small program to generate C headers from shaders. The resulting headers go to the trouble of embedding the code for me, and I can simply include them in the code wherever I wish. I also made the generator write macros so that I can make a single call to get the compiled shader, ready for use.
Mesh creation has changed a little as well. Instead of initializing a buffer with the components that I specify, I've made a simple generic macro (another very neat thing that I learned a few months ago) that can infer what components you want based on the data that you pass in. To extend this I'm planning on making it possible to add/replace data in the mesh even if the data is in the wrong format, since I can figure out what's inside at compile-time. As you can see below, the result is a little more readable.

mesh_new(vertex_count, data);
As you can see, I've taken some decent code and made it even quicker and easier to work with. Changes like these are happening all over the codebase, so I have high hopes for the resulting framework.

One unfortunate thing to mention is that the updated version of dfgame can't load resources from files yet. In many cases, I'd load my meshes and shaders from elsewhere, but I can't compare how that will look until I get to it later down the line...

Binding Data

Now that we've looked at how meshes and shaders are created, let's look at how binding data to shaders works. In order to render a mesh, we need to bind the mesh being rendered as well as any additional data (textures, transformations, etc). Generally, making binding simpler also simplifies drawing code in general.

In the old version, there was a bind function for each type. Aside from textures, every function was more or less the same-it just called a different OpenGL function to bind the data. To try and get around this, I wrote a bunch of rendering functions that would bind things for the user. These would always bind to the same variable names, however.

When deciding how to update this part of the code, I decided to try and give a decent balance of simplicity and control. The rendering functions are no longer present (for now) but I've wrapped the binding functions in another generic macro. Now, I can bind almost anything I want with the same call instead of having to specify the type.

There remains one problem with this approach, however: Since meshes can have multiple types of data in their vertices, they need to potentially be bound to multiple different shader inputs. This wasn't an issue before, since variable names were assumed when binding. To get around this, I made my mesh binding function variadic (that is, I allow a variable number of arguments). The user can simply pass all of the names and data types that they care about in a single function call, resulting in something like this:

shader_bind_attribute_mesh(shader, mesh, "position", VT_POSITION, "color", VT_COLOR);
This is pretty simple, and if I wanted to I could make a name-less version that used the old assumed names.

Conclusion

There are still many parts of dfgame to review, and I'm certain that there are many other places where the API could use a helping hand. The few parts that I've touched have already benefited immensely, and I'm excited to see where things go from here! These posts will hopefully be a little less dry once I get out of the graphics code, but until then hang in there!
Post a Comment