[Day23] 字形渲染(Text Rendering) - 载入字形

先声明: 本篇只处理渲染出ASCII码的部分,渲染ASCII以外的字母还需要处理解析unicode的部分

今日目标

  • 载入字体档(.ttf)

关於渲染字形这件事

这里我直接建议读Learn OpenGL - Text Rendering这篇,如果英文不好的读者,这篇是其他网友好心翻译的简中版本。

假如你跟我一样,以为要在银幕上画一个字很简单嘛?错!我又把我自己搞死了 O_>O

渲染一个字(以英文字母来说),除了大小,需要注意几件事

  1. 字母与基准线的关系,例如: 小写字母pj这些在基准线之下会有资料需要画出(Bearing)
  2. 字母与字母间的间距(Advance)
  3. 曲线(这个有一大堆可以说,碍於本人能力不足,还没研究过)

上方的文章使用了freetype这个libary,但这里使用的是之前介绍过的stb系列,stb_truetype

stb_truetype的使用

如同之前stb_image的好处,stb_truetype一样是single-header file。使用方法与stb_image大致相同,标头档里面其实就有example了,请见line 272

或是我有在github找到另一个范例,有额外用stb_image_write把要输出的文字输出到图片上。

然後我主要参照了上面的范例与raylib载入字形的方法(其实百分之九十都是抄raylib),只把ASCII的字形资料载入至记忆体里,这个会是字形与各个字的结构。

typedef struct CharInfo {
    int unicode;
    unsigned char* data;
    Rect rec;
    int xoffset, yoffset;
    int xadvance;
} CharInfo;

typedef struct Font {
    Texture texture;
    int base_height;
    CharInfo chars[ASCII_CHAR_COUNT];
} Font;

下面直接贴上范例的内容

    /* prepare font */
    stbtt_fontinfo info;
    if (!stbtt_InitFont(&info, fontBuffer, 0))
    {
        printf("failed\n");
    }
    
    int b_w = 512; /* bitmap width */
    int b_h = 128; /* bitmap height */
    int l_h = 64; /* line height */

    /* create a bitmap for the phrase */
    unsigned char* bitmap = calloc(b_w * b_h, sizeof(unsigned char));
    
    /* calculate font scaling */
    float scale = stbtt_ScaleForPixelHeight(&info, l_h);

其实到这里,stb_truetype就已经把所有字形(包括其他国家的文字)的资料全部读完了,剩下的就是依据我们设置的字高之类的找出这个的边界、高、宽、间隙等等...,然後范例中,透过for loop,一遍又一遍,重新迭代这些要输出的文字。

再来就是重点了,我设计会把这些资料,都集成一张图片,也就是Learn OpenGL文章里面提到的bitmap fonts,当然这个就会是我们的图集(Texture Atlas),之後要在游戏内输出文本的时候,就会透过批次渲染画出来,按理来说就会更高效,不需要重叠代找到资料该字的资料,所以批次渲染那边需要对不同Texture又说是图集进行处理(可能是明天那篇)。

目前做了甚麽

目前已经完成了载入字体的部分,Project里面,使用了arial.ttf,如果是Windows,可以在C:/Windows/Fonts里面找到,OS的预设字体都放这里。

既然要把所有要输出的字,挤到一张图里面,这里就会需要2D bin packing的演算法,这个不是单独指一个特定的演算法,而是一个研究主题,目的在於如何在有限空间内,放入最多不同大小的箱子。

很好的是,stb_truetype里面就有内建一套这样的处理,独立出来的标头档是stb_rect_pack.h,stb的作者使用的是skyline bin packing。

也就是说,利用stb_truetype算出每个字放在小箱子里的大小,然後透过stb_rect_pack安排这些箱子要放大箱子的哪一处,下面是如何使用stb_rect_pack,可以看到每个箱子的间隙也要计算。

    stbrp_context* context = (stbrp_context*)malloc(sizeof(stbrp_context));
	stbrp_node* nodes = (stbrp_node*)malloc(sizeof(ASCII_CHAR_COUNT * sizeof(stbrp_node)));

	stbrp_init_target(context, font_atlas.w, font_atlas.h, nodes, ASCII_CHAR_COUNT);
	stbrp_rect* rects = (stbrp_rect*)malloc(ASCII_CHAR_COUNT * sizeof(stbrp_rect));

	for (int i = 0; i < ASCII_CHAR_COUNT; i++) {
		rects[i].id = i;
		rects[i].w = font->chars[i].rec.w + 2 * padding;
		rects[i].h = font->chars[i].rec.h + 2 * padding;
	}

	stbrp_pack_rects(context, rects, ASCII_CHAR_COUNT);

老实说,这块目前没太多研究,也是直接参照raylib的作法。

之後就是把图片的byte资料直接给opengl,产出贴图,就有一张Font Atlas可以用了。

然後还有一段,图片转成贴图的地方,有用到一个叫做Texture Swizzle,原因是true type转成图片的资料只有两个通道,有就是灰阶的,但如果要转成Font Bitmap的话至少需要三通道 RGB,但我们需要Alpha通道,故转成RGBA。

参考

今日上传的内容 - github


<<:  Day-19: 咩啊抓产生假资料,让我们来使用factory_bot

>>:  DAY 18 - 九尾狐妹妹 (2) 线稿

数据分析的好夥伴 - Python基础:资料形式(上)

在学习完SQL之後,接下来让我们进入下一个阶段:Python的学习! 先说为什麽你需要学Python...

[鼠年全马] W38 - 使用Vuex管理资料状态(上)

这周要来介绍一个很好用的套件 - Vuex 看到名称应该马上可以理解他就是Vue专案在使用的套件吧...

Day3.编译器运作流程介绍

编译器做了什麽? 我们知道使用机器指令撰写程序码是非常麻烦的事情,也会使开发程序的效率不高,编译器就...

19 - Traces - 观察应用程序的效能瓶颈 (3/6) - 如何在 Kibana 使用 APM UI

Traces - 观察应用程序的效能瓶颈 系列文章 (1/6) - Elastic APM 基本介绍...

Day20 ( 中级 ) 依序点灯 ( 座标 )

依序点灯 ( 座标 ) 教学原文参考:依序点灯 ( 座标 ) 这篇文章会介绍如何使用「点亮」、「计次...