SDL - 如何从仅Alpha通道的位图渲染文本?

4
我正在尝试使用SDL呈现文本。很明显,SDL本身不支持呈现文本,因此我采用了以下方法:
  • 加载字体文件
  • 将字体中的栅格字形转换为位图
  • 将所有位图打包到一个大纹理中,形成字形的精灵表
  • 将文本呈现为一系列字形精灵:将矩形从纹理复制到目标上
首先使用FreeType库处理前几个步骤。它可以为许多种字体生成位图,并提供有关字形的大量额外信息。默认情况下,FreeType生成的位图仅具有alpha通道。对于每个字形,我基本上获得范围在0-255之间的A值的2D数组。出于简单起见,下面的MCVE只需要SDL,我已经将FreeType生成的位图嵌入到源代码中。
现在的问题是:我应该如何管理由这样的位图组成的纹理?
  • 我应该使用哪种混合模式?
  • 我应该使用哪种调制方式?
  • 纹理应该填充什么?FreeType只提供alpha通道,而SDL通常需要一个RGBA像素的纹理。RGB应该使用什么值?
  • 如何以特定颜色绘制文本?我不想为每种颜色制作单独的纹理。
  • FreeType文档中说:为了在屏幕上获得最佳渲染效果,位图应该作为带有伽马校正的线性混合的alpha通道使用SDL混合模式文档没有列出任何名为线性混合的内容,所以我使用了自定义的方法,但我不确定是否正确。
  • 我不确定一些SDL调用是否正确,因为其中一些文档记录很差(我已经知道使用空矩形锁定会在Direct3D上崩溃),尤其是如何使用SDL_LockTexture复制数据。
#include <string>
#include <stdexcept>

#include <SDL.h>

constexpr unsigned char pixels[] = {
  0,   0,   0,   0,   0,   0,   0,  30,  33,   0,   0,   0,   0,   0,   0,
  0,   0,   0,   0,   0,   1, 169, 255, 155,   0,   0,   0,   0,   0,   0,
  0,   0,   0,   0,   0,  83, 255, 255, 229,   1,   0,   0,   0,   0,   0,
  0,   0,   0,   0,   0, 189, 233, 255, 255,  60,   0,   0,   0,   0,   0,
  0,   0,   0,   0,  33, 254,  83, 250, 255, 148,   0,   0,   0,   0,   0,
  0,   0,   0,   0, 129, 227,   2, 181, 255, 232,   3,   0,   0,   0,   0,
  0,   0,   0,   2, 224, 138,   0,  94, 255, 255,  66,   0,   0,   0,   0,
  0,   0,   0,  68, 255,  48,   0,  15, 248, 255, 153,   0,   0,   0,   0,
  0,   0,   0, 166, 213,   0,   0,   0, 175, 255, 235,   4,   0,   0,   0,
  0,   0,  16, 247, 122,   0,   0,   0,  88, 255, 255,  71,   0,   0,   0,
  0,   0, 105, 255, 192, 171, 171, 171, 182, 255, 255, 159,   0,   0,   0,
  0,   0, 203, 215, 123, 123, 123, 123, 123, 196, 255, 239,   6,   0,   0,
  0,  44, 255, 108,   0,   0,   0,   0,   0,  75, 255, 255,  77,   0,   0,
  0, 142, 252,  22,   0,   0,   0,   0,   0,   5, 238, 255, 164,   0,   0,
  5, 234, 184,   0,   0,   0,   0,   0,   0,   0, 156, 255, 242,   8,   0,
 81, 255,  95,   0,   0,   0,   0,   0,   0,   0,  68, 255, 255,  86,   0,
179, 249,  14,   0,   0,   0,   0,   0,   0,   0,   3, 245, 255, 195,   0
};

[[noreturn]] inline void throw_error(const char* desc, const char* sdl_err)
{
    throw std::runtime_error(std::string(desc) + sdl_err);
}

void update_pixels(
    SDL_Texture& texture,
    const SDL_Rect& texture_rect,
    const unsigned char* src_alpha,
    int src_size_x,
    int src_size_y)
{
    void* pixels;
    int pitch;
    if (SDL_LockTexture(&texture, &texture_rect, &pixels, &pitch))
        throw_error("could not lock texture: ", SDL_GetError());

    auto pixel_buffer = reinterpret_cast<unsigned char*>(pixels);
    for (int y = 0; y < src_size_y; ++y) {
        for (int x = 0; x < src_size_x; ++x) {
            // this assumes SDL_PIXELFORMAT_RGBA8888
            unsigned char* const rgba = pixel_buffer + x * 4;
            unsigned char& r = rgba[0];
            unsigned char& g = rgba[1];
            unsigned char& b = rgba[2];
            unsigned char& a = rgba[3];
            r = 0xff;
            g = 0xff;
            b = 0xff;
            a = src_alpha[x];
        }

        src_alpha += src_size_y;
        pixel_buffer += pitch;
    }

    SDL_UnlockTexture(&texture);
}

