Tutorial 53: GLSL Uniforms and EffectParameter
Custom Effect objects with EffectParameter require the EASYGL or VULKAN backend. The SDL_RENDERER backend does not support programmable shaders.
The EffectParameter Class
In CNA, an Effect represents a compiled GLSL shader program. Each uniform variable declared in the GLSL source has a corresponding EffectParameter on the C++ side. The EffectParameter is the bridge between your game logic and the GPU uniform slot — calling SetValue() on it schedules the upload so that when the effect's pass is applied, the uniform is correctly set in the shader program.
Every Effect in CNA is backed by a .shader.json descriptor file. This file declares the vertex and fragment shader source paths, the technique name, pass names, and the list of uniform names the effect exposes. During loading, CNA's effect system reflects the uniform locations from the compiled GLSL program and creates an EffectParameter entry for each one, storing it in the effect's EffectParameterCollection.
Parameters are owned by the effect. You retrieve them by name, store the reference if you access them frequently, and call SetValue() each frame (or whenever the value changes) before calling pass.Apply(). The lifetime of an EffectParameter reference is tied to its parent effect — do not hold references across effect reloads.
Getting Parameters with GetByName
There are two syntactically equivalent ways to retrieve an EffectParameter from an effect:
// Subscript operator (preferred for brevity)
EffectParameter& p1 = effect->Parameters["u_time"];
// Explicit method (useful when the name is in a variable)
std::string name = "u_resolution";
EffectParameter& p2 = effect->Parameters.GetByName(name);
Both return a reference. Accessing a parameter that does not exist in the shader will throw a std::out_of_range exception in debug builds and exhibit undefined behaviour in release builds, so always ensure the uniform name matches exactly — including case — what is declared in the GLSL source. A common convention is to prefix all uniforms with u_ to make them visually distinct from varyings and attributes.
For performance-sensitive code you can cache the parameter reference in a member variable. Because EffectParameter is returned by reference into the internal collection, caching is safe as long as the effect object is not destroyed:
// Cache at LoadContent time
timeParam_ = &waveEffect_->Parameters["u_time"];
resolutionParam_ = &waveEffect_->Parameters["u_resolution"];
// Use in Update() without string lookup overhead
timeParam_->SetValue(elapsed);
resolutionParam_->SetValue(Vector2(800.0f, 600.0f));
SetValue() Overloads
The EffectParameter::SetValue() method is overloaded for every GLSL-compatible type that CNA supports. The correct overload is selected by the C++ type of the argument you pass. The table below lists each overload, the corresponding GLSL uniform type, and notes on usage:
| C++ argument type | GLSL uniform type | Notes |
|---|---|---|
float | float | Most common scalar uniform (time, intensity, threshold). |
Vector2 | vec2 | Screen resolution, UV offset, 2D position. |
Vector3 | vec3 | World-space positions, RGB colour values, light direction. |
Vector4 | vec4 | RGBA colour, quaternion, homogeneous position. |
Matrix | mat4 | Uploaded column-major (the GLSL default). World, View, Projection, MVP. |
bool | bool | Feature toggles inside the shader (fog enabled, vertex colour enabled). |
Texture2D* | sampler2D | Binds the texture to the next available texture unit and sets the sampler uniform to that unit index. |
TextureCube* | samplerCube | Binds a cubemap to a texture unit and sets the sampler uniform. |
int | int | Integer uniforms; also used for explicit sampler unit binding. |
float*, int | float[N] | Uploads an array of floats. Used for bone matrices (as flat float arrays) and custom kernel weights. |
Vector4*, int | vec4[N] | Uploads an array of vec4, e.g., a palette of colours or skinning data. |
Matrix*, int | mat4[N] | Uploads an array of matrices. Essential for skeletal animation bone palettes. |
Matrix column-major note: CNA's Matrix is stored row-major in memory (matching XNA 4.0 convention) but is uploaded to GLSL as column-major using glUniformMatrix4fv(..., GL_TRUE, ...) with the transpose flag so the shader receives the correct layout. You do not need to transpose manually.
EffectParameterCollection
The effect->Parameters property exposes an EffectParameterCollection, which is an iterable container of all EffectParameter objects belonging to the effect. You can iterate over it to inspect or dump all parameters at runtime — useful during debugging:
// Dump all parameter names for debugging
for (const auto& param : effect->Parameters) {
std::cout << " uniform: " << param.getName()
<< " type: " << (int)param.getParameterType()
<< " rows: " << param.getRowCount()
<< " cols: " << param.getColumnCount()
<< "\n";
}
Each EffectParameter exposes the following metadata properties:
getName()— the string name of the uniform as declared in GLSL.getParameterType()— anEffectParameterTypeenum value (Single,Vector2,Matrix,Texture, etc.).getRowCount()— number of rows (1 for scalars and vectors, 4 for mat4).getColumnCount()— number of columns (1 for scalars, 4 for vec4 and mat4).
The collection size reflects only the uniforms declared in the .shader.json descriptor, not all uniforms that may be present in the GLSL source. Uniforms not listed in the descriptor are not reflected and cannot be set from C++. Keep your descriptor in sync with your shader source.
Passing Arrays
CNA provides SetValue(const float* values, int count) and SetValue(const Matrix* matrices, int count) overloads for uploading arrays to GLSL array uniforms. The GLSL side declares a fixed-size array:
// In vertex shader
uniform float u_weights[16]; // e.g. blur kernel weights
uniform mat4 u_bones[72]; // bone matrix palette for skinning
On the C++ side you pass a pointer and an element count:
float weights[16] = { /* ... */ };
effect->Parameters["u_weights"].SetValue(weights, 16);
std::vector<Matrix> boneMatrices(72, Matrix::Identity);
// ... populate matrices ...
effect->Parameters["u_bones"].SetValue(boneMatrices.data(), (int)boneMatrices.size());
The count must not exceed the array size declared in GLSL. Uploading fewer elements than the declared size is legal — the remaining array slots keep their last-set values. This is commonly used in skeletal animation where the active bone count is less than the maximum palette size of 72.
ConstantBuffer / UBO Patterns
Under the EASYGL backend, uniforms may be grouped into a Uniform Buffer Object (UBO) per effect pass, depending on how the .shader.json specifies the constant buffer layout. UBOs allow the driver to batch uniform uploads more efficiently than per-uniform glUniform* calls. Under the VULKAN backend, small uniform data (up to the push constant limit, typically 128 bytes) may be submitted as push constants, which avoids descriptor set allocation overhead entirely.
As a consequence of these batching strategies, the rule is: call all SetValue() calls before calling pass.Apply(). Calling SetValue() after Apply() within the same draw call is legal but may cause a second UBO upload, which is wasteful. The recommended pattern is:
// Good pattern: set all values, then apply once
effect->Parameters["u_time"].SetValue(t);
effect->Parameters["u_resolution"].SetValue(res);
effect->Parameters["u_mvp"].SetValue(mvp);
for (auto& pass : effect->getCurrentTechnique().Passes) {
pass.Apply(); // uploads all pending uniform values
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, vertCount_);
}
Avoid calling SetValue() inside a tight loop between individual draw calls without also calling Apply() — each Apply() flushes the accumulated dirty uniform state. If multiple objects share the same effect with different parameter values, update and apply per object:
for (auto& obj : scene_) {
effect->Parameters["u_mvp"].SetValue(obj.MVP);
effect->Parameters["u_color"].SetValue(obj.Color);
for (auto& pass : effect->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, obj.TriCount);
}
}
Complete Example: Wave Vertex Animation
The following example demonstrates a custom effect that animates vertex positions in the vertex shader using a sine-wave displacement driven by a time uniform. The fragment shader outputs a colour that shifts over time, demonstrating multiple uniform types in action.
GLSL Vertex Shader (wave.vert)
#version 300 es
precision highp float;
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec2 a_texcoord;
uniform float u_time;
uniform vec2 u_resolution;
uniform mat4 u_mvp;
out vec2 v_texcoord;
void main() {
vec3 pos = a_position;
// Displace Y by a sine wave driven by X position and time
pos.y += sin(pos.x * 4.0 + u_time * 2.0) * 0.1;
gl_Position = u_mvp * vec4(pos, 1.0);
v_texcoord = a_texcoord;
}
GLSL Fragment Shader (wave.frag)
#version 300 es
precision highp float;
in vec2 v_texcoord;
uniform float u_time;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
vec4 texColor = texture(u_texture, v_texcoord);
// Modulate green channel with a slow pulse
float pulse = 0.5 + 0.5 * sin(u_time * 1.5);
fragColor = vec4(texColor.r, texColor.g * pulse, texColor.b, texColor.a);
}
C++ Game Class
class WaveGame final : public Game {
std::unique_ptr<Effect> waveEffect_;
std::unique_ptr<Texture2D> waveTex_;
std::unique_ptr<VertexBuffer> vb_;
int vertCount_ = 0;
// Cached parameter pointers for fast access
EffectParameter* timeParam_ = nullptr;
EffectParameter* resolutionParam_ = nullptr;
EffectParameter* mvpParam_ = nullptr;
EffectParameter* textureParam_ = nullptr;
void LoadContent() override {
waveEffect_ = Content.Load<Effect>("shaders/wave");
waveTex_ = Content.Load<Texture2D>("textures/grid");
// Cache parameter references (valid for the lifetime of waveEffect_)
timeParam_ = &waveEffect_->Parameters["u_time"];
resolutionParam_ = &waveEffect_->Parameters["u_resolution"];
mvpParam_ = &waveEffect_->Parameters["u_mvp"];
textureParam_ = &waveEffect_->Parameters["u_texture"];
// Bind the texture parameter once (does not change per frame)
textureParam_->SetValue(waveTex_.get());
// Build a subdivided grid mesh ...
BuildGrid(32, 32);
}
void Update(const GameTime& gt) override {
float t = (float)gt.getTotalGameTime().TotalSeconds();
timeParam_->SetValue(t);
resolutionParam_->SetValue(Vector2(800.0f, 600.0f));
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::Black);
Matrix mvp = Matrix::CreateRotationY(0.3f)
* Matrix::CreateTranslation(0, 0, -3.0f)
* camera_.View()
* camera_.Projection();
mvpParam_->SetValue(mvp);
gd.setVertexBuffer(*vb_);
for (auto& pass : waveEffect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, vertCount_);
}
gd.Present();
}
};