Tutorial 53: Shader Uniforms and EffectParameter

CNA Tutorials  ·  Advanced Shaders

ℹ

What you’ll learn

  • What an EffectParameter is, which effects populate it, and how to fetch one by name.
  • The SetValue() overloads, including arrays.
  • Why the order of setting parameters relative to Apply matters — and how it differs between stock, compiled and ShaderEffect effects.

Before you start — Tutorial 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 2D-only renderer (SDL_RENDERER) throws on 3D calls.

⚠

ShaderEffect uniforms do not go through EffectParameter. That CNAEXT type exposes SetUniformXxx() instead. In contrast, the renderer-qualified compiled XNA Effect Framework path (11 of 14 renderer identities) does reflect parameters, techniques and passes into the ordinary Effect collections — that is where EffectParameter is the real interface. Stock effects expose typed properties plus a fixed set of parameters that mostly mirror those properties. 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. Defining CNA_STRICT_XNA_API turns any use of it into a [[deprecated]] warning, which becomes a compile error under -Werror=deprecated-declarations — 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: desktop GLSL on OPENGL33, GLSL ES on the ES/WebGL profiles, SPIR-V on VULKAN and SDL_GPU, WGSL on WEBGPU, HLSL compiled at runtime on DIRECTX9/DIRECTX11, MSL on METAL (through SpriteBatch only; a 3D draw with one throws), FNA3D hands back an invalid effect, and SOFTWARE and HEADLESS accept it and never execute 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
SetUniformVec3Array(name, const float*, count)vec3[] — count is the number of vec3s (count * 3 floats)
SetUniformMat4Array(name, const float*, count)mat4[], column-major — count matrices (count * 16 floats); the "name[0]" spelling also works
SetTexture(int unit, Texture2D&)sampler2D on an extra unit
SetTexture(int unit, TextureCube&)samplerCube on an extra unit
SetTexture(int unit, Texture3D&)sampler3D on an extra unit

Note the mismatch between the array methods: SetUniformFloatArray counts scalars, while the vec2, vec3 and mat4 setters count elements. A bone palette goes through SetUniformMat4Array (flatten the matrices to column-major floats with Matrix::ToColumnMajor). There is still no vec4-array setter and no mat3 setter.

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 to that
// program; applying first is the portable order (the GL renderers also make the
// program current inside each setter).
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

std::vector<float> palette(72 * 16);                     // 72 column-major matrices
// ... boneMatrices[i].ToColumnMajor(&palette[i * 16]) for each bone ...
fx_->SetUniformMat4Array("u_bones", palette.data(), 72);
⚠

Call Apply() first. Uniform setters write to the effect's shader program. On the EasyGL identities each setter makes its own program current first (so the alpha.1-era failure, values landing in whichever program happened to be bound, no longer happens there), but other renderers were not checked: call Apply(), then set uniforms, then draw.

The string constructor does not throw when the shader fails to compile. Check IsEffectValid() once after construction, treat false as a hard error, and read GetCompileErrorEXT() (or GetShaderDiagnosticsEXT()) for the reason. The typed ShaderCodeEXT/ShaderPackageEXT constructors (CNA_CNAEXT=ON) throw instead.

What EffectParameter actually is

EffectParameter is CNA's port of XNA's parameter-reflection surface, and there are three different situations behind it:

  • Stock effects build a fixed parameter list at construction time. BasicEffect registers Texture, DiffuseColor, EmissiveColor, SpecularColor, SpecularPower, the three directional lights, EyePosition, the fog parameters, World, WorldInverseTranspose, WorldViewProj and ShaderIndex; AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect and the PBR effects register theirs. On these effects the parameters are mostly outputs: the effect writes them from its own property fields when it is applied, and draws read the fields, not the parameters — so a manual SetValue on, say, DiffuseColor is overwritten or unused. Prefer the typed properties (setDiffuseColorProperty()) and use the parameters to inspect.
  • Compiled XNA effects (Effect(device, bytes) or an XNB Effect, on the 11 capable renderer identities) reflect a complete graph — parameters, array elements, structure members, annotations, techniques and passes — into the same collections, and their parameter values are genuine storage uploaded at EffectPass::Apply() when dirty. This is the real EffectParameter use case.
  • ShaderEffect has no parameter collection: nothing populates it. Use SetUniformXxx().

