Tutorial 22: Blend Modes and Alpha Compositing

Graphics  ·  BlendState  ·  Additive  ·  Alpha

ℹ

What you’ll learn

  • The stock BlendState presets and when each is right.
  • Pre-multiplied versus straight alpha, and why it changes your art pipeline.
  • Building a custom BlendState and reading the BlendFunction enum.

Before you start — Tutorial 21: SpriteBatch Deep Dive — blend state is set in Begin(), so know that call first.

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. It is a premultiplied blend, exactly as in XNA 4.0: Color(255, 255, 255, 100) is not a translucent white here, but Color::White * 0.4f is (see Tutorial 07).

// 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: those built by the XNA
// content pipeline or by CNA's cna-content tool. A plain .png loaded from disk
// is NOT pre-multiplied (see below).

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 for textures that come straight from an image file (a .png loaded by Load<Texture2D> or by Texture2D("file.png", device)) or 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)

The four presets are values of type BlendState (BlendState::AlphaBlend, ::Additive, ::NonPremultiplied, ::Opaque), so they go into Begin() as they are, without &.

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 AAlphaBlendTextures built by the XNA Content Pipeline (.xnb) or by CNA’s cna-content tool (.cnb), both premultiplying by default
Straight alphaRGB independent of ANonPremultipliedA .png/.jpg loaded from disk; raw exports from Photoshop, GIMP, Aseprite

ContentManager::Load<Texture2D> does not pre-multiply. When it finds a loose image file it decodes the pixels exactly as stored and hands you a straight-alpha texture; premultiplication happens at build time, in the content pipeline (the TextureProcessor’s premultiplyAlpha option, on by default, as in XNA), and the compiled .xnb/.cnb then carries premultiplied pixels. So whether a texture is premultiplied depends on how it reached your game. If you load a raw PNG with straight alpha and draw it with AlphaBlend you will see bright or dark fringes around its soft edges. Either:

  • Draw it with BlendState::NonPremultiplied, or
  • Build it through cna-content (or the XNA content pipeline) so it is premultiplied for you, or
  • Pre-multiply the pixels yourself before use, or
  • Export premultiplied images from your art tools.

Custom BlendState

For unusual effects (multiplicative blend, subtractive, screen blend) create a custom BlendState. Its properties are read and written through accessors:

#include "Microsoft/Xna/Framework/Graphics/BlendState.hpp"

// Multiplicative blend: result = src.rgb * dst.rgb
// Useful for shadow or darkening overlays
BlendState multiplyBlend;
multiplyBlend.setColorSourceBlendProperty(Blend::DestinationColor);
multiplyBlend.setColorDestinationBlendProperty(Blend::Zero);
multiplyBlend.setAlphaSourceBlendProperty(Blend::One);
multiplyBlend.setAlphaDestinationBlendProperty(Blend::Zero);

// Pass it by value (or, with the longer overload, as a pointer:
// Begin(SpriteSortMode::Deferred, &multiplyBlend, nullptr, nullptr, nullptr))
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.setColorSourceBlendProperty(Blend::One);
screenBlend.setColorDestinationBlendProperty(Blend::InverseSourceColor);
screenBlend.setAlphaSourceBlendProperty(Blend::One);
screenBlend.setAlphaDestinationBlendProperty(Blend::InverseSourceAlpha);
⚠

Configure a custom state completely before its first use. As in XNA, a BlendState that has been bound to a graphics device (which is what Begin()/End() does) can no longer be changed: calling a setter on it throws InvalidOperationException. The four presets are created already bound, so they are immutable; to start from one, copy it (BlendState mine = BlendState::AlphaBlend; gives you a fresh, mutable copy) and modify the copy before you draw with it.

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.setColorBlendFunctionProperty(BlendFunction::ReverseSubtract);
subtractive.setColorSourceBlendProperty(Blend::SourceAlpha);
subtractive.setColorDestinationBlendProperty(Blend::One);
subtractive.setAlphaBlendFunctionProperty(BlendFunction::Add);
subtractive.setAlphaSourceBlendProperty(Blend::Zero);
subtractive.setAlphaDestinationBlendProperty(Blend::One);

The numeric values of BlendFunction::Min and Max (3 and 4) now match XNA’s; alpha.1 had them the other way round, which only matters if you cast the enum to an integer or persist it.

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
    int alpha = static_cast<int>(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();

    // No gd.Present(): Game::EndDraw() presents the frame after Draw() returns.
}