Tutorial 53: Shader Uniforms and EffectParameter

CNA Tutorials  ·  Advanced Shaders

What you’ll learn

  • What an EffectParameter is and how to fetch one by name.
  • The SetValue() overloads, including arrays.
  • Why the order of setting parameters relative to Apply matters.

Before you startTutorial 52: Writing Custom Shaders (ShaderEffect) — parameters are how you feed the shader written there. Requires a 3D-capable renderer such as OPENGLES3 or VULKAN; the thirteen 2D-only renderers (SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, SKIA, BLEND2D, FREEDIRECT, DIRECTX1, GDI, SVG_DOM, OPENVG, NANOVG, PIXIJS) throw on 3D calls.

ShaderEffect uniforms do not go through EffectParameter. That CNAEXT type exposes SetUniformXxx() instead. In contrast, alpha.1's renderer-qualified compiled XNA Effect Framework path does reflect parameters, techniques and passes into the ordinary Effect collections. Stock effects also expose typed properties and parameters. Choose the model that matches the effect type; do not treat all custom shaders as one interchangeable format.

ShaderEffect is CNAEXT — a CNA extension, not part of the XNA 4.0 API. Its header carries the CNAEXT marker on the class and on nearly every method, and code written against it will not port back to XNA or MonoGame unchanged. Building with CNA_STRICT_XNA_API turns any use of it into a compile error, which is the intended way to prove an XNA-pure port.

The uniform names on this page are GLSL because GLSL is what the GL family expects, but the language is a property of the selected renderer, not of ShaderEffect: SPIR-V on VULKAN and SDL_GPU, HLSL compiled at runtime on DIRECTX9/DIRECTX11/DIRECTX12. GDI, METAL, HTML_DOM and SVG_DOM throw on a custom shader, FNA3D cannot compile one at all, and BGFX accepts it and silently ignores it. The SetUniformXxx() calls below are the same everywhere; what they feed is not. See Tutorial 52.

Setting uniforms on a ShaderEffect

A ShaderEffect is constructed from vertex and fragment shader source text — three arguments, and neither string is a file path. Its uniforms are then set by name through direct methods, each taking a const char*:

MethodGLSL uniform type
SetUniformFloat(name, float)float
SetUniformInt(name, int)int (also explicit sampler-unit binding)
SetUniformVec2(name, x, y)vec2
SetUniformVec3(name, x, y, z)vec3
SetUniformVec4(name, x, y, z, w)vec4
SetUniformMat4(name, const float*)mat4, column-major
SetUniformFloatArray(name, const float*, count)float[]count is the number of scalar elements
SetUniformVec2Array(name, const float*, count)vec2[]count is the number of vec2s, so the buffer holds count * 2 floats
SetTexture(int unit, Texture2D&)sampler2D on an extra unit
SetTexture(int unit, TextureCube&)samplerCube on an extra unit

Note the mismatch between the two array methods: SetUniformFloatArray counts scalars, SetUniformVec2Array counts elements. There is no vec3, vec4 or mat4 array setter — a bone palette has to be flattened and pushed through SetUniformFloatArray, or set one matrix at a time with indexed names such as "u_bones[3]".

There is also no Vector2/Vector3/Matrix overload: these take loose floats and raw pointers, so unpack CNA's math types yourself.

// Apply() binds this effect's compiled program. SetUniformXxx() writes into
// whatever program is currently bound, so this order is mandatory.
fx_->Apply();

fx_->SetUniformFloat("u_time", elapsed);
fx_->SetUniformVec2 ("u_resolution", 800.0f, 600.0f);
fx_->SetUniformVec3 ("u_lightDir", dir.X, dir.Y, dir.Z);
float mvpCM[16];
mvp.ToColumnMajor(mvpCM);                     // SetUniformMat4 wants column-major
fx_->SetUniformMat4("u_mvp", mvpCM);
fx_->SetTexture(1, *normalMapTex_);           // unit 0 is usually the caller's

float weights[16] = { /* ... */ };
fx_->SetUniformFloatArray("u_weights", weights, 16);   // 16 scalars

