SDL - 如何从只有 alpha 的位图中呈现文本?
SDL - how to render text from an alpha-only bitmap?
我正在尝试使用 SDL 呈现文本。显然 SDL 本身不支持渲染文本,所以我采用了这种方法:
- 加载字体文件
- 将字体中的光栅字形转换为位图
- 将所有位图打包到一个大纹理中,形成一个字形 spritesheet
- 将文本呈现为字形精灵序列:将矩形从纹理复制到目标
第一步是使用 FreeType 库处理的。它可以为多种字体生成位图,并提供大量有关字形的额外信息。 FreeType 生成的位图(默认情况下)仅为 alpha 通道。对于每个字形,我基本上得到一个 0 - 255 范围内的 A 值的二维数组。为简单起见,下面的 MCVE 只需要 SDL,我已经在源代码中嵌入了 FreeType 生成的位图。
现在的问题是:我应该如何管理由这些位图组成的纹理?
- 我应该使用什么混合模式?
- 我应该使用什么调制方式?
- 纹理应该填充什么? FreeType 仅提供 alpha 通道,SDL 通常需要 RGBA 像素的纹理。我应该为 RGB 使用什么值?
- 如何以特定颜色绘制文本?我不想为每种颜色制作单独的纹理。
- FreeType 文档说:为了在屏幕上实现最佳渲染,位图应该用作带伽马校正的线性混合中的 alpha 通道。 SDL blending mode documentation 没有列出任何名为线性混合的东西,所以我使用了一个自定义的,但我不确定我是否做对了。
- 我不确定我是否正确地调用了一些 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”。
实际结果:黄底黑方块内红线畸形。
我怀疑线断了,因为 update_pixels
中的指针算法存在错误,但我不知道是什么导致了黑色方块。
首先,部分内容已经在 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
,即'standard'“src-alpha,one-minus-src-alpha”公式大家都用(注意不是混合dst alpha 通道本身,也没有在任何程度上使用它;混合时,我们只关心 src alpha)。
最后还有另一种方法:你可以把你的字形亮度值(alpha,不管它叫什么,关键是它只有一个通道)并把它放入 every 渠道。这样你就可以在根本不使用 alpha 的情况下进行加法混合,甚至不需要 RGBA 纹理。字形颜色仍然可以与颜色 mod 相乘。 SDL_ttf 实现了这一点。
我正在尝试使用 SDL 呈现文本。显然 SDL 本身不支持渲染文本,所以我采用了这种方法:
- 加载字体文件
- 将字体中的光栅字形转换为位图
- 将所有位图打包到一个大纹理中,形成一个字形 spritesheet
- 将文本呈现为字形精灵序列:将矩形从纹理复制到目标
第一步是使用 FreeType 库处理的。它可以为多种字体生成位图,并提供大量有关字形的额外信息。 FreeType 生成的位图(默认情况下)仅为 alpha 通道。对于每个字形,我基本上得到一个 0 - 255 范围内的 A 值的二维数组。为简单起见,下面的 MCVE 只需要 SDL,我已经在源代码中嵌入了 FreeType 生成的位图。
现在的问题是:我应该如何管理由这些位图组成的纹理?
- 我应该使用什么混合模式?
- 我应该使用什么调制方式?
- 纹理应该填充什么? FreeType 仅提供 alpha 通道,SDL 通常需要 RGBA 像素的纹理。我应该为 RGB 使用什么值?
- 如何以特定颜色绘制文本?我不想为每种颜色制作单独的纹理。
- FreeType 文档说:为了在屏幕上实现最佳渲染,位图应该用作带伽马校正的线性混合中的 alpha 通道。 SDL blending mode documentation 没有列出任何名为线性混合的东西,所以我使用了一个自定义的,但我不确定我是否做对了。
- 我不确定我是否正确地调用了一些 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”。
实际结果:黄底黑方块内红线畸形。
我怀疑线断了,因为 update_pixels
中的指针算法存在错误,但我不知道是什么导致了黑色方块。
首先,部分内容已经在 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
,即'standard'“src-alpha,one-minus-src-alpha”公式大家都用(注意不是混合dst alpha 通道本身,也没有在任何程度上使用它;混合时,我们只关心 src alpha)。
最后还有另一种方法:你可以把你的字形亮度值(alpha,不管它叫什么,关键是它只有一个通道)并把它放入 every 渠道。这样你就可以在根本不使用 alpha 的情况下进行加法混合,甚至不需要 RGBA 纹理。字形颜色仍然可以与颜色 mod 相乘。 SDL_ttf 实现了这一点。