Tutorial 52: Writing Custom Shaders (ShaderEffect)

CNA — C++ XNA 4.0 reimplementation

Backend availability. ShaderEffect is supported on the EASYGL (OpenGL ES 3.0 / OpenGL 3.0+) and VULKAN backends. It is not available on SDL_RENDERER or bgfx backends. Always check your CMakeLists.txt backend selection before using custom shaders.

ShaderEffect class

#include "Microsoft/Xna/Framework/Graphics/ShaderEffect.hpp"
using namespace Microsoft::Xna::Framework::Graphics;

// Load from a .shader.json descriptor in the content directory
auto effect = std::make_unique<ShaderEffect>(
    getGraphicsDeviceProperty(),
    "Content/shaders/tinted"); // no extension — ContentManager adds .shader.json

// Set uniform values
effect->Parameters["World"]->SetValue(worldMatrix);
effect->Parameters["View"]->SetValue(viewMatrix);
effect->Parameters["Projection"]->SetValue(projMatrix);
effect->Parameters["TintColor"]->SetValue(Vector4(1.0f, 0.5f, 0.0f, 1.0f));
effect->Parameters["MainTexture"]->SetValue(texture_.get());

// Bind vertex buffer, then apply + draw for each pass
gd.setVertexBuffer(*vb_);
for (auto& pass : effect->getCurrentTechnique().Passes) {
    pass.Apply(); // binds the GLSL program and uploads uniforms
    gd.DrawPrimitives(PrimitiveType::TriangleList, 0, primitiveCount_);
}

.shader.json descriptor

The descriptor lives in your Content/shaders/ directory. It names the vertex and fragment shader source files and declares the uniforms your C++ code will set.

// Content/shaders/tinted.shader.json
{
    "name": "TintedShader",
    "techniques": [
        {
            "name": "Default",
            "passes": [
                {
                    "name": "Pass0",
                    "vertexShader":   "shaders/tinted_vert.glsl",
                    "fragmentShader": "shaders/tinted_frag.glsl",
                    "vulkanVertexSpirv":   "shaders/tinted_vert.spv",
                    "vulkanFragmentSpirv": "shaders/tinted_frag.spv"
                }
            ]
        }
    ],
    "parameters": [
        { "name": "World",        "type": "Matrix" },
        { "name": "View",         "type": "Matrix" },
        { "name": "Projection",   "type": "Matrix" },
        { "name": "TintColor",    "type": "Vector4" },
        { "name": "MainTexture",  "type": "Texture2D", "slot": 0 }
    ]
}

Vertex shader — GLSL (EASYGL backend)

// Content/shaders/tinted_vert.glsl
#version 300 es
precision highp float;

// Vertex attributes — must match the VertexDeclaration
in vec3 aPosition;
in vec3 aNormal;
in vec2 aTexCoord0;

// Uniforms declared in .shader.json
uniform mat4 World;
uniform mat4 View;
uniform mat4 Projection;

// Outputs to fragment shader
out vec2 vTexCoord;
out vec3 vWorldNormal;

void main() {
    mat4 wvp = Projection * View * World;
    gl_Position = wvp * vec4(aPosition, 1.0);

    vTexCoord   = aTexCoord0;
    // Transform normal to world space (use inverse-transpose for non-uniform scale)
    vWorldNormal = normalize(mat3(World) * aNormal);
}

Fragment shader — GLSL (EASYGL backend)

// Content/shaders/tinted_frag.glsl
#version 300 es
precision mediump float;

in vec2 vTexCoord;
in vec3 vWorldNormal;

uniform sampler2D MainTexture;
uniform vec4      TintColor;

out vec4 fragColor;

void main() {
    vec4 texSample = texture(MainTexture, vTexCoord);

    // Simple diffuse: dot with a hard-coded upward light direction
    vec3  lightDir  = normalize(vec3(0.3, 1.0, 0.5));
    float diffuse   = max(dot(vWorldNormal, lightDir), 0.15);

    // Multiply texture colour by tint and light
    fragColor = texSample * TintColor * vec4(vec3(diffuse), 1.0);
}

Vulkan: SPIR-V shaders

For the Vulkan backend, compile the GLSL sources to SPIR-V with glslangValidator or glslc:

