Tutorial 22: Blend Modes and Alpha Compositing
Blending determines how a new pixel (the source) is combined with the existing pixel in the render target (the destination). XNA exposes blending through the BlendState class passed to SpriteBatch::Begin(). CNA implements all four preset states and supports custom BlendState configurations.
BlendState presets
AlphaBlend (default)
Standard Porter-Duff compositing over pre-multiplied alpha images. This is what SpriteBatch::Begin() uses when no blend state is specified:
// Explicit (same as default)
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::AlphaBlend);
// Formula: result = src.rgb + dst.rgb * (1 - src.a)
// Works correctly with pre-multiplied alpha textures
// Textures loaded via ContentManager are pre-multiplied by default
Additive
Adds source colour to destination without darkening. Ideal for fire, sparks, lightning, glow effects, and lens flares:
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Additive);
// Formula: result = src.rgb * src.a + dst.rgb
// The more particles overlap, the brighter the result — naturally simulates light
NonPremultiplied
For images with straight (non-premultiplied) alpha — where the RGB channels have NOT been multiplied by alpha in advance. Use this when loading textures from external tools that export straight alpha:
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::NonPremultiplied);
// Formula: result = src.rgb * src.a + dst.rgb * (1 - src.a)
// Correct for straight-alpha PNG images
Opaque
Disables blending entirely. Every pixel from the source overwrites the destination. Use for rendering full-screen background layers or when you know nothing is transparent:
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Opaque);
// Formula: result = src.rgb (destination discarded entirely)
Pre-multiplied vs straight alpha
Understanding this distinction prevents the most common artefact in 2D games — a dark fringe around sprites.
| Type | RGB storage | BlendState | Typical source |
|---|---|---|---|
| Pre-multiplied (PMA) | RGB already multiplied by A | AlphaBlend | CNA ContentManager, XNA Content Pipeline |
| Straight alpha | RGB independent of A | NonPremultiplied | Raw PNG from Photoshop, GIMP, Aseprite |
CNA's ContentManager::Load<Texture2D> pre-multiplies alpha at load time by default. If you load a raw PNG with straight alpha and draw it with AlphaBlend you will see dark edges. Either:
- Set
blendState = &BlendState::NonPremultiplied, or - Pre-multiply the texture yourself, or
- Configure your art pipeline to export pre-multiplied images.
Custom BlendState
For unusual effects (multiplicative blend, subtractive, screen blend) create a custom BlendState:
#include "Microsoft/Xna/Framework/Graphics/BlendState.hpp"
// Multiplicative blend: result = src.rgb * dst.rgb
// Useful for shadow or darkening overlays
BlendState multiplyBlend;
multiplyBlend.ColorSourceBlend = Blend::DestinationColor;
multiplyBlend.ColorDestinationBlend = Blend::Zero;
multiplyBlend.AlphaSourceBlend = Blend::One;
multiplyBlend.AlphaDestinationBlend = Blend::Zero;
spriteBatch_->Begin(SpriteSortMode::Deferred, &multiplyBlend);
// Screen blend: result = 1 - (1-src) * (1-dst)
// Brightens without blowing out (like layer blending in Photoshop)
BlendState screenBlend;
screenBlend.ColorSourceBlend = Blend::One;
screenBlend.ColorDestinationBlend = Blend::InverseSourceColor;
screenBlend.AlphaSourceBlend = Blend::One;
screenBlend.AlphaDestinationBlend = Blend::InverseSourceAlpha;
BlendFunction enum
The BlendFunction enum controls the arithmetic operator applied between source and destination terms:
| BlendFunction | Formula |
|---|---|
Add (default) | src + dst |
Subtract | src - dst |
ReverseSubtract | dst - src |
Min | min(src, dst) |
Max | max(src, dst) |
// Subtractive blend (darkening / shadow effect)
BlendState subtractive;
subtractive.ColorBlendFunction = BlendFunction::ReverseSubtract;
subtractive.ColorSourceBlend = Blend::SourceAlpha;
subtractive.ColorDestinationBlend = Blend::One;
subtractive.AlphaBlendFunction = BlendFunction::Add;
subtractive.AlphaSourceBlend = Blend::Zero;
subtractive.AlphaDestinationBlend = Blend::One;
Code: fire/glow with additive blend
// In Draw():
// Pass 1 — draw opaque world
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::AlphaBlend);
spriteBatch_->Draw(*worldTex_, Vector2::Zero, Color::White);
spriteBatch_->End();
// Pass 2 — draw additive particles on top
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Additive);
for (auto& p : particles_) {
// Fade out by lowering alpha → multiplied into colour tint
bytecs alpha = (bytecs)(p.life / p.maxLife * 200.0f);
Color tint(255, 180, 80, alpha); // warm orange glow
spriteBatch_->Draw(*sparkTex_,
p.position,
std::nullopt,
tint,
p.rotation,
sparkOrigin_,
p.scale,
SpriteEffects::None,
0.0f);
}
spriteBatch_->End();
Code: masked sprite with NonPremultiplied
// Drawing a mask / stencil overlay with straight-alpha PNG
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::NonPremultiplied);
spriteBatch_->Draw(*vignetteOverlay_,
Vector2::Zero,
Color(255, 255, 255, 180)); // semi-transparent overlay
spriteBatch_->End();
Switching blend modes per layer
Each Begin()/End() pair can use a different blend state. A typical frame uses three passes:
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::Black);
// 1. Opaque background
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Opaque);
spriteBatch_->Draw(*backgroundTex_, Vector2::Zero, Color::White);
spriteBatch_->End();
// 2. Alpha-blended sprites
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::AlphaBlend);
DrawWorldSprites();
spriteBatch_->End();
// 3. Additive particle effects
spriteBatch_->Begin(SpriteSortMode::Deferred, &BlendState::Additive);
DrawParticles();
spriteBatch_->End();
gd.Present();
}