SDL3 Bitmap Font Transparency: Grayscale-to-Alpha and Colored Text with the Renderer API

The root cause: INDEX8 puts grayscale in the wrong place

When stb_truetype rasterizes a glyph, each byte in the output bitmap is a coverage value — 0 is background, 255 is full ink, and everything in between is an antialiased edge. That value needs to drive the alpha channel of your texture. In the typical INDEX8 setup, it drives a palette lookup instead.

SDL_PIXELFORMAT_INDEX8 stores one byte per pixel. That byte is an index into a palette table, not a direct color or alpha value. The palette built in the thread sets every entry’s alpha to 255u, so every pixel is fully opaque regardless of what grayscale value came from the PGM file. SDL_BLENDMODE_BLEND has no real alpha to work with. SDL_BLENDMODE_ADD adds brightness on top of the destination, which explains the glow effect in the screenshots, but it can never cut out the background.

Quick fix: map palette alpha to palette index

The one-line change is to let each entry’s alpha track its index value instead of sitting at 255:

for (std::size_t i = 0; i < grayscale.size(); ++i) {
    auto val = static_cast<std::uint8_t>(i);
    grayscale[i] = { val, val, val, val };  // was 255u — now alpha = grayscale
}

With SDL_BLENDMODE_BLEND set on the texture, palette index 0 becomes fully transparent and 255 becomes fully opaque. Switch the blend mode and the transparent background should appear.

There is a catch. SDL’s INDEX8 alpha handling has had regressions — GitHub issue #3593 documents a case where an alpha of 0 incorrectly renders as fully opaque rather than transparent. Whether this affects a given SDL3 build depends on the exact version. It’s a shaky foundation for font rendering, which is why the RGBA approach below is more commonly recommended.

The reliable approach: RGBA32 with grayscale as alpha

Skip the palette entirely. Create the atlas surface in RGBA32 format and pack each stb_truetype coverage byte directly into the alpha channel, with RGB set to white:

auto* atlas_surface = SDL_CreateSurface(atlas_w, atlas_h, SDL_PIXELFORMAT_RGBA32);
SDL_LockSurface(atlas_surface);

const auto* fmt = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
auto* pixels = static_cast<Uint32*>(atlas_surface->pixels);
int pitch_px = atlas_surface->pitch / 4;  // pitch is in bytes; RGBA32 = 4 bytes/pixel

for (int y = 0; y < atlas_h; ++y) {
    for (int x = 0; x < atlas_w; ++x) {
        unsigned char gray;
        pgm_stream.get(reinterpret_cast<char&>(gray));
        pixels[y * pitch_px + x] = SDL_MapRGBA(fmt, nullptr, 255, 255, 255, gray);
    }
}

SDL_UnlockSurface(atlas_surface);
auto* atlas_texture = SDL_CreateTextureFromSurface(renderer, atlas_surface);
SDL_DestroySurface(atlas_surface);
SDL_SetTextureBlendMode(atlas_texture, SDL_BLENDMODE_BLEND);

A note on pitch: SDL surfaces can have padding bytes at the end of each row to satisfy memory alignment requirements. Indexing pixels as y * atlas_w + x silently corrupts the image whenever pitch != width * 4. The y * pitch_px + x pattern above is always correct and costs nothing extra.

Tinting text with SDL_SetTextureColorMod

The texture is white (RGB = 255, 255, 255) with glyph coverage living in alpha. SDL’s color modulation formula is srcC = srcC * (mod / 255), which means white multiplied by any color gives exactly that color. Call it once before each run of same-colored glyphs:

SDL_SetTextureColorMod(atlas_texture, 255, 200, 0);   // golden text
SDL_RenderTexture(renderer, atlas_texture, &src, &dst);

SDL_SetTextureColorMod(atlas_texture, 200, 200, 200); // light gray text
SDL_RenderTexture(renderer, atlas_texture, &src2, &dst2);

You can change the mod between draw calls to render text in different colors from the same texture without re-uploading anything. SDL_SetTextureAlphaMod works the same way for global opacity — useful for fading UI text in and out.

Why ADD mode produces a glow instead of transparency

SDL_BLENDMODE_ADD composites as dstRGB = srcRGB * srcA + dstRGB. It adds light to the destination; nothing is ever subtracted. On a dark background that reads as glowing text, which is a legitimate effect. On lighter backgrounds it washes everything out. SDL_BLENDMODE_BLEND uses Porter-Duff over-compositing: dstRGB = srcRGB * srcA + dstRGB * (1 - srcA). Background shows through where alpha is low; glyph color wins where alpha is high. That is what standard text rendering needs.

If you specifically want additive glow — light bloom on dark surfaces — you can run two passes or construct a custom blend mode with SDL_ComposeCustomBlendMode. For everyday bitmap font use, BLEND handles everything cleanly without any custom setup.

Sources

Similar Posts