Linking shader programs using the builder pattern
This post is the first in a series on design patterns in game programming.
Design patterns play an important role in computer programming. Not every problem can be solved with a pattern, and not every pattern is useful in all circumstances. However, they can be powerful thinking tools when applied to the right kinds of problem and help us understand and design solutions quickly and without reinventing the wheel every time. Similarly, they can aid us in communicating our ideas efficiently to others.
Today, I would like to take a look at the builder pattern.
I will not go into the formal definition of the pattern itself – there are enough other sources for that. Instead we will look at just one example where I use the builder pattern in my C# OpenGL graphics library.
I will first show how I solved the problem in question previously, point out what I did not like about that solution, and then show how we can apply the builder pattern to a much nicer way of doing ultimately the same thing.
The original code
The problem that we are trying to solve is that of linking individual OpenGL shader objects into shader programs.
In my introduction to OpenGL in C# I showed how we can manually create shaders from shader code, as well as how to link multiple shaders into a usable shader program.
The code to do this – once we set up classes to represent shaders and programs like I did in that post – could look like this:
var vertexShader = new Shader(ShaderType.VertexShader, vertexShaderCode);
var fragmentShader = new Shader(ShaderType.FragmentShader, fragmentShaderCode);
var shaderProgram = new ShaderProgram(vertexShader, fragmentShader);
Simple enough.
But is it really?
For a small example this works well enough. But if our program becomes bigger, and starts using a lot of different shaders this might become quite a mess -especially if we start reusing shaders for different programs, and add more types next to vertex and fragment shaders.
Automating shader loading
To avoid the bulk of the spaghetti code that would result out of this, we will have to load our shaders automatically. This is not what we will look at here however.
Instead we will assume we have already done so, and that all our shaders are now known to a ‘ShaderManager’ object by a string id – possibly the shader code’s file name, if we loaded them from files. We will also assume that the manager knows the type of each shader, and can contain shaders with the same name as long as they are of different types.
Effectively this means that we have the following method available:
public Shader GetShader(ShaderType type, string name)
With this we can rewrite our code above as follows:
var vertexShader = shaderMan.GetShader(ShaderType.VertexShader, "myShader");
var fragmentShader = shaderMan.GetShader(ShaderType.FragmentShader, "myShader");
var shaderProgram = new ShaderProgram(vertexShader, fragmentShader);
Is this better? It does not seem so.
However, while this code has not changed a whole lot we do no longer have to think about how we are creating shaders. We simple ask for the shader we want and the manager takes care of the details.
That is also the idea of what we will do below:
We want to abstract away the specifics of how our shader program is made and have the underlying system figure out what to do itself.
Introducing the builder pattern
Instead of asking the shader manager for shaders, and handing them to a shader program constructor, we will now introduce the builder pattern to do this job for us.
We will create a new builder class that will do all the heavy lifting for us.
Here is a start on this:
class ShaderProgramBuilder
{
private readonly ShaderManager shaderMan;
private readonly List<Shader> shaders = new List<Shader>();
public ShaderProgramBuilder(ShaderManager shaderMan)
{
this.shaderMan = shaderMan;
}
public void AddVertexShader(string id)
{
this.shaders.Add(shaderMan.GetShader(ShaderType.VertexShader, id));
}
public void AddFragmentShader(string id)
{
this.shaders.Add(shaderMan.GetShader(ShaderType.FragmentShader, id));
}
public ShaderProgram Build()
{
return new ShaderProgram(this.shaders);
}
}
This is a fairly standard implementation of the builder pattern, adapted to our requirements.
If we were to use the class, creating a shader program would now look like this:
var builder = new ShaderProgramBuilder(shaderMan);
builder.AddVertexShader("myShader");
builder.AddFragmentShader("myShader");
var shaderProgram = builder.Build();
This is already pretty good.
The builder pattern serves its goal to hide the details of how exactly shader programs are created. Instead, we only have to deal with the information we really care about: What shaders we want our shader program to consist of.
Another advantage is that we can build our program step by step, without keeping track of anything except the builder object itself, which can come in very handy should we want to automate this part of our content pipeline as well.
However, the code is still very verbose, and not necessarily more readable than before – though this may be somewhat subjective.
Method chaining
This is why we will use method chaining to help us out. Method chaining is simply the technique of calling methods right on the return value of a previous method, resulting in chained calls of multiple method making up a single statement.
This of course means we have to modify our builder methods to actually have return values: In this case, since we want to call further methods on the builder itself, it will simple return itself as well.
public ShaderProgramBuilder AddVertexShader(string id)
{
this.shaders.Add(shaderMan.GetShader(ShaderType.VertexShader, id));
return this;
}
public ShaderProgramBuilder AddFragmentShader(string id)
{
this.shaders.Add(shaderMan.GetShader(ShaderType.FragmentShader, id));
return this;
}
With this very simple modification, our code now looks like this:
var shaderProgram = new ShaderProgramBuilder(shaderMan)
.AddVertexShader("myShader")
.AddFragmentShader("myShader")
.Build();
This is much more readable. We are essentially down to the bare bones of what can possibly be abstracted.
Domain specific tricks
There are however a few more things we can do that are specific to our example.
For instance, there is no reason we should create the builder object by ourselves. Instead we can have the shader manager do so for us, to abstract things even further (this is especially useful since the builder needs the shader manager to do its job).
While we are at it, we could also give our created shader program a name as well, so we can easily refer to it in the future.
Without going into the implementation (this is not too complicated) this could then be used as follows – also note that I renamed the builder methods slightly, so that the statement reads more like a regular sentence.
var shaderProgram = shaderMan.BuildShaderProgram()
.WithVertexShader("myShader")
.WithFragmentShader("myShader")
.As("myShaderProgram");
var theSameShaderProgram = shaderMan.GetProgram("MyShaderProgram");
Another things we can do is combine the two lines adding shaders into a single one that adds all shaders of different types that have the same name:
var shaderProgram = shaderMan.BuildShaderProgram()
.TryAllShaders("myShader")
.As("myShaderProgram");
This may not work for everybody, but the way I name my shader code files this works out very well.
- You can find a full implementation of everything we discussed – and more – on GitHub as part of my graphics library.
Conclusion
I hope this has given you an insight into a possible use of the builder pattern in game development.
Make sure to come back next week, when I will write more about how I load and manage shaders – the part of this post that we just took for granted.
Of course, feel free to leave a comment in case you have any questions, or if you know of other interesting applications of the builder pattern in game programming.
Enjoy the pixels!
Reference: | Linking shader programs using the builder pattern from our NCG partner Paul Scharf at the GameDev<T> blog. |