int main(int /* argc */, char* /* argv */[])
{
    if (SDL_Init(SDL_INIT_VIDEO) < 0)
        throw_error("could not init SDL: ", SDL_GetError());

    SDL_Window* window = SDL_CreateWindow("Hello World",
        SDL_WINDOWPOS_UNDEFINED,
        SDL_WINDOWPOS_UNDEFINED,
            1024, 768,
            SDL_WINDOW_RESIZABLE);

    if (!window)
        throw_error("could not create window: ", SDL_GetError());

    SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0);
    if (!renderer)
        throw_error("could not create renderer: ", SDL_GetError());

    SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, 512, 512);
    if (!texture)
        throw_error("could not create texture: ", SDL_GetError());

    SDL_SetTextureColorMod(texture, 255, 0, 0);

    SDL_Rect src_rect;
    src_rect.x = 0;
    src_rect.y = 0;
    src_rect.w = 15;
    src_rect.h = 17;
    update_pixels(*texture, src_rect, pixels, src_rect.w, src_rect.h);

    /*
     * FreeType documentation: For optimal rendering on a screen the bitmap should be used as
     * an alpha channel in linear blending with gamma correction.
     *
     * The blending used is therefore:
     * dstRGB = (srcRGB * srcA) + (dstRGB * (1 - srcA))
     * dstA = (srcA * 0) + (dstA * 1) = dstA
     */
    SDL_BlendMode blend_mode = SDL_ComposeCustomBlendMode(
        SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
        SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_ONE, SDL_BLENDOPERATION_ADD);

    if (SDL_SetTextureBlendMode(texture, blend_mode))
        throw_error("could not set texture blending: ", SDL_GetError());

    while (true) {
        SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
        SDL_RenderClear(renderer);

        SDL_Rect dst_rect;
        dst_rect.x = 100;
        dst_rect.y = 100;
        dst_rect.w = src_rect.w;
        dst_rect.h = src_rect.h;
        SDL_RenderCopy(renderer, texture, &src_rect, &dst_rect);

        SDL_RenderPresent(renderer);
        SDL_Delay(16);

        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_KEYUP:
                    switch (event.key.keysym.sym) {
                        case SDLK_ESCAPE:
                            return 0;
                    }
                    break;
                case SDL_QUIT:
                    return 0;
            }
        }
    }
    return 0;
}

预期结果:黄色背景上的红色字母“A”。 实际结果:黄色背景上黑色正方形内部出现异常的红色线条。actual result 我怀疑这些线条是因为在“update_pixels”中指针算术运算存在错误导致的,但我不知道是什么导致了黑色正方形的出现。
1个回答

1

首先,SDL_ttf库中已经完成了部分工作。您可以使用它将字形光栅化到表面或生成多字符文本表面。

您的src_alpha += src_size_y;是不正确的-您按行复制,但跳过列长度而不是行长度。它应该是src_size_x。这导致每行的偏移量不正确,只有您复制的图像的第一行是正确的。

在写入纹理时,您的颜色打包是反向的。请参见https://wiki.libsdl.org/SDL_PixelFormatEnum#order-Packed component order (high bit -> low bit): SDL_PACKEDORDER_RGBA,意味着R在最高位打包,而A在最低位。因此,当用unsigned char*表示它时,第一个字节是A,第四个字节是R

        unsigned char& r = rgba[3];
        unsigned char& g = rgba[2];
        unsigned char& b = rgba[1];
        unsigned char& a = rgba[0];

你不需要自定义混合模式,使用SDL_BLENDMODE_BLEND即可,这是每个人都使用的“标准”“源alpha,1-源alpha”公式(请注意,它不会混合目标alpha通道本身,也不会在任何程度上使用它;在混合时,我们只关心源alpha)。
最后还有一种方法:将字形亮度值(alpha,无论如何称呼,重点是它只有一个通道)放入每个通道中。这样,您可以进行加法混合,而根本不需要使用alpha,甚至不需要RGBA纹理。字形颜色仍然可以与颜色调节相乘。SDL_ttf就实现了这一点。

1
这些东西中的一部分已经在SDL_ttf库中完成了。您可以使用它将字形光栅化到表面或生成多字符文本表面。这正是问题所在。我想要纹理,而不是表面。我想要一个字形精灵图,而不是表面。此外,SDL_ttf不支持动态添加所需的字形到图集中。我已经多次阅读过SDL_ttf使用CPU而不是GPU,这使其效率低下。 - Xeverous
重点是它只有一个通道,然后将其放入每个通道。这样,您可以进行加法混合,而根本不需要使用alpha,甚至不需要RGBA纹理。如果我在黄色背景(255, 255, 0)上添加红色文本(255, 0, 0),加法混合是否有效?如果我将红色与黄色进行加法混合,我会得到黄色,因为黄色已经充满了红色成分。我以前尝试过加法混合,在黄色背景上的红色文本是不可见的。 - Xeverous
请注意,它本身不会混合目标 alpha 通道,也不会在任何程度上使用它;在混合时,我们只关心源 alpha。那么文档是错误的吗?对于 SDL_BLENDMODE_BLEND,https://wiki.libsdl.org/SDL_SetTextureBlendMode 表示 dstA = srcA + (dstA * (1-srcA)),但我不想影响 dstA。 - Xeverous
1
抱歉,你说得对,加法只适用于白色字体,我还没有考虑得够多。至于端序 - 不用担心,如果您希望内存顺序始终为RGBA,则请使用RGBA32格式(它是RGB8888或ABGR8888的别名,取决于端序)。 - keltar
关于SDL_ttf - 你可以轻松地将生成的表面复制到纹理集中。当然,如果您想考虑字距和其他因素,则字体渲染会更加复杂。 - keltar
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接