// Command-line compilation (run once, ship the .spv files as assets)
// glslc shaders/tinted_vert.glsl -o shaders/tinted_vert.spv
// glslc shaders/tinted_frag.glsl -o shaders/tinted_frag.spv

// The .shader.json vulkanVertex/FragmentSpirv fields point to these .spv files.
// CNA picks GLSL or SPIR-V automatically based on the active backend.

Complete C++ usage — loading and drawing

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/ShaderEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionNormalTexture.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

class TintedShaderDemo final : public Game {
public:
    TintedShaderDemo() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void LoadContent() override {
        auto& content = getContentProperty();
        content.setRootDirectory("Content");

        // Load texture
        texture_ = content.Load<Texture2D>("textures/crate");

        // Load custom ShaderEffect from descriptor
        effect_ = std::make_unique<ShaderEffect>(
            getGraphicsDeviceProperty(), "shaders/tinted");

        // Build a quad (two triangles, positions + normals + texcoords)
        VertexPositionNormalTexture quad[] = {
            { Vector3(-1, -1, 0), Vector3::Backward, Vector2(0, 1) },
            { Vector3(-1,  1, 0), Vector3::Backward, Vector2(0, 0) },
            { Vector3( 1,  1, 0), Vector3::Backward, Vector2(1, 0) },
            { Vector3(-1, -1, 0), Vector3::Backward, Vector2(0, 1) },
            { Vector3( 1,  1, 0), Vector3::Backward, Vector2(1, 0) },
            { Vector3( 1, -1, 0), Vector3::Backward, Vector2(1, 1) },
        };
        vb_ = std::make_unique<VertexBuffer>(
            getGraphicsDeviceProperty(),
            VertexPositionNormalTexture::VertexDeclaration,
            6, BufferUsage::None);
        vb_->SetData(quad, 6);
    }

    void Update(GameTime& gameTime) override {
        float t = static_cast<float>(gameTime.TotalGameTime.TotalSeconds());
        // Pulse the tint colour between orange and white
        float pulse = (std::sin(t * 2.0f) + 1.0f) * 0.5f;
        tintColor_ = Vector4(1.0f, 0.4f + pulse * 0.6f, pulse, 1.0f);
    }

    void Draw(const GameTime& gameTime) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::Black);

        float t = static_cast<float>(gameTime.TotalGameTime.TotalSeconds());
        Matrix world = Matrix::CreateRotationY(t * 0.5f);
        Matrix view  = Matrix::CreateLookAt(
            Vector3(0, 0, 3), Vector3::Zero, Vector3::Up);
        Matrix proj  = Matrix::CreatePerspectiveFieldOfView(
            MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f);

        // Set uniforms
        effect_->Parameters["World"]->SetValue(world);
        effect_->Parameters["View"]->SetValue(view);
        effect_->Parameters["Projection"]->SetValue(proj);
        effect_->Parameters["TintColor"]->SetValue(tintColor_);
        effect_->Parameters["MainTexture"]->SetValue(texture_);

        gd.setVertexBuffer(*vb_);
        for (auto& pass : effect_->getCurrentTechnique().Passes) {
            pass.Apply();
            gd.DrawPrimitives(PrimitiveType::TriangleList, 0, 2);
        }
        gd.Present();
    }

private:
    GraphicsDeviceManager          graphics_;
    std::unique_ptr<ShaderEffect>  effect_;
    std::unique_ptr<VertexBuffer>  vb_;
    Texture2D*                     texture_   = nullptr;
    Vector4                        tintColor_ = Vector4::One;
};

int main() { TintedShaderDemo game; game.Run(); }

Uniform types supported by Parameters

SetValue overloadGLSL uniform type
SetValue(float)float
SetValue(Vector2)vec2
SetValue(Vector3)vec3
SetValue(Vector4)vec4
SetValue(Matrix)mat4
SetValue(int)int / sampler2D slot
SetValue(Texture2D*)sampler2D
SetValue(bool)bool

Summary

  • ShaderEffect wraps a GLSL (or SPIR-V) shader program behind the familiar XNA Effect API.
  • The .shader.json descriptor declares techniques, passes, and uniform names so CNA can bind them automatically.
  • Only the EASYGL and VULKAN backends support ShaderEffect; SDL_RENDERER and bgfx do not.
  • For Vulkan, pre-compile GLSL to SPIR-V with glslc and reference the .spv files in the descriptor.