Call order matters. Uniform setters write directly to the currently bound shader program. Call Apply() first, then set uniforms, then draw. Setting uniforms before Apply() writes them into whichever program happened to be bound.

The constructor does not throw when the shader fails to compile. IsEffectValid() is the only signal — check it once after construction and treat false as a hard error.

What EffectParameter actually is

EffectParameter is CNA's port of XNA's parameter-reflection surface, and it is populated by the stock effects that build a fixed parameter list at construction time — AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect and the PBR effects. It is not built from GLSL reflection, and nothing populates it for a ShaderEffect or a bare BasicEffect. Use it to inspect or drive a stock effect the XNA way; use SetUniformXxx() for anything you wrote yourself.

Getting parameters by name

The collection is reached through getParametersProperty(), and its string subscript returns a pointer, not a reference:

// Returns EffectParameter*, or nullptr if no such parameter exists.
EffectParameter* p1 = effect->getParametersProperty()["DiffuseColor"];
if (p1 != nullptr) {
    p1->SetValue(Vector3(1.0f, 0.5f, 0.25f));
}

// The int subscript returns a reference, by position.
EffectParameter& first = effect->getParametersProperty()[0];

There is no GetByName method, and a missing name does not throw — you get nullptr, so a typo in the name is a silent no-op unless you check. The one lookup helper that does exist is GetParameterBySemantic(const std::string&), which also returns a pointer or nullptr.

Pointers into the collection stay valid across later Add() calls — the elements are held behind std::unique_ptr precisely so that caching a pointer is safe — but only for the lifetime of the parent effect. Do not hold them across an effect reload.

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.
Quaternion vec4 Rotation as a quaternion.
std::string String-valued parameter; no GLSL equivalent, present for XNA parity.
std::vector<T> T[N] Array upload. There is a std::vector overload for every scalar and vector type above, plus std::vector<Matrix>.

Note that arrays are passed as std::vector, not as a pointer-plus-count pair — there is no SetValue(const float*, int) or SetValue(const Matrix*, int) overload. Matrices additionally have SetValueTranspose(const Matrix&) and SetValueTranspose(const std::vector<Matrix>&) when the shader expects the opposite storage order.

Texture note: the texture overloads take a pointer (Texture*, Texture2D*, Texture3D*, TextureCube*). Passing a dereferenced texture — SetValue(*myTexture) — does not compile against any overload.

EffectParameterCollection

getParametersProperty() returns an EffectParameterCollection, 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->getParametersProperty()) {
    std::cout << "  uniform: " << param.getNameProperty()
              << "  type: "    << (int)param.getParameterTypeProperty()
              << "  rows: "    << param.getRowCountProperty()
              << "  cols: "    << param.getColumnCountProperty()
              << "\n";
}

Each EffectParameter exposes the following metadata properties:

  • getNameProperty() — the string name of the uniform as declared in GLSL.
  • getParameterTypeProperty() — an EffectParameterType enum value (Single, Vector2, Matrix, Texture, etc.).
  • getRowCountProperty() — number of rows (1 for scalars and vectors, 4 for mat4).
  • getColumnCountProperty() — number of columns (1 for scalars, 4 for vec4 and mat4).

The collection contains exactly the parameters the concrete effect class registered at construction time. It is not built by reflecting the compiled shader, so it is empty on any effect that does not build one — including every ShaderEffect.

Passing Arrays

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 a stock effect's EffectParameter, arrays go in as a std::vector:

std::vector<float> weights(16);
// ... fill weights ...
if (auto* p = effect->getParametersProperty()["u_weights"]) {
    p->SetValue(weights);
}

std::vector<Matrix> boneMatrices(72, Matrix::getIdentityProperty());
// ... populate matrices ...
if (auto* p = effect->getParametersProperty()["u_bones"]) {
    p->SetValue(boneMatrices);
}

On a ShaderEffect the equivalent is SetUniformFloatArray, whose count is the number of scalar floats:

