Tutorial 22: Blend Modes and Alpha Compositing

Graphics  ·  BlendState  ·  Additive  ·  Alpha

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.

TypeRGB storageBlendStateTypical source
Pre-multiplied (PMA)RGB already multiplied by AAlphaBlendCNA ContentManager, XNA Content Pipeline
Straight alphaRGB independent of ANonPremultipliedRaw 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:

BlendFunctionFormula
Add (default)src + dst
Subtractsrc - dst
ReverseSubtractdst - src
Minmin(src, dst)
Maxmax(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();
}