iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
自我挑戰組

翻車機率極高的2D平台遊戲(2D Platformer)製作系列 第 23

[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 22] 2D批次渲染 (三) - 終於找到問題了
下一篇
[Day 24] 字形渲染(Text Rendering) - 渲染文字
系列文
翻車機率極高的2D平台遊戲(2D Platformer)製作33

尚未有邦友留言

立即登入留言