fx_->Apply();
fx_->SetUniformFloatArray("u_weights", weights.data(), 16);   // 16 floats

There is no matrix-array setter on ShaderEffect, so a bone palette must be flattened to column-major floats and pushed through SetUniformFloatArray, or written one element at a time using indexed uniform names.

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.

Apply ordering

The two effect families have opposite ordering rules, and mixing them up is the most common way to get a shader that silently renders with stale values.

On a stock effect, SetValue() marks state dirty and the pass upload happens inside Apply(), so set values first:

// Stock effect: set all values, then apply once. obj.Tint is a Vector3.
for (auto& obj : scene_) {
    basicFx_->setWorldProperty(obj.World);
    basicFx_->setDiffuseColorProperty(obj.Tint);
    basicFx_->Apply();
    gd.DrawPrimitives(PrimitiveType::TriangleList, 0, obj.TriCount);
}

On a ShaderEffect, SetUniformXxx() writes straight to the currently bound program, so Apply() must come first:

// ShaderEffect: apply (binds the program), then set uniforms, then draw.
for (auto& obj : scene_) {
    fx_->setWorldProperty(obj.World);       // IEffectMatrices, forwarded by the device
    fx_->Apply();
    fx_->SetUniformVec4("u_color", obj.Tint.X, obj.Tint.Y, obj.Tint.Z, 1.0f);
    gd.DrawPrimitives(PrimitiveType::TriangleList, 0, obj.TriCount);
}

ShaderEffect implements IEffectMatrices, so World, View and Projection are set as properties rather than as uniforms; CNA forwards them to the renderer, which binds them as uniforms of exactly those names on your program.

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

The shader source is read straight off disk and handed to the ShaderEffect constructor. There are no parameter handles to cache — uniform names are passed as string literals at the point of use.

#include "Microsoft/Xna/Framework/Graphics/ShaderEffect.hpp"
#include "System/IO/File.hpp"

class WaveGame final : public Game {
    std::unique_ptr<ShaderEffect> waveEffect_;
    Texture2D     waveTex_;
    std::unique_ptr<VertexBuffer> vb_;
    int                           vertCount_ = 0;
    float                         time_      = 0.0f;

    void LoadContent() override {
        auto& gd = getGraphicsDeviceProperty();

        // Three arguments; the two strings are shader SOURCE, never a path.
        waveEffect_ = std::make_unique<ShaderEffect>(
            gd,
            System::IO::File::ReadAllText("Content/shaders/wave.vert.glsl"),
            System::IO::File::ReadAllText("Content/shaders/wave.frag.glsl"));

        // The constructor does not throw on a compile failure.
        if (!waveEffect_->IsEffectValid()) {
            // The GLSL did not compile. Do not draw with it.
        }

        waveTex_ = getContentProperty().Load<Texture2D>("textures/grid");

        // Build a subdivided grid mesh ...
        BuildGrid(32, 32);
    }

    void Update(GameTime& gt) override {
        // Cache the value; uniforms are pushed in Draw(), after Apply().
        time_ = (float)gt.getTotalGameTimeProperty().getTotalSecondsProperty();
    }

    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();
        float mvpCM[16];
        mvp.ToColumnMajor(mvpCM);

        // Apply() binds the program; every SetUniformXxx() below targets it.
        waveEffect_->Apply();
        waveEffect_->SetUniformFloat("u_time", time_);
        waveEffect_->SetUniformVec2 ("u_resolution", 800.0f, 600.0f);
        waveEffect_->SetUniformMat4 ("u_mvp", mvpCM);
        waveEffect_->SetTexture(0, waveTex_);

        gd.SetVertexBuffer(vb_.get());
        gd.DrawPrimitives(PrimitiveType::TriangleList, 0, vertCount_);
        gd.Present();
    }
};

The vertex shader above declares u_mvp rather than the World/View/Projection trio that IEffectMatrices binds automatically, so the matrix is uploaded by hand. If you name your uniforms World, View and Projection instead, set them with setWorldProperty()/setViewProperty()/setProjectionProperty() and CNA forwards them for you — see Tutorial 52.