Tutorial 52: Writing Custom Shaders (ShaderEffect)
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 overload | GLSL 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
ShaderEffectwraps a GLSL (or SPIR-V) shader program behind the familiar XNAEffectAPI.- The
.shader.jsondescriptor declares techniques, passes, and uniform names so CNA can bind them automatically. - Only the EASYGL and VULKAN backends support
ShaderEffect;SDL_RENDERERandbgfxdo not. - For Vulkan, pre-compile GLSL to SPIR-V with
glslcand reference the.spvfiles in the descriptor.