SDL3 Bitmap Font Transparency: Fix Grayscale Atlas Backgrounds With Alpha

The problem is your alpha channel, not your blend mode

If you load a grayscale font atlas into SDL3 and the background refuses to disappear, the blend mode is almost never the culprit. The real issue is that your palette entries are setting alpha = 255 everywhere. SDL treats that as fully opaque regardless of which blend mode you apply later. The grayscale values that encode glyph coverage need to drive transparency — right now they are only driving brightness.

Option 1: Fix the palette alpha

The fastest change is to move the grayscale value from the RGB channels into the alpha channel of each palette entry. Change this:

grayscale[i] = { val, val, val, 255u };

To this:

grayscale[i] = { 255u, 255u, 255u, val };

Now palette index 0 is fully transparent white and index 255 is fully opaque white. The glyph shape is preserved, but the background falls away. Switch the blend mode to SDL_BLENDMODE_BLEND and color modulation works on top:

SDL_SetTextureBlendMode(atlas_texture, SDL_BLENDMODE_BLEND);
SDL_SetTextureColorMod(atlas_texture, r, g, b);

Whether SDL respects palette alpha when converting an SDL_PIXELFORMAT_INDEX8 surface to a hardware texture depends on the renderer backend. It generally works with the software and OpenGL backends. Worth testing on your target platform before committing to this path.

Option 2: Convert to RGBA8888 (more portable)

For production code, converting to a real RGBA format removes the ambiguity entirely. If you already fixed the palette to { 255, 255, 255, val }, you can let SDL do the conversion:

auto* rgba_surface = SDL_ConvertSurface(atlas_surface, SDL_PIXELFORMAT_RGBA8888);
SDL_DestroySurface(atlas_surface);
auto* atlas_texture = SDL_CreateTextureFromSurface(renderer, rgba_surface);
SDL_DestroySurface(rgba_surface);

If you still had the original { val, val, val, 255 } palette, though, this does not help. The RGB channels carry the grayscale data but alpha remains 255, and you get an opaque white glyph on an opaque black background.

The cleanest approach skips the palette altogether and builds the RGBA surface directly from the PGM pixel data:

auto* rgba_surface = SDL_CreateSurface(atlas_w, atlas_h, SDL_PIXELFORMAT_RGBA8888);
SDL_LockSurface(rgba_surface);
auto* pixels = reinterpret_cast<std::uint32_t*>(rgba_surface->pixels);

for (std::size_t i = 0; i < atlas_size; ++i)
{
    char in{};
    pgm_stream.get(in);
    std::uint8_t alpha = static_cast<std::uint8_t>(in);
    pixels[i] = SDL_MapRGBA(rgba_surface->format, 255u, 255u, 255u, alpha);
}

SDL_UnlockSurface(rgba_surface);

White glyphs, per-pixel alpha derived directly from coverage data, no palette involved.

Color tinting with SDL_SetTextureColorMod

Once your texture carries a real alpha channel and the blend mode is SDL_BLENDMODE_BLEND, color modulation multiplies each rendered pixel’s RGB components by the mod values (scaled to the range 0–1 internally). Set the mod to { 255, 0, 0 } and white glyphs render red. A mod of { 100, 200, 255 } gives a cool blue. The alpha channel is untouched by color mod, so glyph edges stay smooth and properly antialiased.

SDL_SetTextureColorMod(atlas_texture, 255u, 64u, 0u);  // orange text

One call, any color, no texture rebuild needed.

Why ADD mode goes wrong with color mod

SDL_BLENDMODE_ADD adds source color to the destination and never subtracts, so dark pixels in the source — your background — contribute zero and effectively disappear. That is why white text looked fine in ADD mode without a color mod. But ADD ignores source alpha entirely. Antialiased glyph edges pick up a hard fringe instead of feathering into the background. And once a color mod reduces some channels, those channels add less to the destination, which shifts the resulting hue unpredictably. SDL_BLENDMODE_BLEND with proper per-pixel alpha is the correct tool for font rendering.

Similar Posts