Nothing is built from GLSL reflection.

Getting parameters by name

The collection is reached through getParametersProperty(), and both its string subscript and its integer subscript return a pointer, not a reference (this is XNA's null-when-absent behaviour, and a source-breaking change since alpha.1, where the integer subscript returned 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 pointer too (nullptr when out of range).
EffectParameter* first = effect->getParametersProperty()[0];
if (first != nullptr) {
    std::cout << first->getNameProperty() << "\n";
}

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 World, View, Projection, MVP. Stored as an XNA (row-major) Matrix; the renderer converts it on upload.
bool bool Feature toggles inside the shader (fog enabled, vertex colour enabled).
Texture2D* sampler2D Stores the texture on the parameter; the renderer binds it when the pass is applied. (On a non-compiled effect SetValue only stores the value — nothing is bound or uploaded until the effect is applied.)
TextureCube* samplerCube Stores a cubemap on the parameter, bound when the pass is applied.
int int Integer values (compiled effects keep them in their own storage).
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 registered at construction time: a stock effect's fixed list, or the full reflected graph of a compiled XNA effect. It is empty on any effect that does not build one — including every ShaderEffect.

Reflecting a compiled effect

A compiled effect gives you techniques, passes, parameters, array elements, structure members and annotations to walk. This is the part of EffectParameter that has no SetUniformXxx() equivalent (see Tutorial 128 for loading one):

for (auto& technique : effect->getTechniquesProperty()) {
    std::cout << "technique " << technique.getNameProperty() << "\n";
    for (auto& pass : technique.getPassesProperty()) {
        std::cout << "  pass " << pass.getNameProperty() << "\n";
        for (auto& annotation : pass.getAnnotationsProperty())
            std::cout << "    annotation " << annotation.getNameProperty() << "\n";
    }
}
for (auto& parameter : effect->getParametersProperty()) {
    if (parameter.getElementsProperty().getCountProperty() > 0) {
        // an array parameter: each element is itself an EffectParameter
    }
    for (auto& member : parameter.getStructureMembersProperty()) {
        // members of a struct-typed parameter
    }
}

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

For a bone palette, SetUniformMat4Array takes the flattened column-major floats directly:

std::vector<float> palette(boneMatrices.size() * 16);
for (std::size_t i = 0; i < boneMatrices.size(); ++i)
    boneMatrices[i].ToColumnMajor(&palette[i * 16]);
fx_->SetUniformMat4Array("u_bones", palette.data(), static_cast<int>(boneMatrices.size()));

The count must not exceed the array size declared in the shader. Uploading fewer elements than the declared size is legal on GL — 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 habits, and mixing them up is the most common way to get a shader that silently renders with stale values on a renderer that does not paper over it.

On a stock effect (and on a compiled effect), state is marked 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 to the effect's own program, so call Apply() first (this is the portable order; the GL renderers tolerate the other one):

// 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.

The two shaders below are GLSL ES 3.00 (#version 300 es), the dialect of OPENGLES3 and WEBGL2; on OPENGL33 change the version line to #version 330 core (and drop the precision lines), and other renderers need their own dialect (Tutorial 52).

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 <iostream>
#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;
    Matrix                        view_, proj_;

    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; say why.
            std::cerr << waveEffect_->GetCompileErrorEXT() << "\n";
        }

        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_ = static_cast<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)
                   * view_
                   * proj_;               // your own camera matrices (Tutorial 34)
        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_);
        // No gd.Present(): Game presents in EndDraw().
    }
};

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.