Over the past month I have been working on revamping the rendering system to being something that's actually usable, mainly making it easy to add new shaders and modify them over time, eventually without having to restart the game for a shader modification. My previous approach was the "OpenGL 2.x" style of using GetUniformLocation() to upload data. This ends up requiring a lot of specialized code for each shader you load. Newer OpenGL versions offer some shader reflection which might have made this less painful, but I think I've been able to transcend most of that by uploading all of the discrete uniforms as a block for each shader. I've accomplished this by defining a struct/UBO for each shader, in a file which gets compiled as both C++ and GLSL. Here's a bit of that file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //shader_uniforms.h #ifdef CPP #define layout(a,b) #define mat4 matrix4 #define vec2 v2 #define vec3 v3 #define vec4 v4 #define uniform struct #endif #define YAM_TEXTURE0_LOCATION 16 layout(std140, binding=0) uniform global_uniforms { mat4 View; mat4 Projection; }; #ifdef SIMPLE_TEXTURED_RECT_SHADER layout(std140, binding=1) uniform simple_textured_rect_uniforms { mat4 Model; vec4 Color; vec4 UVRect; }; #endif // ... another struct per shader |
So now on the game side, I can do something like
1 2 3 4 5 6 7 | //game_rendering.cpp simple_textured_rect_uniforms Uniforms = {}; Uniforms.Model = Model; Uniforms.Color = v4_WHITE; Uniforms.UVRect = RectMinDim(0,0,1,1); UseShader(ShaderHandle, &Uniforms); |
Very easy! This feels a lot like having a .h and a .cpp, but for shaders.
One snag in this is that samplers (AKA textures) cannot be a part of a UBO. So in each shader that uses textures I make sure to force the textures to bind to a YAM_TEXTUREXXX_LOCATION so I know where to bind to them on the game side. So after UseShader I have a UseTexture call.
This system does make shader compilation tricky because I have to cobble the shader together, with a preamble of
1 | #define MY_SHADER |
then the global_uniforms file,
then the actual shader file.
The define ensures that ONLY the UBO for that shader is compiled in.
This isn't too bad though, I'll take complexity in the start up code over having it in the runtime code.
A disclaimer, this wasn't by invention, I first ran into this style of data management in RenderDoc and decided to give it a try for myself.