Neither of your solutions are ideal.
First, realize that the glUniform*
calls are mostly a bad idea. Don't use them. Use Uniform Buffers instead. The buffers offer several primary benefits.
One benefit is that you can separate out big blocks of "constant" uniforms (any per-model or per-instance data) into its own uniform buffer and then only need to bind that buffer rather than needing to bind many individual uniforms. Another is that the buffer binding can also be shared across many shaders if necessary. Third, you can bulk upload related sets of uniform data.
In general, uniform buffer objects are much closer to how the actual hardware works, and will results in faster graphics pipelines. That's not a hard rule, mind, but a decent guideline; you're best off testing and measuring, of course.
Once you move to uniform buffers you might find yourself facing the same problem though slightly rephrased: what uniforms exist in your buffer, what are their types, and what are their offsets?
There are two main solutions here. The first is to standardize your buffers. Different shaders should use an identical layout for, say, your per-frame camera information (eye position, screen dimensions, projection matrices, etc.). Then your CPU-side code doesn't need to "detect" anything or handle different layouts. It just prepares the per-frame uniform buffer in a single hard-coded way and all the shader consume that buffer is a single ahem uniform way (use a shader preprocessor to just #include shader code that declares the uniform buffer, its binding location, and its layout properties). If you change the shader code, change the C++ code to match.
That technique can take you surprisingly far. You're likely to find that the vast majority of your shaders use the same sets of inputs from the native code. Hardcode those and enforce that shaders consume the data in the format your CPU code prepares. This essentially reverses your original problem: instead of CPU code being dependent on shader layouts, the shader layout is dependent on CPU code. The shader preprocessor keeps the code in sync. (Depending on the shader language, you might even be able to share the exact same .h
file between your shader and your CPU code.)
For content-driven shaders with per-model uniform attributes, the above may or may not work. In these cases, though, you might note two things: (1) the runtime code never modifies these values so the buffer contents only matter to the shader and the editor, and (2) since these values are per-model you probably want them in a separate buffer from your per-instance or per-frame values so you can make minimal rebinds and uploads.
The editor doesn't need to be particularly speedy. You can store your uniforms in whatever format you want, so long as you bake them down into the proper bag of bytes for export to the shader. One approach here is to use metadata to describe the uniform values of the model, something like:
struct ShaderUniformMetadata {
char* data = nullptr;
EType* uniformTypes = nullptr;
uint16_t* uniformOffsets = 0;
uint16_t size = 0;
uint16_t uniformCount = 0;
};
The metadata contains information about the available uniforms and their types. To read/write a uniform, memcpy
from/to data + uniformOffsets[index]
, using the value in uniformTypes[index]
to know the correct size.
To calculate the size of the buffer, sum up the appropriate sizes/alignments from the uniforms. An algorithm can pack the values appropriately to get the minimal size while maintaining the necessary alignment for each input.
To actually create the buffer at runtime, just load whole blob from your game data file and then pass the size
and data
values to glBufferStorage
. Now you just bind that buffer before rendering that model.
Your per-instance buffers are the hard-coded layouts so there's not a lot of special magic to do there.
any
is completely ok. Evenstd::aligned_storage_t<>
should be good enough. \$\endgroup\$